@trim21/personal-pi-extensions 0.0.308 → 0.0.310

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -344,6 +344,50 @@ pi -e ./src/talk/index.ts
344
344
 
345
345
  ---
346
346
 
347
+ ## LSP 配置
348
+
349
+ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级诊断)。LSP protocol 是统一的,因此服务器不需要为每个语言写 adapter:用一份 JSON 配置声明如何启动即可。
350
+
351
+ 配置文件(项目优先于全局,逐字段覆盖):
352
+
353
+ - `~/.pi/agent/lsp.json`(全局)
354
+ - `.pi/lsp.json`(项目)
355
+
356
+ ```jsonc
357
+ {
358
+ "version": 1,
359
+ "servers": {
360
+ "gopls": {
361
+ "include": ["**/*.go"],
362
+ "rootMarkers": ["go.mod"],
363
+ "bin": "gopls",
364
+ "args": [],
365
+ "cwd": "{root}", // 支持 {root} / {cwd} 模板
366
+ "languageIdByExtension": { ".go": "go" },
367
+ "startupTimeoutMs": 45000,
368
+ "diagnosticsWaitMs": 1500,
369
+ "initializationOptions": {}, // → initialize 请求
370
+ "settings": {}, // → didChangeConfiguration / workspace/configuration 请求
371
+ },
372
+ },
373
+ }
374
+ ```
375
+
376
+ 字段说明:
377
+
378
+ - `include`:文件 glob,相对项目根或调用 cwd,任一命中即启用;支持 `!` 否定排除,如 `["**/*.go", "!**/*_test.go"]`
379
+ - `rootMarkers`:项目根标记文件,从文件目录向上查找;缺省用调用 cwd 作为根
380
+ - `bin`:可执行文件——绝对路径、相对调用 cwd 的路径,或名字(先在项目内 `node_modules/.bin`、`.venv/bin`、`venv/bin` 找,再走 PATH)
381
+ - `languageIdByExtension`:扩展名 → LSP languageId(didOpen 用);缺省回退内置映射表
382
+ - `startupTimeoutMs` / `diagnosticsWaitMs`:per-server 超时,覆盖全局配置与默认值
383
+ - `initializationOptions` 与 `settings` 按 LSP 语义分离:前者进 initialize 请求,后者进 didChangeConfiguration / workspace/configuration 请求
384
+
385
+ 内置默认服务器(typescript / pyright / ruff / clangd)始终存在;`servers` 以 key 为服务器 id 与默认合并——同 key 覆盖、新 key 新增、`"enabled": false` 移除(如 `"clangd": { "enabled": false }`)。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 `bin` / `args` / `settings` 自行表达。
386
+
387
+ 旧的 `enabled`(白名单)/ `disabled`(排除)与全局超时字段(`initializeTimeoutMs` 等)继续可用。
388
+
389
+ ---
390
+
347
391
  ## 安装
348
392
 
349
393
  ### 通过 npm/git 包
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.308",
3
+ "version": "0.0.310",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -85,9 +85,10 @@
85
85
  "node": ">=24"
86
86
  },
