coc-vscode-loader 1.1.2 → 1.1.5

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.
Binary file
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander'
3
+ import { convert } from './convert.js'
4
+
5
+ const program = new Command()
6
+
7
+ program
8
+ .name('converter')
9
+ .description('vscode → coc.nvim extension converter')
10
+ .version('0.1.0')
11
+
12
+ program
13
+ .command('convert')
14
+ .description('convert a VS Code extension to coc.nvim')
15
+ .argument('<input>', 'input directory (VS Code extension)')
16
+ .option('-o, --output <dir>', 'output directory', './output')
17
+ .option('--bridge', 'generate ts-bridge code')
18
+ .option('-v, --verbose', 'verbose output')
19
+ .action(async (input, opts) => {
20
+ await convert({
21
+ input,
22
+ output: opts.output,
23
+ bridge: opts.bridge,
24
+ verbose: opts.verbose,
25
+ })
26
+ })
27
+
28
+ program.parse()
@@ -0,0 +1,501 @@
1
+ import { Project } from 'ts-morph'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+ import { scan } from './scanner.js'
5
+ import { TransformContext } from './types.js'
6
+ import { transformImportMapping } from './transforms/import-mapping.js'
7
+ import { transformLanguageClient } from './transforms/language-client.js'
8
+ import { transformClassToFactory } from './transforms/class-to-factory.js'
9
+ import { transformProviderRegister } from './transforms/provider-register.js'
10
+ import { transformEnumOffset } from './transforms/enum-offset.js'
11
+ import { getActivePresets, generateBridgeCode } from './presets.js'
12
+
13
+ const TRANSFORMS = [
14
+ { name: 'import-mapping', fn: transformImportMapping },
15
+ { name: 'class-to-factory', fn: transformClassToFactory },
16
+ { name: 'provider-register', fn: transformProviderRegister },
17
+ { name: 'enum-offset', fn: transformEnumOffset },
18
+ { name: 'language-client', fn: transformLanguageClient },
19
+ ]
20
+
21
+ interface Options {
22
+ input: string
23
+ output: string
24
+ bridge?: boolean
25
+ verbose?: boolean
26
+ }
27
+
28
+ export async function convert(opts: Options): Promise<void> {
29
+ const { input, output } = opts
30
+
31
+ if (!fs.existsSync(input)) {
32
+ console.error(`input not found: ${input}`)
33
+ process.exit(1)
34
+ }
35
+
36
+ // 1. Scan
37
+ console.log('Scanning...')
38
+ const result = scan(input)
39
+ console.log(result.summary)
40
+
41
+ if (result.files.length === 0) {
42
+ console.log('No VS Code API usage found, nothing to convert.')
43
+ return
44
+ }
45
+
46
+ // 2. Create output directory
47
+ if (fs.existsSync(output)) {
48
+ fs.rmSync(output, { recursive: true })
49
+ }
50
+ fs.mkdirSync(path.join(output, 'src'), { recursive: true })
51
+
52
+ // 3. Read original package.json
53
+ const origPkgPath = path.join(input, 'package.json')
54
+ const origPkg = fs.existsSync(origPkgPath)
55
+ ? JSON.parse(fs.readFileSync(origPkgPath, 'utf-8'))
56
+ : {}
57
+
58
+ // 4. Copy source files (only .ts that have vscode usage)
59
+ const srcDir = path.join(input, 'src')
60
+ const copiedFiles: string[] = []
61
+ if (fs.existsSync(srcDir)) {
62
+ for (const file of fs.readdirSync(srcDir)) {
63
+ if (!file.endsWith('.ts')) continue
64
+ const srcPath = path.join(srcDir, file)
65
+ const destPath = path.join(output, 'src', file)
66
+ fs.copyFileSync(srcPath, destPath)
67
+ copiedFiles.push(file)
68
+ }
69
+ }
70
+
71
+ // 5. Load files with ts-morph and apply transforms
72
+ const project = new Project()
73
+ const srcFiles = copiedFiles
74
+ .map(f => path.join(output, 'src', f))
75
+ .filter(f => fs.existsSync(f))
76
+
77
+ for (const fp of srcFiles) {
78
+ try { project.addSourceFileAtPath(fp) } catch {}
79
+ }
80
+
81
+ for (const file of project.getSourceFiles()) {
82
+ const ctx: TransformContext = { file, project }
83
+ for (const t of TRANSFORMS) {
84
+ try {
85
+ t.fn(ctx)
86
+ if (opts.verbose) console.log(` ${t.name}: ${path.basename(file.getFilePath())}`)
87
+ } catch (e: any) {
88
+ console.warn(` ${t.name} error: ${e.message}`)
89
+ }
90
+ }
91
+ file.saveSync()
92
+ }
93
+
94
+ // 6. Mark unsupported code patterns in source files
95
+ for (const file of result.files) {
96
+ if (!file.path.startsWith('src/') || !file.path.endsWith('.ts')) continue
97
+ const fp = path.join(output, file.path)
98
+ if (!fs.existsSync(fp)) continue
99
+
100
+ let content = fs.readFileSync(fp, 'utf-8')
101
+
102
+ // Mark unsupported lines
103
+ for (const { pattern, label } of [
104
+ { pattern: 'createTextEditorDecorationType', label: 'decoration API' },
105
+ { pattern: 'createWebviewPanel', label: 'webview API' },
106
+ { pattern: 'registerTreeDataProvider', label: 'tree data provider' },
107
+ { pattern: 'env.openExternal', label: 'env.openExternal' },
108
+ ]) {
109
+ content = content.replace(
110
+ new RegExp(`^(.*${pattern}.*)$`, 'gm'),
111
+ '// [converter] TODO: $1 — coc has no equivalent for ' + label
112
+ )
113
+ }
114
+
115
+ // Replace getWordRangeAtPosition calls with inline word boundary calculation.
116
+ // In coc's TextDocument there's no getWordRangeAtPosition.
117
+ content = content.replace(
118
+ /const range = document\.getWordRangeAtPosition\(([^,]+),\s*([^)]+)\)/g,
119
+ 'const line = document.getText().split("\\n")[$1.line]; const pre = line.slice(0, $1.character); const m = pre.match($2); const range = m ? Range.create($1.line, $1.character - m[0].length, $1.line, $1.character) : undefined'
120
+ )
121
+ content = content.replace(
122
+ /const range = document\.getWordRangeAtPosition\(([^)]+)\)/g,
123
+ 'const line = document.getText().split("\\n")[$1.line]; const pre = line.slice(0, $1.character); const m = pre.match(/[_a-zA-Z0-9-]+/); const range = m ? Range.create($1.line, $1.character - m[0].length, $1.line, $1.character) : undefined'
124
+ )
125
+ // Also replace usage without 'const' (already declared)
126
+ content = content.replace(
127
+ /range = document\.getWordRangeAtPosition\(([^,]+),\s*([^)]+)\)/g,
128
+ '(() => { const ln = document.getText().split("\\n")[$1.line]; const pr = ln.slice(0, $1.character); const mt = pr.match($2); return mt ? Range.create($1.line, $1.character - mt[0].length, $1.line, $1.character) : undefined; })()'
129
+ )
130
+ // fileName → uri (coc's DocumentUri is a string path)
131
+ if (content.includes('.fileName')) {
132
+ content = content.replace(/\.fileName/g, '.uri')
133
+ // uri is a full path in coc, but fileName might have had file:// prefix in vscode
134
+ // Add comment about potential file:// handling
135
+ }
136
+
137
+ // Remove unsupported import packages
138
+ content = content.replace(
139
+ /import .* from ['"]@volar\/vscode['"];?\n?/g,
140
+ ''
141
+ )
142
+ content = content.replace(
143
+ /import .* from ['"]reactive-vscode['"];?\n?/g,
144
+ ''
145
+ )
146
+ content = content.replace(
147
+ /import \* as lsp from ['"]@volar\/vscode\/node['"];?\n?/g,
148
+ ''
149
+ )
150
+
151
+ fs.writeFileSync(fp, content)
152
+ }
153
+
154
+ // 7. Detect config namespace from original properties
155
+ const origProps = origPkg.contributes?.configuration?.properties || {}
156
+ const configNamespace = Object.keys(origProps).length > 0
157
+ ? [...new Set(Object.keys(origProps).map(k => k.split('.')[0]))][0]
158
+ : origPkg.name || path.basename(input)
159
+
160
+ // 8. Detect server module from original source
161
+ const serverModuleNames = detectServerModules(input, result)
162
+ if (serverModuleNames.length > 0) {
163
+ console.log(` detected server: ${serverModuleNames.join(', ')}`)
164
+ }
165
+
166
+ // 9. Determine plugin type and entry point
167
+ const hasTsBridge = result.hasTsBridge
168
+ const hasServer = serverModuleNames.length > 0
169
+ const isDirectApi = !hasServer && !hasTsBridge
170
+ const pluginName = origPkg.name || path.basename(input)
171
+ const description = origPkg.description || ''
172
+ const activePresets = getActivePresets(hasTsBridge, result)
173
+
174
+ // Build server resolution code
175
+ const serverResolveCalls = serverModuleNames.map(name => {
176
+ return `\
177
+ try { serverModule = require.resolve('${escapeStr(name)}') } catch {}
178
+ try {
179
+ const _mainPath = require.resolve('${escapeStr(name)}/package.json');
180
+ let _dir = require('path').dirname(_mainPath);
181
+ while (_dir !== require('path').dirname(_dir)) {
182
+ const _pkgPath = require('path').join(_dir, 'package.json');
183
+ if (require('fs').existsSync(_pkgPath)) {
184
+ const _pkg = JSON.parse(require('fs').readFileSync(_pkgPath, 'utf-8'));
185
+ if (_pkg.bin) {
186
+ const _entry = typeof _pkg.bin === 'string' ? _pkg.bin : Object.values(_pkg.bin)[0];
187
+ serverModule = require('path').join(_dir, _entry);
188
+ }
189
+ break;
190
+ }
191
+ _dir = require('path').dirname(_dir);
192
+ }
193
+ } catch {}`
194
+ }).join('\n')
195
+
196
+ // Generate bridge code from presets
197
+ const bridgeCode = generateBridgeCode(activePresets)
198
+ const needsExtensions = activePresets.some(p => p.name === 'ts-bridge')
199
+ const needsCommands = activePresets.some(p => p.requiresCommand)
200
+
201
+ let fullCode = ''
202
+ let esbuildEntry = 'src/index.ts'
203
+ if (isDirectApi) {
204
+ esbuildEntry = 'src/extension.ts'
205
+ } else {
206
+ fullCode = `\
207
+ import {
208
+ LanguageClient,
209
+ TransportKind,
210
+ workspace,
211
+ window,
212
+ commands,
213
+ services as cocServices,
214
+ ExtensionContext${needsExtensions ? ',\n extensions' : ''},
215
+ } from 'coc.nvim'
216
+ import * as path from 'path'
217
+ import * as fs from 'fs'
218
+
219
+ export async function activate(context: ExtensionContext): Promise<void> {
220
+ try {
221
+ ${needsExtensions ? `\
222
+ const tsExt = extensions.all.find(e => e.id === 'coc-tsserver')
223
+ if (tsExt && !tsExt.isActive) { await tsExt.activate() }
224
+ const tsSvc = cocServices.getService('tsserver')
225
+ if (tsSvc) { await tsSvc.start() }
226
+ ` : ''}\
227
+ const config = workspace.getConfiguration('${escapeStr(configNamespace)}')
228
+ let serverModule = config.get<string>('server.path', '')
229
+ if (serverModule) {
230
+ serverModule = path.isAbsolute(serverModule) ? serverModule : path.join(workspace.root, serverModule)
231
+ }
232
+ if (!serverModule || !fs.existsSync(serverModule)) {
233
+ ${serverResolveCalls}
234
+ }
235
+ if (!serverModule) { window.showErrorMessage('Cannot find language server.'); return }
236
+
237
+ const client = new LanguageClient(
238
+ '${escapeStr(pluginName)}',
239
+ '${escapeStr(description || pluginName)}',
240
+ { module: serverModule, transport: TransportKind.ipc },
241
+ {
242
+ documentSelector: [{ language: '${escapeStr(configNamespace)}', scheme: 'file' }],
243
+ outputChannelName: '${escapeStr(description || pluginName)}',
244
+ },
245
+ )
246
+ context.subscriptions.push({ dispose: () => client.stop() })
247
+ context.subscriptions.push(cocServices.registerLanguageClient(client))
248
+ client.start()
249
+
250
+ ${bridgeCode ? bridgeCode + '\n\n' : ''}\
251
+ // Restart command
252
+ context.subscriptions.push(
253
+ commands.registerCommand('${escapeStr(pluginName)}.restartServer', async () => {
254
+ await client.stop()
255
+ client.start()
256
+ }),
257
+ )
258
+ } catch (e: any) {
259
+ window.showErrorMessage('${escapeStr(pluginName)} error: ' + (e.message || String(e)))
260
+ }
261
+ }
262
+ `
263
+ }
264
+ if (fullCode) {
265
+ fs.writeFileSync(path.join(output, 'src', 'index.ts'), fullCode)
266
+ }
267
+
268
+ // 10. Detect dependencies from original package.json + preset extras
269
+ const serverDeps: Record<string, string> = {}
270
+ // Add preset extra deps (e.g. typescript for ts-bridge)
271
+ for (const preset of activePresets) {
272
+ for (const dep of (preset.extraDeps || [])) {
273
+ if (!serverDeps[dep]) serverDeps[dep] = '*'
274
+ }
275
+ }
276
+ if (isDirectApi) {
277
+ // Non-LSP: keep all original deps (including devDeps, since they may be imported at runtime)
278
+ const allDeps = { ...origPkg.dependencies, ...origPkg.devDependencies }
279
+ for (const [dep, ver] of Object.entries(allDeps)) {
280
+ if (ver.startsWith('workspace:')) continue
281
+ if (dep.startsWith('@types/')) continue // skip type packages
282
+ if (['typescript', 'mocha', 'c8', 'prettier', 'rollup', '@vscode/'].some(p => dep.startsWith(p))) continue
283
+ if (['tslib'].includes(dep)) continue
284
+ serverDeps[dep] = ver as string
285
+ }
286
+ } else {
287
+ // LSP-based: only include server-related dependencies
288
+ const knownPatterns = ['language-server', 'language-server/node', '-server', 'languageserver', '/lsp']
289
+ for (const [dep, ver] of Object.entries(origPkg.dependencies || {})) {
290
+ if (ver.startsWith('workspace:')) {
291
+ // Workspace deps: if it matches a known server pattern, add with wildcard version
292
+ if (knownPatterns.some(p => dep.includes(p))) {
293
+ serverDeps[dep] = '*'
294
+ }
295
+ continue
296
+ }
297
+ if (knownPatterns.some(p => dep.includes(p))) {
298
+ serverDeps[dep] = ver as string
299
+ }
300
+ }
301
+ for (const name of serverModuleNames) {
302
+ const pkgName = name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0]
303
+ if (!serverDeps[pkgName]) serverDeps[pkgName] = '*'
304
+ }
305
+ }
306
+ const activationEvents = Array.isArray(origPkg.activationEvents) ? origPkg.activationEvents : []
307
+ const activationEvent = activationEvents.find((e: string) => e.startsWith('onLanguage:'))
308
+ || (activationEvents.includes('*') ? '*' : undefined)
309
+ || (activationEvents.includes('onStartupFinished') ? '*' : undefined)
310
+ || `onLanguage:${configNamespace}`
311
+ || 'onLanguage'
312
+ const pkg = {
313
+ name: pluginName.startsWith('coc-') ? pluginName : `coc-${pluginName}`,
314
+ version: origPkg.version || '0.1.0',
315
+ dependencies: {
316
+ ...serverDeps,
317
+ },
318
+ devDependencies: {
319
+ esbuild: '^0.28.0',
320
+ },
321
+ description: description,
322
+ main: 'lib/index.js',
323
+ engines: { coc: '^0.0.82' },
324
+ activationEvents: [activationEvent],
325
+ contributes: {
326
+ configuration: origPkg.contributes?.configuration ? {
327
+ type: 'object',
328
+ title: origPkg.contributes.configuration.title || pluginName,
329
+ properties: origPkg.contributes.configuration.properties || {},
330
+ } : undefined,
331
+ commands: origPkg.contributes?.commands?.map((c: any) => ({
332
+ command: c.command,
333
+ title: c.title,
334
+ })) || undefined,
335
+ ...(hasTsBridge ? {
336
+ typescriptServerPlugins: (origPkg.contributes?.typescriptServerPlugins?.length
337
+ ? origPkg.contributes?.typescriptServerPlugins
338
+ : scanTypeScriptPlugins(input, result).map(p => ({
339
+ ...p,
340
+ languages: p.languages.length ? p.languages : [configNamespace],
341
+ }))
342
+ ) || [],
343
+ } : {}),
344
+ },
345
+ }
346
+
347
+ // Clean null fields
348
+ for (const key of ['configuration', 'commands'] as const) {
349
+ if (!pkg.contributes[key]) delete pkg.contributes[key]
350
+ }
351
+ if (Object.keys(pkg.contributes).length === 0) delete pkg.contributes
352
+
353
+ fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkg, null, 2))
354
+
355
+ // 10. Generate esbuild config
356
+ const externalMods = ['coc.nvim', ...serverModuleNames]
357
+ .map(n => n.startsWith('@') ? n.split('/').slice(0, 2).join('/') : n.split('/')[0])
358
+ .filter((v, i, a) => v && a.indexOf(v) === i)
359
+ const esbuildConfig = `\
360
+ import * as esbuild from 'esbuild'
361
+
362
+ const options = {
363
+ entryPoints: ['${escapeStr(esbuildEntry)}'],
364
+ bundle: true,
365
+ minify: false,
366
+ mainFields: ['module', 'main'],
367
+ external: [${externalMods.map(m => `'${escapeStr(m)}'`).join(', ')}],
368
+ platform: 'node',
369
+ target: 'node18',
370
+ outfile: 'lib/index.js',
371
+ }
372
+
373
+ const result = await esbuild.build(options)
374
+ if (result.errors.length) {
375
+ console.error(result.errors)
376
+ process.exit(1)
377
+ }
378
+ `
379
+ fs.writeFileSync(path.join(output, 'esbuild.mjs'), esbuildConfig)
380
+
381
+ // 11. Generate report
382
+ console.log('\n=== Conversion Report ===')
383
+ console.log(` Plugin: ${pkg.name}`)
384
+ console.log(` Type: ${hasTsBridge ? 'ts-bridge' : 'pure-lsp'}`)
385
+ console.log(` Source files: ${copiedFiles.length + (hasTsBridge ? 0 : 0)}`)
386
+
387
+ if (hasTsBridge) {
388
+ console.log('\n ⚠ TS-bridge mode:')
389
+ console.log(' - Generated src/index.ts with tsserver/request bridge')
390
+ console.log(' - Package.json includes typescriptServerPlugins')
391
+ console.log(' - Requires modified coc-tsserver (PR #493)')
392
+ console.log(' - Install: cd ~/.config/coc/extensions && npm install ChuYanLon/coc-tsserver')
393
+ }
394
+
395
+ console.log(`\n ${output}/`)
396
+ console.log(' ├── src/index.ts ← main entry')
397
+ console.log(' ├── package.json ← coc plugin config')
398
+ console.log(' ├── esbuild.mjs ← build config')
399
+ console.log(' └── src/*.ts ← converted source')
400
+ console.log('\n Next:')
401
+ console.log(` cd ${output}`)
402
+ console.log(' npm install')
403
+ console.log(' npm run build')
404
+ }
405
+
406
+ /**
407
+ * Scan original source for TypeScript plugin names used by the extension.
408
+ * Checks: explicit imports, require() calls, package.json dependencies.
409
+ */
410
+ function walkTsFiles(dir: string): string[] {
411
+ const files: string[] = []
412
+ try {
413
+ for (const entry of fs.readdirSync(dir)) {
414
+ const full = path.join(dir, entry)
415
+ const stat = fs.statSync(full)
416
+ if (stat.isDirectory()) {
417
+ files.push(...walkTsFiles(full))
418
+ } else if (entry.endsWith('.ts') || entry.endsWith('.d.ts')) {
419
+ files.push(full)
420
+ }
421
+ }
422
+ } catch {}
423
+ return files
424
+ }
425
+
426
+ function scanTypeScriptPlugins(input: string, _result: any): Array<{ name: string; languages: string[]; enableForWorkspaceTypeScriptVersions: boolean }> {
427
+ const plugins: Array<{ name: string; languages: string[]; enableForWorkspaceTypeScriptVersions: boolean }> = []
428
+ const scanDir = path.join(input, 'src')
429
+ if (!fs.existsSync(scanDir)) return plugins
430
+
431
+ // Collect all .ts file content (recursive)
432
+ const allContent = walkTsFiles(scanDir).map(f => fs.readFileSync(f, 'utf-8'))
433
+ const content = allContent.join('\n')
434
+
435
+ // Check for plugin names in require() / import statements
436
+ // Patterns like: @vue/typescript-plugin, @angular/language-service, etc.
437
+ const pluginRefs = content.matchAll(/['"](@[^'"]+\/(?:typescript-plugin|language-service)[^'"]*)['"]|['"](\w+(?:-typescript-plugin|-language-service))['"]/g)
438
+ for (const m of pluginRefs) {
439
+ const name = m[1] || m[2]
440
+ if (name && !plugins.some(p => p.name === name)) {
441
+ plugins.push({ name, languages: [], enableForWorkspaceTypeScriptVersions: true })
442
+ }
443
+ }
444
+
445
+ // Check package.json for typescript plugin dependencies
446
+ const pkgPath = path.join(input, 'package.json')
447
+ if (fs.existsSync(pkgPath)) {
448
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
449
+ for (const dep of Object.keys(pkg.dependencies || {})) {
450
+ if (dep.includes('typescript-plugin') || dep.includes('language-service')) {
451
+ if (!plugins.some(p => p.name === dep)) {
452
+ plugins.push({ name: dep, languages: [], enableForWorkspaceTypeScriptVersions: true })
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ return plugins
459
+ }
460
+
461
+ /** Escape string for template literal */
462
+ function escapeStr(s: string): string {
463
+ return s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$')
464
+ }
465
+
466
+ /** Clean up server path: relative → bare package name, subpath → base */
467
+ function sanitizeServerPath(p: string): string | null {
468
+ if (p.startsWith('../node_modules/')) p = p.replace(/^\.\.\/node_modules\//, '')
469
+ if (p.startsWith('./')) return null
470
+ if (p.startsWith('../')) return null
471
+ if (p.startsWith('@')) {
472
+ const parts = p.split('/')
473
+ if (parts.length > 2) return parts.slice(0, 2).join('/')
474
+ } else {
475
+ const parts = p.split('/')
476
+ if (parts.length > 1) return parts[0]
477
+ }
478
+ return p
479
+ }
480
+
481
+ /**
482
+ * Detect language server module names from original source code.
483
+ * Returns an array of `require.resolve` argument strings (bare module names).
484
+ */
485
+ function detectServerModules(input: string, _result: any): string[] {
486
+ const seen = new Set<string>()
487
+ const serverModules: string[] = []
488
+ const scanDir = path.join(input, 'src')
489
+ if (fs.existsSync(scanDir)) {
490
+ for (const file of walkTsFiles(scanDir)) {
491
+ const content = fs.readFileSync(file, 'utf-8')
492
+ for (const re of [/(?:require\s*(?:\.\s*resolve)?\s*\(|from\s+)['"]([^'"]+(?:language-server|server|Server|lsp)[^'"]*)['"]\s*\)?/g]) {
493
+ for (const m of content.matchAll(re)) {
494
+ const name = sanitizeServerPath(m[1])
495
+ if (name && !seen.has(name) && !name.includes('${')) { seen.add(name); serverModules.push(name) }
496
+ }
497
+ }
498
+ }
499
+ }
500
+ return serverModules
501
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Bridge preset definitions.
3
+ * Each preset describes a notification → handler → response pattern
4
+ * that the converter uses to generate bridge code.
5
+ */
6
+
7
+ export interface BridgePreset {
8
+ /** Used to match presets in registry */
9
+ name: string
10
+ /** Notification to listen for from the language server */
11
+ notification: string
12
+ /** Notification to send back as response */
13
+ responseNotification?: string
14
+ /** Generated bridge code (template with placeholders) */
15
+ code: string
16
+ /** Required coc command (will be mentioned in report) */
17
+ requiresCommand?: string
18
+ /** Extra package.json contributions */
19
+ packageContributes?: Record<string, any>
20
+ /** Extra dependencies */
21
+ extraDeps?: string[]
22
+ }
23
+
24
+ const PRESETS: Record<string, BridgePreset> = {
25
+ 'ts-bridge': {
26
+ name: 'ts-bridge',
27
+ notification: 'tsserver/request',
28
+ responseNotification: 'tsserver/response',
29
+ requiresCommand: 'typescript.tsserverRequest',
30
+ extraDeps: ['typescript'],
31
+ code: `\
32
+ // tsserver bridge: forward TypeScript requests from language server
33
+ client.onNotification('tsserver/request', async ([seq, command, args]: [number, string, any]) => {
34
+ try {
35
+ const result = await commands.executeCommand<any>('typescript.tsserverRequest', command, args, { isAsync: true, lowPriority: true })
36
+ client.sendNotification('tsserver/response', [seq, result?.body])
37
+ } catch { client.sendNotification('tsserver/response', [seq, undefined]) }
38
+ })`,
39
+ },
40
+ }
41
+
42
+ export function getPreset(name: string): BridgePreset | undefined {
43
+ return PRESETS[name]
44
+ }
45
+
46
+ export function getActivePresets(hasTsBridge: boolean, result: any): BridgePreset[] {
47
+ const presets: BridgePreset[] = []
48
+ if (hasTsBridge) {
49
+ const preset = getPreset('ts-bridge')
50
+ if (preset) presets.push(preset)
51
+ }
52
+ return presets
53
+ }
54
+
55
+ export function generateBridgeCode(presets: BridgePreset[]): string {
56
+ return presets.map(p => p.code).join('\n\n')
57
+ }