鸿蒙 PC Markdown 编辑器命令面板:把桌面高频操作收束到一个入口

📅 2026/7/21 19:26:30
鸿蒙 PC Markdown 编辑器命令面板:把桌面高频操作收束到一个入口
鸿蒙 PC Markdown 编辑器命令面板把桌面高频操作收束到一个入口在桌面编辑器里命令面板不是一个“搜索按钮集合”的弹窗。它真正解决的是功能增长之后的入口失控菜单越来越深、工具栏越来越宽、快捷键越来越难记而用户的注意力仍然应该停留在文档上。鸿蒙 PC Markdown 编辑器需要同时服务键盘型用户、触控板用户和偶尔触屏操作的用户因此命令面板必须成为一种稳定的任务入口而不是另一套与界面状态脱节的功能清单。本文只讨论已经进入项目主分支并完成模拟器验证的实现。对应仓库为 https://gitcode.com/VON-/codex_md_oh主要代码基线是提交ad1e31a后续工作区搜索命令在2ca99e9中接入同一模型。文中不会把尚未完成的插件系统、用户自定义命令或全局索引包装成现有能力。从桌面任务而不是按钮数量出发Markdown 编辑器的常用任务通常横跨文件、编辑、导航、视图和导出。用户可能正在源代码视图里输入下一秒要打开文件夹再下一秒要切换分栏或导出 HTML。如果每类任务只在一个固定区域提供入口就会出现两个问题第一鼠标行程和视觉搜索成本随功能增加而增加第二熟练用户无法形成统一的键盘操作节奏。命令面板的产品目标因此被定义为“在不离开当前上下文的前提下定位并执行可用命令”。这里有三个关键词。当前上下文意味着面板覆盖编辑区但不销毁文档会话定位意味着标题、类别和关键词都能参与筛选可用命令意味着大文档模式等运行时约束必须反映在命令项状态上不能允许用户执行必然失败的操作。这也解释了为什么实现没有直接把工具栏按钮克隆一份。按钮只表达视觉入口命令对象还需要稳定标识、检索元数据、启用条件与执行函数。只有把这些信息收敛成模型菜单、快捷键、自动化测试以及未来的右键菜单才有机会共享同一事实来源。命令模型是最小而明确的契约Web 编辑器中的命令接口位于web-editor/src/main.ts。当前模型刻意保持扁平没有引入通用依赖注入容器或复杂总线interfaceEditorCommand{id:string;title:string;category:string;keywords:string;enabled:()boolean;execute:()void;}id是自动化和 DOM 标识的稳定键不能使用会被本地化改变的标题title面向用户category用于扫描keywords补充用户可能想到但标题中没有出现的表达enabled在每次渲染时读取运行状态execute只描述动作不把面板关闭、焦点恢复等展示细节塞进每个命令。这份接口看起来简单但边界非常重要。它没有让命令直接访问 ArkUI 文件 API也没有把原生 URI 暴露给 ArkWeb。文件打开、保存、选择工作区仍然由原生层完成。编辑器内部的撤销、重做可以直接执行文件和窗口类命令则通过受限 Bridge 发给原生壳层。命令面板统一的是入口不是抹平权限边界。注册表同时表达能力与降级规则命令注册表是一组静态对象。下面是其中一部分真实代码consteditorCommands:ArrayEditorCommand[{id:file.openWorkspace,title:Open Folder,category:File,keywords:workspace directory,enabled:()true,execute:()requestNativeCommand(openWorkspace)},{id:edit.undo,title:Undo,category:Edit,keywords:revert history,enabled:()true,execute:(){undo(editor);}},{id:view.split,title:Show Split View,category:View,keywords:editor preview side by side,enabled:()!largeDocumentMode,execute:()requestNativeCommand(viewSplit)}];分栏预览在大文档模式下被禁用不是因为按钮需要变灰而是因为大文档模式以保持输入响应为首要目标。把条件写在命令对象里之后命令面板不会绕过这条性能规则。未来同一命令若出现在菜单中也应该读取同一个enabled判断避免某个入口能执行、另一个入口不能执行的状态分裂。注册表还为后续能力保留了自然扩展点。提交2ca99e9新增Search Workspace与Quick Open时只增加命令对象和受限命令名没有重做面板结构。这个结果证明模型的粒度基本合适扩展一个真实任务时修改集中在命令声明、Bridge 白名单和原生路由而不是散落地复制 UI。检索不追求炫技而追求可解释第一版筛选使用规范化后的包含匹配functionnormalizeCommandQuery(value:string):string{returnvalue.trim().toLocaleLowerCase();}functioncommandMatchesQuery(command:EditorCommand,query:string):boolean{if(query.length0){returntrue;}return${command.title}${command.category}${command.keywords}.toLocaleLowerCase().includes(query);}这不是一个模糊搜索算法但它有三个工程优点。其一命令数量当前很小线性扫描的成本可以忽略其二用户输入为什么命中某项是可解释的不会因为隐蔽权重让列表跳动其三代码不依赖网络、索引服务或额外运行库符合编辑器离线优先的约束。如果未来命令数量增加到数百项可以在不改变EditorCommand契约的前提下替换排序函数例如加入标题前缀、词边界、最近使用频率和连续字符奖励。但升级前需要先记录真实任务数据因为过早引入复杂评分会制造不可预测排序也会让键盘用户依赖的位置发生变化。当前实现选择的是与规模相称的复杂度而不是把“模糊搜索”作为宣传标签。DOM 构建必须把文本当文本命令结果没有通过拼接innerHTML生成。每一项使用 DOM API 创建按钮、标题和类别并通过textContent写入文本functionrenderCommandPalette():void{constquerynormalizeCommandQuery(commandQuery.value);filteredCommandseditorCommands.filter((command)commandMatchesQuery(command,query));commandResults.replaceChildren();filteredCommands.forEach((command,index){constoptiondocument.createElement(button);option.typebutton;option.classNamecommand-palette__option;option.idcommand-option-${command.id};option.dataset.commandIdcommand.id;option.setAttribute(role,option);option.setAttribute(aria-selected,indexselectedCommandIndex?true:false);option.disabled!command.enabled();consttitledocument.createElement(span);title.textContentcommand.title;constcategorydocument.createElement(span);category.textContentcommand.category;option.append(title,category);commandResults.append(option);});}即使当前命令由开发者静态注册仍然值得坚持文本节点而不是 HTML 字符串。这样做减少未来接入本地化、插件元数据或用户配置时的注入风险也让 CSP 策略保持简单。按钮元素还能天然获得禁用、焦点和点击语义比在普通div上模拟交互更可靠。渲染函数会在每次查询变化后重建有限数量的结果。当前命令规模下这比维护复杂的增量虚拟列表更稳。命令面板不是工作区全文搜索结果不应该为了理论上的海量数据引入不必要的状态同步成本。键盘导航是一套状态机面板内部维护filteredCommands和selectedCommandIndex。输入变化后重新筛选如果旧索引超过新结果长度就把索引收敛到有效范围上下方向键只改变选中项Enter 执行当前项Escape 关闭面板。索引更新还同步aria-selected和输入框的aria-activedescendant使视觉选择与辅助技术感知保持一致。这里最容易出现的缺陷不是“方向键没反应”而是结果变少后仍引用旧索引或鼠标点击与键盘选择维护两份状态。实现通过统一的applyCommandSelection和executeCommand减少分叉。鼠标悬停、点击和键盘导航最终都落到同一个命令对象上禁用判断也在执行前再次检查。执行顺序同样有要求先确认命令可用再关闭面板最后执行动作。这样原生命令打开系统选择器时不会被遗留遮罩挡住编辑器内部命令完成后焦点可以回到编辑区。若先执行再关闭在异步原生窗口出现时可能产生焦点竞争若关闭后不执行可用性复核运行状态刚刚改变时会触发过期命令。快捷键需要明确作用域当前面板使用CtrlShiftP同时兼容 macOS 风格的 Meta 修饰键。全局监听先处理三方差异视图和已打开的命令面板再判断修饰键window.addEventListener(keydown,(event){if(event.keyEscape!conflictComparison.hidden){event.preventDefault();closeThreeWayDiff();return;}if(!conflictComparison.hidden){return;}if(!(event.ctrlKey||event.metaKey)||event.altKey){return;}constkeyevent.key.toLowerCase();if(keypevent.shiftKey){event.preventDefault();commandPalette.hidden?openCommandPalette():closeCommandPalette();}});三方差异界面优先于命令面板是一个明确的模态规则。冲突比较正在显示时不应该再叠加第二层全局操作界面。Alt组合被排除避免抢占输入法或系统级组合键。只有确认命中应用快捷键时才调用preventDefault普通输入和浏览器自身未占用的键不会被无差别吞掉。工作区快速打开使用CtrlP命令面板使用CtrlShiftP。二者在监听顺序中明确区分不依赖字符串拼接或模糊判断。这种细节决定了桌面应用长期扩展时快捷键是否可维护。后续若支持用户重映射仍需先建立冲突检测和作用域优先级而不是简单地把事件监听器继续堆叠。焦点恢复决定面板是否顺手打开面板时先取消隐藏状态、清空查询、重建结果然后在下一帧聚焦输入框。下一帧而非同步聚焦是为了确保元素已经参与布局。关闭时清理查询状态并把焦点交还编辑器。这个焦点闭环对 PC 用户比动画更重要连续执行“打开面板、输入、回车、继续打字”时不应该额外点击正文。焦点处理还与 ArkWeb 宿主有关。命令面板位于 Web 编辑器内部原生 ArkUI 仍管理窗口、侧栏和文件能力。执行Open Folder后系统选择器取得焦点返回应用时原生层更新工作区状态Web 层不能在错误时机强制夺回焦点。因此面板只在本地关闭动作后恢复编辑器焦点涉及系统窗口的最终焦点由宿主和操作结果共同决定。这是一种克制的焦点策略。为了追求“始终聚焦编辑器”而在多个异步回调里调用focus()会导致搜索输入框、设置控件或系统对话框被抢焦点最终损害键盘操作。桌面交互的稳定感往往来自少做一次错误聚焦。ArkWeb 不接触文件权限文件类命令通过requestNativeCommand进入白名单typeNativeCommandnew|open|openWorkspace|save|saveAs|autoSave|find|findWorkspace|quickOpen|viewSource|viewSplit|viewPreview|exportHtml|print;functionrequestNativeCommand(command:NativeCommand):void{window.OhMarkdownEditor?.requestCommand(command);}类型联合限制 Web 侧能发出的命令名称原生层的onEditorCommand再做一次显式分支。传输内容只有命令名和必要正文不传任意函数名不允许 Web 层构造文件系统路径也没有“执行任意脚本”一类后门。系统文件选择、URI 授权、CoreFileKit 读写继续留在 ArkTS。这条边界让命令面板即使未来显示更多动作也不会自动扩大权限。新增文件能力必须同时经过产品入口、类型白名单、Bridge 接口、原生路由和测试形成可审查的变更链。对于本地优先编辑器统一入口不能以统一权限为代价。大文档模式的可用性表达大文档模式下分栏、预览和导出命令会禁用。禁用项仍可显示是因为它向用户解释“功能存在但当前不可用”也保持命令列表结构稳定。若直接过滤掉用户会误以为功能消失并且在文件大小跨越阈值时列表位置发生剧烈变化。不过禁用状态不能只有颜色差异。真实按钮的disabled属性阻止点击和键盘激活视觉样式再作为补充。命令执行函数仍复核enabled()用于防止自动化或状态竞态绕过 DOM 禁用。状态源是编辑器已有的largeDocumentMode没有为命令面板复制第二份“大文件”判断。这种降级也体现了产品优先级输入可靠性高于实时预览。用户可以继续编辑、保存和查找只是暂时关闭高成本能力。命令面板把这个决策一致地呈现出来而不是让不同入口给出相互矛盾的结果。真实界面与设备证据下图来自 HarmonyOS MateBook Pro 2in1 模拟器中的实际应用。面板显示筛选输入、类别和当前选中项背景文档会话仍然存在。命令执行后分栏状态由原生与 Web 两层共同更新截图记录了界面同步结果截图的意义不是证明视觉稿完成而是证明真实链路从快捷键、筛选、执行、Bridge 到视图状态可走通。测试报告保存在仓库的docs/test/ohmarkdown/2026-07-18-g3-02-command-palette/。后续提交增加命令时仍需复用这条链路而不是只验证按钮能否点击。自动化覆盖的是行为契约Playwright 测试在浏览器环境中加载离线编辑器单页通过 Bridge mock 记录命令调用。测试覆盖打开与关闭、输入筛选、方向键、Enter 执行、Escape 退出、禁用命令以及快捷键。工作区搜索加入后自动化还验证CtrlShiftF与CtrlP产生不同命令避免快捷键回归到同一分支。DOM 测试不能替代鸿蒙模拟器。它可以稳定检查命令列表和 Bridge 载荷却无法证明 ArkUI 获得命令后能打开系统选择器也无法覆盖真实窗口焦点。因此证据分成两层Playwright 检查 Web 行为模拟器检查宿主集成。当前统一验证在2ca99e9达到 Playwright29/29命令面板对应的基础提交ad1e31a已在主分支。测试还应避免依赖列表的偶然顺序。稳定断言应使用data-command-id、可访问角色和 Bridge 载荷而不是通过“第三个按钮”定位。只有键盘导航测试需要检查顺序此时顺序本身就是用户行为契约。没有采用的方案没有把命令面板完全做在 ArkUI 中因为编辑器内部的撤销、重做和视图状态已经由 Web 编辑器掌握原生面板若直接操作会增加一套跨层状态同步。也没有全部做成 Web 文件操作因为那会突破 URI 授权和 CoreFileKit 边界。当前方案让面板与编辑器交互保持低延迟同时把受保护能力留在原生层。没有引入第三方命令面板组件。现有需求只需要有限结果、键盘导航和可访问语义引入完整组件库会增加离线包体、样式冲突与供应链面。也没有实现命令历史学习因为缺少足够使用数据未经验证的权重容易让命令位置漂移。没有在第一版支持用户任意注册脚本命令。Markdown 编辑器处理本地文件任意脚本会直接改变威胁模型。未来插件能力必须经过独立的权限、隔离、签名和审计设计不能借命令面板入口偷渡。性能预算与后续演进当前筛选复杂度约为命令数量乘查询长度命令数十余项时远低于一次帧预算。每次输入重建少量 DOM避免维护复杂 diff。面板打开不触发文件扫描不读取文档正文也不启动网络请求。工作区快速打开虽然可从命令面板触发但实际扫描由SearchService的 TaskPool 与取消机制负责不把重任务塞进命令筛选。真正需要关注的是功能增长后的治理。新增命令必须提供稳定id、可检索关键词、启用条件、权限边界和测试快捷键必须通过统一冲突表高成本命令必须解释禁用原因本地化后应保留语言无关的标识。等命令规模和用户数据足够时再引入最近使用和模糊评分并保留可预测的类别排序。命令面板的优势最终不在“按一个快捷键能弹出来”而在它把功能增长变成可治理的模型。鸿蒙 PC 版本用一条受限而清楚的调用链连接 CodeMirror、ArkWeb Bridge 和 ArkUI 文件能力同时保持离线、焦点和大文档降级规则。这个基础足以支撑后续链接、导出和窗口命令继续进入同一入口却没有提前承诺尚不存在的插件平台。验证清单与结论对命令面板进行回归时至少需要逐项确认空查询显示全部命令标题、类别和关键词都能筛选上下键不会越界Enter 不执行禁用项Escape 只关闭当前最上层界面执行后焦点回到合理目标文件命令只产生白名单 Bridge 消息大文档模式禁用高成本能力CtrlShiftP不与CtrlP混淆模拟器中的命令结果和原生界面状态一致。当前实现满足上述基础条件但仍有明确边界没有用户自定义快捷键没有命令历史同步没有插件命令也没有屏幕阅读器真机完整验收。这些限制不会被隐藏成“即将完成”的宣传。对现阶段产品而言可靠、可解释、可测试的统一入口比功能更多但状态分裂的菜单更有价值。