鸿蒙 PC Markdown 编辑器内核隔离:CodeMirror EditorState 多会话切换
鸿蒙 PC Markdown 编辑器内核隔离CodeMirror EditorState 多会话切换多标签编辑器最隐蔽的数据错误不是标签标题显示错而是撤销历史、保存基线或恢复计时器跨文档串联。用户在文档甲输入一段切到文档乙按撤销如果乙退回甲的内容编辑器已经失去可信度。只在原生层保存每个标签的正文字符串无法解决 CodeMirror 内部状态归属。本文基于鸿蒙 PC Markdown 编辑器 OhMarkdown说明如何为每个 session保存完整EditorState如何同时隔离撤销栈、基线、revision 与恢复版本以及切换时为什么必须清理延迟任务。完整代码位于 https://gitcode.com/VON-/codex_md_oh本文对应提交3a9146e。EditorState 才是编辑会话CodeMirror 6 将正文、选择区、扩展状态和撤销历史组织在EditorState中EditorView负责把状态渲染到 DOM。多标签可以为每个文档创建一个独立 View但多个 DOM 编辑器会增加内存和布局成本。OhMarkdown 复用单个EditorView切换其 state。如果切换时只调用setDocument(content)创建新 state正文虽然正确旧标签的光标、选择和撤销历史会丢失。切回后只能得到一份新编辑器。真正会话快照需要保留interfaceEditorSessionSnapshot{state:EditorState;baselineDocument:Text;forcedDirty:boolean;pendingDirty:boolean;documentRevision:number;lastRecoveryRevision:number;}consteditorSessionsnewMapstring,EditorSessionSnapshot();state包含 CodeMirror 文档与历史其余字段是应用建立在编辑器之上的业务状态。它们必须与 state 同时切换不能继续作为全局状态留在上一文档。Map 的键是原生层提供的稳定sessionId。不能用文件名因为两个未命名文档都叫Untitled.md不能用正文哈希因为两份文件内容可能相同不能用 URI因为新文档还没有 URI。sessionId 表达的是编辑会话身份与外部文件身份分开。为什么保存的是 EditorState 引用会话持久化函数直接保存当前 statefunctionpersistActiveSession():void{editorSessions.set(activeSessionId,{state:editor.state,baselineDocument,forcedDirty,pendingDirty,documentRevision,lastRecoveryRevision});}CodeMirror state 是不可变值。一次 transaction 不会原地修改旧 state而是产生新 state 并由 view持有。Map 中保存旧会话 state 后切到其他会话继续编辑不会修改它。切回时可以直接editor.setState(storedSession.state)。不可变结构还使用持久化文本树保存 state 引用不等于每次复制全文字符串。历史中不同版本可以共享未变化结构比手工深拷贝正文和选区更适合编辑器。不过撤销历史仍会占用内存十二标签上限是当前保护措施。长期需要通过真实文档监控每会话历史成本。Map 只在 Web 进程内有效不是跨进程恢复。系统回收 ArkWeb 后原生层可以用session.content重建正文但光标和撤销历史会丢失。跨进程会话恢复需要显式序列化可恢复字段不能把内存 Map误称为持久化。保存基线不能只用 dirty 布尔值编辑器需要判断当前正文是否等于最近保存版本。OhMarkdown 保留 CodeMirrorText基线letbaselineDocument:Text;EditorView.updateListener.of((update){if(!update.docChanged){return;}documentRevision1;pendingDirtyforcedDirty||!update.state.doc.eq(baselineDocument);// 省略预览、通知和恢复})用户输入后 dirty 为 true撤销回与保存基线完全相同的 Text 时eq返回 truedirty 自动恢复为 false。若只在第一次输入时把布尔值设成 true撤销回原文后标签仍带星号关闭还会多余询问。每个会话必须保存自己的 baseline。甲保存后基线是甲正文乙可能从未保存。切换只恢复 EditorState、不恢复 baseline乙的内容会与甲基线比较脏状态随机变化。Text.eq比把两边sliceDoc()成完整字符串后比较更符合 CodeMirror 数据结构也避免每次按键都生成两份大字符串。大文档性能尤其依赖这种结构化比较。forcedDirty 处理恢复文档崩溃恢复内容可能恰好与创建 state 时的正文相同但它并未写回用户文件必须显示为未保存。重置函数设置baselineDocumenteditor.state.doc;forcedDirtyrecovered;pendingDirtyrecovered;普通打开文档recoveredfalse创建后的正文就是磁盘基线。恢复记录recoveredtrue虽然editor.state.doc.eq(baselineDocument)为 trueforcedDirty仍让 dirty 保持 true直到显式保存。这是“内容相等”与“持久化事实”分离的例子。恢复正文与某个内存基线相等不代表外部文件已经包含它。forcedDirty 只在保存成功后清除也要随会话快照切换。否则从恢复标签切到普通标签再返回星号可能消失。revision 与恢复版本属于会话每次文档 transaction 递增documentRevisiondocumentRevision1;pendingDirtyforcedDirty||!update.state.doc.eq(baselineDocument);scheduleRecoverySnapshot();恢复快照只在 dirty、非大文档且 revision 与上次快照不同的情况下发送functionflushRecoverySnapshot():void{window.clearTimeout(recoveryTimer);recoveryTimerundefined;if(!pendingDirty||largeDocumentMode||documentRevisionlastRecoveryRevision){return;}window.ohMarkdownBridge?.onSnapshot(editor.state.sliceDoc(),documentRevision);lastRecoveryRevisiondocumentRevision;}甲可能 revision 为 15乙为 2。若切换只换正文而不恢复计数乙的下一次快照可能错误继承 15或者与甲的lastRecoveryRevision相等而被跳过。两个数字必须进入 snapshot。revision 目前是会话内单调计数不跨进程。恢复记录带 revision主要用于当前会话写入排序和保存竞争不是全局文档版本。未来多会话恢复需要把 sessionId、URI 和 revision一起作为记录身份。创建新 state 必须隔离旧历史打开一个完全不同的文档时程序使用createEditorState(content)而不是对当前 state dispatch 一个“替换全文”事务functionresetEditorDocument(content:string,recovered:boolean):void{window.clearTimeout(bridgeTimer);window.clearTimeout(recoveryTimer);recoveryTimerundefined;pendingNativeChangefalse;isApplyingNativeDocumenttrue;editor.setState(createEditorState(content));isApplyingNativeDocumentfalse;// 初始化基线和版本}若把新文件作为一次全文 replacement dispatch撤销可能恢复上一份文件。即使两个文件内容恰好相同旧重做历史也可能在新文件中生效。新建 EditorState 从结构上切断历史是文档身份变更应有的语义。自动化专门验证了两种情况加载不同内容后第一次撤销只撤销新文件内刚输入的内容第二次撤销返回 false加载相同内容的新文件后旧文档遗留的重做也必须返回 false。相同字符串不能让编辑器误以为还是同一会话。isApplyingNativeDocument抑制程序设置 state引发的业务通知。虽然setState本身不是普通用户 transaction扩展和后续演进仍可能触发监听明确来源标记可以防止切换被计入 revision 或 dirty。活动会话切换顺序切换函数首先持久化当前快照functionactivateSession(sessionId:string,content:string,recovered:booleanfalse):void{if(sessionIdactiveSessionId){return;}persistActiveSession();window.clearTimeout(bridgeTimer);window.clearTimeout(recoveryTimer);recoveryTimerundefined;pendingNativeChangefalse;activeSessionIdsessionId;conststoredSessioneditorSessions.get(sessionId);if(!storedSession){resetEditorDocument(content,recovered);return;}// 恢复快照}先保存、后改变 activeSessionId 是关键。如果先改 idpersistActiveSession会把甲 state写到乙键下两个会话立即混乱。清理bridgeTimer防止甲延迟的 onChange 在乙激活后发给原生层。清理recoveryTimer防止甲快照以乙身份写入。pendingNativeChangefalse丢弃旧会话尚未发出的状态通知甲的正文已经由原生切换前捕获并且完整 state保存在 Map不需要把过期通知发送给当前页面。如果目标没有 Web 快照说明它是原生新建、刚打开或 ArkWeb 状态已丢失使用传入 content 建立 state。已有快照则恢复isApplyingNativeDocumenttrue;editor.setState(storedSession.state);isApplyingNativeDocumentfalse;baselineDocumentstoredSession.baselineDocument;forcedDirtystoredSession.forcedDirty;pendingDirtystoredSession.pendingDirty;documentRevisionstoredSession.documentRevision;lastRecoveryRevisionstoredSession.lastRecoveryRevision;pendingSaveDocumentundefined;pendingSaveDocument不随会话恢复。它只属于当前正在进行的保存命令快照而原生层通过操作互斥防止保存期间切换。恢复一个旧 pending 保存引用可能把另一个会话基线标记错误因此明确清空。恢复后主动同步原生状态设置 state后Web 重新计算大文档模式、预览和字数并主动通知原生constwordCountlargeDocumentMode?-1:countWords(editor.state.sliceDoc());window.ohMarkdownBridge?.onState(wordCount);window.ohMarkdownBridge?.onChange(wordCount,pendingDirty);if(pendingDirty){scheduleRecoverySnapshot();}原生标签栏和状态栏不能只靠之前缓存。恢复通知让当前 wordCount、dirty 与实际 EditorState 对齐。如果目标会话为脏重新安排它自己的恢复快照干净会话不写。大文档用-1表示跳过字数统计原生状态栏显示保护模式。每个会话的内容长度不同切换后必须重新计算不能把大文档模式当全局开关。预览也根据当前模式更新。纯源码只标记 dirty分栏或预览立即渲染目标正文。若不处理标签已切换右侧还可能短暂显示上一文档。保存基线与异步输入用户请求保存时Web 先保存当前 Text引用requestCommand:(command){flushToNative();if(commandsave){flushRecoverySnapshot();pendingSaveDocumenteditor.state.doc;}constcontentcommandsave?editor.state.sliceDoc():;window.ohMarkdownBridge?.onCommand(command,content);}原生写入成功后调用markSavedmarkSaved:(){baselineDocumentpendingSaveDocument??editor.state.doc;pendingSaveDocumentundefined;forcedDirtyfalse;pendingDirty!editor.state.doc.eq(baselineDocument);window.clearTimeout(recoveryTimer);recoveryTimerundefined;persistActiveSession();}基线使用保存请求时的 Text而不是完成时的当前 state。如果磁盘写入期间用户继续输入新内容不等于 pendingSaveDocumentdirty 继续为 true。若用完成时editor.state.doc作为基线会错误认为尚未写入的新输入已保存。清除 forcedDirty 表示恢复内容已经成功写入。重新比较当前 state后决定是否仍脏再把结果持久化到会话 Map。保存与切换交错由原生互斥阻止但 Web 仍保持版本正确性。关闭会话的内存清理关闭非活动 Web 会话时删除 Map 项functioncloseSession(sessionId:string):void{if(sessionId!activeSessionId){editorSessions.delete(sessionId);}}活动会话不能在仍由 EditorView 使用时直接从逻辑上清空原生层关闭活动标签时先激活后继再调用旧 id 的 closeSession此时旧会话已不活动可以删除。如果关闭后不删除十二标签虽然限制可见会话反复打开关闭仍会让 Map无限增长历史和 Text结构无法释放。内存生命周期必须与标签生命周期一致。最后一个标签关闭后原生层创建新空会话并激活再清理旧会话。Web始终有一个活动 id不进入无 state 特例。鸿蒙 PC 模拟器中的隔离结果下图来自 MateBook Pro 2in1 模拟器。两个未命名标签拥有独立正文活动会话显示Session-B另一个标签保持自己的状态。切换和撤销测试确认两个 EditorState历史不串联。截图只能展示正文与标签撤销历史必须用行为验证。自动化在 session-a输入“修改”session-b输入“新增”切回 a后撤销预期只得到“文档甲”再切回 b必须仍为“文档乙新增”。host.OhMarkdownEditor.activateSession(session-a,文档甲);// 输入“修改”host.OhMarkdownEditor.activateSession(session-b,文档乙);// 输入“新增”host.OhMarkdownEditor.activateSession(session-a,文档甲);expect(host.OhMarkdownEditor.undo()).toBe(true);expect(host.OhMarkdownEditor.getDocument()).toBe(文档甲);还应增加选区、滚动、重做、搜索选择、保存基线和恢复 revision 的逐项隔离断言。当前 EditorState天然保留选区与历史但产品验收不能只依赖库设计推断。内存与性能边界单 View多 state避免同时渲染多个 CodeMirror DOM但所有打开会话的 Text和历史仍驻留内存。十二个十兆文档即使结构高效也可能超过桌面模拟器和真机可接受范围。当前单文档五兆以上进入最小扩展模式不代表多标签总量已有控制。后续可以统计每会话内容长度限制总打开字节后台会话可以截断撤销历史或序列化正文后释放 EditorState激活时重建最近使用的少量会话保留完整状态。冻结策略必须告知用户撤销历史是否会丢并在后台脏会话释放前完成恢复快照。切换性能也要测量editor.setState、预览渲染、字数统计和 Bridge全文捕获都会随文档变大。Map 查找是常数时间不代表完整切换是常数成本。真实指标应从点击标签到光标可输入并分别记录源码、分栏和大文档模式。当前边界会话隔离目前存在于进程内。应用重启后不会恢复全部标签、光标、选区和撤销历史崩溃恢复重点仍是活动文档内容。固定标签、标签排序、跨窗口和会话持久化需要更高层模型。CodeMirror state与原生 DocumentSession是两份互补状态也存在一致性成本。正文切换前由原生主动捕获Web切换后主动回报 dirty和字数。未来可以给 Bridge消息增加 sessionId避免所有回调隐含作用于当前活动会话进一步降低异步误归属。结语多标签编辑内核的正确单位不是字符串而是完整会话。OhMarkdown 为每个 session保存 CodeMirror EditorState、保存基线、恢复脏标记、revision和快照版本切换前保存当前状态并取消旧定时器切换后恢复目标 state并重新同步原生保存使用请求时 Text作为基线关闭删除不再使用的快照。这些细节让正文、撤销、恢复和保存都归属于正确标签。鸿蒙 PC 编辑器要承担长期写作用户不应感知 ArkUI 与 ArkWeb 的边界更不应承担跨边界状态串联的后果。EditorState 会话隔离就是把这种技术复杂度留在应用内部的基础。

