coc-vscode-loader 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,9 +37,10 @@ npm install coc-vscode-loader
37
37
  | `x` | Toggle mark package for batch operations |
38
38
  | `f` | Cycle filter: all → installed → available |
39
39
  | `s` | Cycle sort: default → name → status → type |
40
+ | `j` / `k` | Scroll through packages (virtual scroll) |
40
41
  | `gg` | Jump to first package |
41
42
  | `G` | Jump to last package |
42
- | `<CR>` | Toggle details (commit / type / source) or install log |
43
+ | `<CR>` | Open detail popup (info / install log with syntax highlights) |
43
44
  | `/` | Search filter |
44
45
  | `q` | Close (auto `:CocRestart` if changes detected) |
45
46
  | `<Esc>` | Help→Search→Clear marks→Cancel|Close |
@@ -60,12 +61,13 @@ npm install coc-vscode-loader
60
61
 
61
62
  - **Real conversion pipeline** — git clone → converter → npm install → esbuild → register to coc
62
63
  - **Auto-fetch registry** — remote registry fetched in background when TUI opens, no manual refresh needed
64
+ - **Virtual scrolling** — `j`/`k` smooth scroll through packages, handles 100k+ registry entries
63
65
  - **Incremental cache** — source/ keeps git repo, updates via git pull only
64
66
  - **Commit tracking** — records commit SHA after install, visible in detail view
65
67
  - **Update check** — `C` key compares against remote HEAD, shows `↑` when outdated
66
68
  - **Auto restart** — `:CocRestart` triggered automatically on close when changes detected
67
69
  - **Manual registry update** — `:CocCommand loader.updateRegistry` also available for re-fetch
68
- - **Install logs** — real command output per step, expandable
70
+ - **Detail popup** — `<CR>` opens centered float window with package info or live install log (syntax highlighted, auto-scroll to latest)
69
71
  - **Mark & batch** — `x` toggle mark, visual indicator, `D` clean orphaned packages
70
72
  - **Filter & sort** — `f` cycle view filter, `s` cycle sort order (name/status/type)
71
73
  - **Concurrency limit** — max 3 parallel operations for `U` (Update All)
Binary file
@@ -18,11 +18,25 @@ cd ~/.config/coc/extensions && npm install /path/to/coc-ext
18
18
 
19
19
  ## Verified conversions
20
20
 
