鸿蒙 PC Markdown 编辑器快速打开模糊排序、最近权重与纯键盘路径快速打开是桌面编辑器中频率极高的导航动作。用户通常记得文件名的一部分未必记得目录也不想先把手从键盘移到文件树。一个可用的快速打开需要在几次按键内完成入口、输入、候选选择、打开和焦点归还与此同时它不能把扫描工作放在 UI 线程不能让旧查询覆盖新结果也不能因“最近使用”权重把不相关文件推到前面。OhMarkdown 的快速打开随工作区搜索进入公开仓库 https://gitcode.com/VON-/codex_md_oh完成提交为2ca99e9。本文只讨论当前基于每轮授权目录枚举、TaskPool 模糊排序、最近标签权重和CtrlP键盘交互的实现。持久索引、拼音搜索、用户自定义排序和 1000 文件竞品测量仍未完成。快速打开与全文搜索是不同任务全文搜索读取文档正文返回匹配行、列和上下文快速打开只需要文件元数据返回文件候选。两者可以共享安全目录枚举但不应共享“查询为空”的规则、结果模型展示和触发频率。快速打开允许空查询。空查询用于显示最近文件和工作区文件用户按一次方向键即可选择。全文搜索空查询会匹配所有位置没有意义且成本巨大所以拒绝。快速打开输入时 160 ms 防抖自动刷新全文搜索通常由 Enter 或按钮显式触发。界面把 Current、Workspace、Quick Open 做成三段式模式。每种模式保存自己的查询切换时取消旧任务、清理结果和序号。这样用户从全文搜索切到快速打开后不会在输入框看到语义不同的旧正则。目录事实与全文搜索共享WorkspaceSearchController.quickOpen调用同一个collectDocuments所以继承授权根、符号链接跳过、目录排除、文本扩展白名单和 5000 文档上限。快速打开不会为了速度绕过安全枚举也不会显示图片资源、.git对象或node_modules里的 Markdown。asyncquickOpen(rootUri:string,query:string,recentUris:Arraystring):PromiseWorkspaceSearchSummary{this.cancel();if(rootUri.length0){thrownewError(Open a workspace before using Quick Open.);}if(query.length128){thrownewError(Enter a file name query up to 128 characters.);}constgenerationthis.generation;constcollectionawaitthis.collectDocuments(rootUri,generation);// 文件名排序进入 TaskPool。}查询最多 128 字符避免异常输入扩大评分成本和 UI 宽度。文件相对路径来自枚举过程不由用户查询构造。结果打开仍使用文档 URI但 URI 只保留在 ArkTS 内不传给 ArkWeb。当前每次查询都会重新枚举。两文件热缓存时很快但数千文件会重复 I/O。这是已知边界不会用“快速打开”名称掩盖。持续索引是否值得引入必须由 G3-10 压力数据决定。排序函数在 TaskPool 中执行文件名模糊评分是 CPU 遍历标记Concurrent并以低优先级执行consttasknewtaskpool.Task(rankQuickOpenDocuments,collection.documents,query,recentUris,MAX_QUICK_OPEN_RESULTS);this.activeTasktask;letresults:ArrayWorkspaceSearchResult;try{resultsawaittaskpool.execute(task,taskpool.Priority.LOW)asArrayWorkspaceSearchResult;}finally{if(this.activeTasktask){this.activeTaskundefined;}}this.assertActive(generation);文件列表和最近 URI 都是可序列化数据不携带 ArkUI 对象。排序最多返回 50 项UI 不会渲染数千候选。若结果刚好达到上限摘要标记截断。TaskPool 返回后复核代际。用户输入第二个字符时第一轮可能仍在排序取消请求与代际令牌保证第一轮不会提交。UI 还有请求序号旧 Promise 的finally不能把新一轮 loading 关闭。评分从可解释的匹配层级开始排序依次判断文件名完全等于查询、文件名以前缀开始、完整相对路径包含查询、子序列模糊匹配。每层有明显分数区间保证完全匹配通常高于前缀前缀高于路径包含包含高于松散子序列。if(normalizedQuery.length0){score1;}elseif(namenormalizedQuery){score1200;}elseif(name.startsWith(normalizedQuery)){score900-Math.min(200,name.length-normalizedQuery.length);}elseif(candidate.includes(normalizedQuery)){score700-Math.min(250,candidate.indexOf(normalizedQuery)*3);}else{// 进入子序列评分。}前缀分数对多余长度做有限惩罚短而精确的文件名更靠前。路径包含根据出现位置惩罚目录深处很晚出现的查询略降。分数都设下限范围避免长名称因为惩罚跌到比模糊子序列更低的意外区间。当前使用toLocaleLowerCase做大小写归一化没有拼音转写或语言词法。中文查询需要直接输入对应汉字英文大小写不敏感。引入拼音会增加依赖、索引和多音字排序需要独立产品验证。子序列匹配奖励连续与边界当查询不是直接包含时算法按顺序寻找每个字符。连续字符奖励 22非连续奖励 9匹配位于路径开头、分隔符、连字符、下划线或空格之后再奖励 18。全部查询字符都匹配才成为候选。letqueryIndex0;letpreviousMatch-2;for(letcandidateIndex0;candidateIndexcandidate.lengthqueryIndexnormalizedQuery.length;candidateIndex1){if(candidate[candidateIndex]normalizedQuery[queryIndex]){scorepreviousMatchcandidateIndex-1?22:9;if(candidateIndex0||candidate[candidateIndex-1]/||candidate[candidateIndex-1]-||candidate[candidateIndex-1]_||candidate[candidateIndex-1] ){score18;}previousMatchcandidateIndex;queryIndex1;}}if(queryIndex!normalizedQuery.length)return;score300-Math.min(200,candidate.length);例如gsa更倾向命中g3-search-architecture.md的词边界而不是一个很长路径里分散的三个字母。算法是线性扫描文件数乘相对路径长度的复杂度在当前 5000 上限内可控。评分没有随机因素。相同分数按relativePath.localeCompare排序保证列表稳定。稳定性对键盘用户很重要输入和删除同一查询时不应因遍历顺序变化让选择跳动。最近文档只是加权信号原生层构造 recentUris当前文档 URI 在最前随后是标签会话倒序去重列表。排序函数按索引加分越近分数越高但最低仍有限constrecentIndexrecentUris.indexOf(document.uri);if(recentIndex0){scoreMath.max(40,320-recentIndex*32);}空查询下所有文档基础分为 1最近文件因此排在前面。输入明确查询时完全匹配 1200 分最近权重最多 320不会让一个仅模糊匹配的最近文件轻易压过完全匹配。权重是导航习惯的补充不改变文件事实。最近列表只来自当前进程标签不写入私有历史数据库也不跨设备同步。关闭应用后空查询顺序可能回到相对路径排序。持久历史若要做需要隐私开关、容量和清理策略当前没有假装已实现。输入防抖减少无意义扫描快速打开每次输入变化先取消计划任务、使当前结果失效再安排 160 ms 后搜索privatescheduleQuickOpenSearch():void{this.cancelScheduledWorkspaceSearch();this.workspaceSearchTimerIdsetTimeout((){this.workspaceSearchTimerId-1;if(this.searchPanelModeSearchPanelMode.QUICK_OPEN){this.executeWorkspaceSearch();}},160);}防抖不是缓存。它只把连续按键合并为较少查询目录仍会重新枚举。160 ms 需要在真机输入延迟与扫描成本之间复测太短会频繁 I/O太长会让候选明显滞后。打开 Quick Open 模式时空查询立即执行用户可以不输入直接选择最近文件。输入框通过 ArkUIfocusControl.requestFocus在面板打开后获得焦点实现CtrlP后直接打字。PC 快捷键入口避免与命令面板冲突Web 编辑器监听CtrlP请求quickOpenCtrlShiftP打开命令面板。修饰键分支顺序明确if(keypevent.shiftKey){event.preventDefault();commandPalette.hidden?openCommandPalette():closeCommandPalette();return;}if(keyp!event.shiftKey){event.preventDefault();window.OhMarkdownEditor?.requestCommand(quickOpen);return;}原生收到quickOpen后切换搜索面板模式、打开侧栏并聚焦输入。ArkWeb 不枚举文件仍然只发送受限命令。Playwright 通过 Bridge mock 断言两个快捷键产生不同动作。Meta 键兼容保留但鸿蒙 PC 设备主路径验证使用 Control。Alt 组合不被抢占。后续用户自定义快捷键需要冲突检测不能简单覆盖这一监听。方向键选择与列表滚动同步ArkUI 输入框接收 KeyEvent。只有非 Current 模式、Key Down 事件才进入快速导航。上下方向更新workspaceSearchSelectedIndex限制在有效范围并让 Scroller 滚动到对应项Enter 打开当前结果搜索运行时 Escape 取消。if(event.keyCodeKeyCode.KEYCODE_DPAD_DOWN||event.keyCodeKeyCode.KEYCODE_DPAD_UP){constdeltaevent.keyCodeKeyCode.KEYCODE_DPAD_DOWN?1:-1;constnextIndexMath.max(0,Math.min(this.workspaceSearchResults.length-1,this.workspaceSearchSelectedIndexdelta));this.workspaceSearchSelectedIndexnextIndex;this.workspaceSearchScroller.scrollToIndex(nextIndex,true);returntrue;}if(event.keyCodeKeyCode.KEYCODE_ENTER){this.openWorkspaceSearchResult(this.workspaceSearchResults[this.workspaceSearchSelectedIndex]);returntrue;}输入控件的onSubmit还调用submitSearchInput作为 Enter 在某些平台事件路径未被onKeyEvent消费时的保障。函数只在结果存在且非运行中时打开避免一键触发两次。列表项具有稳定高度约束选中背景不会改变尺寸。长相对路径限制行数并省略防止动态内容让方向键滚动跳动。完整路径仍可通过当前结果文本识别未来可增加 tooltip。打开结果沿用安全文档会话快速打开结果kind为fileoffset 为 0。点击或 Enter 后原生调用readUtf8Document(result.uri)进入与文件树打开相同的文档会话逻辑而不是直接让 Web 改页面 URL。已有标签应激活未打开文件创建新会话BOM、换行和指纹照常保留。工作区搜索文本结果会精确选区快速打开只把光标放到开头状态栏显示Opened docs/plan.md。打开期间使用文件操作锁防止重复 Enter 创建两个标签。完成后焦点回到编辑器用户可以继续输入。文件在候选生成后被删除或权限失效时读取失败并给出错误列表不会构造任意替代路径。文件系统是事实来源候选只是短期导航快照。真实纯键盘设备路径MateBook Pro 2in1 模拟器中在编辑器按CtrlPQuick Open 输入框获得焦点输入plan后同一工作区热缓存条件下扫描两份文件耗时 1 ms返回docs/plan.md按 Enter 打开目标。清空查询后显示两项按方向下键再按 Enter 打开requirements.md。这条路径验证了入口、焦点、空查询、最近权重、方向选择、滚动和打开而不只是鼠标点击列表。完整报告在docs/test/ohmarkdown/2026-07-19-g3-05-workspace-search/。1 ms 是两文件热缓存结果不能用于和 Typora、Obsidian、VS Code 比较。自动化与 TaskPool 设备验证纯函数测试构造中文文件名与不同相对路径验证完全、前缀、包含、子序列、最近权重和稳定排序。Playwright 验证CtrlPBridge 命令。ohosTest 在设备 TaskPool 中递归两层工作区查询quick返回唯一docs/quick-notes.md并执行取消断言。统一基线结果为 Playwright29/29、ohosTest7/7、Web TypeScript、Debug HAP 和 UnitTestBuild 通过。代码提交2ca99e9已推送主分支。HAP 未签名远程 CI Runner 与真机 Release 仍待验证。后续测试应加入相同文件名不同目录、超长中文路径、5000 文件、快速连续输入与退格、空查询最近顺序、打开时文件删除、重复 Enter、标签已打开和多个窗口焦点。性能边界与索引演进当前查询成本包含每轮目录枚举和 TaskPool 评分。评分本身线性且轻量主要成本可能是文件系统枚举。热缓存两文件 1 ms 不代表冷启动数千文件。G3-10 要分别记录首次打开、热查询、每键防抖、取消延迟和内存。如果 1000 文件任务无法达到交互阈值可以引入会话内目录缓存并在工作区变化或明确刷新时失效再往后才是持久增量索引。每次升级都必须保留符号链接、排除目录、授权根和取消规则。最近使用列表也可持久化但属于隐私数据。应提供清除入口、容量上限和工作区隔离不能将完整路径上传。排序权重需要真实任务实验避免“智能”让完全匹配退后。安全与隐私Quick Open 只展示授权工作区内白名单文本文件相对路径留在应用 UIArkWeb 不获得 URI。查询与最近列表不联网、不上传。符号链接跳过资源、版本库和依赖目录排除。查询字符串只参与内存匹配不进入正则也不构造路径避免正则拒绝服务和路径注入。相对路径显示使用 ArkUI Text不作为富 HTML。打开文件重新经过文档读取和格式验证。空查询显示最近文件可能暴露文件名给旁观者这是桌面导航功能的固有界面信息。未来隐私模式可关闭最近权重或面板预览但当前不保存跨启动历史风险范围有限。没有采用的方案没有调用全文搜索后从结果推导文件列表因为无匹配文件会消失且读取正文浪费资源。没有使用首字母排序冒充模糊搜索当前子序列对目录边界和连续字符有明确奖励。没有使用机器学习排序缺少数据且不可解释。没有默认扫描所有文件扩展名。二进制、JSON 和代码文件不属于当前 Markdown 编辑器工作区范围显示它们会增加噪声与读取风险。未来可配置扩展名但需要明确产品边界。没有持久化最近绝对路径也没有云同步历史。标准文件和本地工作区仍是产品中心导航便利不能引入隐形隐私数据。验收清单与结论快速打开验收需要覆盖CtrlP聚焦CtrlShiftP不冲突空查询完全/前缀/包含/子序列最近权重不压过强匹配相同分数稳定方向键边界Enter 单次打开Escape 取消160 ms 防抖旧查询不覆盖工作区未打开提示删除文件失败可解释已打开标签复用编辑输入不被后台排序阻塞。当前 OhMarkdown 已形成一条完整纯键盘路径并把文件权限、排序和 UI 状态分在合适层。它不是持久索引产品的最终形态却已经具备可解释匹配、最近权重、代际取消和真实设备打开证据。下一步应以大工作区数据决定缓存与索引而不是为了宣传“秒开”提前增加难以维护的数据库。