相关新闻

前端 Prompt 工程化:模板版本管理与 A/B 评测的工程闭环

前端 Prompt 工程化:模板版本管理与 A/B 评测的工程闭环

前端 Prompt 工程化:模板版本管理与 A/B 评测的工程闭环 一、Prompt 散落在组件里的代价:从硬编码到工程化治理 大模型应用前端落地时,Prompt 通常经历三个阶段。初期直接写在组件里,一个字符串搞定。中期抽到常量文件&#xff0c…

2026/7/26 1:13:00 阅读更多 →
Monorepo 构建缓存治理:pnpm 与 Turbo 的增量管线实践

Monorepo 构建缓存治理:pnpm 与 Turbo 的增量管线实践

Monorepo 构建缓存治理:pnpm 与 Turbo 的增量管线实践 一、规模膨胀下的构建债务:从分钟级到秒级的 CI 攻坚 当一个前端仓库从 5 个包增长到 80 个包,CI 构建时间会从 40 秒膨胀到 12 分钟。这不是线性增长,而是指数级恶化。根因在…

2026/7/26 1:13:00 阅读更多 →
历史事件记忆失效的5个隐性信号,AI脑波反馈系统已识别其中3种——错过本次更新将影响2025高考历史得分率

历史事件记忆失效的5个隐性信号,AI脑波反馈系统已识别其中3种——错过本次更新将影响2025高考历史得分率

