Files
openidea/docs/zh/plugins/guides/language-features.mdx
T
l0ng-ai 0cfe0c82fe fix(settings): move plugin formatter choices into Code Style › Formatter (#2616)
Languages & Frameworks had a standalone Plugin Formatters page that, with no
plugin providing a formatter, only showed a Format on save toggle duplicating
Tools > Actions on Save and Editor > General. The per-language plugin
formatter choice now lives in Editor > Code Style > Formatter, appears only
when an enabled plugin provides a formatter, and follows the Code Style
scheme (Copy to Project / Copy to IDE include it). Docs and the plugin README
point to the new location.
2026-11-11 08:36:34 +08:00

360 lines
23 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "语言功能"
description: "为任意语言添加补全、悬停、导航、嵌入提示、语义着色、格式化等功能,或者贡献一门全新的语言。"
---
插件可以在三个层次上扩展编辑器:
1. **声明式地添加语言。** `[[contributes.languages]]` 提供文件识别、注释、缩进、语言服务器和调试器,
不需要写代码。`[[contributes.snippets]]` 提供实时模板。
2. **为 IDE 没有语法的语言提供语法。** 可选的 `syntax-provider` 导出返回高亮、结构和折叠。
3. **编辑器提供者(provider)**,适用于任何语言,内置语言也可以:补全、悬停、跳转到定义、行号槽标记、
语义 token、嵌入提示、格式化、代码透镜等。在 `[[contributes.providers]]` 下声明。
快速修复和问题报告见 [诊断与检查](/zh/plugins/guides/diagnostics);基于全部文件构建的项目级导航见
[索引](/zh/plugins/guides/indexing)。
## 声明提供者
```toml
engines.openidea = "^1.3"
wasm = "dist/plugin.wasm"
[[contributes.providers]]
kinds = ["completion", "hover"]
languages = ["markdown"] # 编辑器语言 id;"*" 表示任意语言(1–64 项)
```
只有声明的语言与文档语言匹配的插件才会被调用,代码会在第一次调用时加载,所以不需要 `activation`。
宿主只调用你声明过的类型(kind)。提供者需要代码(`wasm`),但不需要额外权限:文档会随调用一起传入。
在提供者里调用的宿主函数仍按原有规则检查权限。
| 类型(kind) | 导出(WIT) | Rust trait,`register_plugin!` 名称 | API |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------- | --- |
| `completion` | `providers` | `providers::Providers`,`providers` | 1.0 |
| `inline-completion` | `providers` | `providers::Providers`,`providers` | 1.0 |
| `definition` | `providers` | `providers::Providers`,`providers` | 1.0 |
| `line-markers` | `providers` | `providers::Providers`,`providers` | 1.0 |
| `code-actions` | `providers`、`file-actions` | 见 [诊断](/zh/plugins/guides/diagnostics) | 1.0 |
| `semantic-tokens` | `semantic-tokens-provider` | `semantic_tokens::SemanticTokens`,`semantic_tokens` | 1.3 |
| `hover` | `hover-provider` | `hover::HoverProvider`,`hover` | 1.3 |
| `inlay-hints` | `inlay-hints-provider` | `inlay_hints::InlayHints`,`inlay_hints` | 1.3 |
| `diagnostics` | `diagnostics-provider` | 见 [诊断](/zh/plugins/guides/diagnostics) | 1.3 |
| `formatting`、`signature-help`、`document-highlights`、`document-links`、`code-lenses`、`selection-ranges`、`folding`、`colors`、`document-symbols`、`implementation`、`type-definition`、`gutter-actions` | `language-features` | `language_features::LanguageFeatures`,`language_features` | 1.4 |
| `injections` | `injections` | `language_features::Injections`,`injections` | 1.4 |
多个导出可以一次注册:`register_plugin!(MyPlugin, providers, hover, language_features)`。
每个 trait 方法都有返回空结果的默认实现,只实现声明过的即可。
## 最小示例
```rust
use openidea_plugin::hover::{Hover, HoverProvider};
use openidea_plugin::providers::{CompletionItem, CompletionKind, Document, Providers};
use openidea_plugin::{Error, Invocation, Plugin, Position, register_plugin, text};
#[derive(Default)]
struct Todos;
impl Plugin for Todos {
fn run_command(&mut self, id: &str, _ctx: &Invocation, _arg: Option<&str>) -> Result<Option<String>, Error> {
Err(Error::NotFound(id.into()))
}
}
impl Providers for Todos {
fn completion(&mut self, _doc: &Document, _at: Position, _trigger: Option<&str>) -> Vec<CompletionItem> {
let mut item = CompletionItem::new("todo", CompletionKind::Snippet);
item.insert_text = "TODO(${1:owner}): $0".into();
item.snippet = true;
item.detail = Some("TODO comment".into());
vec![item]
}
}
impl HoverProvider for Todos {
fn hover(&mut self, doc: &Document, at: Position) -> Option<Hover> {
let offset = text::offset(&doc.text, at)?;
doc.text[offset..].starts_with("TODO").then(|| Hover::new("**TODO**: tracked work"))
}
}
register_plugin!(Todos, providers, hover);
```
每个提供者都会收到一个 `Document`:绝对路径 `path`、编辑器语言 id `language`、版本号 `version`,
以及包含未保存修改的完整 `text`。位置使用从 0 开始的行号和 UTF-16 列号;
`openidea_plugin::text::offset` 和 `text::position` 用于与字节偏移互相转换。
## 时间预算
编辑器提供者运行在编辑路径上:
- 超过约 **50 ms** 的结果会被丢弃,**500 ms** 时仍在运行的调用会被中断。同一类型的所有插件同时被调用,
各自都有完整的预算。
- 提供者与插件的命令、事件、视图共用同一个实例:能看到同一份内存,但也要排在它们后面。排在长命令之后的调用
会像慢调用一样在 50 ms 时被丢弃,所以命令和事件处理要尽量短。
- `inline-completion` 不受 50 ms 限制(可能要访问网络),但用户继续输入时会立即取消。
可以检查 `Task::is_cancelled`,或者交给宿主中断。
- `formatting` 和代码操作的延迟解析使用 5 秒的命令预算。
## 补全、内联补全、导航与行号槽标记
这些都是 `providers::Providers`(WIT `providers`)的方法:
- **`completion(doc, at, trigger)`** 返回 `CompletionItem`,与语言服务器的结果合并显示。`kind`
(`CompletionKind`)决定图标。补全项可以是 `snippet`(`$1`、`${1:name}`、`$0`),可以替换 `replace` 范围,
按 `filter_text` 而不是标签匹配,按 `sort_text` 排序,带 `documentation`,还能用 `additional_edits`
修改文档其他位置(比如加 import)。
- **`inline_completion(doc, at)`** 在光标处返回幽灵文本(`InlineSuggestion { insert_text, replace }`)。
可参考 [AI Completion](https://github.com/l0ng-ai/openidea/tree/main/plugins/official/ai-completion)。
- **`definition(doc, at)`** 返回 `Location` 列表,与语言服务器的结果合并,用于跳转到声明(`⌘B` / `Ctrl+B`)。
- **`line_markers(doc)`** 返回行号槽图标,带 `tooltip` 和可选的点击跳转目标 `target`。
```rust
use openidea_plugin::providers::{Document, LineMarker, Providers};
impl Providers for Mapper {
fn line_markers(&mut self, doc: &Document) -> Vec<LineMarker> {
self.links(doc)
.into_iter()
.map(|(line, target)| LineMarker {
line,
icon: Some("implemented".into()),
tooltip: "Go to the XML statement".into(),
target: Some(target),
})
.collect()
}
}
```
[MyBatis Navigator](https://github.com/l0ng-ai/openidea/tree/main/plugins/official/mybatis) 在 Java 和 XML 中使用了
`definition`、`line-markers` 和 `completion`。
**标记图标。** `icon` 可以是图标名(包括已注册的插件图标)、别名 `navigate`(默认)、`implemented`、
`implementing`、`overridden`、`overriding`、`run`,或者声明为标记图标的包内 SVG(API 1.3):
```toml
[[contributes.markerIcons]]
id = "resource"
icon = "icons/resource.svg"
iconDark = "icons/resource_dark.svg" # 可选
```
返回 `icon: Some("icons/resource.svg".into())` 即可使用。标记 SVG 最大 64 KiB,与视图图标一样经过净化;
未知图标会回退为导航图标。如果点击时要执行命令而不是跳转,请用下文的 `gutter-actions`。
## 语义 token
`semantic-tokens`(API 1.3)为声明语言的文档着色,也包括插件没有贡献的语言,比如 `php`:
```rust
use openidea_plugin::semantic_tokens::{Document, SemanticToken, SemanticTokens, TokenKind, TokenModifier};
use openidea_plugin::{Range, text};
impl SemanticTokens for MyPlugin {
fn semantic_tokens(&mut self, doc: &Document) -> Vec<SemanticToken> {
let (start, end) = (text::position(&doc.text, 10), text::position(&doc.text, 14));
vec![SemanticToken::new(Range { start, end }, TokenKind::Method).modifier(TokenModifier::Static)]
}
}
```
每个 token 覆盖一行中的一段,带 LSP token 类型和修饰符(WIT 中为 kebab-case 字符串)。插件 token 与语言服务器的
合并,重叠处以插件为准,所以配色方案会像对待服务器 token 一样为它们着色。每次编辑后都会重新请求;每个文档最多
65,536 个。
## 悬停
`hover`(API 1.3)在悬停提示中追加 Markdown。宿主同时询问语言服务器和插件,先显示服务器的内容,
再用分隔线依次显示各插件的内容。`Hover::new(markdown).range(range)` 指明悬停描述的文本范围(服务器没给范围时使用)。
每个插件最多 64 KiB Markdown。
## 嵌入提示
`inlay-hints`(API 1.3)在代码中显示简短标签:
```rust
use openidea_plugin::inlay_hints::{Document, InlayHint, InlayHints, InlayKind};
use openidea_plugin::Range;
impl InlayHints for MyPlugin {
fn inlay_hints(&mut self, _doc: &Document, range: Range) -> Vec<InlayHint> {
vec![InlayHint::new(range.start, ": string").kind(InlayKind::Type).tooltip("returns string")]
}
}
```
`range` 是文档的可见部分,范围外的提示会被丢弃。`kind`(`Type`、`Parameter` 或 `Other`)决定由哪个
**嵌入提示** 设置控制显示。`.padding_left()` 和 `.padding_right()` 添加空格。标签截断到 256 字节,
提示文字截断到 1 KiB,每次调用最多 10,000 个。
## 更多语言功能(API 1.4)
声明 `engines.openidea = "^1.4"` 后可以使用下列类型,实现 `language_features::LanguageFeatures`
并注册 `language_features`:
| 类型 | 方法 | 宿主如何使用 |
| --------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `formatting` | `format(doc, range, options)` | 在 **设置 › 编辑器 › 代码样式 › 格式化工具 › 插件格式化工具** 中为每种语言选择插件(有已启用的插件提供格式化工具时才显示)。用于重新格式化代码、格式化选区和保存时格式化。预算 5 秒。 |
| `signature-help` | `signature_help(doc, at)` | 语言服务器没有返回签名时使用。 |
| `document-highlights` | `document_highlights(doc, at)` | 追加到服务器的高亮之后(`text`、`read`、`write`)。 |
| `document-links` | `document_links(doc)` | Ctrl/⌘ 点击目标:绝对路径、项目相对路径或 HTTP(S) URL。 |
| `code-lenses` | `code_lenses(doc)` | 与服务器的透镜一起显示。点击时执行插件声明的命令。 |
| `selection-ranges` | `selection_ranges(doc, at)` | 用于 **扩展选区**,从内到外;无结果时回退到 tree-sitter。 |
| `folding` | `folding(doc)` | 与 tree-sitter 的折叠合并。 |
| `colors` | `colors(doc)`、`color_presentations(doc, color)` | 颜色色块与取色器;各通道是 0–1 的浮点数。 |
| `document-symbols` | `document_symbols(doc)` | 用于结构视图和痕迹导航,仅当服务器没有返回符号时生效。 |
| `implementation` | `implementation(doc, at)` | 与服务器的实现结果合并。 |
| `type-definition` | `type_definition(doc, at)` | 与服务器的类型声明结果合并。 |
| `gutter-actions` | `gutter_actions(doc)` | 行号槽按钮,执行插件声明的命令,可带参数。 |
下面是 [Blame Annotations](https://github.com/l0ng-ai/openidea/tree/main/plugins/official/blame-annotations)
示例中提供命令入口的代码透镜:
```rust
use openidea_plugin::language_features::{CodeLens, Document, LanguageFeatures};
use openidea_plugin::Range;
impl LanguageFeatures for BlameAnnotations {
fn code_lenses(&mut self, _doc: &Document) -> Vec<CodeLens> {
vec![CodeLens {
range: Range::default(),
title: "Show blame annotations".into(),
command: "annotate".into(), // 本插件声明的命令
argument: None,
}]
}
}
register_plugin!(BlameAnnotations, language_features);
```
[Test Runner](https://github.com/l0ng-ai/openidea/tree/main/plugins/official/test-runner) 演示了 `gutter-actions`。
所有这些功能共同的规则:
- 语言服务器缺失或不可用时,插件结果照常生效;服务器的其他错误仍按错误处理。
- `document-symbol.parent` 是返回的扁平列表中的下标。父节点必须排在前面并包含子节点的范围;
无效的父节点会变成根节点,层级最多 64 层。
- 每个列表最多 10,000 项,每个字符串最多 64 KiB;无效范围会被丢弃。格式化编辑必须有效且互不重叠。
- 禁用已选定的格式化插件后,会报告为不可用,而不是悄悄换用其他格式化工具。
## 嵌入语言(injections)
`injections`(API 1.4)标出宿主文档中属于另一种语言的范围,比如字符串字面量里的 SQL。
为**宿主**语言声明这个类型,实现 `Injections` 并注册 `injections`:
```rust
use openidea_plugin::language_features::{Document, Injection, Injections};
use openidea_plugin::text;
impl Injections for MyPlugin {
fn injections(&mut self, doc: &Document) -> Vec<Injection> {
let Some(start) = doc.text.find("[[") else { return vec![] };
let Some(len) = doc.text[start + 2..].find("]]") else { return vec![] };
vec![Injection {
range: openidea_plugin::Range {
start: text::position(&doc.text, start + 2),
end: text::position(&doc.text, start + 2 + len),
},
language: "sql".into(),
}]
}
}
```
宿主会这样处理这些范围:
- 用嵌入语言的解析器和当前配色方案为每个范围着色,编辑后 300 ms 刷新。
- 把补全、悬停、诊断以及 API 1.4 的各项功能转给嵌入语言的**插件**提供者。传入的是一份虚拟文档:
保留原路径和坐标,范围外的文本替换为空白。
- 如果嵌入语言配置了语言服务器,还会把补全、悬停、签名帮助、导航、引用、高亮和代码操作转给它;
每个范围有一份独立的内存中同级文档(不会写入磁盘)。格式化和重命名仍由宿主语言的服务器处理。
限制:每个文档最多 1,000 个互不重叠的范围、32 种嵌入语言;超过 2 MiB 的文档不处理;嵌入不会递归。
## 为插件语言提供语法
对于插件贡献、但 IDE 没有 tree-sitter 语法的语言,可以导出 `syntax-provider`(world `plugin-syntax`)。
这里的偏移是文本中的 **UTF-8 字节**范围,由 IDE 负责转换。
```rust
use openidea_plugin::syntax::{Fold, Highlight, Symbol, SyntaxProvider, Token};
impl SyntaxProvider for Solidity {
fn highlights(&mut self, _language: &str, text: &str) -> Vec<Token> {
lex(text).map(|(range, keyword)| Token { range, kind: if keyword { Highlight::Keyword } else { Highlight::Variable } }).collect()
}
fn outline(&mut self, _language: &str, text: &str) -> Vec<Symbol> { outline(text) } // 可选
fn folding(&mut self, _language: &str, text: &str) -> Vec<Fold> { // 可选
openidea_plugin::syntax::bracket_folds(text, b'{', b'}', &[])
}
}
register_plugin!(Solidity, syntax);
```
它运行在编辑路径上,每个文档版本只调用一次(有缓存),所以要快;通常手写一个词法分析器即可。
加上 `activation = ["on-language:<id>"]`,让组件在打开第一个文件时加载。结果会被校验:裁剪到文本范围内,
重叠部分丢弃。完整示例见 [Solidity](https://github.com/l0ng-ai/openidea/tree/main/plugins/official/solidity)。
## 贡献一门语言
```toml
[[contributes.languages]]
path = "languages/solidity.toml"
```
文件使用 IDE 自己的 [语言定义格式](/zh/languages/adding)(与仓库中的 `languages/<id>.toml` 相同):
```toml
id = "solidity"
name = "Solidity"
extensions = ["sol"]
line-comment = "//"
block-comment = ["/*", "*/"]
indent = { tab-size = 4, insert-spaces = true }
[[language-servers]]
name = "solidity-language-server"
command = "nomicfoundation-solidity-language-server"
args = ["--stdio"]
root-markers = ["hardhat.config.ts", "foundry.toml", "package.json"]
install-hint = "npm install -g @nomicfoundation/solidity-language-server"
```
- 插件语言和内置语言一样加入语言注册表:文件识别(`extensions`、`filenames`、`first-line`)、注释、缩进、
`ui-mode`、`framework-ui-modes`,以及其中声明的 `language-servers` 和 `debugger`。
- 每个语言服务器或调试器命令都会变成一个可选的 `process:<program>` 权限。拒绝它不影响插件其他部分。
- 插件不能覆盖内置或用户定义:它们的 id、扩展名和文件名优先。用户仍可以在 **设置 › 文件类型**
中把文件映射到插件语言。
- 在远程项目中,语言定义会发送到服务器,由服务器在那边启动语言服务器。
`openidea-plugin new my-lang --template language` 可以创建一个纯数据的语言插件。
## 代码片段
```toml
[[contributes.snippets]] # API 1.4
language = "sql"
path = "snippets/sql.json"
```
```json
{
"Select rows": {
"prefix": ["sel", "select"],
"body": ["SELECT ${1:column}", "FROM ${2:table}$0"],
"description": "Select rows from a table"
}
}
```
`prefix` 和 `body` 可以是字符串或字符串数组(body 各行以换行连接)。`$1`、`${1:default}` 和结尾的 `$0`
会成为联动的实时模板字段;`\$` 表示字面量美元符号。片段会出现在补全中,可按 Tab 展开,并以只读形式按插件分组列在
**设置 › 编辑器 › 实时模板** 中。不需要代码或权限。限制:每个文件 1 MiB、1,000 个条目、每条 32 个前缀、每个 body 64 KiB。
## 测试
在本机运行时 SDK 调用的是模拟宿主,所以可以在 `cargo test` 中直接调用 trait 方法。
`MockHost::provider_document()` 返回模拟编辑器当前未保存的快照。参见 [测试](/zh/plugins/testing)。