D0008: Language Server

Notist 的 LSP 是编辑器与 notist-analysis 之间的协议适配层。它负责文档同步、LSP position 转换、请求路由和结果发布,不负责重新实现 parser、模块解析、类型检查或求值语义。

Analyzer 的输入、revision 发布、不可变 WorkspaceSnapshot 与核心 query identity 由 vault::designs::D0011-analyzer-and-workspace-snapshot 定义;本文只说明 LSP 如何提供输入并消费这些查询。

Language server 作为现有 CLI 的子命令发布:

notist lsp

这样命令行、Zed、VS Code 和其他编辑器只需要安装一个版本一致的 notist 可执行文件。notist lsp 是跟随编辑器生命周期的协议 adapter;共享 VaultEngine、daemon discovery 与 embedded fallback 由 vault::designs::D0012-daemon-and-client-interfaces 定义。

Ownership

Editor
  -> LSP JSON-RPC
  -> notist-cli::lsp
       document lifecycle
       URI and Position conversion
       capability handlers
       diagnostics publishing
  -> local Notist client
  -> daemon or embedded service
  -> notist-analysis
       workspace snapshot
       module graph
       name and reference resolution
       completion / hover / navigation queries
  -> notist-syntax / notist-eval / notist-model

notist-analysis 必须保持编辑器和协议无关。它使用 PathBufModulePathTextRange 和自身的查询结果类型,不应暴露 lsp_types::Position、URI 或 CompletionItem。

LSP 层只做机械转换。例如 core query 返回一个目标 FileId 和 byte range,LSP handler 再将其转换为 Location。查询可以经 daemon 执行,也可以在 embedded service 中执行;两种形态使用相同的 analysis API。

Process and Transport

编辑器与 notist lsp 之间使用 stdio transport。server 的 stdin/stdout 专用于 LSP JSON-RPC:

  • stdout 不能输出日志、进度文字或 diagnostics。

  • 日志只能写入 stderr 或显式 log file。

  • 普通 CLI 的彩色输出在 LSP 模式下完全禁用。

  • adapter 生命周期跟随编辑器创建的进程;它按 canonical Vault root 通过本地 Notist protocol 连接该 Vault 的共享 daemon,或在 embedded mode 中为各 Vault 承载同一 core service。

Zed extension 等编辑器集成通过 language server adapter 从 PATH 查找 notist,并用参数 lsp 启动。自动下载和版本管理属于扩展分发问题,不进入 language server 核心。

Vault Discovery and Unsaved Documents

编辑器提供的 workspace root 只是 worktree,不一定是 vault。Notist.toml 所在目录才是显式 vault root;配置文件可以为空,只承担 marker 作用。

LSP 初始化时在 worktree 内发现所有 marker,为每个 root 连接独立的 Vault daemon,并在其 VaultEngine 上打开 Analyzer View:

editor worktree
  -> discover Notist.toml markers
  -> VaultState { root, daemon connection, ViewHandle }
  -> BTreeMap<VaultRoot, VaultState>

一个 source 归属于最近祖先 marker。嵌套 marker 会建立新的 vault,外层 Workspace 扫描到该目录时停止递归。这样同一个 source 不会同时进入两个 Module graph。

如果 worktree 中没有 marker,server 将 worktree root 作为单个 implicit vault,保持轻量用法兼容。有关 marker 与 CLI discovery 的完整规则见 vault::designs::D0007-notist-toml

每个 vault 的 Analyzer View 都接收属于自己的 open document overlays:

filesystem sources
       +
view overlays { path -> version + Arc<str> }
       |
       v
Workspace snapshot
  sources and parses
  module index
  resolved references
  diagnostics

规则如下:

  • 已打开文档的 overlay 优先于磁盘内容。

  • didChange 更新 overlay,不写入用户文件。

  • didSave 可以保留 overlay,直到收到保存后的最终文本或磁盘状态确认。

  • didClose 移除 overlay,并重新采用磁盘内容。

  • overlay 只进入 source 所属的最近 vault。

  • snapshot 内的 source、Parse、TextRange、reference 和 diagnostics 必须属于同一 revision。

  • definition、references、completion 和 Hover 不跨 vault 查询。

Analyzer View 可以全量重建 snapshot,也可以利用 VaultEngine cache 和依赖信息增量更新;这属于 analysis 优化,不能改变 overlay precedence 和 snapshot consistency。

Text Synchronization

Language server 声明 TextDocumentSyncKind::FULL,每次 didChange 接收完整文本。这样 document store 始终先形成完整 source,再交给 syntax 和 analysis,而不要求 parser 的增量能力与 LSP edit protocol 同步演进。

切换到 incremental sync 时,document store 先将 LSP range edit 应用到 source,再把新的完整 source 交给 syntax 和 analysis;parser 是否增量解析是内部优化,不影响 analysis API。

文件系统 watcher 只负责未在编辑器中打开的文件。若客户端已经为 workspace file events 提供可靠通知,可以优先使用 LSP notification;server 自身 watcher 作为客户端能力不足时的补充。

Position Encoding

Notist 内部的 TextRange 使用 UTF-8 byte offset,而 LSP Position 通常使用 UTF-16 code unit。两者不能直接互换。

analysis source 应附带统一的 LineIndex

byte offset
  <-> line number + UTF-8 column
  <-> LSP line + UTF-16 column

所有 diagnostics、definition、references、hover range 和 symbol range 都必须经过同一个转换实现。需要覆盖 ASCII、中文、emoji、组合字符、CRLF 和文件末尾位置测试。

如果客户端协商支持 UTF-8 position encoding,可以使用 UTF-8;但 UTF-16 仍必须正确支持,不能假设所有编辑器都使用 byte column。

Capabilities

Diagnostics

