pi-lens 3.8.18 → 3.8.21

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.
@@ -24,20 +24,154 @@ import {
24
24
 
25
25
  export type RootFunction = (file: string) => Promise<string | undefined>;
26
26
 
27
+ export interface LSPSpawnOptions {
28
+ allowInstall?: boolean;
29
+ }
30
+
27
31
  export interface LSPServerInfo {
28
32
  id: string;
29
33
  name: string;
30
34
  extensions: string[];
31
35
  root: RootFunction;
36
+ installPolicy?: "none" | "interactive" | "managed" | "package-manager";
32
37
  spawn(
33
38
  root: string,
39
+ options?: LSPSpawnOptions,
34
40
  ): Promise<
35
- | { process: LSPProcess; initialization?: Record<string, unknown> }
41
+ | {
42
+ process: LSPProcess;
43
+ initialization?: Record<string, unknown>;
44
+ source?: "direct" | "managed" | "package-manager" | "interactive";
45
+ }
36
46
  | undefined
37
47
  >;
38
48
  autoInstall?: () => Promise<boolean>;
39
49
  }
40
50
 
51
+ function isLspInstallDisabled(): boolean {
52
+ return process.env.PI_LENS_DISABLE_LSP_INSTALL === "1";
53
+ }
54
+
55
+ function canInstall(allowInstall?: boolean): boolean {
56
+ return allowInstall !== false && !isLspInstallDisabled();
57
+ }
58
+
59
+ function isCommandNotFoundError(error: unknown): boolean {
60
+ const msg = String(error);
61
+ return msg.includes("not found") || msg.includes("ENOENT");
62
+ }
63
+
64
+ async function launchViaPackageManagerWithPolicy(
65
+ packageName: string,
66
+ args: string[],
67
+ options: { cwd: string; allowInstall?: boolean },
68
+ ): Promise<LSPProcess | undefined> {
69
+ if (!canInstall(options.allowInstall)) {
70
+ return undefined;
71
+ }
72
+ return launchViaPackageManager(packageName, args, options);
73
+ }
74
+
75
+ function nodeBinCandidates(root: string, baseName: string): string[] {
76
+ const localBase = path.join(root, "node_modules", ".bin", baseName);
77
+ if (process.platform === "win32") {
78
+ return [
79
+ `${localBase}.cmd`,
80
+ `${localBase}.ps1`,
81
+ `${localBase}.exe`,
82
+ localBase,
83
+ baseName,
84
+ ];
85
+ }
86
+ return [localBase, baseName];
87
+ }
88
+
89
+ async function launchWithDirectOrPackageManager(
90
+ directCommands: string[],
91
+ packageName: string,
92
+ args: string[],
93
+ options: { cwd: string; env?: NodeJS.ProcessEnv; allowInstall?: boolean },
94
+ ): Promise<{ process: LSPProcess; source: "direct" | "package-manager" } | undefined> {
95
+ for (const command of directCommands) {
96
+ try {
97
+ const process = await launchLSP(command, args, options);
98
+ return { process, source: "direct" };
99
+ } catch (error) {
100
+ if (!isCommandNotFoundError(error)) {
101
+ throw error;
102
+ }
103
+ }
104
+ }
105
+
106
+ const process = await launchViaPackageManagerWithPolicy(packageName, args, {
107
+ cwd: options.cwd,
108
+ allowInstall: options.allowInstall,
109
+ });
110
+ if (!process) return undefined;
111
+ return { process, source: "package-manager" };
112
+ }
113
+
114
+ type InitializationConfig = Record<string, unknown>;
115
+
116
+ interface InteractiveServerSpec {
117
+ id: string;
118
+ name: string;
119
+ extensions: string[];
120
+ root: RootFunction;
121
+ language: string;
122
+ command: string | ((root: string) => string);
123
+ args?: string[] | ((root: string) => string[]);
124
+ initialization?: InitializationConfig | ((root: string) => InitializationConfig);
125
+ }
126
+
127
+ function createInteractiveServer(spec: InteractiveServerSpec): LSPServerInfo {
128
+ return {
129
+ id: spec.id,
130
+ name: spec.name,
131
+ installPolicy: "interactive",
132
+ extensions: spec.extensions,
133
+ root: spec.root,
134
+ async spawn(root, options) {
135
+ const command =
136
+ typeof spec.command === "function" ? spec.command(root) : spec.command;
137
+ const args =
138
+ typeof spec.args === "function"
139
+ ? spec.args(root)
140
+ : spec.args || [];
141
+ const proc = await spawnWithInteractiveInstall(
142
+ spec.language,
143
+ command,
144
+ args,
145
+ { cwd: root, allowInstall: options?.allowInstall },
146
+ async () => await launchLSP(command, args, { cwd: root }),
147
+ );
148
+ if (!proc) return undefined;
149
+ const initialization =
150
+ typeof spec.initialization === "function"
151
+ ? spec.initialization(root)
152
+ : spec.initialization;
153
+ return { process: proc, source: "interactive", initialization };
154
+ },
155
+ };
156
+ }
157
+
158
+ export function PriorityRoot(
159
+ markerGroups: string[][],
160
+ excludePatterns?: string[],
161
+ stopDir?: string,
162
+ ): RootFunction {
163
+ const resolvers = markerGroups.map((markers) =>
164
+ NearestRoot(markers, excludePatterns, stopDir),
165
+ );
166
+ return async (file: string) => {
167
+ for (const resolve of resolvers) {
168
+ const root = await resolve(file);
169
+ if (root) return root;
170
+ }
171
+ return undefined;
172
+ };
173
+ }
174
+
41
175
  // --- Root Detection Helpers ---
42
176
 
43
177
  import { dirname } from "node:path";
@@ -58,12 +192,15 @@ async function spawnWithInteractiveInstall(
58
192
  language: string,
59
193
  _command: string,
60
194
  _args: string[],
61
- options: { cwd: string },
195
+ options: { cwd: string; allowInstall?: boolean },
62
196
  spawnFn: () => LSPProcess | Promise<LSPProcess>,
63
197
  ): Promise<LSPProcess | undefined> {
64
198
  try {
65
199
  return await spawnFn();
66
200
  } catch (error) {
201
+ if (!canInstall(options.allowInstall)) {
202
+ return undefined;
203
+ }
67
204
  // Check if this is a "command not found" error
68
205
  const errorMsg = String(error);
69
206
  if (!errorMsg.includes("not found") && !errorMsg.includes("ENOENT")) {
@@ -103,9 +240,9 @@ export function NearestRoot(
103
240
  stopDir?: string,
104
241
  ): RootFunction {
105
242
  return async (file: string): Promise<string | undefined> => {
106
- let currentDir = path.dirname(file);
243
+ let currentDir = path.resolve(path.dirname(file));
107
244
  const fsRoot = path.parse(currentDir).root;
108
- const stop = stopDir ?? fsRoot;
245
+ const stop = stopDir ? path.resolve(stopDir) : fsRoot;
109
246
 
110
247
  while (currentDir !== fsRoot) {
111
248
  // Bail out if we've reached the stop boundary
@@ -160,6 +297,7 @@ export const createRootDetector = NearestRoot;
160
297
  export const TypeScriptServer: LSPServerInfo = {
161
298
  id: "typescript",
162
299
  name: "TypeScript Language Server",
300
+ installPolicy: "managed",
163
301
  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
164
302
  root: createRootDetector([
165
303
  "package-lock.json",
@@ -169,9 +307,10 @@ export const TypeScriptServer: LSPServerInfo = {
169
307
  "yarn.lock",
170
308
  "package.json",
171
309
  ]),
172
- async spawn(root) {
310
+ async spawn(root, options) {
173
311
  const path = await import("node:path");
174
312
  const fs = await import("node:fs/promises");
313
+ let source: "direct" | "managed" = "direct";
175
314
 
176
315
  // Find typescript-language-server - prefer local project version
177
316
  let lspPath: string | undefined;
@@ -201,7 +340,10 @@ export const TypeScriptServer: LSPServerInfo = {
201
340
 
202
341
  // Fall back to auto-installed version
203
342
  if (!lspPath) {
204
- lspPath = await ensureTool("typescript-language-server");
343
+ if (canInstall(options?.allowInstall)) {
344
+ lspPath = await ensureTool("typescript-language-server");
345
+ source = "managed";
346
+ }
205
347
  if (!lspPath) {
206
348
  console.error("[lsp] typescript-language-server not found");
207
349
  return undefined;
@@ -242,6 +384,26 @@ export const TypeScriptServer: LSPServerInfo = {
242
384
  }
243
385
  }
244
386
 
387
+ if (!tsserverPath && canInstall(options?.allowInstall)) {
388
+ const tscPath = await ensureTool("typescript");
389
+ if (tscPath) {
390
+ const managedTsserverCandidates = [
391
+ path.join(path.dirname(tscPath), "..", "typescript", "lib", "tsserver.js"),
392
+ path.join(path.dirname(tscPath), "..", "..", "typescript", "lib", "tsserver.js"),
393
+ ];
394
+ for (const checkPath of managedTsserverCandidates) {
395
+ try {
396
+ await fs.access(checkPath);
397
+ tsserverPath = checkPath;
398
+ source = "managed";
399
+ break;
400
+ } catch {
401
+ /* not found */
402
+ }
403
+ }
404
+ }
405
+ }
406
+
245
407
  // Use absolute path and proper environment
246
408
  const env = await getToolEnvironment();
247
409
  const proc = await launchLSP(lspPath, ["--stdio"], {
@@ -254,6 +416,7 @@ export const TypeScriptServer: LSPServerInfo = {
254
416
 
255
417
  return {
256
418
  process: proc,
419
+ source,
257
420
  initialization: tsserverPath
258
421
  ? { tsserver: { path: tsserverPath } }
259
422
  : undefined,
@@ -264,8 +427,10 @@ export const TypeScriptServer: LSPServerInfo = {
264
427
  export const PythonServer: LSPServerInfo = {
265
428
  id: "python",
266
429
  name: "Pyright Language Server",
430
+ installPolicy: "managed",
267
431
  extensions: [".py", ".pyi"],
268
432
  root: createRootDetector([
433
+ ".git",
269
434
  "pyproject.toml",
270
435
  "setup.py",
271
436
  "setup.cfg",
@@ -273,10 +438,11 @@ export const PythonServer: LSPServerInfo = {
273
438
  "Pipfile",
274
439
  "poetry.lock",
275
440
  ]),
276
- async spawn(root) {
441
+ async spawn(root, options) {
277
442
  const path = await import("node:path");
278
443
  const fs = await import("node:fs/promises");
279
444
  const env = await getToolEnvironment();
445
+ let source: "direct" | "managed" | "package-manager" = "direct";
280
446
 
281
447
  // Strategy 1: Find pyright - prefer local project version
282
448
  let pyrightPath: string | undefined;
@@ -301,7 +467,10 @@ export const PythonServer: LSPServerInfo = {
301
467
 
302
468
  // Strategy 2: Fall back to auto-installed version
303
469
  if (!pyrightPath) {
304
- pyrightPath = await ensureTool("pyright");
470
+ if (canInstall(options?.allowInstall)) {
471
+ pyrightPath = await ensureTool("pyright");
472
+ source = "managed";
473
+ }
305
474
  if (!pyrightPath) {
306
475
  console.error("[lsp] pyright not found, falling back to npx");
307
476
  }
@@ -327,7 +496,9 @@ export const PythonServer: LSPServerInfo = {
327
496
  try {
328
497
  await fs.access(candidate);
329
498
  langserverPath = candidate;
330
- console.error(`[lsp] Found pyright-langserver: ${candidate}`);
499
+ if (process.env.PI_LENS_DEBUG === "1") {
500
+ console.error(`[lsp] Found pyright-langserver: ${candidate}`);
501
+ }
331
502
  break;
332
503
  } catch {
333
504
  /* not found */
@@ -344,12 +515,27 @@ export const PythonServer: LSPServerInfo = {
344
515
  env,
345
516
  });
346
517
  } else {
347
- // Fallback to npx for auto-download
348
- console.error("[lsp] Falling back to npx for pyright-langserver");
349
- proc = await launchViaPackageManager("pyright-langserver", ["--stdio"], {
350
- cwd: root,
351
- env,
352
- });
518
+ if (!canInstall(options?.allowInstall)) {
519
+ try {
520
+ proc = await launchLSP("pyright-langserver", ["--stdio"], {
521
+ cwd: root,
522
+ env,
523
+ });
524
+ } catch {
525
+ return undefined;
526
+ }
527
+ } else {
528
+ // Fallback to npx for auto-download
529
+ console.error("[lsp] Falling back to npx for pyright-langserver");
530
+ const managed = await launchViaPackageManagerWithPolicy(
531
+ "pyright-langserver",
532
+ ["--stdio"],
533
+ { cwd: root, allowInstall: options?.allowInstall },
534
+ );
535
+ if (!managed) return undefined;
536
+ proc = managed;
537
+ source = "package-manager";
538
+ }
353
539
  }
354
540
 
355
541
  // Detect virtual environment
@@ -377,7 +563,7 @@ export const PythonServer: LSPServerInfo = {
377
563
  }
378
564
  }
379
565
 
380
- return { process: proc, initialization };
566
+ return { process: proc, initialization, source };
381
567
  },
382
568
  };
383
569
 
@@ -385,13 +571,14 @@ export const GoServer: LSPServerInfo = {
385
571
  id: "go",
386
572
  name: "gopls",
387
573
  extensions: [".go"],
388
- root: createRootDetector(["go.mod", "go.sum"]),
389
- async spawn(root) {
574
+ installPolicy: "interactive",
575
+ root: PriorityRoot([["go.work"], ["go.mod", "go.sum"]]),
576
+ async spawn(root, options) {
390
577
  const proc = await spawnWithInteractiveInstall(
391
578
  "go",
392
579
  "gopls",
393
580
  [],
394
- { cwd: root },
581
+ { cwd: root, allowInstall: options?.allowInstall },
395
582
  async () => await launchLSP("gopls", [], { cwd: root }),
396
583
  );
397
584
  // gopls works best with minimal initialization options
@@ -414,13 +601,14 @@ export const RustServer: LSPServerInfo = {
414
601
  id: "rust",
415
602
  name: "rust-analyzer",
416
603
  extensions: [".rs"],
604
+ installPolicy: "interactive",
417
605
  root: createRootDetector(["Cargo.toml", "Cargo.lock"]),
418
- async spawn(root) {
606
+ async spawn(root, options) {
419
607
  const proc = await spawnWithInteractiveInstall(
420
608
  "rust",
421
609
  "rust-analyzer",
422
610
  [],
423
- { cwd: root },
611
+ { cwd: root, allowInstall: options?.allowInstall },
424
612
  async () => await launchLSP("rust-analyzer", [], { cwd: root }),
425
613
  );
426
614
  // rust-analyzer needs minimal initialization to avoid capability mismatches
@@ -443,165 +631,122 @@ export const RustServer: LSPServerInfo = {
443
631
  export const RubyServer: LSPServerInfo = {
444
632
  id: "ruby",
445
633
  name: "Ruby LSP",
634
+ installPolicy: "interactive",
446
635
  extensions: [".rb", ".rake", ".gemspec", ".ru"],
447
- root: createRootDetector(["Gemfile", ".ruby-version"]),
448
- async spawn(root) {
636
+ root: PriorityRoot([["Gemfile", ".ruby-version"], [".git"]]),
637
+ async spawn(root, options) {
449
638
  // Try ruby-lsp first (prompts to install via gem if missing), fall back to solargraph
450
639
  const proc = await spawnWithInteractiveInstall(
451
640
  "ruby",
452
641
  "ruby-lsp",
453
642
  [],
454
- { cwd: root },
643
+ { cwd: root, allowInstall: options?.allowInstall },
455
644
  async () => {
456
645
  try {
457
646
  return await launchLSP("ruby-lsp", [], { cwd: root });
458
647
  } catch {
459
- return await launchViaPackageManager("solargraph", ["stdio"], { cwd: root });
648
+ const fallback = await launchWithDirectOrPackageManager(
649
+ nodeBinCandidates(root, "solargraph"),
650
+ "solargraph",
651
+ ["stdio"],
652
+ { cwd: root, allowInstall: options?.allowInstall },
653
+ );
654
+ if (!fallback) throw new Error("ENOENT: command not found");
655
+ return fallback.process;
460
656
  }
461
657
  },
462
658
  );
463
- return proc ? { process: proc } : undefined;
659
+ return proc ? { process: proc, source: "interactive" } : undefined;
464
660
  },
465
661
  };
466
662
 
467
663
  export const PHPServer: LSPServerInfo = {
468
664
  id: "php",
469
665
  name: "Intelephense",
666
+ installPolicy: "package-manager",
470
667
  extensions: [".php"],
471
668
  root: createRootDetector(["composer.json", "composer.lock"]),
472
- async spawn(root) {
473
- const proc = await launchViaPackageManager("intelephense", ["--stdio"], {
474
- cwd: root,
475
- });
669
+ async spawn(root, options) {
670
+ const launched = await launchWithDirectOrPackageManager(
671
+ nodeBinCandidates(root, "intelephense"),
672
+ "intelephense",
673
+ ["--stdio"],
674
+ { cwd: root, allowInstall: options?.allowInstall },
675
+ );
676
+ if (!launched) return undefined;
476
677
  return {
477
- process: proc,
678
+ process: launched.process,
679
+ source: launched.source,
478
680
  initialization: { storagePath: path.join(__dirname, ".intelephense") },
479
681
  };
480
682
  },
481
683
  };
482
684
 
483
- export const CSharpServer: LSPServerInfo = {
685
+ export const CSharpServer = createInteractiveServer({
484
686
  id: "csharp",
485
687
  name: "csharp-ls",
486
688
  extensions: [".cs"],
487
689
  root: createRootDetector([".sln", ".csproj", ".slnx"]),
488
- async spawn(root) {
489
- const proc = await spawnWithInteractiveInstall(
490
- "csharp",
491
- "csharp-ls",
492
- [],
493
- { cwd: root },
494
- async () => await launchLSP("csharp-ls", [], { cwd: root }),
495
- );
496
- return proc ? { process: proc } : undefined;
497
- },
498
- };
690
+ language: "csharp",
691
+ command: "csharp-ls",
692
+ });
499
693
 
500
- export const FSharpServer: LSPServerInfo = {
694
+ export const FSharpServer = createInteractiveServer({
501
695
  id: "fsharp",
502
696
  name: "FSAutocomplete",
503
697
  extensions: [".fs", ".fsi", ".fsx"],
504
698
  root: createRootDetector([".sln", ".fsproj"]),
505
- async spawn(root) {
506
- const proc = await spawnWithInteractiveInstall(
507
- "fsharp",
508
- "fsautocomplete",
509
- [],
510
- { cwd: root },
511
- async () => await launchLSP("fsautocomplete", [], { cwd: root }),
512
- );
513
- return proc ? { process: proc } : undefined;
514
- },
515
- };
699
+ language: "fsharp",
700
+ command: "fsautocomplete",
701
+ });
516
702
 
517
- export const JavaServer: LSPServerInfo = {
703
+ export const JavaServer = createInteractiveServer({
518
704
  id: "java",
519
705
  name: "JDT Language Server",
520
706
  extensions: [".java"],
521
707
  root: createRootDetector(["pom.xml", "build.gradle", ".classpath"]),
522
- async spawn(root) {
523
- const jdtlsPath = process.env.JDTLS_PATH || "jdtls";
524
- const proc = await spawnWithInteractiveInstall(
525
- "java",
526
- jdtlsPath,
527
- [],
528
- { cwd: root },
529
- async () => await launchLSP(jdtlsPath, [], { cwd: root }),
530
- );
531
- return proc ? { process: proc } : undefined;
532
- },
533
- };
708
+ language: "java",
709
+ command: () => process.env.JDTLS_PATH || "jdtls",
710
+ });
534
711
 
535
- export const KotlinServer: LSPServerInfo = {
712
+ export const KotlinServer = createInteractiveServer({
536
713
  id: "kotlin",
537
714
  name: "Kotlin Language Server",
538
715
  extensions: [".kt", ".kts"],
539
716
  root: createRootDetector(["build.gradle.kts", "build.gradle", "pom.xml"]),
540
- async spawn(root) {
541
- const proc = await spawnWithInteractiveInstall(
542
- "kotlin",
543
- "kotlin-language-server",
544
- [],
545
- { cwd: root },
546
- async () => await launchLSP("kotlin-language-server", [], { cwd: root }),
547
- );
548
- return proc ? { process: proc } : undefined;
549
- },
550
- };
717
+ language: "kotlin",
718
+ command: "kotlin-language-server",
719
+ });
551
720
 
552
- export const SwiftServer: LSPServerInfo = {
721
+ export const SwiftServer = createInteractiveServer({
553
722
  id: "swift",
554
723
  name: "SourceKit-LSP",
555
724
  extensions: [".swift"],
556
725
  root: createRootDetector(["Package.swift"]),
557
- async spawn(root) {
558
- const proc = await spawnWithInteractiveInstall(
559
- "swift",
560
- "sourcekit-lsp",
561
- [],
562
- { cwd: root },
563
- async () => await launchLSP("sourcekit-lsp", [], { cwd: root }),
564
- );
565
- return proc ? { process: proc } : undefined;
566
- },
567
- };
726
+ language: "swift",
727
+ command: "sourcekit-lsp",
728
+ });
568
729
 
569
- export const DartServer: LSPServerInfo = {
730
+ export const DartServer = createInteractiveServer({
570
731
  id: "dart",
571
732
  name: "Dart Analysis Server",
572
733
  extensions: [".dart"],
573
734
  root: createRootDetector(["pubspec.yaml"]),
574
- async spawn(root) {
575
- const proc = await spawnWithInteractiveInstall(
576
- "dart",
577
- "dart",
578
- ["language-server", "--protocol=lsp"],
579
- { cwd: root },
580
- async () =>
581
- await launchLSP("dart", ["language-server", "--protocol=lsp"], { cwd: root }),
582
- );
583
- return proc ? { process: proc } : undefined;
584
- },
585
- };
735
+ language: "dart",
736
+ command: "dart",
737
+ args: ["language-server", "--protocol=lsp"],
738
+ });
586
739
 
587
- export const LuaServer: LSPServerInfo = {
740
+ export const LuaServer = createInteractiveServer({
588
741
  id: "lua",
589
742
  name: "Lua Language Server",
590
743
  extensions: [".lua"],
591
744
  root: createRootDetector([".luarc.json", ".luacheckrc"]),
592
- async spawn(root) {
593
- const proc = await spawnWithInteractiveInstall(
594
- "lua",
595
- "lua-language-server",
596
- [],
597
- { cwd: root },
598
- async () => await launchLSP("lua-language-server", [], { cwd: root }),
599
- );
600
- return proc ? { process: proc } : undefined;
601
- },
602
- };
745
+ language: "lua",
746
+ command: "lua-language-server",
747
+ });
603
748
 
604
- export const CppServer: LSPServerInfo = {
749
+ export const CppServer = createInteractiveServer({
605
750
  id: "cpp",
606
751
  name: "clangd",
607
752
  extensions: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"],
@@ -611,167 +756,99 @@ export const CppServer: LSPServerInfo = {
611
756
  "CMakeLists.txt",
612
757
  "Makefile",
613
758
  ]),
614
- async spawn(root) {
615
- const proc = await spawnWithInteractiveInstall(
616
- "cpp",
617
- "clangd",
618
- ["--background-index"],
619
- { cwd: root },
620
- async () => await launchLSP("clangd", ["--background-index"], { cwd: root }),
621
- );
622
- return proc ? { process: proc } : undefined;
623
- },
624
- };
759
+ language: "cpp",
760
+ command: "clangd",
761
+ args: ["--background-index"],
762
+ });
625
763
 
626
- export const ZigServer: LSPServerInfo = {
764
+ export const ZigServer = createInteractiveServer({
627
765
  id: "zig",
628
766
  name: "ZLS",
629
767
  extensions: [".zig", ".zon"],
630
768
  root: createRootDetector(["build.zig"]),
631
- async spawn(root) {
632
- const proc = await spawnWithInteractiveInstall(
633
- "zig",
634
- "zls",
635
- [],
636
- { cwd: root },
637
- async () => await launchLSP("zls", [], { cwd: root }),
638
- );
639
- return proc ? { process: proc } : undefined;
640
- },
641
- };
769
+ language: "zig",
770
+ command: "zls",
771
+ });
642
772
 
643
- export const HaskellServer: LSPServerInfo = {
773
+ export const HaskellServer = createInteractiveServer({
644
774
  id: "haskell",
645
775
  name: "Haskell Language Server",
646
776
  extensions: [".hs", ".lhs"],
647
777
  root: createRootDetector(["stack.yaml", "cabal.project", "*.cabal"]),
648
- async spawn(root) {
649
- const proc = await spawnWithInteractiveInstall(
650
- "haskell",
651
- "haskell-language-server-wrapper",
652
- ["--lsp"],
653
- { cwd: root },
654
- async () =>
655
- await launchLSP("haskell-language-server-wrapper", ["--lsp"], { cwd: root }),
656
- );
657
- return proc ? { process: proc } : undefined;
658
- },
659
- };
778
+ language: "haskell",
779
+ command: "haskell-language-server-wrapper",
780
+ args: ["--lsp"],
781
+ });
660
782
 
661
- export const ElixirServer: LSPServerInfo = {
783
+ export const ElixirServer = createInteractiveServer({
662
784
  id: "elixir",
663
785
  name: "ElixirLS",
664
786
  extensions: [".ex", ".exs"],
665
787
  root: createRootDetector(["mix.exs"]),
666
- async spawn(root) {
667
- const proc = await spawnWithInteractiveInstall(
668
- "elixir",
669
- "elixir-ls",
670
- [],
671
- { cwd: root },
672
- async () => await launchLSP("elixir-ls", [], { cwd: root }),
673
- );
674
- return proc ? { process: proc } : undefined;
675
- },
676
- };
788
+ language: "elixir",
789
+ command: "elixir-ls",
790
+ });
677
791
 
678
- export const GleamServer: LSPServerInfo = {
792
+ export const GleamServer = createInteractiveServer({
679
793
  id: "gleam",
680
794
  name: "Gleam LSP",
681
795
  extensions: [".gleam"],
682
796
  root: createRootDetector(["gleam.toml"]),
683
- async spawn(root) {
684
- const proc = await spawnWithInteractiveInstall(
685
- "gleam",
686
- "gleam",
687
- ["lsp"],
688
- { cwd: root },
689
- async () => await launchLSP("gleam", ["lsp"], { cwd: root }),
690
- );
691
- return proc ? { process: proc } : undefined;
692
- },
693
- };
797
+ language: "gleam",
798
+ command: "gleam",
799
+ args: ["lsp"],
800
+ });
694
801
 
695
- export const OCamlServer: LSPServerInfo = {
802
+ export const OCamlServer = createInteractiveServer({
696
803
  id: "ocaml",
697
804
  name: "ocamllsp",
698
805
  extensions: [".ml", ".mli"],
699
806
  root: createRootDetector(["dune-project", "opam"]),
700
- async spawn(root) {
701
- const proc = await spawnWithInteractiveInstall(
702
- "ocaml",
703
- "ocamllsp",
704
- [],
705
- { cwd: root },
706
- async () => await launchLSP("ocamllsp", [], { cwd: root }),
707
- );
708
- return proc ? { process: proc } : undefined;
709
- },
710
- };
807
+ language: "ocaml",
808
+ command: "ocamllsp",
809
+ });
711
810
 
712
- export const ClojureServer: LSPServerInfo = {
811
+ export const ClojureServer = createInteractiveServer({
713
812
  id: "clojure",
714
813
  name: "Clojure LSP",
715
814
  extensions: [".clj", ".cljs", ".cljc", ".edn"],
716
815
  root: createRootDetector(["deps.edn", "project.clj"]),
717
- async spawn(root) {
718
- const proc = await spawnWithInteractiveInstall(
719
- "clojure",
720
- "clojure-lsp",
721
- [],
722
- { cwd: root },
723
- async () => await launchLSP("clojure-lsp", [], { cwd: root }),
724
- );
725
- return proc ? { process: proc } : undefined;
726
- },
727
- };
816
+ language: "clojure",
817
+ command: "clojure-lsp",
818
+ });
728
819
 
729
- export const TerraformServer: LSPServerInfo = {
820
+ export const TerraformServer = createInteractiveServer({
730
821
  id: "terraform",
731
822
  name: "Terraform LSP",
732
823
  extensions: [".tf", ".tfvars"],
733
824
  root: createRootDetector([".terraform.lock.hcl"]),
734
- async spawn(root) {
735
- const proc = await spawnWithInteractiveInstall(
736
- "terraform",
737
- "terraform-ls",
738
- ["serve"],
739
- { cwd: root },
740
- async () => await launchLSP("terraform-ls", ["serve"], { cwd: root }),
741
- );
742
- return proc ? { process: proc } : undefined;
743
- },
744
- };
825
+ language: "terraform",
826
+ command: "terraform-ls",
827
+ args: ["serve"],
828
+ });
745
829
 
746
- export const NixServer: LSPServerInfo = {
830
+ export const NixServer = createInteractiveServer({
747
831
  id: "nix",
748
832
  name: "nixd",
749
833
  extensions: [".nix"],
750
834
  root: createRootDetector(["flake.nix"]),
751
- async spawn(root) {
752
- const proc = await spawnWithInteractiveInstall(
753
- "nix",
754
- "nixd",
755
- [],
756
- { cwd: root },
757
- async () => await launchLSP("nixd", [], { cwd: root }),
758
- );
759
- return proc ? { process: proc } : undefined;
760
- },
761
- };
835
+ language: "nix",
836
+ command: "nixd",
837
+ });
762
838
 
763
839
  export const BashServer: LSPServerInfo = {
764
840
  id: "bash",
765
841
  name: "Bash Language Server",
766
842
  extensions: [".sh", ".bash", ".zsh"],
843
+ installPolicy: "interactive",
767
844
  root: async () => process.cwd(),
768
- async spawn() {
845
+ async spawn(_root, options) {
769
846
  const cwd = process.cwd();
770
847
  const proc = await spawnWithInteractiveInstall(
771
848
  "bash",
772
849
  "bash-language-server",
773
850
  ["start"],
774
- { cwd },
851
+ { cwd, allowInstall: options?.allowInstall },
775
852
  async () => await launchLSP("bash-language-server", ["start"], {}),
776
853
  );
777
854
  return proc ? { process: proc } : undefined;
@@ -781,16 +858,18 @@ export const BashServer: LSPServerInfo = {
781
858
  export const DockerServer: LSPServerInfo = {
782
859
  id: "docker",
783
860
  name: "Dockerfile Language Server",
861
+ installPolicy: "package-manager",
784
862
  extensions: [".dockerfile", "Dockerfile"],
785
- root: async () => process.cwd(),
786
- async spawn() {
787
- // Use npx since it's not auto-installed
788
- const proc = await launchViaPackageManager(
863
+ root: PriorityRoot([["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"], [".git"]]),
864
+ async spawn(_root, options) {
865
+ const launched = await launchWithDirectOrPackageManager(
866
+ nodeBinCandidates(process.cwd(), "docker-langserver"),
789
867
  "dockerfile-language-server-nodejs",
790
868
  ["--stdio"],
791
- {},
869
+ { cwd: process.cwd(), allowInstall: options?.allowInstall },
792
870
  );
793
- return { process: proc };
871
+ if (!launched) return undefined;
872
+ return { process: launched.process, source: launched.source };
794
873
  },
795
874
  };
796
875
 
@@ -798,14 +877,15 @@ export const YamlServer: LSPServerInfo = {
798
877
  id: "yaml",
799
878
  name: "YAML Language Server",
800
879
  extensions: [".yaml", ".yml"],
801
- root: async () => process.cwd(),
802
- async spawn() {
880
+ installPolicy: "interactive",
881
+ root: PriorityRoot([[".yamllint", "yamllint.yml", "yamllint.yaml", "pyproject.toml"], [".git"]]),
882
+ async spawn(_root, options) {
803
883
  const cwd = process.cwd();
804
884
  const proc = await spawnWithInteractiveInstall(
805
885
  "yaml",
806
886
  "yaml-language-server",
807
887
  ["--stdio"],
808
- { cwd },
888
+ { cwd, allowInstall: options?.allowInstall },
809
889
  async () => await launchLSP("yaml-language-server", ["--stdio"], {}),
810
890
  );
811
891
  return proc ? { process: proc } : undefined;
@@ -816,14 +896,15 @@ export const JsonServer: LSPServerInfo = {
816
896
  id: "json",
817
897
  name: "VSCode JSON Language Server",
818
898
  extensions: [".json", ".jsonc"],
819
- root: async () => process.cwd(),
820
- async spawn() {
899
+ installPolicy: "interactive",
900
+ root: PriorityRoot([["package.json", "tsconfig.json", "jsconfig.json"], [".git"]]),
901
+ async spawn(_root, options) {
821
902
  const cwd = process.cwd();
822
903
  const proc = await spawnWithInteractiveInstall(
823
904
  "json",
824
905
  "vscode-json-language-server",
825
906
  ["--stdio"],
826
- { cwd },
907
+ { cwd, allowInstall: options?.allowInstall },
827
908
  async () =>
828
909
  await launchLSP("vscode-json-language-server", ["--stdio"], {}),
829
910
  );
@@ -834,16 +915,18 @@ export const JsonServer: LSPServerInfo = {
834
915
  export const PrismaServer: LSPServerInfo = {
835
916
  id: "prisma",
836
917
  name: "Prisma Language Server",
918
+ installPolicy: "package-manager",
837
919
  extensions: [".prisma"],
838
920
  root: createRootDetector(["prisma/schema.prisma"]),
839
- async spawn(root) {
840
- // Use npx since it's not auto-installed
841
- const proc = await launchViaPackageManager(
921
+ async spawn(root, options) {
922
+ const launched = await launchWithDirectOrPackageManager(
923
+ nodeBinCandidates(root, "prisma-language-server"),
842
924
  "@prisma/language-server",
843
925
  ["--stdio"],
844
- { cwd: root },
926
+ { cwd: root, allowInstall: options?.allowInstall },
845
927
  );
846
- return { process: proc };
928
+ if (!launched) return undefined;
929
+ return { process: launched.process, source: launched.source };
847
930
  },
848
931
  };
849
932
 
@@ -853,6 +936,7 @@ export const VueServer: LSPServerInfo = {
853
936
  id: "vue",
854
937
  name: "Vue Language Server",
855
938
  extensions: [".vue"],
939
+ installPolicy: "package-manager",
856
940
  root: createRootDetector([
857
941
  "package-lock.json",
858
942
  "bun.lockb",
@@ -860,16 +944,15 @@ export const VueServer: LSPServerInfo = {
860
944
  "pnpm-lock.yaml",
861
945
  "yarn.lock",
862
946
  ]),
863
- async spawn(root) {
864
- // Use npx since it's not auto-installed
865
- const proc = await launchViaPackageManager(
947
+ async spawn(root, options) {
948
+ const launched = await launchWithDirectOrPackageManager(
949
+ nodeBinCandidates(root, "vue-language-server"),
866
950
  "@vue/language-server",
867
951
  ["--stdio"],
868
- {
869
- cwd: root,
870
- },
952
+ { cwd: root, allowInstall: options?.allowInstall },
871
953
  );
872
- return { process: proc };
954
+ if (!launched) return undefined;
955
+ return { process: launched.process, source: launched.source };
873
956
  },
874
957
  };
875
958
 
@@ -877,6 +960,7 @@ export const SvelteServer: LSPServerInfo = {
877
960
  id: "svelte",
878
961
  name: "Svelte Language Server",
879
962
  extensions: [".svelte"],
963
+ installPolicy: "package-manager",
880
964
  root: createRootDetector([
881
965
  "package-lock.json",
882
966
  "bun.lockb",
@@ -884,20 +968,22 @@ export const SvelteServer: LSPServerInfo = {
884
968
  "pnpm-lock.yaml",
885
969
  "yarn.lock",
886
970
  ]),
887
- async spawn(root) {
888
- // Use npx since it's not auto-installed
889
- const proc = await launchViaPackageManager(
971
+ async spawn(root, options) {
972
+ const launched = await launchWithDirectOrPackageManager(
973
+ [...nodeBinCandidates(root, "svelteserver"), ...nodeBinCandidates(root, "svelte-language-server")],
890
974
  "svelte-language-server",
891
975
  ["--stdio"],
892
- { cwd: root },
976
+ { cwd: root, allowInstall: options?.allowInstall },
893
977
  );
894
- return { process: proc };
978
+ if (!launched) return undefined;
979
+ return { process: launched.process, source: launched.source };
895
980
  },
896
981
  };
897
982
 
898
983
  export const ESLintServer: LSPServerInfo = {
899
984
  id: "eslint",
900
985
  name: "ESLint Language Server",
986
+ installPolicy: "package-manager",
901
987
  extensions: [".js", ".jsx", ".vue", ".svelte"], // Note: .ts/.tsx handled by TypeScript LSP + Biome
902
988
  root: createRootDetector([
903
989
  ".eslintrc",
@@ -907,15 +993,17 @@ export const ESLintServer: LSPServerInfo = {
907
993
  "eslint.config.mjs",
908
994
  "package.json",
909
995
  ]),
910
- async spawn(root) {
996
+ async spawn(root, options) {
911
997
  // Try via package manager (npx) since it's not auto-installed
912
998
  try {
913
- const proc = await launchViaPackageManager(
999
+ const launched = await launchWithDirectOrPackageManager(
1000
+ nodeBinCandidates(root, "vscode-eslint-language-server"),
914
1001
  "vscode-eslint-language-server",
915
1002
  ["--stdio"],
916
- { cwd: root },
1003
+ { cwd: root, allowInstall: options?.allowInstall },
917
1004
  );
918
- return { process: proc };
1005
+ if (!launched) return undefined;
1006
+ return { process: launched.process, source: launched.source };
919
1007
  } catch {
920
1008
  // Fall back to global install message
921
1009
  console.error(
@@ -929,16 +1017,18 @@ export const ESLintServer: LSPServerInfo = {
929
1017
  export const CssServer: LSPServerInfo = {
930
1018
  id: "css",
931
1019
  name: "CSS Language Server",
1020
+ installPolicy: "package-manager",
932
1021
  extensions: [".css", ".scss", ".sass", ".less"],
933
- root: async () => process.cwd(),
934
- async spawn() {
935
- // Use npx since it's not auto-installed
936
- const proc = await launchViaPackageManager(
1022
+ root: PriorityRoot([["package.json", "postcss.config.js", "tailwind.config.js", "vite.config.ts"], [".git"]]),
1023
+ async spawn(_root, options) {
1024
+ const launched = await launchWithDirectOrPackageManager(
1025
+ nodeBinCandidates(process.cwd(), "vscode-css-language-server"),
937
1026
  "vscode-css-languageserver",
938
1027
  ["--stdio"],
939
- {},
1028
+ { cwd: process.cwd(), allowInstall: options?.allowInstall },
940
1029
  );
941
- return { process: proc };
1030
+ if (!launched) return undefined;
1031
+ return { process: launched.process, source: launched.source };
942
1032
  },
943
1033
  };
944
1034