
OpenPLC 编辑器内嵌了 Monaco Editor——即驱动 VS Code 的同一引擎——作为其文本代码编辑界面支持四种 IEC 61131-3 及扩展语言结构化文本 (ST)、指令表 (IL)、Python和C/C。除了基础编辑功能外该集成还将自定义 Monarch 分词器、上下文感知补全提供程序、调试时的内联值装饰、AI 辅助的内联补全、差异审查、拖放 POU 插入、Python LSP 和主题同步整合到一个 React 组件中该组件充当应用程序内所有文本 PLC 编程的中枢神经系统。架构概述Monaco 集成被组织为编辑器域下的一个独立功能模块。每个关注点——语言定义、主题、补全、拖放、AI 和 LSP——都位于monaco/目录中顶层index.tsx通过连接到 Zustand 存储的 React 副作用和回调将它们全部协调起来。该组件的属性被刻意精简——只有path、name和language——因为所有丰富的上下文POU 数据、变量、调试状态、设备信息都是通过 Zustand 存储和平台提供程序钩子流入的。这种设计保持了组件公共接口的稳定同时其内部数据订阅可以独立演进。语言注册管道Monaco 本身并不附带 IEC 61131-3 支持因此 OpenPLC 使用 Monaco 的 Monarch 分词器系统将ST和IL注册为自定义语言。一个共享的registerLanguage工具负责处理这两步过程首先调用monaco.languages.register()声明语言标识然后通过setLanguageConfiguration和setMonarchTokensProvider绑定LanguageConfiguration括号、自动闭合对、缩进规则和IMonarchLanguage分词器规则。注册管道在模块加载时即刻执行——index.tsx顶部的import ./configs会在任何组件渲染之前触发所有语言注册。这确保了在编辑器挂载的瞬间Monaco 的语言服务就已经可用。语言文件扩展名MIME 类型分词器类型结构化文本 (ST).sttext/structured-textMonarch (自定义)指令表 (IL).iltext/instruction-listMonarch (自定义)Python.py内置Monaco 内置 代码片段C/C.cpp内置Monaco 内置 自定义补全结构化文本分词器深入剖析ST 语言定义是项目中最全面的自定义分词器。它声明了超过 280 个关键字涵盖结构声明PROGRAM、FUNCTION_BLOCK、CLASS、变量作用域VAR_INPUT、VAR_OUTPUT、VAR_IN_OUT、IEC 61131-3 原始类型BOOL、DINT、LREAL、泛型类型说明符ANY_NUM、ANY_BIT、访问修饰符PUBLIC、OVERRIDE、SFC 元素STEP、TRANSITION、ACTION、标准功能块TON、CTU、RS以及跨越数学、类型转换、字符串操作和位操作的 70 多个内置函数。Monarch 分词器使用了一个状态机为块注释commentBlock、commentBlockC、文档注释变体commentBlockDoc、commentBlockCDoc、单引号字符串stringSingle、类型化数字字面量typedNumber和编译指示/属性attribute设置了专用状态。分词优先级经过精心排序带标签的文档注释//TODO:、(*FIXME:在常规注释之前匹配函数调用在常规标识符解析之前匹配而成员访问点则拥有自己的delimiter.accessortoken 类。分词器通过 Monarch 的cases指令利用优先级级联来解决标识符歧义localVariables→keywords→builtinFunctions→operators→standardFBVariables→customDataTypeVariables→TRUE|FALSE→default。这意味着动态注入的局部变量名始终优先于关键字从而能够正确高亮可能与语言关键字同名的用户自定义名称。动态分词器更新ST 分词器并非静态——有三个导出函数会在运行时修改它以反映当前的项目状态updateLocalVariablesInTokenizer(variableNames)— 将当前 POU 的变量名注入到localVariables分词器数组中使它们作为keywordtoken 高亮显示区别于普通标识符。updateDataTypeVariablesInTokenizer(customDataTypes)— 从结构化数据类型中提取字段名并将它们推入customDataTypeVariables从而能够正确高亮结构体成员访问模式。updateEnumValuesInTokenizer(customDataTypes)— 从枚举数据类型中提取枚举值名称并在分词器root状态的最前面添加一条正则表达式规则确保像Red、Running或Error这样的枚举字面量获得keyword高亮。每个修改函数都会调用monaco.languages.setMonarchTokensProvider(st, language)以重新注册分词器使 Monaco 在下一个空闲帧重新对所有可见的 ST 模型进行分词。补全提供程序架构Monaco 编辑器注册了特定语言的补全提供程序这些程序聚合来自多个领域来源的建议。由于 ST/IL 和 C/C 在 OpenPLC 生态系统中本质上的不同两者的架构也有所差异。ST/IL 补全管道对于 ST 和 IL注册了一个单一的registerCompletionItemProvider以.作为触发字符启用功能块成员的点访问补全。当提供程序触发时它会聚合来自七个不同来源的建议按标签去重并返回合并后的列表优先级提供程序来源数据触发条件1fbSuggestions来自pous的自定义功能块始终2dataTypeSuggestions来自dataTypes的自定义数据类型始终3snippetsSTSuggestions预定义的 ST/IL 代码片段始终4variablesSuggestions当前 POU 的局部变量始终5globalVariablesSuggestions项目全局变量始终6librarySuggestions已加载的库定义始终7keywordsSuggestionsIEC 61131-3 关键字始终8identifiersSuggestions源码中的自由标识符回退点访问检测使用对(\w)\.$的正则表达式匹配——如果前面的标识符解析为具有原始类型的变量对照baseTypeSchema.options检查提供程序将返回空列表以抑制不相关的成员建议。对于非原始类型功能块、结构体功能块和数据类型提供程序会提供适当的成员补全。C/C 补全管道C/C 补全结合了 Monaco 内置的 C 智能与 OpenPLC 特有的添加项。自定义提供程序分四个来源叠加标准库补全常见 C/C 头文件和函数、代码片段补全常见模式、Arduino API 补全当目标设备为 Arduino 开发板时有条件激活以及解析的局部变量通过基于正则表达式的声明解析从当前源码中提取。parseCppVariables函数使用两种正则表达式模式扫描整个编辑器内容一种类型声明模式匹配int、float、uint8_t、String及其他常见 C 类型另一种参数列表模式提取函数参数。这提供了轻量级的变量感知能力而无需完整的 C 解析器。编辑器生命周期与状态同步编辑器的生命周期遵循由monaco-editor/react包装器协调的精确顺序handleEditorBeforeMount→ 模型创建 →handleEditorDidMount。状态在 Zustand 存储和 Monaco 模型之间双向流动。挂载序列模型-存储同步组件维护着一个localText状态它作为编辑器内容的可信来源。存在两条同步路径存储 → 编辑器当pous在外部发生更改例如通过撤销/重做操作或文件重新加载时一个副作用会检测到不匹配并更新localText然后通过受控的value属性传播到编辑器。编辑器 → 存储onChange处理程序在所考察的范围内未显示但由架构隐含在存储上调用updatePou将文件标记为脏状态并通过pushToHistory推入撤销历史记录。isSyncingModelRef标志防止重入更新当 AI 差异审查以编程方式将编辑应用到模型时它会将该标志设置为true以便 onChange 处理程序可以区分用户编辑和编程式编辑。调试内联值装饰当调试器处于活动状态且编辑器正在显示 ST 或 IL 代码时Monaco 会渲染内联值装饰——即出现在变量名之后的小注释显示其当前运行时值。这是图形编辑器中显示的值徽章在文本编辑器中的等效实现。装饰管道分两个阶段工作位置扫描debugVarPositionsmemo扫描模型的每一行使用一个跟踪多行块注释状态的有状态注释剥离器剥离行注释//和块注释(*...*)和/*...*/。对于每一行它将调试值映射中的变量名与剥离后的文本进行匹配记录列位置。重叠声明机制防止相同的字符范围被装饰两次。装饰应用副作用使用 Monaco 带有inlineClassName: debug-inline-value的after装饰选项将扫描到的位置转换为IModelDeltaDecoration对象这会将值文本渲染为跟随在变量名之后的 CSS 样式内联注释。对于功能块 POU前缀从${name}:变为${programName}:${fbVariableName}.反映了调试协议中使用的限定变量路径。这意味着在调试Main程序中名为motor1的MotorFB 实例时调试值的键名将是Main:motor1.Speed、Main:motor1.Running等。fbInstanceContextmemo 从fbSelectedInstance和fbDebugInstances存储 slice 中解析当前选定的 FB 实例。当调试器可见时编辑器还会进入只读模式以防止在调试会话期间意外修改代码。AI 集成与差异审查Monaco 编辑器通过两种互补机制集成 AI 辅助这两种机制都受hasAIAssistant平台能力和用户明确授权的管控内联补全通过aiPort.registerInlineCompletions()注册该函数接收 Monaco 实例、POU 名称和语言。AI 端口处理幽灵文本建议的整个生命周期而编辑器组件仅作为轻量级的协调器。差异审查当 AI 生成全量主体编辑时存储会接收一个pendingDiffs条目其中包含旧主体、新主体和计算出的差异块。编辑器使用renderDiffReview函数将这些差异块渲染为彩色装饰已接受与待定。用户可以与各个差异块进行交互——“保留”接受或“撤销”拒绝——并实现细粒度控制。拒绝一个差异块会通过applyAcceptedHunks仅应用保留的块来重建主体更新编辑器模型并针对新的基线重新计算剩余的差异块。editorInstanceId计数器解决了一个微妙的时序 bug在 Web 构建中monaco-editor/react会在选项卡切换时重新挂载底层编辑器由key{path}驱动。差异审查副作用需要重新附加到新的编辑器实例但副作用的依赖项更改会在新编辑器的onMount回调之前触发。通过在handleEditorDidMount中递增editorInstanceId副作用会在新编辑器准备就绪后重新运行。主题管理Monaco 的主题系统与应用程序的 CSS 主题解耦。通过ensureOpenplcThemes向 Monaco 注册了两个自定义的 OpenPLC 主题并且applyThemeNow会根据shouldUseDarkMode在它们之间切换。主题应用发生在三个时间点挂载前handleEditorBeforeMount将主题注册到 Monaco 的主题注册表中。挂载时handleEditorDidMount应用与当前存储状态匹配的初始主题。暗黑模式更改时副作用每当shouldUseDarkMode切换时重新应用主题。模块级的didApplyInitialTheme标志防止冗余的初始应用尽管当前代码在两个分支中都无条件应用了主题——这似乎是一种安全措施确保无论挂载顺序如何主题始终保持一致。文件监视与外部更改检测在具有hasFileWatcher能力的平台Electron 构建上编辑器会监视其当前 POU 文件的外部修改。当检测到外部更改时编辑器会检查文件是否处于已保存状态如果是它会从磁盘重新加载内容使用parseTextualPouFromString用于 ST/IL或parseHybridPouFromString用于 Python/C对其进行解析并更新本地文本状态和存储。此外在每次挂载时编辑器都会通过从磁盘读取文件并将其与当前存储值进行比较主动检查外部更改——这处理了在编辑器选项卡关闭时文件被外部修改的情况。拖放 POU 插入drag-and-drop/st.ts模块导出parsePouToStText该函数将 POU 结构包括其名称、类型、语言、主体、文档和变量转换为有效的 ST 文本。这支持了拖放操作即可将 POU 从项目树拖动并放入 ST 编辑器中自动在插入点格式化 POU 声明和主体。Python LSP 集成在具有hasPythonLSP能力的平台上编辑器会初始化 Python 语言服务器协议连接为 Python POU 提供实时诊断、悬停信息和转到定义功能。其生命周期为挂载时initPythonLSP(monacoInstance)启动 LSP 客户端然后setupPythonLSPForEditor(editorInstance)连接特定的编辑器实例。卸载时cleanupPythonLSP()关闭客户端。模板注入injectPythonTemplateIfNeeded确保 Python POU 具有所需的样板结构。在 Web 构建中没有hasPythonLSP仅运行模板注入——跳过 LSP 连接。能力管控摘要Monaco 编辑器的功能集根据声明的平台能力而有所不同。下表总结了哪些功能受管控及其方式功能能力管控不可用时的行为文件监视hasFileWatcher无外部更改检测Python LSPhasPythonLSP仅模板注入无实时诊断AI 内联补全hasAIAssistant无幽灵文本建议AI 差异审查hasAIAssistant 授权无差异装饰唯一模型 URIhasLocalFilesystem使用普通path代替${projectPath}${path}Arduino API 补全deviceBoard不包含 OpenPLC Runtime无 Arduino 特定建议