21
- | Plugin | Type | Auto-detected | Build | Working | Notes |
22
- |--------|------|---------------|-------|---------|-------|
23
- | Volar (Vue) | TS bridge | `@vue/language-server` + `typescript` | ✅ | ✅ | Requires modified coc-tsserver |
24
- | Prisma | Pure LSP | `@prisma/language-server` | ✅ | ✅ | Auto-detects bin entry |
25
- | HTML CSS Support | Direct API | — | ✅ | ✅ | Handles API differences |
21
+ | Plugin | Type | Notes |
22
+ |--------|------|-------|
23
+ | Volar (Vue) | TS bridge | Requires modified coc-tsserver |
24
+ | Prisma | Pure LSP | Auto-detects bin entry |
25
+ | HTML CSS Support | Direct API | Handles API differences |
26
+ | Deno | Pure LSP | Binary server download |
27
+ | TOML (Taplo) | Pure LSP | Binary server download |
28
+ | Ansible | Pure LSP | npm package server + pip install |
29
+ | YAML | Pure LSP | npm package server |
30
+ | Tailwind CSS | Pure LSP | npm package server, bin entry |
31
+ | Biome | Pure LSP | Binary server download |
32
+ | Stylelint | Pure LSP | npm package server |
33
+ | Prettier | Direct API | Source transforms |
34
+ | Svelte | Pure LSP | npm package server |
35
+ | Astro | Pure LSP | npm package server |
36
+ | Lua | Pure LSP | npm package server |
37
+ | gitignore | Direct API | Source transforms |
38
+
39
+ See the [registry](https://github.com/coc-plugin/coc-vscode-registry) for the full list and latest status.
26
40
 
27
41
  ### Plugin types
28
42
 
@@ -77,21 +91,21 @@ const PRESETS = {
77
91
  `convert.ts` only calls `getActivePresets()` + `generateBridgeCode()`, it never touches bridge logic directly.
78
92
  Adding a new bridge type = add a new preset in `presets.ts`, no changes to main flow.
79
93
 
80
- See [coc-vscode-registry/docs/converter-design-v2.md](https://github.com/coc-plugin/coc-vscode-registry/blob/main/docs/converter-design-v2.md).
94
+ See [../docs/converter-design-v2.md](../docs/converter-design-v2.md).
81
95
 
82
96
  ## File structure
83
97
 
84
98
  | File | Lines | Description |
85
99
  |------|-------|-------------|
86
- | `src/cli.ts` | 28 | CLI entry |
87
- | `src/convert.ts` | 484 | Main flow + template generation + API replacement |
88
- | `src/scanner.ts` | 136 | API scanner + plugin classification |
89
- | `src/transforms/import-mapping.ts` | 47 | Import replacement |
100
+ | `src/cli.ts` | 59 | CLI entry |
101
+ | `src/convert.ts` | 461 | Main flow + template generation + API replacement |
102
+ | `src/scanner.ts` | 52 | API scanner + plugin classification |
103
+ | `src/transforms/import-mapping.ts` | 193 | Import replacement |
90
104
  | `src/transforms/language-client.ts` | 48 | LanguageClient adaptation |
91
- | `src/transforms/class-to-factory.ts` | 54 | new Xxx() → Xxx.create() |
92
- | `src/transforms/provider-register.ts` | 55 | Provider registration signature fixes |
93
- | `src/transforms/enum-offset.ts` | 49 | Enum value offset annotations |
94
- | **Total** | **~870** | |
105
+ | `src/transforms/class-to-factory.ts` | 53 | new Xxx() → Xxx.create() |
106
+ | `src/transforms/provider-register.ts` | 61 | Provider registration signature fixes |
107
+ | `src/transforms/enum-offset.ts` | 32 | Enum value offset annotations |
108
+ | **Total** | **~959** | |
95
109
 
96
110
  ## Handled API differences
97
111
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "converter",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "converter",
9
- "version": "1.2.5",
9
+ "version": "1.2.7",
10
10
  "dependencies": {
11
11
  "commander": "^15.0.0",
12
12
  "ts-morph": "^28.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converter",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "private": true,
5
5
  "description": "vscode → coc.nvim converter prototype",
6
6
  "type": "module",
@@ -326,6 +326,7 @@ export async function convert(opts: ConvertOptions): Promise<void> {
326
326
  command: c.command,
327
327
  title: c.title,
328
328
  })) || undefined,
329
+ snippets: origPkg.contributes?.snippets || undefined,
329
330
  ...(tsPlugins.length > 0 ? {
330
331
  typescriptServerPlugins: tsPlugins.map(p => ({
331
332
  ...p,
@@ -338,6 +339,7 @@ export async function convert(opts: ConvertOptions): Promise<void> {
338
339
  // Clean null fields
339
340
  if (!pkg.contributes?.configuration) delete (pkg.contributes as any).configuration
340
341
  if (!pkg.contributes?.commands) delete (pkg.contributes as any).commands
342
+ if (!pkg.contributes?.snippets) delete (pkg.contributes as any).snippets
341
343
  if (Object.keys(pkg.contributes).length === 0) delete (pkg as any).contributes
342
344
 
343
345
  fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkg, null, 2))
@@ -23,7 +23,7 @@ export function scan(dir: string): ScanResult {
23
23
  const apis: string[] = []
24
24
  const relative = path.relative(dir, filePath)
25
25
 
26
- if (content.includes("from 'vscode'") || content.includes('from "vscode"') || content.includes('require("vscode")')) {
26
+ if (content.includes("from 'vscode'") || content.includes('from "vscode"') || content.includes('require("vscode")') || content.includes("require('vscode')")) {
27
27
  apis.push('vscode')
28
28
  }
29
29
 
@@ -3,6 +3,7 @@ import { languageClientGenerator } from './language-client.js'
3
3
  import { sourceGenerator } from './source.js'
4
4
  import { bridgeGenerator } from './bridge.js'
5
5
  import { markUnsupportedGenerator } from './mark-unsupported.js'
6
+ import { snippetsGenerator } from './snippets.js'
6
7
 
7
8
  const REGISTRY: Record<string, StepGenerator> = {}
8
9
 
@@ -27,3 +28,4 @@ registerGenerator(languageClientGenerator)
27
28
  registerGenerator(sourceGenerator)
28
29
  registerGenerator(bridgeGenerator)
29
30
  registerGenerator(markUnsupportedGenerator)
31
+ registerGenerator(snippetsGenerator)
@@ -108,7 +108,7 @@ ${ls.verbose ? ` console.log('[${escapeStr(id)}] creating LanguageClient')\n`
108
108
  {
109
109
  documentSelector: ${docSelectorCode},
110
110
  outputChannelName: '${escapeStr(description)}',
111
- ${ls.initializationOptions ? `initializationOptions: ${ls.initializationOptions},` : ''}
111
+ ${ls.initializationOptions ? `initializationOptions: ${ls.initializationOptions.replace(/`/g, '\\`').replace(/\$\{/g, '\\${')},` : ''}
112
112
  },
113
113
  )
114
114
  context.subscriptions.push({ dispose: () => c.stop() })
@@ -117,23 +117,27 @@ ${ls.verbose ? ` console.log('[${escapeStr(id)}] creating LanguageClient')\n`
117
117
  }
118
118
 
119
119
  ${ls.verbose ? ` console.log('[${escapeStr(id)}] registering LanguageClient')\n` : ''}\
120
- let client: LanguageClient
120
+ let clients: LanguageClient[]
121
121
  if (${multiRoot ? 'workspace.workspaceFolders && workspace.workspaceFolders.length > 1' : 'false'}) {
122
122
  ${ls.verbose ? ` console.log('[${escapeStr(id)}] multiRoot mode')\n` : ''}\
123
- for (const folder of workspace.workspaceFolders) {
124
- client = createClient()
125
- client.start()
126
- }
123
+ clients = workspace.workspaceFolders.map(folder => {
124
+ const c = createClient()
125
+ c.start()
126
+ return c
127
+ })
127
128
  } else {
128
- client = createClient()
129
+ const c = createClient()
129
130
  ${ls.verbose ? ` console.log('[${escapeStr(id)}] starting client')\n` : ''}\
130
- client.start()
131
+ c.start()
132
+ clients = [c]
131
133
  }
132
134
 
133
135
  context.subscriptions.push(
134
136
  commands.registerCommand('${escapeStr(pluginName)}.restart', async () => {
135
- await client.stop()
136
- await client.start()
137
+ for (const c of clients) {
138
+ await c.stop()
139
+ await c.start()
140
+ }
137
141
  }),
138
142
  )
139
143
  } catch (e: any) {
@@ -0,0 +1,122 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+ import { execFileSync } from 'child_process'
4
+ import { StepGenerator, StepContext, SnippetsStep, StepResult } from '../types.js'
5
+
6
+ export const snippetsGenerator: StepGenerator = {
7
+ type: 'snippets',
8
+
9
+ generate(ctx: StepContext, step: any): StepResult {
10
+ const ss = step as SnippetsStep
11
+ const { input, output, origPkg, verbose } = ctx
12
+
13
+ const contributedSnippets: Array<{ language: string; path: string }> =
14
+ origPkg.contributes?.snippets || []
15
+
16
+ if (contributedSnippets.length === 0 && !ss.languages) {
17
+ throw new Error('snippets step: source package.json has no contributes.snippets, and no languages specified in step config')
18
+ }
19
+
20
+ // Collect unique (sourcePath → [languages]) mappings
21
+ const fileToLanguages = new Map<string, string[]>()
22
+ if (ss.languages) {
23
+ for (const lang of ss.languages) {
24
+ const entry = contributedSnippets.find(s => s.language === lang)
25
+ if (entry) {
26
+ const langs = fileToLanguages.get(entry.path) || []
27
+ langs.push(lang)
28
+ fileToLanguages.set(entry.path, langs)
29
+ } else {
30
+ const defaultPath = `./snippets/${lang}.json`
31
+ const fp = path.join(input, defaultPath)
32
+ if (fs.existsSync(fp)) {
33
+ const langs = fileToLanguages.get(defaultPath) || []
34
+ langs.push(lang)
35
+ fileToLanguages.set(defaultPath, langs)
36
+ } else if (verbose) {
37
+ console.warn(` snippets: no snippet file found for language "${lang}", skipping`)
38
+ }
39
+ }
40
+ }
41
+ } else {
42
+ for (const s of contributedSnippets) {
43
+ const langs = fileToLanguages.get(s.path) || []
44
+ langs.push(s.language)
45
+ fileToLanguages.set(s.path, langs)
46
+ }
47
+ }
48
+
49
+ if (fileToLanguages.size === 0) {
50
+ throw new Error('snippets step: no snippet files found to copy')
51
+ }
52
+
53
+ // Create output directories and copy files to original relative paths
54
+ const srcDir = path.join(output, 'src')
55
+ fs.mkdirSync(srcDir, { recursive: true })
56
+
57
+ const generatedFiles: Array<{ path: string; content: string }> = []
58
+ let copiedCount = 0
59
+ const allLanguages: string[] = []
60
+
61
+ for (const [sourceRelPath, languages] of fileToLanguages) {
62
+ const sourceFile = path.join(input, sourceRelPath)
63
+ if (!fs.existsSync(sourceFile)) {
64
+ if (verbose) console.warn(` snippets: source file not found: ${sourceFile}, skipping`)
65
+ continue
66
+ }
67
+ const dest = path.join(output, sourceRelPath)
68
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
69
+ fs.copyFileSync(sourceFile, dest)
70
+ copiedCount++
71
+ allLanguages.push(...languages)
72
+ if (verbose) console.log(` snippets: copied ${sourceRelPath} (${languages.join(', ')})`)
73
+ }
74
+
75
+ if (copiedCount === 0 && ss.build) {
76
+ // Run build script to generate snippet files (e.g. node merge.js)
77
+ if (verbose) console.log(` snippets: running build: ${ss.build}`)
78
+ execFileSync('npm', ['install', '--legacy-peer-deps'], { cwd: input, stdio: verbose ? 'inherit' : 'pipe' })
79
+ const [cmd, ...args] = ss.build.split(' ')
80
+ execFileSync(cmd, args, { cwd: input, stdio: verbose ? 'inherit' : 'pipe' })
81
+ // Retry copying
82
+ for (const [sourceRelPath, languages] of fileToLanguages) {
83
+ const sourceFile = path.join(input, sourceRelPath)
84
+ if (fs.existsSync(sourceFile)) {
85
+ const dest = path.join(output, sourceRelPath)
86
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
87
+ fs.copyFileSync(sourceFile, dest)
88
+ copiedCount++
89
+ allLanguages.push(...languages)
90
+ if (verbose) console.log(` snippets: copied ${sourceRelPath} (${languages.join(', ')})`)
91
+ }
92
+ }
93
+ }
94
+
95
+ if (copiedCount === 0) {
96
+ throw new Error('snippets step: no snippet files were copied')
97
+ }
98
+
99
+ // Generate empty src/index.ts
100
+ const indexContent = `\
101
+ import { ExtensionContext } from 'coc.nvim'
102
+
103
+ export function activate(context: ExtensionContext): void {
104
+ // coc-snippets discovers snippets via package.json's contributes.snippets
105
+ }
106
+ `
107
+ generatedFiles.push({ path: 'src/index.ts', content: indexContent })
108
+
109
+ const activationEvents = [...new Set(allLanguages)].map(l => `onLanguage:${l}`)
110
+
111
+ if (verbose) {
112
+ console.log(` snippets: ${copiedCount} files, ${new Set(allLanguages).size} languages`)
113
+ }
114
+
115
+ return {
116
+ generatedFiles,
117
+ entryPoint: 'src/index.ts',
118
+ keepDeps: {},
119
+ activationEvents,
120
+ }
121
+ },
122
+ }
@@ -20,16 +20,14 @@ export const transformClassToFactory: Transform = (ctx) => {
20
20
 
21
21
  // AST approach: try to replace via ts-morph
22
22
  const nodes = file.getDescendantsOfKind(SyntaxKind.NewExpression)
23
- const astReplacements: Array<{ node: any, text: string }> = []
23
+ // Sort by position descending so inner nodes are processed first
24
+ nodes.sort((a, b) => b.getPos() - a.getPos())
24
25
  for (const expr of nodes) {
25
26
  const text = expr.getText()
26
27
  const m = text.match(/^new\s+(\w+)\(/)
27
28
  if (!m || !FACTORY_TYPES.has(m[1])) continue
28
29
  const args = text.slice(m[0].length, -1)
29
- astReplacements.push({ node: expr, text: `${m[1]}.create(${args})` })
30
- }
31
- for (const { node, text } of astReplacements) {
32
- try { node.replaceWithText(text) } catch {}
30
+ try { expr.replaceWithText(`${m[1]}.create(${args})`) } catch {}
33
31
  }
34
32
 
35
33
  // Text fallback: catch remaining new Xxx() that AST might have missed
@@ -44,14 +44,36 @@ export const transformProviderRegister: Transform = (ctx) => {
44
44
  `registerCompletionItemProvider('${pluginName}', '${shortcut}', `
45
45
  )
46
46
  // Wrap the last argument in an array if it's a string (trigger chars)
47
- content = content.replace(
48
- /(registerCompletionItemProvider\([^)]+),\s*'([^']+)'\)/g,
49
- '$1, ["$2"])'
50
- )
51
- content = content.replace(
52
- /(registerCompletionItemProvider\([^)]+),\s*"([^"]+)"\)/g,
53
- '$1, ["$2"])'
54
- )
47
+ // Use paren-balancing to handle nested parentheses in arguments
48
+ // Build result by iterating over all matches, replacing each full call
49
+ const providerRe = /registerCompletionItemProvider\(/g
50
+ let result = ''
51
+ let lastIdx = 0
52
+ let m: RegExpExecArray | null
53
+ while ((m = providerRe.exec(content)) !== null) {
54
+ const start = m.index
55
+ let depth = 1
56
+ let i = start + m[0].length
57
+ while (i < content.length && depth > 0) {
58
+ if (content[i] === '(') depth++
59
+ else if (content[i] === ')') depth--
60
+ i++
61
+ }
62
+ const end = i
63
+ const fullCall = content.slice(start, end)
64
+ const lastStrMatch = fullCall.match(/,?\s*'([^']+)'\s*\)$/)
65
+ const lastDblMatch = fullCall.match(/,?\s*"([^"]+)"\s*\)$/)
66
+ let replacement = fullCall
67
+ if (lastStrMatch) {
68
+ replacement = fullCall.slice(0, fullCall.length - lastStrMatch[0].length) + ', ["' + lastStrMatch[1] + '"])'
69
+ } else if (lastDblMatch) {
70
+ replacement = fullCall.slice(0, fullCall.length - lastDblMatch[0].length) + ', ["' + lastDblMatch[1] + '"])'
71
+ }
72
+ result += content.slice(lastIdx, start) + replacement
73
+ lastIdx = end
74
+ }
75
+ result += content.slice(lastIdx)
76
+ content = result
55
77
  changed = true
56
78
  }
57
79
 
@@ -73,7 +73,15 @@ export interface MarkUnsupportedStep {
73
73
  verbose?: boolean
74
74
  }
75
75
 
76
- export type ConvertStep = LanguageClientStep | SourceStep | BridgeStep | MarkUnsupportedStep
76
+ export interface SnippetsStep {
77
+ type: 'snippets'
78
+ /** Optional: override languages to generate (default: read from source package.json's contributes.snippets) */
79
+ languages?: string[]
80
+ /** Optional: build command to run in source dir before collecting snippet files (e.g. "node merge.js") */
81
+ build?: string
82
+ }
83
+
84
+ export type ConvertStep = LanguageClientStep | SourceStep | BridgeStep | MarkUnsupportedStep | SnippetsStep
77
85
 
78
86
  // ---- Step execution ----
79
87