实时 diagnostics 包括:

  • syntax errors。

  • duplicate module。

  • unresolved module reference。

  • unsupported label reference。

  • evaluation 和 builtin function argument diagnostics。

  • name resolution 和 type diagnostics。

Server 使用 textDocument/publishDiagnostics。每次 snapshot 更新后,为受影响的已知文件发布完整 diagnostics 集合;原有错误消失时必须发布空数组清除客户端状态。

Diagnostic 应包含稳定的 source = "notist"、可机器识别的 code、准确 range 和简洁 message。跨文件冲突可以通过 related information 指向另一个定义位置。

Go to Definition

Wiki Reference 提供 Module 跳转:

[[notes::today]]

光标位于 reference path 时,analysis 根据当前 Module 解析目标 ModulePath。目标有 source file 时跳到文件开头;虚拟目录 Module 没有 README.not 时不伪造源码位置,可以不返回结果或由客户端展示逻辑目录信息。

Block label、函数、变量、类型和插件导出名称也使用统一的 definition_at(file, offset) 查询;协议 handler 不针对每种符号复制逻辑。

Find References

Module reference 已经形成 workspace 级引用图,因此可以提供:

  • 查找指向当前 Module 的所有 Wiki Reference。

  • 在 reference 上查找同一目标的其他引用。

  • 可选地包含 Module 自身的定义位置。

Block label 和语言符号同样使用统一的 SymbolId,而不是仅按显示文本搜索。

Completion

Completion 必须由 source context 和语义环境共同决定,不能只做全局字符串列表。

Completion 支持:

  • [[...]] 内补全当前可达的 ModulePath。

  • vault::self::super:: 后补全合法 segment。

  • #... 后补全内置 Function 名称。

  • call arguments 中补全 parameter name。

  • 属性列表中补全已知 attribute key。

Module completion 应插入相对于当前 Module 的自然路径,而不总是插入绝对 vault::...。结果可以显示目标的逻辑路径、是否为虚拟 Module 和简短文档摘要。

Content body 内继续提供 Notist completion;String literal 与 fenced Raw 的 language injection 由 Tree-sitter query 和对应语言自己的 language server 处理。当前语法不存在 Raw Call body。

Completion query 应返回协议无关的数据:label、insert text、kind、documentation 和 replace byte range。LSP 层再根据客户端 snippet 与 completion capability 生成 CompletionItem。

Hover

Hover 包括:

  • Module reference 的完整逻辑路径和 source path。

  • Module 是否为虚拟目录节点。

  • 内置 Function 的签名、参数类型、默认值和 trailing Content 绑定。

  • Attribute 或其他 Symbol 的类型与定义位置。

  • diagnostics 之外有帮助的简短说明。

LSP Hover 使用 Markdown MarkupContent。简单语义信息不依赖 notist-html;Content preview 则复用 semantic renderer 生成经过清理的片段,不能在 LSP 层另写渲染语义。

Document Symbols

宿主 = Heading 与显式 #heading[...] 都进入 DocumentSymbol。名称来自求值后的标题 Content,symbol range 使用整个 Heading;symbol 按 level 组成父子层级。Fenced Raw 中的示例标题和注入语言符号不进入 LSP DocumentSymbol。带 id 的 Annotation、函数和变量定义也可以进入符号树。

Workspace Symbol 索引 Module 和带稳定 id 的文档元素;它复用 analysis identity,不建立独立于核心导航的字符串索引语义。

Syntax Source of Truth

编辑器中的 Tree-sitter grammar 负责即时高亮、折叠和注入;LSP 不使用 Tree-sitter tree 作为语义来源。

LSP server 必须调用主仓库的 notist-syntax parser。这样 CLI、build、preview、diagnostics 和编辑器语义共享相同 AST 与错误恢复行为,不会因为 Tree-sitter grammar 更新节奏不同而产生两套语言语义。

Tree-sitter 仍然可以在 server 尚未响应时提供稳定的视觉结构,两者是互补关系。

Evaluation and Safety

Hover、completion 和 diagnostics 可能需要 Function registry 和类型信息,但语言服务器不应为了普通编辑操作执行具有外部副作用的插件代码。

建议区分:

  • builtin 和纯用户定义:允许 analysis/type checking,并在必要时做受控常量求值。

  • plugin schema:读取声明的函数签名、文档和导出符号。

  • plugin runtime:默认不在 LSP 请求路径中执行。

未知或无法执行的 Function 仍可以依据 schema 提供 completion 和 type diagnostics。需要插件生成 Content 才能知道的动态结果,可以降级为 unknown,而不是阻塞整个 workspace analysis。

Concurrency

Server state 为每个 vault 维护 ViewHandle,并观察该 View 发布的 immutable snapshot revision:

didChange(version)
  -> submit overlay to Analyzer View
  -> schedule View revision N
  -> publish snapshot N
  -> publish only if N is still current

旧 revision 的慢任务完成后不能覆盖更新结果。Hover、completion 等读取请求捕获一个 snapshot 并在其上完成;如果客户端取消请求,应尽快停止昂贵查询。

一个 View 可以使用单个 analysis worker 串行重建,也可以并行 parse;publication check 始终以 View revision 为边界。

Analysis API

notist-analysis 提供协议无关接口:

WorkspaceSnapshot::build(root, overlays)
snapshot.source(file_id)
snapshot.module_at(file_id)
snapshot.diagnostics(file_id)
snapshot.definition_at(file_id, byte_offset)
snapshot.references_at(file_id, byte_offset)
snapshot.completions_at(file_id, byte_offset)
snapshot.hover_at(file_id, byte_offset)
snapshot.document_symbols(file_id)

查询结果引用稳定的 FileId、SymbolId 和 TextRange。Path、ModulePath 与 display text 是附加信息,不应代替稳定 identity。