87
87
  "dependencies": {
88
- "@cortexkit/aft-bridge": "0.51.0",
88
+ "@cortexkit/aft-bridge": "0.51.2",
89
89
  "@vscode/tree-sitter-wasm": "^0.3.1",
90
90
  "jsonc-parser": "^3.3.1",
91
+ "minimatch": "^10.2.6",
91
92
  "vscode-jsonrpc": "^9.0.1",
92
93
  "vscode-languageserver-types": "^3.18.0",
93
94
  "web-tree-sitter": "^0.26.12"
@@ -12,13 +12,22 @@ import { exists, walkUp } from "./bin.js";
12
12
 
13
13
  export interface LspServerHandle {
14
14
  process: ChildProcessWithoutNullStreams;
15
+ /** initialize 请求的 initializationOptions。 */
15
16
  initialization?: Record<string, unknown>;
17
+ /** didChangeConfiguration / workspace/configuration 的 settings;缺省回退 initialization。 */
18
+ settings?: Record<string, unknown>;
19
+ /** didOpen 的 per-server languageId 映射;缺省回退内置 LANGUAGE_EXTENSIONS 表。 */
20
+ languageIds?: Record<string, string>;
16
21
  }
17
22
 
18
23
  export interface LspServerAdapter {
19
24
  readonly id: string;
20
25
  /** 关联的文件扩展名(含点,小写);空数组表示匹配所有文件。 */
21
26
  readonly extensions: readonly string[];
27
+ /** per-server initialize 握手超时(ms);缺省用全局配置 / client 默认。 */
28
+ readonly startupTimeoutMs?: number;
29
+ /** per-server 诊断等待时长(ms);缺省用全局配置 / client 默认。 */
30
+ readonly diagnosticsWaitMs?: number;
22
31
  findRoot(file: string, cwd: string): Promise<string | undefined>;
23
32
  spawn(root: string, cwd: string): Promise<LspServerHandle | undefined>;
24
33
  }
@@ -62,19 +62,6 @@ export function findBinaryInWorkspace(
62
62
  return Promise.resolve(undefined);
63
63
  }
64
64
 
65
- /** 从 fromDir 向上(到 stopDir)找 node_modules/<modulePath>(如 typescript/lib/tsserver.js)。 */
66
- export function findModuleInWorkspace(
67
- modulePath: string,
68
- fromDir: string,
69
- stopDir: string,
70
- ): Promise<string | undefined> {
71
- for (const dir of walkUp(fromDir, stopDir)) {
72
- const candidate = join(dir, "node_modules", modulePath);
73
- if (exists(candidate)) return Promise.resolve(candidate);
74
- }
75
- return Promise.resolve(undefined);
76
- }
77
-
78
65
  /** PATH 查找(同步,逻辑简单;调用频率低,无需缓存)。 */
79
66
  export function which(cmd: string): string | undefined {
80
67
  const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
@@ -232,7 +232,9 @@ export async function create(input: CreateInput): Promise<LspClient> {
232
232
  connection.onRequest("window/workDoneProgress/create", () => null);
233
233
  connection.onRequest("workspace/configuration", (params) => {
234
234
  const items = (params as { items?: { section?: string }[] }).items ?? [];
235
- return items.map((item) => configurationValue(input.server.initialization, item.section));
235
+ return items.map((item) =>
236
+ configurationValue(input.server.settings ?? input.server.initialization, item.section),
237
+ );
236
238
  });
237
239
  connection.onRequest("client/registerCapability", (params) => {
238
240
  const registrations =
@@ -296,10 +298,9 @@ export async function create(input: CreateInput): Promise<LspClient> {
296
298
 
297
299
  await connection.sendNotification("initialized", {});
298
300
 
299
- if (input.server.initialization) {
300
- await connection.sendNotification("workspace/didChangeConfiguration", {
301
- settings: input.server.initialization,
302
- });
301
+ const settings = input.server.settings ?? input.server.initialization;
302
+ if (settings) {
303
+ await connection.sendNotification("workspace/didChangeConfiguration", { settings });
303
304
  }
304
305
 
305
306
  const files: Record<string, { version: number; text: string }> = {};
@@ -618,7 +619,9 @@ export async function create(input: CreateInput): Promise<LspClient> {
618
619
  isAbsolute(request.path) ? request.path : resolve(input.directory, request.path),
619
620
  );
620
621
  const text = await readFile(resolvedPath, "utf8");
621
- const languageId = LANGUAGE_EXTENSIONS[extname(resolvedPath)] ?? "plaintext";
622
+ const extension = extname(resolvedPath);
623
+ const languageId =
624
+ input.server.languageIds?.[extension] ?? LANGUAGE_EXTENSIONS[extension] ?? "plaintext";
622
625
  const uri = pathToFileURL(resolvedPath).href;
623
626
 
624
627
  const document = files[resolvedPath];
@@ -4,8 +4,9 @@
4
4
  * - state(client 缓存、broken 集合、spawning 去重)是 createLspService 的
5
5
  * 闭包变量,不做成模块级全局;
6
6
  * - 配置来源:全局 `~/.pi/agent/lsp.json` + 本地 `<cwd>/.pi/lsp.json`
7
- * (本地逐字段覆盖全局):`enabled` 白名单(缺省全部)、`disabled` 排除
8
- * (缺省无),以及各超时参数;配置在每个工具的调用 cwd 下惰性读取;
7
+ * (本地逐字段覆盖全局):`servers` 数组配置驱动地定义语言服务器
8
+ * (按 id 与内置默认服务器合并),`enabled`/`disabled` 白名单与各超时
9
+ * 参数继续生效;配置在每个工具的调用 cwd 下惰性读取;
9
10
  * - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入 broken
10
11
  * 集合(服务实例生命周期内不再重试);
11
12
  * - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile 三个方法打交道。
@@ -20,15 +21,19 @@ import { type Static, Type } from "typebox";
20
21
  import { Value } from "typebox/value";
21
22
 
22
23
  import { type LspServerAdapter } from "./adapter.js";
23
- import { createAdapters } from "./adapters/index.js";
24
24
  import { create, type CreateInput, type Diagnostic, type Info as LspClient } from "./client.js";
25
25
  import { report } from "./diagnostic.js";
26
+ import { createAdapters, serverConfigSchema } from "./server-config.js";
26
27
 
27
28
  /** 超时值:number(毫秒,>=1)或字符串("500"、"5s"、"1m"),Parse 后由 toMs 统一换算。 */
28
29
  const timeoutValue = Type.Union([Type.Number({ minimum: 1 }), Type.String()]);
29
30
 
30
31
  /** lsp.json 的配置项(全局与本地同构)。 */
31
32
  const lspConfigSchema = Type.Object({
33
+ /** 配置文件版本(当前 1);未知版本会被 typebox 严格校验拒绝并回退空配置。 */
34
+ version: Type.Optional(Type.Number()),
35
+ /** 配置驱动的语言服务器定义(id → 配置);按 id 与内置默认服务器合并(覆盖/enabled:false 禁用)。 */
36
+ servers: Type.Optional(Type.Record(Type.String(), serverConfigSchema)),
32
37
  /** 只启用列出的服务器 id(缺省 = 全部启用)。 */
33
38
  enabled: Type.Optional(Type.Array(Type.String())),
34
39
  /** 从启用集中排除的服务器 id(缺省 = 无)。 */
@@ -123,7 +128,6 @@ export function filterAdapters(
123
128
 
124
129
  interface LspState {
125
130
  clients: LspClient[];
126
- adapters: LspServerAdapter[];
127
131
  broken: Set<string>;
128
132
  spawning: Map<string, Promise<LspClient | undefined>>;
129
133
  }
@@ -144,16 +148,16 @@ function containsPath(file: string, cwd: string): boolean {
144
148
 
145
149
  /**
146
150
  * 创建 LSP 服务实例。state 由闭包持有;adapters 可注入(测试传 [] 或
147
- * mock adapters 即可隔离真实服务器)。globalConfigPath 供测试注入
148
- * 固定的全局配置路径,避免被本机 ~/.pi/agent/lsp.json 影响。
151
+ * mock adapters 即可隔离真实服务器),不注入时按配置文件 servers 与
152
+ * 内置默认服务器合并构建。globalConfigPath 供测试注入固定的全局配置
153
+ * 路径,避免被本机 ~/.pi/agent/lsp.json 影响。
149
154
  */
150
155
  export function createLspService(
151
- adapters: LspServerAdapter[] = createAdapters(),
156
+ adapters?: LspServerAdapter[],
152
157
  globalConfigPath?: string,
153
158
  ): LspService {
154
159
  const state: LspState = {
155
160
  clients: [],
156
- adapters,
157
161
  broken: new Set(),
158
162
  spawning: new Map(),
159
163
  };
@@ -162,11 +166,11 @@ export function createLspService(
162
166
  if (!containsPath(file, cwd)) return [];
163
167
  const config = await loadLspConfig(cwd, globalConfigPath);
164
168
  const timeout = timeoutOptions(config);
165
- const adapters = filterAdapters(state.adapters, config);
169
+ const active = filterAdapters(adapters ?? createAdapters(config.servers), config);
166
170
  const extension = extname(file) || file;
167
171
  const result: LspClient[] = [];
168
172
 
169
- for (const adapter of adapters) {
173
+ for (const adapter of active) {
170
174
  if (adapter.extensions.length > 0 && !adapter.extensions.includes(extension)) continue;
171
175
  const root = await adapter.findRoot(file, cwd);
172
176
  if (!root) continue;
@@ -199,6 +203,9 @@ export function createLspService(
199
203
  root,
200
204
  directory: cwd,
201
205
  ...timeout,
206
+ initializeTimeoutMs: adapter.startupTimeoutMs ?? timeout.initializeTimeoutMs,
207
+ diagnosticsDocumentWaitTimeoutMs:
208
+ adapter.diagnosticsWaitMs ?? timeout.diagnosticsDocumentWaitTimeoutMs,
202
209
  });
203
210
  const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
204
211
  if (duplicate) {
@@ -0,0 +1,219 @@
1
+ /**
2
+ * 配置驱动的 LSP 服务器:一份 JSON 配置定义一个语言服务器(bin/args/rootMarkers/
3
+ * languageId/超时等),替代为每个语言写一个 adapter class。
4
+ *
5
+ * 配置文件沿用 lsp.json(全局 ~/.pi/agent/lsp.json + 本地 <cwd>/.pi/lsp.json):
6
+ * 顶层 `servers` 是 id → 配置的 record,按 id 与内置默认服务器合并
7
+ * (覆盖同名、新增 id、enabled:false 禁用),之后仍受现有 enabled/disabled
8
+ * 白名单过滤。
9
+ *
10
+ * executable 发现统一由用户配置:bin 支持绝对路径 / 项目工作区
11
+ * (node_modules/.bin、.venv/bin、venv/bin)/ PATH,不再内置各语言的
12
+ * 特殊探测逻辑(tsserver 路径、venv python 等)。
13
+ */
14
+
15
+ import { existsSync } from "node:fs";
16
+ import { isAbsolute, join, relative, sep } from "node:path";
17
+
18
+ import { minimatch } from "minimatch";
19
+ import { type Static, Type } from "typebox";
20
+
21
+ import { type LspServerAdapter, type LspServerHandle, nearestRoot } from "./adapter.js";
22
+ import { exists, findBinaryInWorkspace, which } from "./bin.js";
23
+ import { spawnProcess } from "./launch.js";
24
+
25
+ export const serverConfigSchema = Type.Object({
26
+ /** false = 从启用集中移除(覆盖同名内置服务器);缺省启用。 */
27
+ enabled: Type.Optional(Type.Boolean()),
28
+ /** 文件 glob(相对项目根或调用 cwd,任一命中即可);缺省匹配所有文件。 */
29
+ include: Type.Optional(Type.Array(Type.String())),
30
+ /** 项目根标记文件(从文件目录向上查找);缺省用调用 cwd 作为根。 */
31
+ rootMarkers: Type.Optional(Type.Array(Type.String())),
32
+ /** 可执行文件:绝对路径、相对调用 cwd 的路径,或名字(项目工作区优先,PATH 兜底)。 */
33
+ bin: Type.Optional(Type.String()),
34
+ args: Type.Optional(Type.Array(Type.String())),
35
+ /** 启动工作目录,支持 {root} / {cwd} 模板;缺省 {root}。 */
36
+ cwd: Type.Optional(Type.String()),
37
+ /** 文件扩展名(含点)→ LSP languageId,didOpen 用;缺省回退内置映射表。 */
38
+ languageIdByExtension: Type.Optional(Type.Record(Type.String(), Type.String())),
39
+ /** initialize 握手超时(ms);缺省用全局配置 / client 默认。 */
40
+ startupTimeoutMs: Type.Optional(Type.Number({ minimum: 1 })),
41
+ /** 写文件后等待诊断的时长(ms);缺省用全局配置 / client 默认。 */
42
+ diagnosticsWaitMs: Type.Optional(Type.Number({ minimum: 1 })),
43
+ /** initialize 请求的 initializationOptions。 */
44
+ initializationOptions: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
45
+ /** didChangeConfiguration / workspace/configuration 请求的 settings。 */
46
+ settings: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
47
+ });
48
+
49
+ export type ServerConfig = Static<typeof serverConfigSchema>;
50
+
51
+ /** 内置默认服务器(id → 配置,可被用户配置按 id 覆盖,全量字段均可配置化表达)。 */
52
+ export const defaultServers: Record<string, ServerConfig> = {
53
+ typescript: {
54
+ include: ["**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}"],
55
+ rootMarkers: ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"],
56
+ bin: "typescript-language-server",
57
+ args: ["--stdio"],
58
+ cwd: "{root}",
59
+ languageIdByExtension: {
60
+ ".ts": "typescript",
61
+ ".tsx": "typescriptreact",
62
+ ".js": "javascript",
63
+ ".jsx": "javascriptreact",
64
+ ".mjs": "javascript",
65
+ ".cjs": "javascript",
66
+ ".mts": "typescript",
67
+ ".cts": "typescript",
68
+ },
69
+ },
70
+ pyright: {
71
+ include: ["**/*.py", "**/*.pyi"],
72
+ rootMarkers: [
73
+ "pyproject.toml",
74
+ "setup.py",
75
+ "setup.cfg",
76
+ "requirements.txt",
77
+ "Pipfile",
78
+ "pyrightconfig.json",
79
+ ],
80
+ bin: "pyright-langserver",
81
+ args: ["--stdio"],
82
+ cwd: "{root}",
83
+ languageIdByExtension: { ".py": "python", ".pyi": "python" },
84
+ },
85
+ ruff: {
86
+ include: ["**/*.py", "**/*.pyi"],
87
+ rootMarkers: ["pyproject.toml", "ruff.toml", ".ruff.toml"],
88
+ bin: "ruff",
89
+ args: ["server"],
90
+ cwd: "{root}",
91
+ languageIdByExtension: { ".py": "python", ".pyi": "python" },
92
+ },
93
+ clangd: {
94
+ include: ["**/*.{c,h,cpp,hpp,cc,cxx,c++,hh,hxx,h++}"],
95
+ rootMarkers: ["compile_commands.json", "compile_flags.txt", ".clangd"],
96
+ bin: "clangd",
97
+ args: ["--background-index", "--clang-tidy"],
98
+ cwd: "{root}",
99
+ languageIdByExtension: {
100
+ ".c": "c",
101
+ ".h": "c",
102
+ ".cpp": "cpp",
103
+ ".hpp": "cpp",
104
+ ".cc": "cpp",
105
+ ".cxx": "cpp",
106
+ ".c++": "cpp",
107
+ ".hh": "cpp",
108
+ ".hxx": "cpp",
109
+ ".h++": "cpp",
110
+ },
111
+ },
112
+ };
113
+
114
+ /** 用户 servers(id → 配置)与默认配置合并:覆盖同名、新增 id、enabled:false 移除。 */
115
+ export function mergeServerConfigs(
116
+ defaults: Readonly<Record<string, ServerConfig>>,
117
+ user: Readonly<Record<string, ServerConfig>> | undefined,
118
+ ): Record<string, ServerConfig> {
119
+ if (!user) return { ...defaults };
120
+ const merged = { ...defaults };
121
+ for (const [id, server] of Object.entries(user)) {
122
+ if (server.enabled === false) {
123
+ delete merged[id];
124
+ continue;
125
+ }
126
+ merged[id] = server;
127
+ }
128
+ return merged;
129
+ }
130
+
131
+ /** {root} / {cwd} 模板替换(bin / cwd 字段均支持)。 */
132
+ function resolveTemplate(template: string, root: string, cwd: string): string {
133
+ return template.split("{root}").join(root).split("{cwd}").join(cwd);
134
+ }
135
+
136
+ /** 解析可执行文件:绝对/相对路径直接用;名字走项目工作区(node_modules/.bin 等)→ PATH。 */
137
+ async function resolveBinary(bin: string, root: string, cwd: string): Promise<string | undefined> {
138
+ if (isAbsolute(bin)) return existsSync(bin) ? bin : undefined;
139
+ if (bin.includes("/") || bin.includes("\\")) {
140
+ const relativePath = join(cwd, bin);
141
+ return exists(relativePath) ? relativePath : undefined;
142
+ }
143
+ return (await findBinaryInWorkspace(bin, root, cwd)) ?? which(bin);
144
+ }
145
+
146
+ /**
147
+ * include glob 匹配:相对项目根或调用 cwd 的路径,任一命中即可。
148
+ * 支持 `!` 否定模式排除;多 pattern 数组拆开判断(任意 positive 命中且
149
+ * 不被任何 negative 排除),避免库对混合数组的语义差异。
150
+ */
151
+ function matchesInclude(
152
+ patterns: readonly string[],
153
+ file: string,
154
+ root: string,
155
+ cwd: string,
156
+ ): boolean {
157
+ if (patterns.length === 0) return true;
158
+ const positives: string[] = [];
159
+ const negatives: string[] = [];
160
+ for (const pattern of patterns) {
161
+ if (pattern.startsWith("!")) negatives.push(pattern.slice(1));
162
+ else positives.push(pattern);
163
+ }
164
+ if (positives.length === 0) positives.push("**");
165
+ const candidates = [relative(root, file), relative(cwd, file)]
166
+ .map((p) => p.split(sep).join("/"))
167
+ .filter((p) => !p.startsWith(".."));
168
+ const matches = (candidate: string): boolean =>
169
+ positives.some((pattern) => minimatch(candidate, pattern)) &&
170
+ negatives.every((pattern) => !minimatch(candidate, pattern));
171
+ return candidates.some((candidate) => matches(candidate));
172
+ }
173
+
174
+ /** 由配置构建的通用 adapter;include 精确过滤在 findRoot 完成,extensions 不设扩展名过滤。 */
175
+ export class ConfigAdapter implements LspServerAdapter {
176
+ readonly id: string;
177
+ readonly extensions: readonly string[] = [];
178
+ readonly startupTimeoutMs: number | undefined;
179
+ readonly diagnosticsWaitMs: number | undefined;
180
+ readonly config: ServerConfig;
181
+
182
+ constructor(id: string, config: ServerConfig) {
183
+ this.id = id;
184
+ this.config = config;
185
+ this.startupTimeoutMs = config.startupTimeoutMs;
186
+ this.diagnosticsWaitMs = config.diagnosticsWaitMs;
187
+ }
188
+
189
+ findRoot(file: string, cwd: string): Promise<string | undefined> {
190
+ const root = nearestRoot(this.config.rootMarkers ?? [], file, cwd);
191
+ return root.then((resolved) =>
192
+ matchesInclude(this.config.include ?? [], file, resolved, cwd) ? resolved : undefined,
193
+ );
194
+ }
195
+
196
+ async spawn(root: string, cwd: string): Promise<LspServerHandle | undefined> {
197
+ const bin = this.config.bin;
198
+ if (!bin) return undefined;
199
+ const resolved = await resolveBinary(bin, root, cwd);
200
+ if (!resolved) return undefined;
201
+ return {
202
+ process: spawnProcess(resolved, this.config.args ?? [], {
203
+ cwd: resolveTemplate(this.config.cwd ?? "{root}", root, cwd),
204
+ }),
205
+ initialization: this.config.initializationOptions,
206
+ settings: this.config.settings,
207
+ languageIds: this.config.languageIdByExtension,
208
+ };
209
+ }
210
+ }
211
+
212
+ /** 组装启用的服务器列表:默认配置 + 用户 servers(id → 配置)合并。 */
213
+ export function createAdapters(
214
+ userServers?: Readonly<Record<string, ServerConfig>>,
215
+ ): LspServerAdapter[] {
216
+ return Object.entries(mergeServerConfigs(defaultServers, userServers)).map(
217
+ ([id, config]) => new ConfigAdapter(id, config),
218
+ );
219
+ }
@@ -1,26 +0,0 @@
1
- /**
2
- * C/C++ LSP 服务器(clangd,随 LLVM/clang 工具链安装,走 PATH)。
3
- */
4
-
5
- import { type LspServerAdapter, type LspServerHandle, nearestRoot } from "../adapter.js";
6
- import { which } from "../bin.js";
7
- import { spawnProcess } from "../launch.js";
8
-
9
- const PROJECT_MARKERS = ["compile_commands.json", "compile_flags.txt", ".clangd"];
10
-
11
- export class ClangdAdapter implements LspServerAdapter {
12
- readonly id = "clangd";
13
- readonly extensions = [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"];
14
-
15
- findRoot(file: string, cwd: string): Promise<string> {
16
- return nearestRoot(PROJECT_MARKERS, file, cwd);
17
- }
18
-
19
- spawn(root: string): Promise<LspServerHandle | undefined> {
20
- const bin = which("clangd");
21
- if (!bin) return Promise.resolve(undefined);
22
- return Promise.resolve({
23
- process: spawnProcess(bin, ["--background-index", "--clang-tidy"], { cwd: root }),
24
- });
25
- }
26
- }
@@ -1,11 +0,0 @@
1
- import { ClangdAdapter } from "./clangd.js";
2
- import { PyrightAdapter } from "./pyright.js";
3
- import { RuffAdapter } from "./ruff.js";
4
- import { TypescriptAdapter } from "./typescript.js";
5
-
6
- export type { LspServerAdapter, LspServerHandle } from "../adapter.js";
7
-
8
- /** 组装启用的服务器列表(新增语言:实现 adapter 后在这里注册)。 */
9
- export function createAdapters() {
10
- return [new TypescriptAdapter(), new PyrightAdapter(), new RuffAdapter(), new ClangdAdapter()];
11
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Python 类型检查 LSP 服务器(pyright-langserver)。
3
- * pythonPath 优先取 VIRTUAL_ENV,其次项目 .venv / venv 里的解释器。
4
- */
5
-
6
- import { join } from "node:path";
7
-
8
- import { type LspServerAdapter, nearestRoot } from "../adapter.js";
9
- import { exists, findBinaryInWorkspace, which } from "../bin.js";
10
- import { spawnProcess } from "../launch.js";
11
-
12
- const PROJECT_MARKERS = [
13
- "pyproject.toml",
14
- "setup.py",
15
- "setup.cfg",
16
- "requirements.txt",
17
- "Pipfile",
18
- "pyrightconfig.json",
19
- ];
20
-
21
- export class PyrightAdapter implements LspServerAdapter {
22
- readonly id = "pyright";
23
- readonly extensions = [".py", ".pyi"];
24
-
25
- findRoot(file: string, cwd: string): Promise<string> {
26
- return nearestRoot(PROJECT_MARKERS, file, cwd);
27
- }
28
-
29
- async spawn(root: string, cwd: string) {
30
- const bin =
31
- (await findBinaryInWorkspace("pyright-langserver", root, cwd)) ?? which("pyright-langserver");
32
- if (!bin) return;
33
-
34
- const initialization: Record<string, string> = {};
35
- const venvs = [process.env.VIRTUAL_ENV, join(root, ".venv"), join(root, "venv")];
36
- for (const venv of venvs) {
37
- if (!venv) continue;
38
- const python = join(venv, process.platform === "win32" ? "Scripts" : "bin", "python");
39
- if (exists(python)) {
40
- initialization.pythonPath = python;
41
- break;
42
- }
43
- }
44
-
45
- return {
46
- process: spawnProcess(bin, ["--stdio"], { cwd: root }),
47
- initialization,
48
- };
49
- }
50
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Python lint LSP 服务器(ruff 内置的 `ruff server`)。
3
- * 二进制优先项目 .venv / venv,其次 PATH。
4
- */
5
-
6
- import { type LspServerAdapter, nearestRoot } from "../adapter.js";
7
- import { findBinaryInWorkspace, which } from "../bin.js";
8
- import { spawnProcess } from "../launch.js";
9
-
10
- const PROJECT_MARKERS = ["pyproject.toml", "ruff.toml", ".ruff.toml"];
11
-
12
- export class RuffAdapter implements LspServerAdapter {
13
- readonly id = "ruff";
14
- readonly extensions = [".py", ".pyi"];
15
-
16
- findRoot(file: string, cwd: string): Promise<string> {
17
- return nearestRoot(PROJECT_MARKERS, file, cwd);
18
- }
19
-
20
- async spawn(root: string, cwd: string) {
21
- const bin = (await findBinaryInWorkspace("ruff", root, cwd)) ?? which("ruff");
22
- if (!bin) return;
23
- return {
24
- process: spawnProcess(bin, ["server"], { cwd: root }),
25
- };
26
- }
27
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * TypeScript / JavaScript LSP 服务器(typescript-language-server)。
3
- * tsserver 路径解析自项目工作区的 node_modules,语言服务器二进制同样
4
- * 工作区优先、PATH 兜底。
5
- */
6
-
7
- import { type LspServerAdapter, nearestRoot } from "../adapter.js";
8
- import { findBinaryInWorkspace, findModuleInWorkspace, which } from "../bin.js";
9
- import { spawnProcess } from "../launch.js";
10
-
11
- const LOCK_FILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
12
-
13
- export class TypescriptAdapter implements LspServerAdapter {
14
- readonly id = "typescript";
15
- readonly extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
16
-
17
- findRoot(file: string, cwd: string): Promise<string> {
18
- return nearestRoot(LOCK_FILES, file, cwd);
19
- }
20
-
21
- async spawn(root: string, cwd: string) {
22
- // typescript-language-server 依赖项目里的 tsserver(本身不带)
23
- const tsserver = await findModuleInWorkspace("typescript/lib/tsserver.js", root, cwd);
24
- if (!tsserver) return;
25
- const bin =
26
- (await findBinaryInWorkspace("typescript-language-server", root, cwd)) ??
27
- which("typescript-language-server");
28
- if (!bin) return;
29
- return {
30
- process: spawnProcess(bin, ["--stdio"], { cwd: root }),
31
- initialization: { tsserver: { path: tsserver } },
32
- };
33
- }
34
- }