coc-vscode-loader 1.2.1 → 1.2.3

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converter",
3
- "version": "0.1.0",
3
+ "version": "1.2.3",
4
4
  "private": true,
5
5
  "description": "vscode → coc.nvim converter prototype",
6
6
  "type": "module",
@@ -68,7 +68,9 @@ export async function convert(opts: ConvertOptions): Promise<void> {
68
68
  }
69
69
  console.log(result.summary)
70
70
 
71
- if (result.files.length === 0) {
71
+ // Source-only steps need source files; bridge/language-client steps can generate code
72
+ const hasNonSourceStep = steps.some(s => s.type !== 'source' && s.type !== 'mark-unsupported')
73
+ if (result.files.length === 0 && !hasNonSourceStep) {
72
74
  console.log('No VS Code API usage found, nothing to convert.')
73
75
  return
74
76
  }
@@ -174,7 +176,7 @@ export async function convert(opts: ConvertOptions): Promise<void> {
174
176
  if (fs.existsSync(outputSrc)) {
175
177
  for (const f of fs.readdirSync(outputSrc, { recursive: true })) {
176
178
  const fp = typeof f === 'string' ? path.join(outputSrc, f) : f
177
- if (!fp.endsWith('.ts')) continue
179
+ if (!fp.endsWith('.ts') && !fp.endsWith('.js')) continue
178
180
  let content = fs.readFileSync(fp, 'utf-8')
179
181
  let changed = false
180
182
 
@@ -198,13 +200,38 @@ export async function convert(opts: ConvertOptions): Promise<void> {
198
200
  }
199
201
 
200
202
  if (content.includes('.fileName') || content.includes('.uri.fsPath')) {
201
- // .fileName → .uri (coc's TextDocument#uri returns a string path, not a URI object)
202
- content = content.replace(/(document|this\.document|textDocument|scope)\.fileName/g, '$1.uri')
203
- // .uri.fsPath .uri (coc's uri is already a string path, not a URI object)
204
- content = content.replace(/\.uri\.fsPath/g, '.uri')
203
+ // .fileName → Uri.parse($1.uri).fsPath (coc's TextDocument#uri returns a file:// URI string)
204
+ content = content.replace(/(document|this\.document|textDocument|scope|doc)\.fileName/g, 'Uri.parse($1.uri).fsPath')
205
+ // Handle destructuring: const { fileName, ...rest } = document/doc/textDocument
206
+ content = content.replace(
207
+ /^(\s*)(const|let|var)\s*\{([^}]*)\}\s*=\s*(document|doc|textDocument)\s*;?\s*$/gm,
208
+ (m: string, indent: string, kw: string, props: string, varName: string) => {
209
+ const parts = props.split(',').map((p: string) => p.trim())
210
+ if (!parts.some((p: string) => p === 'fileName')) return m
211
+ const rest = parts.filter((p: string) => p !== 'fileName' && p !== '')
212
+ return rest.length > 0
213
+ ? `${indent}${kw} {${rest.join(', ')}} = ${varName};\n${indent}const fileName = Uri.parse(${varName}.uri).fsPath`
214
+ : `${indent}const fileName = Uri.parse(${varName}.uri).fsPath`
215
+ }
216
+ )
217
+ // .uri.fsPath → Uri.parse(...).fsPath (coc's uri is a file:// URI string, not a path)
218
+ content = content.replace(/(\w+(?:\.\w+)*?)\.uri\.fsPath/g, 'Uri.parse($1.uri).fsPath')
205
219
  changed = true
206
220
  }
207
221
 
222
+ // Ensure Uri is imported when introduced by Uri.parse() replacements
223
+ if (content.includes('Uri.parse(') && content.match(/from\s+['"]coc\.nvim['"]/)) {
224
+ content = content.replace(
225
+ /(import\s*\{\s*)([^}]*?)(\s*\}\s*from\s*['"]coc\.nvim['"])/g,
226
+ (_m: string, prefix: string, existing: string, suffix: string) => {
227
+ if (!existing.includes('Uri')) {
228
+ const sep = existing.trim() ? ', ' : ''
229
+ return `${prefix}${existing.trim()}${sep}Uri${suffix}`
230
+ }
231
+ return _m
232
+ }
233
+ )
234
+ }
208
235
  if (changed) fs.writeFileSync(fp, content)
209
236
  }
210
237
  }
@@ -72,7 +72,32 @@ export const bridgeGenerator: StepGenerator = {
72
72
  ${code}`
73
73
  }
74
74
 
75
- // Generate the bridge module
75
+ const extIds = generated.injectExts || []
76
+ const svcIds = generated.injectSvcs || []
77
+ const callAfter = generated.callAfter
78
+ const isStandalone = !callAfter && extIds.length === 0 && svcIds.length === 0
79
+
80
+ if (isStandalone) {
81
+ // Standalone preset (e.g. prettier): generate entry point directly
82
+ const moduleContent = `\
83
+ import { ExtensionContext, languages, Range, TextEdit, Uri, window, workspace } from 'coc.nvim'
84
+
85
+ export async function activate(context: ExtensionContext): Promise<void> {
86
+ ${code}
87
+ }
88
+ `
89
+ return {
90
+ generatedFiles: [{ path: 'src/index.ts', content: moduleContent }],
91
+ entryPoint: 'src/index.ts',
92
+ keepDeps: Object.fromEntries((generated.extraDeps || []).map((d: string) => {
93
+ const ver = ctx.origPkg.dependencies?.[d] || ctx.origPkg.devDependencies?.[d]
94
+ return [d, ver || '*']
95
+ })),
96
+ activationEvents: ['*'],
97
+ }
98
+ }
99
+
100
+ // Generate the bridge module (for tsserver-forward etc)
76
101
  const moduleContent = `\
77
102
  import { commands, ExtensionContext } from 'coc.nvim'
78
103
 
@@ -83,9 +108,6 @@ ${code}
83
108
 
84
109
  // Build code injections
85
110
  const codeInjections: StepResult['codeInjections'] = []
86
- const extIds = generated.injectExts || []
87
- const svcIds = generated.injectSvcs || []
88
- const callAfter = generated.callAfter
89
111
 
90
112
  if (callAfter) {
91
113
  codeInjections.push({
@@ -42,21 +42,30 @@ export const languageClientGenerator: StepGenerator = {
42
42
  const entry = ls.server.entry || 'main'
43
43
 
44
44
  // Same resolution as old converter: resolve main entry first, then walk for bin
45
+ // Use require.resolve('pkg/package.json') as fallback for packages without main entry
46
+ const binName = ls.server.binName || ''
47
+ const binLookupCode = binName
48
+ ? `(_pkg.bin && _pkg.bin['${escapeStr(binName)}'] ? _pkg.bin['${escapeStr(binName)}'] : Object.values(_pkg.bin)[0])`
49
+ : `(typeof _pkg.bin === 'string' ? _pkg.bin : Object.values(_pkg.bin)[0])`
45
50
  serverPathCode = `\
46
51
  let serverPath: string | undefined
52
+ let _mainEntry: string | undefined
47
53
  try {
48
- serverPath = require.resolve('${escapeStr(pkg)}')
54
+ _mainEntry = require.resolve('${escapeStr(pkg)}')
49
55
  } catch {}
56
+ if (!_mainEntry) {
57
+ try { _mainEntry = require.resolve('${escapeStr(pkg)}/package.json') } catch {}
58
+ }
50
59
  try {
51
60
  // Walk up from the resolved main entry to find the package's package.json
52
61
  // We can't use require.resolve('pkg/package.json') because exports field may block it
53
- let _dir = require('path').dirname(require.resolve('${escapeStr(pkg)}'));
54
- while (_dir !== require('path').dirname(_dir)) {
62
+ let _dir = _mainEntry ? require('path').dirname(_mainEntry) : undefined;
63
+ while (_dir && _dir !== require('path').dirname(_dir)) {
55
64
  const _pkgPath = require('path').join(_dir, 'package.json');
56
65
  if (require('fs').existsSync(_pkgPath)) {
57
66
  const _pkg = JSON.parse(require('fs').readFileSync(_pkgPath, 'utf-8'));
58
67
  if (_pkg.bin) {
59
- const _entry = typeof _pkg.bin === 'string' ? _pkg.bin : Object.values(_pkg.bin)[0];
68
+ const _entry = ${binLookupCode};
60
69
  serverPath = require('path').join(_dir, _entry);
61
70
  }
62
71
  break;
@@ -65,7 +74,7 @@ export const languageClientGenerator: StepGenerator = {
65
74
  }
66
75
  } catch {}`
67
76
  // Use full require.resolve path (including bin walking) if available, else fallback to simple main entry
68
- serverOptionsCode = `{ module: serverPath || require.resolve('${escapeStr(pkg)}'), transport: ${transportExpr} }`
77
+ serverOptionsCode = `{ module: serverPath || _mainEntry || require.resolve('${escapeStr(pkg)}/package.json'), transport: ${transportExpr} }`
69
78
  }
70
79
 
71
80
  const docSelectorCode = `[${languages.map(l => `{ scheme: 'file', language: '${l}' }`).join(', ')}]`
@@ -99,6 +108,7 @@ ${ls.verbose ? ` console.log('[${escapeStr(id)}] creating LanguageClient')\n`
99
108
  {
100
109
  documentSelector: ${docSelectorCode},
101
110
  outputChannelName: '${escapeStr(description)}',
111
+ ${ls.initializationOptions ? `initializationOptions: ${ls.initializationOptions},` : ''}
102
112
  },
103
113
  )
104
114
  context.subscriptions.push({ dispose: () => c.stop() })
@@ -39,18 +39,19 @@ export const sourceGenerator: StepGenerator = {
39
39
  const outputsDir = path.join(output, 'src')
40
40
  fs.mkdirSync(outputsDir, { recursive: true })
41
41
 
42
- // Copy ALL .ts/.tsx files from source directory (try src/ first, fall back to input root)
42
+ // Copy ALL .ts/.tsx/.js files from source directory (try src/ first, fall back to input root)
43
43
  let srcDir = path.join(input, 'src')
44
44
  if (!fs.existsSync(srcDir)) {
45
45
  srcDir = input
46
46
  }
47
47
  const hasStripVolar = ss.transforms.includes('strip-volar')
48
48
  const allFiles: Array<{ src: string; rel: string }> = []
49
+ const jsFiles: string[] = []
49
50
  const vscodeFiles: string[] = []
50
51
 
51
52
  for (const f of walkFiles(srcDir)) {
52
53
  const rel = path.relative(srcDir, f)
53
- if (!rel.endsWith('.ts') && !rel.endsWith('.tsx')) continue
54
+ if (!rel.endsWith('.ts') && !rel.endsWith('.tsx') && !rel.endsWith('.js')) continue
54
55
 
55
56
  // Skip framework files that are replaced by generated code
56
57
  if (hasStripVolar) {
@@ -61,8 +62,10 @@ export const sourceGenerator: StepGenerator = {
61
62
  allFiles.push({ src: f, rel })
62
63
 
63
64
  const content = fs.readFileSync(f, 'utf-8')
64
- if (content.includes("from 'vscode'") || content.includes('from "vscode"') || content.includes('require("vscode")')) {
65
+ const hasVscode = content.includes("from 'vscode'") || content.includes('from "vscode"') || content.includes('require("vscode")')
66
+ if (hasVscode) {
65
67
  vscodeFiles.push(rel)
68
+ if (rel.endsWith('.js')) jsFiles.push(rel)
66
69
  }
67
70
  }
68
71
 
@@ -74,13 +77,13 @@ export const sourceGenerator: StepGenerator = {
74
77
  }
75
78
 
76
79
  if (verbose) {
77
- console.log(` source: copied ${allFiles.length} files (${vscodeFiles.length} with vscode imports)`)
80
+ console.log(` source: copied ${allFiles.length} files (${vscodeFiles.length} with vscode imports, ${jsFiles.length} .js)`)
78
81
  }
79
82
 
80
- // Apply transforms via ts-morph (only to files with vscode imports)
83
+ // Apply transforms via ts-morph (only to TS files with vscode imports; .js files get text-level only)
81
84
  for (const rel of vscodeFiles) {
82
85
  const fp = path.join(outputsDir, rel)
83
- if (!fs.existsSync(fp)) continue
86
+ if (!fs.existsSync(fp) || rel.endsWith('.js')) continue
84
87
  try { project.addSourceFileAtPath(fp) } catch {}
85
88
  }
86
89
 
@@ -104,6 +107,36 @@ export const sourceGenerator: StepGenerator = {
104
107
  sf.saveSync()
105
108
  }
106
109
 
110
+ // Apply text-level replacements to .js files (ts-morph can't handle JS)
111
+ for (const rel of jsFiles) {
112
+ const fp = path.join(outputsDir, rel)
113
+ if (!fs.existsSync(fp)) continue
114
+ let code = fs.readFileSync(fp, 'utf-8')
115
+ const orig = code
116
+ code = code.replace(/require\(['"]vscode['"]\)/g, "require('coc.nvim')")
117
+ code = code.replace(/(\w+)\.fileName\b/g, "Uri.parse($1.uri).fsPath")
118
+ code = code.replace(/(\w+(?:\.\w+)*?)\.uri\.fsPath/g, 'Uri.parse($1.uri).fsPath')
119
+ if (code.includes('window.activeTextEditor')) {
120
+ code = `\
121
+ if (typeof window !== 'undefined' && !('activeTextEditor' in window)) {
122
+ try {
123
+ Object.defineProperty(window, 'activeTextEditor', {
124
+ get() {
125
+ try {
126
+ var doc = typeof workspace !== 'undefined' ? workspace.getDocument() : undefined;
127
+ return doc ? { document: doc } : undefined;
128
+ } catch(e) { return undefined }
129
+ },
130
+ configurable: true,
131
+ });
132
+ } catch {}
133
+ }
134
+ ` + code
135
+ }
136
+ code = code.replace(/window\.onDidChangeActiveTextEditor/g, 'workspace.onDidOpenTextDocument')
137
+ if (code !== orig) fs.writeFileSync(fp, code)
138
+ }
139
+
107
140
  // Resolve keepDeps from origPkg (with workspace root fallback)
108
141
  const keepDeps: Record<string, string> = {}
109
142
  if (ss.keepDeps) {
@@ -5,12 +5,12 @@ import { Transform } from '../types.js'
5
5
  * and apply name remapping for known API differences.
6
6
  */
7
7
  const MAPPINGS: Record<string, string> = {
8
- // namespace
9
- 'vscode': 'coc.nvim',
8
+ // namespace (module specifier is rewritten by AST, keep identifier as-is)
10
9
 
11
10
  // naming differences
12
11
  'EventEmitter': 'Emitter',
13
12
  'Disposable': 'Disposable',
13
+ 'StatusBarAlignment': '(void 0) as any',
14
14
 
15
15
  // function/method renames
16
16
  'getExtension': 'getExtensionById',
@@ -25,23 +25,169 @@ export const transformImportMapping: Transform = (ctx) => {
25
25
  const { file } = ctx
26
26
 
27
27
  // Rewrite import declarations
28
- file.getImportDeclarations().forEach(decl => {
29
- const mod = decl.getModuleSpecifierValue()
30
- if (mod === 'vscode') {
31
- decl.setModuleSpecifier('coc.nvim')
32
- }
33
- })
28
+ try {
29
+ file.getImportDeclarations().forEach(decl => {
30
+ const mod = decl.getModuleSpecifierValue()
31
+ if (mod === 'vscode') {
32
+ decl.setModuleSpecifier('coc.nvim')
33
+ }
34
+ })
35
+ } catch {}
34
36
 
35
37
  // Rewrite named references
36
- file.getDescendantsOfKind(192 /* Identifier */).forEach(node => {
37
- const text = node.getText()
38
- const mapped = MAPPINGS[text]
39
- if (mapped && mapped !== text) {
40
- // Only replace if it's a direct reference, not part of a string
41
- const parent = node.getParent()
42
- if (parent && parent.getKindName() !== 'StringLiteral') {
43
- node.replaceWithText(mapped)
38
+ try {
39
+ file.getDescendantsOfKind(80 /* Identifier */).forEach(node => {
40
+ const text = node.getText()
41
+ if (Object.prototype.hasOwnProperty.call(MAPPINGS, text)) {
42
+ const mapped = MAPPINGS[text]
43
+ if (mapped !== text) {
44
+ const parent = node.getParent()
45
+ if (parent && parent.getKindName() !== 'StringLiteral') {
46
+ node.replaceWithText(mapped)
47
+ }
48
+ }
49
+ }
50
+ })
51
+ } catch {}
52
+
53
+ // Replace CodeActionKind.SourceFixAll.append('xxx') with string literal
54
+ try {
55
+ file.getDescendantsOfKind(214 /* CallExpression */).forEach(node => {
56
+ const text = node.getText()
57
+ const match = text.match(/^CodeActionKind\.SourceFixAll\.append\(['"](.+)['"]\)$/)
58
+ if (match) {
59
+ node.replaceWithText(`'source.fixAll.${match[1]}'`)
60
+ }
61
+ })
62
+ } catch {}
63
+
64
+ // Text-level replacements for coc.nvim API differences
65
+
66
+ // Convert require('vscode') to require('coc.nvim') (JS-style imports)
67
+ let content = file.getText()
68
+ let newContent = content.replace(
69
+ /require\(['"]vscode['"]\)/g,
70
+ "require('coc.nvim')",
71
+ )
72
+
73
+ // Convert dynamic import() to require()
74
+ newContent = newContent.replace(
75
+ /await\s+import\(/g,
76
+ 'require(',
77
+ )
78
+
79
+ // Convert createStatusBarItem(name, alignment, priority) → createStatusBarItem(priority)
80
+ newContent = newContent.replace(
81
+ /createStatusBarItem\([^,]+,\s*(?:\w+\.)?(?:Right|Left),\s*/g,
82
+ 'createStatusBarItem(',
83
+ )
84
+
85
+ // Replace LanguageStatusSeverity.xxx → 2
86
+ newContent = newContent.replace(
87
+ /LanguageStatusSeverity\.\w+/g,
88
+ '2',
89
+ )
90
+
91
+ // Replace StatusBar with a no-op mock so formatting works without status UI
92
+ newContent = newContent.replace(
93
+ /new\s+StatusBar\(\)/g,
94
+ 'new (class { update(){} hide(){} updateConfig(){} dispose(){} } as any)()',
95
+ )
96
+
97
+ // Treat all workspaces as trusted (coc.nvim doesn't have workspace.isTrusted)
98
+ newContent = newContent.replace(
99
+ /workspace\.isTrusted/g,
100
+ 'true',
101
+ )
102
+
103
+ // Wrap new CodeAction() in try-catch (coc.nvim may not have CodeAction)
104
+ newContent = newContent.replace(
105
+ /const action = new CodeAction\(/g,
106
+ 'let action; try { action = new CodeAction(',
107
+ )
108
+ // Close the try-catch before return [action]
109
+ newContent = newContent.replace(
110
+ /return \[action\];/g,
111
+ '}catch(e){action={title:"",kind:""}};return [action];',
112
+ )
113
+
114
+ // window.createOutputChannel works in coc.nvim (workspace variant is deprecated)
115
+ // no replacement needed
116
+
117
+ // Polyfill window.activeTextEditor (VS Code API, not in coc.nvim)
118
+ if (newContent.includes('window.activeTextEditor')) {
119
+ newContent = `\
120
+ if (typeof window !== 'undefined' && !('activeTextEditor' in window)) {
121
+ try {
122
+ Object.defineProperty(window, 'activeTextEditor', {
123
+ get() {
124
+ try {
125
+ var doc = typeof workspace !== 'undefined' ? workspace.getDocument() : undefined;
126
+ return doc ? { document: doc } : undefined;
127
+ } catch(e) { return undefined }
128
+ },
129
+ configurable: true,
130
+ });
131
+ } catch {}
132
+ }
133
+ ` + newContent
134
+ }
135
+
136
+ // window.onDidChangeActiveTextEditor → workspace.onDidOpenTextDocument
137
+ newContent = newContent.replace(/window\.onDidChangeActiveTextEditor/g, 'workspace.onDidOpenTextDocument')
138
+
139
+ // languages.createLanguageStatusItem → no-op (coc.nvim doesn't have this)
140
+ newContent = newContent.replace(
141
+ /languages\.createLanguageStatusItem\([^)]+\)/g,
142
+ '({ dispose(){}, text: "", command: void 0, name: "", accessibilityInformation: void 0, severity: void 0 }) as any'
143
+ )
144
+
145
+ // window.showOpenDialog → not available in coc, return undefined
146
+ newContent = newContent.replace(
147
+ /window\.showOpenDialog\([^)]*\)/g,
148
+ 'void 0 as any'
149
+ )
150
+
151
+ // Add priority 1 to document format providers (default 0 gets overridden by LanguageClient)
152
+ newContent = newContent.replace(
153
+ /registerDocumentFormatProvider\s*\(\s*(\w[\w.]*)\s*,\s*(\w[\w.]*)\s*,?\s*\)/g,
154
+ 'registerDocumentFormatProvider($1, $2, 1)'
155
+ )
156
+ newContent = newContent.replace(
157
+ /registerDocumentRangeFormatProvider\s*\(\s*(\w[\w.]*)\s*,\s*(\w[\w.]*)\s*,?\s*\)/g,
158
+ 'registerDocumentRangeFormatProvider($1, $2, 1)'
159
+ )
160
+
161
+ // authentication.getSession → undefined (coc.nvim has no auth API)
162
+ newContent = newContent.replace(
163
+ /authentication\.getSession\s*\([^)]*\)/g,
164
+ 'undefined as any'
165
+ )
166
+
167
+ // editor.setDecorations → no-op (coc has different decoration API)
168
+ newContent = newContent.replace(/editor\.setDecorations\s*\([^)]+\)/g, '/* setDecorations */')
169
+
170
+ // Guard workspace.workspaceFolders when accessed via index (coc.nvim may return undefined)
171
+ newContent = newContent.replace(
172
+ /workspace\.workspaceFolders(?=\[)/g,
173
+ '(workspace.workspaceFolders || [])'
174
+ )
175
+
176
+ // Ensure workspace is imported from coc.nvim when we introduced workspace. references
177
+ if (newContent.includes('workspace.') && newContent.match(/from\s+['"]coc\.nvim['"]/)) {
178
+ newContent = newContent.replace(
179
+ /(import\s*\{\s*)([^}]*?)(\s*\}\s*from\s*['"]coc\.nvim['"])/g,
180
+ (match, prefix, existing, suffix) => {
181
+ if (!existing.includes('workspace')) {
182
+ const sep = existing.trim() ? ', ' : ''
183
+ return `${prefix}${existing.trim()}${sep}workspace${suffix}`
184
+ }
185
+ return match
44
186
  }
45
- }
46
- })
187
+ )
188
+ }
189
+
190
+ if (newContent !== content) {
191
+ file.replaceWithText(newContent)
192
+ }
47
193
  }
@@ -13,6 +13,9 @@ export interface ServerModuleConfig {
13
13
  kind: 'module'
14
14
  package: string
15
15
  entry?: 'main' | 'bin'
16
+ /** When entry is 'bin', pick a specific bin entry by name (e.g. "tailwindcss-language-server").
17
+ * Defaults to the first entry in the bin object. */
18
+ binName?: string
16
19
  }
17
20
 
18
21
  export interface ServerBinaryConfig {
@@ -37,6 +40,8 @@ export interface LanguageClientStep {
37
40
  multiRoot?: boolean
38
41
  /** Enable debug logging in generated code */
39
42
  verbose?: boolean
43
+ /** Extra options passed as initializationOptions to LanguageClient (JS object expression, inserted as-is) */
44
+ initializationOptions?: string
40
45
  }
41
46
 
42
47
  export interface SourceStep {
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.2.1",
42
+ version: "1.2.3",
43
43
  description: "Run VS Code extensions seamlessly in coc.nvim",
44
44
  main: "lib/index.js",
45
45
  keywords: [
@@ -89,13 +89,34 @@ var require_package = __commonJS({
89
89
  ],
90
90
  contributes: {
91
91
  commands: [
92
- { command: "loader.open", title: "Open VS Code extension loader" },
93
- { command: "loader.install", title: "Install a VS Code extension" },
94
- { command: "loader.uninstall", title: "Uninstall a package" },
95
- { command: "loader.update", title: "Update a package" },
96
- { command: "loader.uninstallAll", title: "Uninstall all packages" },
97
- { command: "loader.updateRegistry", title: "Update package registry" },
98
- { command: "loader._dispatch", title: "" }
92
+ {
93
+ command: "loader.open",
94
+ title: "Open VS Code extension loader"
95
+ },
96
+ {
97
+ command: "loader.install",
98
+ title: "Install a VS Code extension"
99
+ },
100
+ {
101
+ command: "loader.uninstall",
102
+ title: "Uninstall a package"
103
+ },
104
+ {
105
+ command: "loader.update",
106
+ title: "Update a package"
107
+ },
108
+ {
109
+ command: "loader.uninstallAll",
110
+ title: "Uninstall all packages"
111
+ },
112
+ {
113
+ command: "loader.updateRegistry",
114
+ title: "Update package registry"
115
+ },
116
+ {
117
+ command: "loader._dispatch",
118
+ title: ""
119
+ }
99
120
  ]
100
121
  }
101
122
  };
@@ -500,7 +521,7 @@ async function downloadSource(info, name, onProgress) {
500
521
  }
501
522
  async function convertSource(inputDir, name, info, onProgress) {
502
523
  const build = buildDir(name);
503
- if (fs3.existsSync(build)) fs3.rmSync(build, { recursive: true });
524
+ await rimraf(build);
504
525
  const cli = converterCliPath();
505
526
  const converterDir = path3.resolve(path3.dirname(path3.dirname(cli)));
506
527
  if (!fs3.existsSync(path3.join(converterDir, "node_modules", "commander"))) {
@@ -593,34 +614,53 @@ async function buildPackage(name, inputDir, info, onProgress) {
593
614
  onProgress(3, 5, "Installing server dependencies...", `npm install in ${serverDir}`);
594
615
  await run("npm", ["install", "--legacy-peer-deps"], serverDir, npmLog);
595
616
  const destServer = path3.join(build, "server");
596
- if (fs3.existsSync(destServer)) fs3.rmSync(destServer, { recursive: true });
597
- fs3.cpSync(serverDir, destServer, { recursive: true });
617
+ await rimraf(destServer);
618
+ await cpdir(serverDir, destServer);
598
619
  }
599
620
  onProgress(4, 5, "Building...", "node esbuild.mjs");
600
621
  const buildLog = (chunk) => onProgress(4, 5, chunk.trim(), "");
601
622
  await run("node", ["esbuild.mjs"], build, buildLog);
623
+ const archMap = {
624
+ arm64: "aarch64",
625
+ x64: "x86_64"
626
+ };
627
+ const platformMap = {
628
+ darwin: "apple-darwin",
629
+ linux: "unknown-linux-gnu",
630
+ win32: "pc-windows-msvc"
631
+ };
632
+ const arch2 = os3.arch() === "arm64" ? "arm64" : "x64";
633
+ const platform = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux";
634
+ const rawArch = archMap[arch2] || arch2;
635
+ const rustTarget = `${rawArch}-${platformMap[platform] || platform}`;
636
+ const indexPath = path3.join(build, "lib", "index.js");
637
+ if (fs3.existsSync(indexPath)) {
638
+ let code = fs3.readFileSync(indexPath, "utf-8");
639
+ for (const [key, val] of [["platform", platform], ["arch", arch2], ["raw-arch", rawArch], ["rust-target", rustTarget]]) {
640
+ code = code.replace(new RegExp(`\\{\\{${key}}}`, "g"), val);
641
+ }
642
+ if (code !== fs3.readFileSync(indexPath, "utf-8")) {
643
+ fs3.writeFileSync(indexPath, code);
644
+ }
645
+ }
602
646
  if (info.serverBinary) {
603
647
  const sb = info.serverBinary;
604
648
  onProgress(4, 5, "Downloading language server...", `fetching ${sb.repo}`);
605
649
  try {
606
- const tagRes = await fetch(`https://api.github.com/repos/${sb.repo}/releases/latest`);
607
- if (!tagRes.ok) throw new Error(`GitHub API: HTTP ${tagRes.status}`);
608
- const tagData = await tagRes.json();
650
+ let tagData;
651
+ try {
652
+ const tagRes = await fetch(`https://api.github.com/repos/${sb.repo}/releases/latest`);
653
+ if (!tagRes.ok) throw new Error(`HTTP ${tagRes.status}`);
654
+ tagData = await tagRes.json();
655
+ } catch {
656
+ const { execFile: execFile2 } = require("child_process");
657
+ const out = await new Promise((resolve2, reject) => {
658
+ execFile2("curl", ["-sL", `https://api.github.com/repos/${sb.repo}/releases/latest`], { encoding: "utf-8", maxBuffer: 1024 * 1024 }, (err, stdout) => err ? reject(err) : resolve2(stdout));
659
+ });
660
+ tagData = JSON.parse(out);
661
+ }
609
662
  const tag = tagData.tag_name;
610
663
  const version = tag.replace(/^v/, "");
611
- const archMap = {
612
- arm64: "aarch64",
613
- x64: "x86_64"
614
- };
615
- const platformMap = {
616
- darwin: "apple-darwin",
617
- linux: "unknown-linux-gnu",
618
- win32: "pc-windows-msvc"
619
- };
620
- const arch2 = os3.arch() === "arm64" ? "arm64" : "x64";
621
- const platform = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux";
622
- const rawArch = archMap[arch2] || arch2;
623
- const rustTarget = `${rawArch}-${platformMap[platform] || platform}`;
624
664
  const filename = sb.asset.replace(/\{\{version}}/g, version).replace(/\{\{platform}}/g, platform).replace(/\{\{arch}}/g, arch2).replace(/\{\{raw-arch}}/g, rawArch).replace(/\{\{rust-target}}/g, rustTarget);
625
665
  const url = `https://github.com/${sb.repo}/releases/download/${tag}/${filename}`;
626
666
  onProgress(4, 5, "Downloading...", `curl ${filename}`);
@@ -630,12 +670,15 @@ async function buildPackage(name, inputDir, info, onProgress) {
630
670
  fs3.mkdirSync(serverDir2, { recursive: true });
631
671
  if (filename.endsWith(".zip")) {
632
672
  await run("unzip", ["-o", filename, "-d", serverDir2], build);
673
+ } else if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
674
+ await run("tar", ["xzf", filename, "-C", serverDir2], build);
633
675
  } else if (filename.endsWith(".gz") && !filename.endsWith(".tar.gz")) {
634
676
  const outName = filename.replace(/\.gz$/, "");
635
677
  await run("gunzip", [filename], build);
636
678
  fs3.renameSync(path3.join(build, outName), path3.join(serverDir2, outName));
637
679
  } else {
638
- await run("tar", ["xzf", filename, "-C", serverDir2], build);
680
+ const binName = sb.binaryPath || filename;
681
+ fs3.renameSync(path3.join(build, filename), path3.join(serverDir2, binName));
639
682
  }
640
683
  try {
641
684
  fs3.readdirSync(serverDir2).forEach((f) => {
@@ -643,19 +686,18 @@ async function buildPackage(name, inputDir, info, onProgress) {
643
686
  });
644
687
  } catch {
645
688
  }
689
+ if (version) {
690
+ const verPath = path3.join(build, "lib", "index.js");
691
+ if (fs3.existsSync(verPath)) {
692
+ let vc = fs3.readFileSync(verPath, "utf-8");
693
+ if (vc.includes("{{version}}")) {
694
+ fs3.writeFileSync(verPath, vc.replace(/\{\{version}}/g, version));
695
+ }
696
+ }
697
+ }
646
698
  if (sb.binaryPath || filename.match(/\.(zip|gz)$/)) {
647
699
  const archivePath = path3.join(build, filename);
648
- if (fs3.existsSync(archivePath)) fs3.rmSync(archivePath);
649
- }
650
- const indexPath = path3.join(build, "lib", "index.js");
651
- if (fs3.existsSync(indexPath)) {
652
- let code = fs3.readFileSync(indexPath, "utf-8");
653
- code = code.replace(/\{\{version}}/g, version);
654
- code = code.replace(/\{\{platform}}/g, platform);
655
- code = code.replace(/\{\{arch}}/g, arch2);
656
- code = code.replace(/\{\{raw-arch}}/g, rawArch);
657
- code = code.replace(/\{\{rust-target}}/g, rustTarget);
658
- fs3.writeFileSync(indexPath, code);
700
+ if (fs3.existsSync(archivePath)) await rimraf(archivePath);
659
701
  }
660
702
  } catch (e) {
661
703
  onProgress(4, 5, `Warning: serverBinary setup failed (${e.message})`, "install server binary manually");
@@ -669,9 +711,9 @@ async function installToCoc(name, onProgress) {
669
711
  const src = buildDir(name);
670
712
  const dest = pluginDir(name);
671
713
  onProgress(5, 5, "Installing to coc...", `copy to ${dest} + register in extensions/package.json`);
672
- if (fs3.existsSync(dest)) fs3.rmSync(dest, { recursive: true });
714
+ await rimraf(dest);
673
715
  fs3.mkdirSync(path3.dirname(dest), { recursive: true });
674
- fs3.cpSync(src, dest, { recursive: true });
716
+ await cpdir(src, dest);
675
717
  const pkgPath = extensionsPkgPath();
676
718
  const pkg = fs3.existsSync(pkgPath) ? JSON.parse(fs3.readFileSync(pkgPath, "utf-8")) : { dependencies: {} };
677
719
  pkg.dependencies = pkg.dependencies || {};
@@ -737,13 +779,23 @@ async function installPackage(state, name) {
737
779
  state.setPackageStatus(name, "failed", { error: e.message });
738
780
  }
739
781
  }
782
+ function rimraf(dir) {
783
+ return new Promise((resolve2, reject) => {
784
+ if (!fs3.existsSync(dir)) return resolve2();
785
+ (0, import_child_process2.spawn)("rm", ["-rf", dir], { stdio: "ignore" }).on("close", (code) => code === 0 ? resolve2() : reject(new Error(`rm -rf exited ${code}`)));
786
+ });
787
+ }
788
+ function cpdir(src, dest) {
789
+ return new Promise((resolve2, reject) => {
790
+ const parent = path3.dirname(dest);
791
+ if (!fs3.existsSync(parent)) fs3.mkdirSync(parent, { recursive: true });
792
+ (0, import_child_process2.spawn)("cp", ["-r", src, dest], { stdio: "ignore" }).on("close", (code) => code === 0 ? resolve2() : reject(new Error(`cp -r exited ${code}`)));
793
+ });
794
+ }
740
795
  async function uninstallPackage(state, name) {
741
796
  state.setPackageStatus(name, "uninstalling", { progress: "[1/3] Removing from coc..." });
742
797
  try {
743
- const dest = pluginDir(name);
744
- if (fs3.existsSync(dest)) {
745
- fs3.rmSync(dest, { recursive: true });
746
- }
798
+ await rimraf(pluginDir(name));
747
799
  state.setPackageStatus(name, "uninstalling", { progress: "[2/3] Removing from package.json..." });
748
800
  const pkgPath = extensionsPkgPath();
749
801
  const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
@@ -755,10 +807,7 @@ async function uninstallPackage(state, name) {
755
807
  fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
756
808
  }
757
809
  state.setPackageStatus(name, "uninstalling", { progress: "[3/3] Removing cache..." });
758
- const cache = cacheDir(name);
759
- if (fs3.existsSync(cache)) {
760
- fs3.rmSync(cache, { recursive: true });
761
- }
810
+ await rimraf(cacheDir(name));
762
811
  state.setPackageStatus(name, "not-installed");
763
812
  state.setDirty();
764
813
  import_coc.window.showInformationMessage(`coc-${name} uninstalled`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coc-vscode-loader",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Run VS Code extensions seamlessly in coc.nvim",
5
5
  "main": "lib/index.js",
6
6
  "keywords": [
@@ -50,13 +50,34 @@
50
50
  ],
51
51
  "contributes": {
52
52
  "commands": [
53
- { "command": "loader.open", "title": "Open VS Code extension loader" },
54
- { "command": "loader.install", "title": "Install a VS Code extension" },
55
- { "command": "loader.uninstall", "title": "Uninstall a package" },
56
- { "command": "loader.update", "title": "Update a package" },
57
- { "command": "loader.uninstallAll", "title": "Uninstall all packages" },
58
- { "command": "loader.updateRegistry", "title": "Update package registry" },
59
- { "command": "loader._dispatch", "title": "" }
53
+ {
54
+ "command": "loader.open",
55
+ "title": "Open VS Code extension loader"
56
+ },
57
+ {
58
+ "command": "loader.install",
59
+ "title": "Install a VS Code extension"
60
+ },
61
+ {
62
+ "command": "loader.uninstall",
63
+ "title": "Uninstall a package"
64
+ },
65
+ {
66
+ "command": "loader.update",
67
+ "title": "Update a package"
68
+ },
69
+ {
70
+ "command": "loader.uninstallAll",
71
+ "title": "Uninstall all packages"
72
+ },
73
+ {
74
+ "command": "loader.updateRegistry",
75
+ "title": "Update package registry"
76
+ },
77
+ {
78
+ "command": "loader._dispatch",
79
+ "title": ""
80
+ }
60
81
  ]
61
82
  }
62
83
  }