coc-vscode-loader 1.1.8 → 1.2.0

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.
@@ -39,8 +39,7 @@ export const transformClassToFactory: Transform = (ctx) => {
39
39
  (match, type) => `${type}.create(`
40
40
  )
41
41
 
42
- // CompletionItem.create(label) doesn't accept kind in coc.
43
- // Convert `CompletionItem.create(label, kind)` to `item = CompletionItem.create(label); item.kind = kind`
42
+ // CompletionItem.create(label, kind) item = CompletionItem.create(label); item.kind = kind
44
43
  text = text.replace(
45
44
  /const\s+(\w+)\s*=\s*CompletionItem\.create\(([^,]+),\s*([^)]+)\)/g,
46
45
  (_, varName, label, kind) => {
@@ -19,23 +19,6 @@ export const transformEnumOffset: Transform = (ctx) => {
19
19
  const { file } = ctx
20
20
  let content = file.getText()
21
21
 
22
- // Detect hardcoded numbers used in enum position (e.g., CompletionItemKind.Xxx).
23
- // This is hard to detect perfectly, so we log a note when numeric literals
24
- // appear near enum-type names.
25
- const enumPatterns = [
26
- 'CompletionItemKind', 'SymbolKind', 'DocumentHighlightKind', 'DiagnosticSeverity',
27
- 'CompletionTriggerKind', 'InlineCompletionTriggerKind',
28
- ]
29
-
30
- for (const enumName of enumPatterns) {
31
- // Check if the enum is imported/used with a hardcoded number nearby
32
- const enumRefs = content.match(new RegExp(`${enumName}\\.\\w+`, 'g'))
33
- if (enumRefs) {
34
- // Symbol references are fine - they resolve at runtime
35
- // Only note if there are raw numbers being compared
36
- }
37
- }
38
-
39
22
  // Replace any numeric enum comparisons with comments
40
23
  // e.g., `severity === 0` → `severity === 0 /* DiagnosticSeverity.Error = 1 in coc */`
41
24
  content = content.replace(
@@ -1,3 +1,4 @@
1
+ import * as path from 'path'
1
2
  import { Transform } from '../types.js'
2
3
 
3
4
  /**
@@ -32,10 +33,15 @@ export const transformProviderRegister: Transform = (ctx) => {
32
33
  }
33
34
 
34
35
  // 2. registerCompletionItemProvider: insert name + shortcut at beginning
36
+ // Shortcut derived from source file name (first 2 uppercase letters)
37
+ const fileName = file.getFilePath() || 'plugin'
38
+ const baseName = path.basename(fileName, '.ts')
39
+ const shortcut = baseName.replace(/[^a-zA-Z]/g, '').substring(0, 2).toUpperCase() || 'PL'
35
40
  if (content.includes('registerCompletionItemProvider')) {
41
+ const pluginName = path.basename(path.dirname(path.dirname(fileName))) || 'plugin'
36
42
  content = content.replace(
37
43
  /registerCompletionItemProvider\(/g,
38
- `registerCompletionItemProvider('plugin', 'PL', `
44
+ `registerCompletionItemProvider('${pluginName}', '${shortcut}', `
39
45
  )
40
46
  // Wrap the last argument in an array if it's a string (trigger chars)
41
47
  content = content.replace(
@@ -0,0 +1,29 @@
1
+ import { Transform } from '../types.js'
2
+
3
+ /**
4
+ * Remove Volar-specific framework imports that are not compatible with coc.nvim.
5
+ * The Volar extension uses @volar/vscode and reactive-vscode meta-framework,
6
+ * which need to be stripped since the bridge step provides alternative setup.
7
+ */
8
+ export const transformStripVolar: Transform = (ctx) => {
9
+ const { file } = ctx
10
+ let content = file.getText()
11
+ let changed = false
12
+
13
+ const patterns = [
14
+ /import .* from ['"]@volar\/vscode['"];?\n?/g,
15
+ /import .* from ['"]reactive-vscode['"];?\n?/g,
16
+ /import \* as lsp from ['"]@volar\/vscode\/node['"];?\n?/g,
17
+ ]
18
+
19
+ for (const re of patterns) {
20
+ if (re.test(content)) {
21
+ content = content.replace(re, '')
22
+ changed = true
23
+ }
24
+ }
25
+
26
+ if (changed) {
27
+ file.replaceWithText(content)
28
+ }
29
+ }
@@ -6,3 +6,120 @@ export interface TransformContext {
6
6
  }
7
7
 
8
8
  export type Transform = (ctx: TransformContext) => void
9
+
10
+ // ---- Step type definitions ----
11
+
12
+ export interface ServerModuleConfig {
13
+ kind: 'module'
14
+ package: string
15
+ entry?: 'main' | 'bin'
16
+ }
17
+
18
+ export interface ServerBinaryConfig {
19
+ kind: 'binary'
20
+ package: string
21
+ binary: {
22
+ repo: string
23
+ asset: string
24
+ binaryPath?: string
25
+ }
26
+ args?: string[]
27
+ }
28
+
29
+ export type ServerConfig = ServerModuleConfig | ServerBinaryConfig
30
+
31
+ export interface LanguageClientStep {
32
+ type: 'language-client'
33
+ id?: string
34
+ server: ServerConfig
35
+ transport?: 'ipc' | 'stdio'
36
+ languages: string[]
37
+ multiRoot?: boolean
38
+ /** Enable debug logging in generated code */
39
+ verbose?: boolean
40
+ }
41
+
42
+ export interface SourceStep {
43
+ type: 'source'
44
+ transforms: string[]
45
+ entry?: string
46
+ keepDeps?: string[] | Record<string, string>
47
+ activationEvents?: string[]
48
+ /** Enable debug logging in generated/transformed code */
49
+ verbose?: boolean
50
+ }
51
+
52
+ export interface BridgeStep {
53
+ type: 'bridge'
54
+ preset?: string
55
+ /** Override preset options (extensions, services, etc.) */
56
+ options?: {
57
+ extensions?: string[]
58
+ services?: string[]
59
+ }
60
+ /** Enable debug logging in generated bridge code */
61
+ verbose?: boolean
62
+ }
63
+
64
+ export interface MarkUnsupportedStep {
65
+ type: 'mark-unsupported'
66
+ features: string[]
67
+ /** Enable detailed output during conversion */
68
+ verbose?: boolean
69
+ }
70
+
71
+ export type ConvertStep = LanguageClientStep | SourceStep | BridgeStep | MarkUnsupportedStep
72
+
73
+ // ---- Step execution ----
74
+
75
+ export interface StepResult {
76
+ generatedFiles: Array<{ path: string; content: string }>
77
+ entryPoint?: string
78
+ keepDeps: Record<string, string>
79
+ activationEvents: string[]
80
+ serverBinary?: {
81
+ repo: string
82
+ asset: string
83
+ binaryPath?: string
84
+ args?: string[]
85
+ }
86
+ /** Code to inject into previously generated files (target path, code to insert, insertion point) */
87
+ codeInjections?: Array<{
88
+ target: string // file to modify (e.g. 'src/index.ts')
89
+ importCode?: string // import line to add at top
90
+ insertBefore?: string // regex pattern to insert code before
91
+ insertAfter?: string // regex pattern to insert code after
92
+ code: string // code to insert
93
+ }>
94
+ }
95
+
96
+ export interface StepContext {
97
+ input: string
98
+ output: string
99
+ project: Project
100
+ origPkg: Record<string, any>
101
+ verbose?: boolean
102
+ /** Preset definitions from registry (e.g. bridge presets) */
103
+ presets?: Record<string, any>
104
+ }
105
+
106
+ export interface StepGenerator {
107
+ type: string
108
+ generate(ctx: StepContext, step: ConvertStep): StepResult
109
+ }
110
+
111
+ export function isLanguageClientStep(s: ConvertStep): s is LanguageClientStep {
112
+ return s.type === 'language-client'
113
+ }
114
+
115
+ export function isSourceStep(s: ConvertStep): s is SourceStep {
116
+ return s.type === 'source'
117
+ }
118
+
119
+ export function isBridgeStep(s: ConvertStep): s is BridgeStep {
120
+ return s.type === 'bridge'
121
+ }
122
+
123
+ export function isMarkUnsupportedStep(s: ConvertStep): s is MarkUnsupportedStep {
124
+ return s.type === 'mark-unsupported'
125
+ }
package/lib/index.js CHANGED
@@ -39,7 +39,7 @@ var require_package = __commonJS({
39
39
  "package.json"(exports2, module2) {
40
40
  module2.exports = {
41
41
  name: "coc-vscode-loader",
42
- version: "1.1.8",
42
+ version: "1.2.0",
43
43
  description: "Run VS Code extensions seamlessly in coc.nvim",
44
44
  main: "lib/index.js",
45
45
  keywords: [
@@ -60,7 +60,8 @@ var require_package = __commonJS({
60
60
  homepage: "https://www.npmjs.com/package/coc-vscode-loader",
61
61
  files: [
62
62
  "lib/",
63
- "converter/",
63
+ "converter/src/",
64
+ "converter/package.json",
64
65
  "assets/"
65
66
  ],
66
67
  license: "MIT",
@@ -68,7 +69,7 @@ var require_package = __commonJS({
68
69
  coc: ">= 0.0.80"
69
70
  },
70
71
  scripts: {
71
- "bundle-converter": "if [ -d ../converter ]; then rm -rf converter && cp -r ../converter ./converter && cd converter && npm install --legacy-peer-deps 2>/dev/null && npm prune --production 2>/dev/null; fi",
72
+ "bundle-converter": "if [ -d ../converter ]; then rm -rf converter && cp -r ../converter ./converter && cd converter && npm install --legacy-peer-deps 2>/dev/null; fi",
72
73
  build: "npm run bundle-converter && node esbuild.mjs",
73
74
  prepare: "npm run bundle-converter && node esbuild.mjs"
74
75
  },
@@ -158,9 +159,13 @@ async function fetchRegistryJSON(url) {
158
159
  (0, import_child_process.execFile)("curl", ["-sL", url], { encoding: "utf-8", maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
159
160
  if (err) reject(new Error(`curl failed: ${err.message}`));
160
161
  else {
161
- const data = JSON.parse(stdout);
162
- if (!Array.isArray(data)) reject(new Error("Invalid registry format"));
163
- else resolve2(data);
162
+ try {
163
+ const data = JSON.parse(stdout);
164
+ if (!Array.isArray(data)) reject(new Error("Invalid registry format"));
165
+ else resolve2(data);
166
+ } catch (e) {
167
+ reject(new Error(`Invalid JSON from registry: ${e.message}`));
168
+ }
164
169
  }
165
170
  });
166
171
  });
@@ -436,8 +441,10 @@ function pluginDir(name) {
436
441
  function converterCliPath() {
437
442
  const base = path3.resolve(__dirname, "..");
438
443
  const candidates = [
439
- path3.join(base, "converter", "src", "cli.ts"),
440
- path3.join(base, "..", "converter", "src", "cli.ts")
444
+ // In dev mode (symlink), the converter is at repo root
445
+ path3.join(base, "..", "converter", "src", "cli.ts"),
446
+ // In npm install, the converter is bundled inside the package
447
+ path3.join(base, "converter", "src", "cli.ts")
441
448
  ];
442
449
  for (const p of candidates) {
443
450
  if (fs3.existsSync(p)) return p;
@@ -449,7 +456,7 @@ function converterCliPath() {
449
456
  var CMD_TIMEOUT = 3e5;
450
457
  async function run(cmd, args, cwd, onLine) {
451
458
  return new Promise((resolve2, reject) => {
452
- const child = (0, import_child_process2.spawn)(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: true });
459
+ const child = (0, import_child_process2.spawn)(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
453
460
  const timer = setTimeout(() => {
454
461
  child.kill("SIGTERM");
455
462
  reject(new Error(`Timed out after ${CMD_TIMEOUT / 1e3}s: ${cmd} ${args.join(" ")}`));
@@ -491,13 +498,33 @@ async function downloadSource(info, name, onProgress) {
491
498
  }
492
499
  return info.source.subdir ? path3.join(srcDir, info.source.subdir) : srcDir;
493
500
  }
494
- async function convertSource(inputDir, name, onProgress) {
501
+ async function convertSource(inputDir, name, info, onProgress) {
495
502
  const build = buildDir(name);
496
503
  if (fs3.existsSync(build)) fs3.rmSync(build, { recursive: true });
497
504
  const cli = converterCliPath();
505
+ const converterDir = path3.resolve(path3.dirname(path3.dirname(cli)));
506
+ if (!fs3.existsSync(path3.join(converterDir, "node_modules", "commander"))) {
507
+ onProgress(2, 5, "Installing converter dependencies...", "");
508
+ const log2 = (chunk) => onProgress(2, 5, chunk.trim(), "");
509
+ await run("npm", ["install", "--legacy-peer-deps", "--production"], converterDir, log2);
510
+ }
511
+ if (!info.convert || !Array.isArray(info.convert) || info.convert.length === 0) {
512
+ throw new Error(`Registry entry "${name}" has no "convert" config. Please update the registry.`);
513
+ }
514
+ const convertFile = path3.join(cacheDir(name), "convert-config.json");
515
+ fs3.mkdirSync(path3.dirname(convertFile), { recursive: true });
516
+ fs3.writeFileSync(convertFile, JSON.stringify(info.convert));
517
+ const presetsFile = path3.join(cacheDir(name), "presets-config.json");
518
+ const registryDir = path3.resolve(__dirname, "..", "..", "coc-vscode-registry");
519
+ const presetsPath = path3.join(registryDir, "presets.json");
520
+ if (fs3.existsSync(presetsPath)) {
521
+ fs3.writeFileSync(presetsFile, fs3.readFileSync(presetsPath));
522
+ }
523
+ const args = ["tsx", cli, "convert", inputDir, "-o", build, "--convert-file", convertFile];
524
+ if (fs3.existsSync(presetsFile)) args.push("--presets-file", presetsFile);
498
525
  onProgress(2, 5, "Converting...", `converter convert ${inputDir} -o ${build}`);
499
526
  const log = (chunk) => onProgress(2, 5, chunk.trim(), "");
500
- await run("npx", ["tsx", cli, "convert", inputDir, "-o", build], cacheDir(name), log);
527
+ await run("npx", args, cacheDir(name), log);
501
528
  }
502
529
  async function buildPackage(name, inputDir, info, onProgress) {
503
530
  const build = buildDir(name);
@@ -607,56 +634,18 @@ async function buildPackage(name, inputDir, info, onProgress) {
607
634
  }
608
635
  const indexPath = path3.join(build, "lib", "index.js");
609
636
  if (fs3.existsSync(indexPath)) {
610
- const binPath = (sb.binaryPath || sb.asset.split(/-?\{\{/)[0]).replace(/\{\{version}}/g, version).replace(/\{\{platform}}/g, platform).replace(/\{\{arch}}/g, arch2).replace(/\{\{raw-arch}}/g, rawArch).replace(/\{\{rust-target}}/g, rustTarget);
611
637
  let code = fs3.readFileSync(indexPath, "utf-8");
612
- const svrArgs = sb.args?.length ? JSON.stringify(sb.args) : "[]";
613
- code = code.replace(
614
- /\{ module:\s*serverModule,\s*transport:\s*\w+\.TransportKind\.\w+\s*\}/,
615
- `{ command: serverModule, args: ${svrArgs} }`
616
- );
617
- const serverPath = `require('path').join(__dirname, '..', 'server', '${binPath}')`;
618
- code = code.replace(
619
- /try\s*\{[^}]*?require\.resolve\([^)]+\)\s*;?\s*\}\s*catch\s*\{\s*\}/g,
620
- `try { serverModule = ${serverPath} } catch {}`
621
- );
622
- code = code.replace(
623
- /let\s+serverModule\s*=\s*config\.get\([^)]+\)\s*;?\s*/g,
624
- `let serverModule = ${serverPath};`
625
- );
638
+ code = code.replace(/\{\{version}}/g, version);
639
+ code = code.replace(/\{\{platform}}/g, platform);
640
+ code = code.replace(/\{\{arch}}/g, arch2);
641
+ code = code.replace(/\{\{raw-arch}}/g, rawArch);
642
+ code = code.replace(/\{\{rust-target}}/g, rustTarget);
626
643
  fs3.writeFileSync(indexPath, code);
627
644
  }
628
645
  } catch (e) {
629
646
  onProgress(4, 5, `Warning: serverBinary setup failed (${e.message})`, "install server binary manually");
630
647
  }
631
648
  }
632
- const docSelPath = path3.join(build, "lib", "index.js");
633
- if (fs3.existsSync(docSelPath)) {
634
- let code = fs3.readFileSync(docSelPath, "utf-8");
635
- const langSelector = info.languages.map((l) => `{ scheme: "file", language: "${l}" }`).join(", ");
636
- code = code.replace(
637
- /documentSelector:\s*\[\s*\{[^}]*?language:\s*['"][^'"]*['"][^}]*\}\s*\]/,
638
- `documentSelector: [${langSelector}]`
639
- );
640
- code = code.replace(
641
- /client\.start\(\);/g,
642
- "client.start().catch(() => {/* init may complete async */});"
643
- );
644
- fs3.writeFileSync(docSelPath, code);
645
- }
646
- const pkgPath = path3.join(build, "package.json");
647
- if (fs3.existsSync(pkgPath)) {
648
- try {
649
- const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
650
- const events = pkg.activationEvents || [];
651
- const langEvents = info.languages.map((l) => `onLanguage:${l}`);
652
- const newEvents = events.filter((e) => !e.startsWith("onLanguage:")).concat(langEvents);
653
- if (newEvents.length > 0 && JSON.stringify(newEvents) !== JSON.stringify(events)) {
654
- pkg.activationEvents = newEvents;
655
- fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
656
- }
657
- } catch {
658
- }
659
- }
660
649
  }
661
650
  function extensionsPkgPath() {
662
651
  return path3.join(os3.homedir(), ".config", "coc", "extensions", "package.json");
@@ -669,7 +658,7 @@ async function installToCoc(name, onProgress) {
669
658
  fs3.mkdirSync(path3.dirname(dest), { recursive: true });
670
659
  fs3.cpSync(src, dest, { recursive: true });
671
660
  const pkgPath = extensionsPkgPath();
672
- const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
661
+ const pkg = fs3.existsSync(pkgPath) ? JSON.parse(fs3.readFileSync(pkgPath, "utf-8")) : { dependencies: {} };
673
662
  pkg.dependencies = pkg.dependencies || {};
674
663
  const depName = `coc-${name}`;
675
664
  if (!pkg.dependencies[depName]) {
@@ -707,7 +696,7 @@ async function installPackage(state, name) {
707
696
  state.setPackageStatus(name, "installing", { progress: "Starting..." });
708
697
  try {
709
698
  const input = await downloadSource(info, name, prog);
710
- await convertSource(input, name, prog);
699
+ await convertSource(input, name, info, prog);
711
700
  await buildPackage(name, input, info, prog);
712
701
  await installToCoc(name, prog);
713
702
  await saveMeta(name);
@@ -779,13 +768,13 @@ async function updatePackage(state, name) {
779
768
  state.setPackageStatus(name, "updating", { progress: "Starting..." });
780
769
  try {
781
770
  const input = await downloadSource(info, name, prog);
782
- await convertSource(input, name, prog);
771
+ await convertSource(input, name, info, prog);
783
772
  await buildPackage(name, input, info, prog);
784
773
  await installToCoc(name, prog);
785
774
  await saveMeta(name);
786
775
  state.setDirty();
787
776
  state.setPackageStatus(name, "installed");
788
- import_coc.window.showInformationMessage(`coc-${name} installed`);
777
+ import_coc.window.showInformationMessage(`coc-${name} updated`);
789
778
  try {
790
779
  const meta = JSON.parse(fs3.readFileSync(metaPath(name), "utf-8"));
791
780
  if (meta.commit) {
@@ -807,7 +796,7 @@ async function updatePackage(state, name) {
807
796
  }
808
797
  async function runWithOutput(cmd, args, cwd) {
809
798
  return new Promise((resolve2, reject) => {
810
- const child = (0, import_child_process2.spawn)(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: true });
799
+ const child = (0, import_child_process2.spawn)(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
811
800
  const timer = setTimeout(() => {
812
801
  child.kill("SIGTERM");
813
802
  reject(new Error(`Timed out after ${CMD_TIMEOUT / 1e3}s: ${cmd} ${args.join(" ")}`));
@@ -1220,7 +1209,7 @@ var TUI = class {
1220
1209
  return;
1221
1210
  }
1222
1211
  if (id === "X" && entry.status === "installed") {
1223
- uninstallPackage(this.state, pkgName);
1212
+ await uninstallPackage(this.state, pkgName);
1224
1213
  return;
1225
1214
  }
1226
1215
  if (id === "R" && entry.status === "installed") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coc-vscode-loader",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "Run VS Code extensions seamlessly in coc.nvim",
5
5
  "main": "lib/index.js",
6
6
  "keywords": [
@@ -21,7 +21,8 @@
21
21
  "homepage": "https://www.npmjs.com/package/coc-vscode-loader",
22
22
  "files": [
23
23
  "lib/",
24
- "converter/",
24
+ "converter/src/",
25
+ "converter/package.json",
25
26
  "assets/"
26
27
  ],
27
28
  "license": "MIT",
@@ -29,7 +30,7 @@
29
30
  "coc": ">= 0.0.80"
30
31
  },
31
32
  "scripts": {
32
- "bundle-converter": "if [ -d ../converter ]; then rm -rf converter && cp -r ../converter ./converter && cd converter && npm install --legacy-peer-deps 2>/dev/null && npm prune --production 2>/dev/null; fi",
33
+ "bundle-converter": "if [ -d ../converter ]; then rm -rf converter && cp -r ../converter ./converter && cd converter && npm install --legacy-peer-deps 2>/dev/null; fi",
33
34
  "build": "npm run bundle-converter && node esbuild.mjs",
34
35
  "prepare": "npm run bundle-converter && node esbuild.mjs"
35
36
  },
@@ -1,134 +0,0 @@
1
- # converter — vscode → coc converter prototype
2
-
3
- CLI tool that automatically converts VS Code extensions to coc.nvim plugins.
4
-
5
- ## Usage
6
-
7
- ```bash
8
- # Convert a VS Code extension directory
9
- npx tsx src/cli.ts convert ./vscode-ext/ -o ./coc-ext/
10
-
11
- # Build and install to coc
12
- cd ./coc-ext && npm install && npm run build
13
- cd ~/.config/coc/extensions && npm install /path/to/coc-ext
14
- ```
15
-
16
- ## Verified conversions
17
-
18
- | Plugin | Type | Auto-detected | Build | Working | Notes |
19
- |--------|------|---------------|-------|---------|-------|
20
- | Volar (Vue) | TS bridge | `@vue/language-server` + `typescript` | ✅ | ✅ | Requires modified coc-tsserver |
21
- | Prisma | Pure LSP | `@prisma/language-server` | ✅ | ✅ | Auto-detects bin entry |
22
- | HTML CSS Support | Direct API | — | ✅ | ✅ | Handles API differences |
23
-
24
- ### Plugin types
25
-
26
- | Type | Description | Approach | Example |
27
- |------|-------------|----------|---------|
28
- | **TS bridge** | Language plugins depending on TypeScript LSP | Generate `tsserver/request` bridge + `typescriptServerPlugins` | Volar |
29
- | **Pure LSP** | Standard LSP using LanguageClient | Generate LanguageClient entry + server dependency injection | Prisma |
30
- | **Direct API** | Direct coc.nvim API calls (no LanguageClient) | Keep original `extension.ts` as entry, no bridge | HTML CSS Support |
31
-
32
- TS bridge plugins require a modified coc-tsserver ([PR #493](https://github.com/neoclide/coc-tsserver/pull/493)):
33
-
34
- ```bash
35
- cd ~/.config/coc/extensions
36
- npm install ChuYanLon/coc-tsserver --legacy-peer-deps
37
- ```
38
-
39
- ## Architecture
40
-
41
- ```
42
- Input: VS Code extension directory
43
-
44
- ├─ scanner Analyze API → detect plugin type
45
- ├─ transforms/ AST transforms
46
- │ ├─ import-mapping from 'vscode' → from 'coc.nvim'
47
- │ ├─ class-to-factory new Xxx() → Xxx.create()
48
- │ ├─ provider-register Adapt provider registration signatures
49
- │ ├─ language-client Adapt LanguageClient signatures
50
- │ └─ enum-offset Comment on enum value offsets
51
- ├─ mark-unsupported Replace/mark missing APIs (getWordRangeAtPosition, fileName, etc.)
52
- ├─ generate src/index.ts Main entry (bridge / LanguageClient / direct templates)
53
- ├─ generate package.json Dependencies / esbuild external config
54
- └─ generate esbuild.mjs Build config
55
- ```
56
-
57
- ## Bridge preset system
58
-
59
- Bridge logic is preset-driven rather than hardcoded:
60
-
61
- ```typescript
62
- // presets.ts - all bridge presets defined here
63
- const PRESETS = {
64
- 'ts-bridge': {
65
- notification: 'tsserver/request',
66
- responseNotification: 'tsserver/response',
67
- handler: { type: 'command', command: 'typescript.tsserverRequest' },
68
- extraDeps: ['typescript'],
69
- },
70
- // future: python-bridge, rust-bridge, etc.
71
- }
72
- ```
73
-
74
- `convert.ts` only calls `getActivePresets()` + `generateBridgeCode()`, it never touches bridge logic directly.
75
- Adding a new bridge type = add a new preset in `presets.ts`, no changes to main flow.
76
-
77
- See [coc-vscode-registry/docs/converter-design-v2.md](https://github.com/coc-plugin/coc-vscode-registry/blob/main/docs/converter-design-v2.md).
78
-
79
- ## File structure
80
-
81
- | File | Lines | Description |
82
- |------|-------|-------------|
83
- | `src/cli.ts` | 28 | CLI entry |
84
- | `src/convert.ts` | 484 | Main flow + template generation + API replacement |
85
- | `src/scanner.ts` | 136 | API scanner + plugin classification |
86
- | `src/transforms/import-mapping.ts` | 47 | Import replacement |
87
- | `src/transforms/language-client.ts` | 48 | LanguageClient adaptation |
88
- | `src/transforms/class-to-factory.ts` | 54 | new Xxx() → Xxx.create() |
89
- | `src/transforms/provider-register.ts` | 55 | Provider registration signature fixes |
90
- | `src/transforms/enum-offset.ts` | 49 | Enum value offset annotations |
91
- | **Total** | **~870** | |
92
-
93
- ## Handled API differences
94
-
95
- | API | VS Code | coc.nvim | Handling |
96
- |-----|---------|----------|----------|
97
- | import | `from 'vscode'` | `from 'coc.nvim'` | Direct replace |
98
- | Position/Range/Location etc. | `new Xxx()` | `Xxx.create()` | AST replace |
99
- | EventEmitter | `EventEmitter<T>` | `Emitter<T>` | Direct replace |
100
- | registerCompletionItemProvider | `(sel, p, ...t)` | `(name, shortcut, sel, p, t?)` | Pad arguments |
101
- | registerCodeActionsProvider | `registerCodeActionsProvider` | `registerCodeActionProvider` | Rename |
102
- | registerReferenceProvider | `registerReferenceProvider` | `registerReferencesProvider` | Rename |
103
- | CompletionItem.create | `new CompletionItem(label, kind)` | `CompletionItem.create(label)` + `item.kind = kind` | kind set separately |
104
- | Trigger characters | `" "` (string) | `[" "]` (array) | Rest param → array |
105
- | CompletionItemKind enum | `Value = 11`, `Enum = 12` | `Value = 12`, `Enum = 13` | Offset by 1, symbols auto-adapt |
106
- | documentSelector | `[{ language: 'xxx' }]` | Same | Auto-infer from package.json |
107
- | getWordRangeAtPosition | `document.getWordRangeAtPosition()` | Not available | Inline word boundary calculation |
108
- | fileName | `document.fileName` | Not available | Replace with `document.uri` |
109
- | createTextEditorDecorationType | `window.createTextEditorDecorationType()` | Not available | Mark TODO |
110
- | createWebviewPanel | `window.createWebviewPanel()` | Not available | Mark TODO |
111
-
112
- ### Missing API strategy
113
-
114
- When a VS Code API has no coc.nvim equivalent, the approach is:
115
-
116
- 1. Find the [VS Code source](https://github.com/microsoft/vscode) implementation
117
- 2. Evaluate complexity:
118
- - **Simple** (e.g. `getWordRangeAtPosition`) → inline polyfill
119
- - **Complex** (e.g. decoration, webview) → mark TODO with explanation
120
- 3. Polyfill using existing coc APIs where possible, avoid new dependencies
121
-
122
- Known VS Code API source locations:
123
- - `getWordRangeAtPosition` → `src/vs/editor/common/core/wordHelper.ts`
124
- - `TextDocument.fileName` → coc uses `document.uri` instead (`DocumentUri = string`)
125
- - Decoration system → `src/vs/editor/common/viewModel/viewDecorations.ts`
126
-
127
- ## Key design decisions
128
-
129
- - **Zero hardcoding** — server package names auto-detected from source
130
- - **Bin entry fallback** — auto-detect and prefer `package.json` bin entry
131
- - **Auto esbuild external injection** — detected server packages marked as external
132
- - **Auto TS bridge injection** — `typescriptServerPlugins` + `tsserver/request` forwarding
133
- - **Plugin classification** — auto-detect TS bridge / pure LSP / direct API
134
- - **Missing API handling** — polyfill where possible, mark TODO otherwise