
在语言服务器中支持 NotebookPyrefly 对 Jupyter Notebook 的 LSP 3.17 实现解析【免费下载链接】pyreflyA fast type checker and language server for Python项目地址: https://gitcode.com/GitHub_Trending/py/pyreflyJupyter Notebook 已成为 Python 数据科学、机器学习与快速原型开发的主流交互载体但语言服务器Language Server长期以来以普通源文件为设计前提Notebook 一度沦为 IDE 功能的二等公民。本文以 Pyrefly 的 notebook 支持为主线梳理 LSP 3.17 之前编辑器侧的 proxy 拼接方案、LSP 3.17 引入的原生notebookDocument/*同步协议、file-based 与 cell-based 两种内部表示选型并结合 Pyrefly 仓库源码notebook.rs、LSP notebook 协议层、同步测试拆解其实现与取舍为其他语言服务器作者提供可直接借鉴的架构参考。Notebook 的交互范式与 IDE 支持的历史缺口Jupyter Notebook 之所以成为数据科学家的主战场核心在于其基于单元格cell的交互式工作流改动代码中的一小部分就能立刻内联看到更新后的输出而无需等待整个程序运行。快速原型、数据探索与科学计算恰好都属于局部修改、即时反馈的典型场景因此 Notebook 也成为 Positron 等面向数据科学的新型 IDE 主打的交互方式。然而IDE 能力的历史欠账同样明显实现 go-to-definition、hover、diagnostics 等功能的语言服务器是基于 Language Server Protocol 构建的而该协议诞生之初只考虑普通源文件——直到协议发布五年后才加入 Notebook 同步方法。默认的 Jupyter Notebook 体验长期缺少上述大部分 IDE 特性。LSP 3.17 之前编辑器侧的 proxy 拼接方案在 LSP 3.172022 年发布之前协议仅支持普通源文件。编辑器必须编写自定义适配器才能让语言服务器与 Notebook 协同工作。最通用的做法是proxy 层方案JupyterLab 的 LSP 扩展与 Meta 内部的 Bento Notebooks 均采用此路线proxy 将 Notebook 中所有单元格拼接成一个虚拟文档把这个拼接后的视图交给语言服务器请求到达服务器前将相对于单元格的位置映射为拼接文件中的位置响应返回编辑器前再将位置反向映射回对应单元格。对语言服务器而言它分析的不过是一个普通文件。这种方案可行但把全部复杂度都推给了编辑器或中间层每个想支持 Notebook 的编辑器都必须独立实现 cell 拼接与位置映射逻辑。它的优势在于与语言无关且在 LSP 尚未原生支持时能让语言服务器以较低成本用上 Notebook。LSP 3.17把 Notebook 意识下沉到协议本身LSP 3.17 的 Notebook Document Synchronization 引入了四个新操作notebookDocument/didOpennotebookDocument/didChangenotebookDocument/didSavenotebookDocument/didClose与常规textDocument操作的关键区别在于内容的表示方式notebookDocument/didOpen不再发送单个文件的全部文本而是把每个单元格作为独立文档分别发送并由编辑器为每个单元格分配唯一 URInotebookDocument/didChange则编码了单元格的增删信息以及单元格内部的内容变更。把 Notebook 认知移入协议本身后规范允许语言服务器直接处理 Notebook 语义而不再依赖每个编辑器各自实现 proxy 层。两种内部表示file-based 与 cell-basedLSP 规范并未规定语言服务器内部应如何表示 Notebook。对于一个已能处理普通文件的语言服务器扩展出 Notebook 支持至少有两条路线file-based基于文件把 proxy 逻辑直接构建进语言服务器——将所有单元格内容拼接成一个虚拟文件并维护每个单元格到对应行的映射。核心分析逻辑保持不变只需在请求进入和响应返回时做位置换算。cell-based基于单元格把每个单元格视为一个独立文件并携带单元格顺序信息。每个单元格隐式导入前面所有单元格中定义的符号从而允许各单元格被独立分析。两条路线的本质区别在于谁来承担 cell 到文件的位置映射file-based 由语言服务器内置完成cell-based 则是协议原生思路的自然延伸。Pyrefly 的落地file-based 的侵入式改动最小化Pyrefly 的初始实现选择了file-based 方案理由很直接改动侵入性最小。一切行为与普通文件完全一致只需在每次操作前后于单元格与拼接源码之间映射位置。这与 jedi-language-server、Ruff 处理 Notebook 的方式类似。这一选择并非不可更改团队明确保留了未来重新评估的空间。对比两种方案的效率差异cell-based当某个单元格变化时只有在其导出类型发生变化时才需要重查后续单元格前面的单元格完全无需重查file-based任何单元格变化都会触发整个 Notebook 重新检查当客户端仅请求单个单元格的诊断时服务器仍须取回整个 Notebook 的诊断结果再过滤出目标单元格。不过在实践中这些额外开销对 Pyrefly 并未构成问题尤其是在数据可缓存的前提下——普通 Notebook 的规模远小于 Pyrefly 设计要处理的项目量级。源码佐证从LspNotebook到拼接源码仓库中的实现细节印证了上述设计。核心状态结构位于 pyrefly/lib/state/notebook.rsLspNotebook持有一个ruff_notebook::Notebook负责将单元格源码拼接为整体源码一个NotebookDocument记录 LSP 3.17 的单元格元数据以及两张映射表cell_url_to_index从单元格 URL 到索引的反向查找cell_index_to_url从索引到单元格 URL 的正向查找。get_code_cell_index负责把全部单元格中的索引换算为代码单元格中的索引与Notebook::cell_offsets()的索引语义对齐并过滤掉 Markdown 等非代码单元格get_cell_contents则按单元格 URL 取回对应源码。文件头部注释明确写着Based on LSP 3.17.0 specification。在服务端调度层面pyrefly/lib/lsp/non_wasm/server.rs 通过open_notebook_cells映射维护已打开单元格 URI → 所属 Notebook 路径从而把任意格式的单元格 URI 关联回 Notebook 内容队列层 将DidOpenNotebookDocument、DidChangeNotebookDocument、DidSaveNotebookDocument、DidCloseNotebookDocument四种事件统一并入 LSP 事件枚举。位置换算的典型例子出现在 selection ranges 处理中若 URI 对应某个打开的 Notebook 单元格则通过notebook.cell_offsets().content_ranges().nth(cell)取出该单元格在拼接源码中的行区间再基于此做节点定位与范围裁剪。LSP 协议层逐字段建模notebookDocument/*协议层的完整建模位于 pyrefly/lib/lsp/wasm/notebook.rs覆盖了 LSP 3.17 Notebook 同步所需的全部类型核心类型NotebookDocument含uri、notebook_type、version、metadata、cells、NotebookCellkind、document、metadata、execution_summary、NotebookCellKindMarkup 1/Code 2、ExecutionSummaryexecution_order、success过滤器NotebookCellTextDocumentFilter、NotebookDocumentFilter可按notebook_type、scheme、pattern匹配变更事件NotebookCellArrayChangestart/delete_count/cells、NotebookCellStructureChange含伴随的did_open/did_close、NotebookCellTextContentChange、NotebookCellChanges、NotebookDocumentChangeEvent通知参数与定义DidOpenNotebookDocumentParams、DidChangeNotebookDocumentParams、DidSaveNotebookDocumentParams、DidCloseNotebookDocumentParams并分别实现了对应NotificationMETHOD 分别为notebookDocument/didOpen等四个字符串。其中NotebookDocument::to_ruff_notebook值得注意它把 LSP 的单元格结构转换为 ruff 的 Notebook 表示同时从 cell 元数据中提取id与execution_count并将 Markdown 与 Code 单元格分别映射为MarkdownCell/CodeCell。测试验证同步、诊断与跨单元格类型解析pyrefly/lib/test/lsp/lsp_interaction/notebook_sync.rs 提供了完整的端到端验证覆盖以下场景打开与诊断发布test_notebook_did_open打开含 3 个代码单元格的 Notebook验证第三个单元格中的z: str ; z 1能发布bad-assignment诊断且单元格内的行/列位置line 1, character 4-5被正确换算内容变更test_notebook_did_change_cell_contents通过notebookDocument/didChange的textContent修改单元格文本修复类型错误后诊断清零单元格增删test_notebook_did_change_delete_cell、test_notebook_did_change_add_cell验证结构变更structure.array.start/deleteCount/cells以及伴随的didOpen/didClose的同步正确性包括删除 100 个单元格不应崩溃的健壮性测试单元格交换test_notebook_did_change_swap_cells先把 cell2 删除再在位置 0 插入验证x 1定义被移走后新位置的z x触发unbound-name诊断证明跨单元格符号可见性与顺序相关回归测试test_unsaved_notebook_did_open验证untitled:scheme 的未保存 Notebook 可被正常接收并检查此前url.to_file_path()不支持untitled:而失败自定义 scheme以quarto-cells:scheme 打开.qmd文档、以marimo-notebook类型打开.py文件验证 Pyrefly 不仅处理 Jupyter.ipynb也能处理其他 Notebook 格式的投影单元格包括跨单元格名称解析与兄弟模块导入解析。命令行检查路径pyrefly check对.ipynb的支持Notebook 支持不止于 LSP。仓库的 test/notebooks.md 记录了 CLI 侧的行为pyrefly check notebook.ipynb会按 Jupyter 规范解析 JSON非法 JSON 报Expected a Jupyter Notebook, which must be internally stored as JSON缺少cells字段报 schema 错误并在单元格粒度报告诊断如notebook.ipynb#2:1:11表示第二个单元格中的错误位置。有意思的细节是Notebook 支持顶层await、async with、async for普通.py文件中这些写法会被判为invalid-syntax%matplotlib inline等 cell magic 指令可被正常识别以#!/usr/bin/env -S notebookrunner --kernel default开头的.py文件会被当作 Notebook 处理同样允许顶层 await# type: ignore抑制在 Notebook 单元格中同样生效。编辑器侧配置VS Code 扩展如何声明 Notebook 支持Pyrefly 的 VS Code 扩展在 lsp/src/extension.ts 中注册语言客户端documentSelector同时包含file、untitled、vscode-notebook-cell、inmemory四种 scheme 的 Python 文档覆盖已保存文件、未保存文件、Notebook 单元格与 Positron Console 等内存文档notebookDocumentSync.notebookSelector使用{ notebook: *, cells: [{ language: python }] }即匹配任意 Notebook 类型中的 Python 单元格——这也从配置层面印证了前文格式无关的设计意图。服务端侧server.rs 从初始化参数中读取syncNotebooks默认true开启NotebookDocumentSyncOptions并按notebookType jupyter-notebook等条件注册单元格选择器。其他 Notebook 格式同一个问题的不同载体Jupyter 的.ipynb包含 cells、outputs、metadata 的 JSON 文件已不再是唯一选择Quarto使用内嵌代码块的 Markdown 文件Marimo把 Notebook 存为纯 Python 文件单元格即一个个函数。这些格式对版本控制更友好也更贴合标准开发工具链。但有趣的是它们在 IDE 功能上面临同样的根本性挑战其编辑器扩展底层仍需要把单元格拼接成连贯视图交给语言服务器与前述 legacy proxy 方案如出一辙。无论由编辑器还是语言服务器承担 cell 到文件的映射总得有人去做这件事——本文讨论的架构取舍对这些格式同样适用。此外Python 并非 Notebook 中唯一的语言Julia、R 同样重度依赖交互式环境其他 Notebook 环境还支持 SQL、JavaScript 等。只要你拥有某一语言的可用语言服务器并希望为其增加 Notebook 支持前述两种方案都可直接套用。试用与反馈Pyrefly 在 v0.41.0 起内置了 Notebook 支持目前可在任何支持 LSP 的编辑器中体验包括 VS Code、Positron、JupyterLab 与 Marimo。仓库中的 conformance、test/notebooks.md 与 notebook 系列 LSP 测试 可作为进一步探索的入口前者覆盖 CLI 行为后者覆盖协议交互细节。小结从编辑器 proxy 拼接到 LSP 3.17 的原生notebookDocument/*同步再到 Pyrefly 内部 file-based 表示与逐字段协议建模Notebook 的 IDE 支持走过了一条复杂度在编辑器与语言服务器之间迁移的演进路径。对语言服务器作者而言核心经验有三条优先选择侵入性最小的表示方案如 file-based快速落地把位置映射与协议建模做扎实单元格 URI → 索引 → 拼接源码行区间用端到端测试覆盖单元格增删、交换、内容变更与自定义 scheme确保格式无关的承诺不流于口号。而无论是 Jupyter、Quarto 还是 Marimo那句总得有人把单元格拼起来的判断正是本文所有架构讨论的出发点。【免费下载链接】pyreflyA fast type checker and language server for Python项目地址: https://gitcode.com/GitHub_Trending/py/pyrefly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考