更多请点击: https://intelliparadigm.com 第一章:历史事件记忆失效的神经认知机制解析 人类对历史事件的记忆并非静态存储,而是一个动态重构过程,其失效往往源于海马体与前额叶皮层协同功能的阶段性衰退。当编码阶段缺乏显著情绪…

2026/7/26 1:13:00 阅读更多 →

最新新闻

如何快速打造个性化AI桌面宠物:开源框架DyberPet完整指南

如何快速打造个性化AI桌面宠物:开源框架DyberPet完整指南

如何快速打造个性化AI桌面宠物:开源框架DyberPet完整指南 【免费下载链接】DyberPet Desktop Cyber Pet Framework based on PySide6 项目地址: https://gitcode.com/GitHub_Trending/dy/DyberPet 厌倦了单调的电脑桌面?想要一个能陪伴工作学习、…

2026/7/26 1:23:03 阅读更多 →
医疗系统集成UEditor与OCR实现高效病历录入

医疗系统集成UEditor与OCR实现高效病历录入

1. 医疗系统集成UEditor截图OCR识别的必要性在医疗信息化系统中,医生工作站、电子病历等模块经常需要处理大量影像资料。传统的手动录入方式效率低下,而直接复制粘贴又无法提取图片中的关键信息。UEditor作为国内广泛使用的富文本编辑器,其截…

2026/7/26 1:23:03 阅读更多 →
Unity卡牌游戏UI框架设计:MVC与事件驱动架构实战

Unity卡牌游戏UI框架设计:MVC与事件驱动架构实战

1. 项目概述:为什么需要一个专业的卡牌UI框架?如果你正在开发一款卡牌游戏,无论是集换式卡牌、策略卡牌还是RPG卡牌,你大概率会遇到一个共同的痛点:UI界面越做越乱。初期,你可能只是简单地拖几个按钮和图片…

2026/7/26 1:23:03 阅读更多 →
工业视觉检测系统:AI质检在制造业的应用与突破

工业视觉检测系统:AI质检在制造业的应用与突破

1. 项目背景与行业痛点鲁威制造作为典型的传统工业企业,在产品质量检测环节长期面临人工目检效率低、漏检率高的问题。传统质检方式存在三个致命短板:人力成本持续攀升:每条产线需要配置6-8名质检员三班倒,年人力成本超过200万元检…

2026/7/26 1:23:03 阅读更多 →
基于YOLOv5的双向人流计数系统设计与优化

基于YOLOv5的双向人流计数系统设计与优化

1. 项目背景与核心价值在零售门店、公共交通枢纽、展览场馆等场景中,准确统计人员进出流量是运营管理的基础需求。传统红外对射或闸机计数方式存在安装复杂、易受干扰、无法区分进出方向等问题。我们团队基于YOLOv系列算法开发的这套双向计数系统,通过普…

2026/7/26 1:23:03 阅读更多 →
Obsidian笔记导出神器:如何让知识库在不同平台间自由迁移?

Obsidian笔记导出神器:如何让知识库在不同平台间自由迁移?

Obsidian笔记导出神器:如何让知识库在不同平台间自由迁移? 【免费下载链接】obsidian-export Rust library and CLI to export an Obsidian vault to regular Markdown 项目地址: https://gitcode.com/gh_mirrors/ob/obsidian-export 你是否曾经遇…

2026/7/26 1:22:03 阅读更多 →

日新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/26 0:00:31 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/26 0:00:31 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/26 0:00:31 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/26 0:00:31 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/26 0:00:31 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/26 0:00:31 阅读更多 →

月新闻