coc-vscode-loader 1.2.6 → 1.2.8

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,10 +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
- | `[` / `]` | Previous / next page |
41
- | `gg` | Jump to first page |
42
- | `G` | Jump to last page |
43
- | `<CR>` | Toggle details (commit / type / source) or install log |
40
+ | `j` / `k` | Scroll through packages (virtual scroll) |
41
+ | `gg` | Jump to first package |
42
+ | `G` | Jump to last package |
43
+ | `<CR>` | Open detail popup (info / install log with syntax highlights) |
44
44
  | `/` | Search filter |
45
45
  | `q` | Close (auto `:CocRestart` if changes detected) |
46
46
  | `<Esc>` | Help→Search→Clear marks→Cancel|Close |
@@ -61,13 +61,13 @@ npm install coc-vscode-loader
61
61
 
62
62
  - **Real conversion pipeline** — git clone → converter → npm install → esbuild → register to coc
63
63
  - **Auto-fetch registry** — remote registry fetched in background when TUI opens, no manual refresh needed
64
- - **Pagination** — `[`/`]` prev/next page, 50 packages per page, handles 5000+ registry entries
64
+ - **Virtual scrolling** — `j`/`k` smooth scroll through packages, handles 100k+ registry entries
65
65
  - **Incremental cache** — source/ keeps git repo, updates via git pull only
66
66
  - **Commit tracking** — records commit SHA after install, visible in detail view
67
67
  - **Update check** — `C` key compares against remote HEAD, shows `↑` when outdated
68
68
  - **Auto restart** — `:CocRestart` triggered automatically on close when changes detected
69
69
  - **Manual registry update** — `:CocCommand loader.updateRegistry` also available for re-fetch
70
- - **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)
71
71
  - **Mark & batch** — `x` toggle mark, visual indicator, `D` clean orphaned packages
72
72
  - **Filter & sort** — `f` cycle view filter, `s` cycle sort order (name/status/type)
73
73
  - **Concurrency limit** — max 3 parallel operations for `U` (Update All)
Binary file
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "converter",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "converter",
9
- "version": "1.2.6",
9
+ "version": "1.2.8",
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.6",
3
+ "version": "1.2.8",
4
4
  "private": true,
5
5
  "description": "vscode → coc.nvim converter prototype",
6
6
  "type": "module",
@@ -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
 
@@ -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) {
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'fs'
2
2
  import * as path from 'path'
3
+ import { execFileSync } from 'child_process'
3
4
  import { StepGenerator, StepContext, SnippetsStep, StepResult } from '../types.js'
4
5
 
5
6
  export const snippetsGenerator: StepGenerator = {
@@ -46,7 +47,19 @@ export const snippetsGenerator: StepGenerator = {
46
47
  }
47
48
 
48
49
  if (fileToLanguages.size === 0) {
49
- throw new Error('snippets step: no snippet files found to copy')
50
+ if (ss.languages) {
51
+ // No paths resolved. Generate default paths from languages list so empty file
52
+ // fallback can create them later.
53
+ if (verbose) console.warn(' snippets: no paths resolved from contributedSnippets, using defaults')
54
+ for (const lang of ss.languages) {
55
+ const defaultPath = `./snippets/${lang}.json`
56
+ const langs = fileToLanguages.get(defaultPath) || []
57
+ langs.push(lang)
58
+ fileToLanguages.set(defaultPath, langs)
59
+ }
60
+ } else {
61
+ throw new Error('snippets step: no snippet files found to copy')
62
+ }
50
63
  }
51
64
 
52
65
  // Create output directories and copy files to original relative paths
@@ -71,8 +84,42 @@ export const snippetsGenerator: StepGenerator = {
71
84
  if (verbose) console.log(` snippets: copied ${sourceRelPath} (${languages.join(', ')})`)
72
85
  }
73
86
 
87
+ if (copiedCount === 0 && ss.build) {
88
+ // Run build script to generate snippet files (e.g. node merge.js)
89
+ if (verbose) console.log(` snippets: running build: ${ss.build}`)
90
+ execFileSync('npm', ['install', '--legacy-peer-deps'], { cwd: input, stdio: verbose ? 'inherit' : 'pipe' })
91
+ const [cmd, ...args] = ss.build.split(' ')
92
+ execFileSync(cmd, args, { cwd: input, stdio: verbose ? 'inherit' : 'pipe' })
93
+ // Retry copying
94
+ for (const [sourceRelPath, languages] of fileToLanguages) {
95
+ const sourceFile = path.join(input, sourceRelPath)
96
+ if (fs.existsSync(sourceFile)) {
97
+ const dest = path.join(output, sourceRelPath)
98
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
99
+ fs.copyFileSync(sourceFile, dest)
100
+ copiedCount++
101
+ allLanguages.push(...languages)
102
+ if (verbose) console.log(` snippets: copied ${sourceRelPath} (${languages.join(', ')})`)
103
+ }
104
+ }
105
+ }
106
+
74
107
  if (copiedCount === 0) {
75
- throw new Error('snippets step: no snippet files were copied')
108
+ // No snippet files found in source. Generate empty files at the original source paths
109
+ // so contributes.snippets references resolve correctly in the output package.json.
110
+ if (verbose) console.warn(' snippets: no snippet files found, generating empty files')
111
+ for (const [sourceRelPath, languages] of fileToLanguages) {
112
+ const dest = path.join(output, sourceRelPath)
113
+ if (fs.existsSync(dest)) continue
114
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
115
+ fs.writeFileSync(dest, '{}')
116
+ copiedCount++
117
+ allLanguages.push(...languages)
118
+ if (verbose) console.log(` snippets: generated empty ${sourceRelPath}`)
119
+ }
120
+ if (copiedCount === 0) {
121
+ throw new Error('snippets step: no snippet files were copied and no languages configured')
122
+ }
76
123
  }
77
124
 
78
125
  // Generate empty src/index.ts
@@ -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
 
@@ -77,6 +77,8 @@ export interface SnippetsStep {
77
77
  type: 'snippets'
78
78
  /** Optional: override languages to generate (default: read from source package.json's contributes.snippets) */
79
79
  languages?: string[]
80
+ /** Optional: build command to run in source dir before collecting snippet files (e.g. "node merge.js") */
81
+ build?: string
80
82
  }
81
83
 
82
84
  export type ConvertStep = LanguageClientStep | SourceStep | BridgeStep | MarkUnsupportedStep | SnippetsStep
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.6",
42
+ version: "1.2.8",
43
43
  description: "Run VS Code extensions seamlessly in coc.nvim",
44
44
  main: "lib/index.js",
45
45
  keywords: [
@@ -167,22 +167,47 @@ function loadCache() {
167
167
  }
168
168
  return null;
169
169
  }
170
- async function fetchRegistryJSON(url) {
170
+ async function fetchRegistryJSON(url, onProgress) {
171
171
  try {
172
172
  const ctrl = new AbortController();
173
173
  const t = setTimeout(() => ctrl.abort(), 1e4);
174
174
  const res = await fetch(url, { signal: ctrl.signal });
175
175
  clearTimeout(t);
176
176
  if (res.ok) {
177
- const data = await res.json();
177
+ const total = parseInt(res.headers.get("content-length") || "0");
178
+ const reader = res.body.getReader();
179
+ const chunks = [];
180
+ let received = 0;
181
+ while (true) {
182
+ const { done, value } = await reader.read();
183
+ if (done) break;
184
+ if (value) {
185
+ chunks.push(value);
186
+ received += value.length;
187
+ if (total && onProgress) {
188
+ onProgress(`Downloading registry... ${Math.round(received / total * 100)}%`);
189
+ }
190
+ }
191
+ }
192
+ if (onProgress) onProgress("Parsing registry entries...");
193
+ const buf = new Uint8Array(received);
194
+ let pos = 0;
195
+ for (const c of chunks) {
196
+ buf.set(c, pos);
197
+ pos += c.length;
198
+ }
199
+ const text = new TextDecoder().decode(buf);
200
+ const data = JSON.parse(text);
178
201
  if (Array.isArray(data)) return data;
179
202
  }
180
203
  } catch {
181
204
  }
182
205
  return new Promise((resolve2, reject) => {
206
+ if (onProgress) onProgress("Downloading registry (curl)...");
183
207
  (0, import_child_process.execFile)("curl", ["-sL", "--compressed", url], { encoding: "utf-8", maxBuffer: 20 * 1024 * 1024 }, (err, stdout) => {
184
208
  if (err) reject(new Error(`curl failed: ${err.message}`));
185
209
  else {
210
+ if (onProgress) onProgress("Parsing registry entries...");
186
211
  try {
187
212
  const data = JSON.parse(stdout);
188
213
  if (!Array.isArray(data)) reject(new Error("Invalid registry format"));
@@ -194,10 +219,11 @@ async function fetchRegistryJSON(url) {
194
219
  });
195
220
  });
196
221
  }
197
- async function updateRegistry() {
222
+ async function updateRegistry(onProgress) {
198
223
  const localPath = process.env.COC_REGISTRY_PATH || getLocalRegistryPath();
199
224
  if (localPath) {
200
225
  if (!fs.existsSync(localPath)) throw new Error(`Local registry not found: ${localPath}`);
226
+ if (onProgress) onProgress("Reading local registry...");
201
227
  const data2 = JSON.parse(fs.readFileSync(localPath, "utf-8"));
202
228
  if (!Array.isArray(data2)) throw new Error("Invalid registry format");
203
229
  fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
@@ -205,11 +231,12 @@ async function updateRegistry() {
205
231
  cached = data2;
206
232
  return data2.length;
207
233
  }
208
- const data = await fetchRegistryJSON(REMOTE_REGISTRY_URL);
234
+ const data = await fetchRegistryJSON(REMOTE_REGISTRY_URL, onProgress);
209
235
  if (!Array.isArray(data)) throw new Error("Invalid registry format");
210
236
  fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
211
237
  fs.writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2));
212
238
  cached = data;
239
+ if (onProgress) onProgress(`Registry updated: ${data.length} packages`);
213
240
  return data.length;
214
241
  }
215
242
  function satisfiesVersion(required) {
@@ -236,13 +263,19 @@ function getPackage(name) {
236
263
  var path2 = __toESM(require("path"));
237
264
  var fs2 = __toESM(require("fs"));
238
265
  var os2 = __toESM(require("os"));
239
- function isInstalled(name) {
240
- return fs2.existsSync(path2.join(os2.homedir(), ".config", "coc", "extensions", "node_modules", `coc-${name}`));
266
+ var EXT_DIR = path2.join(os2.homedir(), ".config", "coc", "extensions", "node_modules");
267
+ function getInstalledSet() {
268
+ try {
269
+ const entries = fs2.readdirSync(EXT_DIR);
270
+ return new Set(entries.filter((n) => n.startsWith("coc-")).map((n) => n.slice(4)));
271
+ } catch {
272
+ return /* @__PURE__ */ new Set();
273
+ }
241
274
  }
242
- var PAGE_SIZE = 50;
243
275
  function createInitialState() {
276
+ const installedSet = getInstalledSet();
244
277
  const packages = getAllPackages().map((info) => {
245
- const installed = isInstalled(info.name);
278
+ const installed = installedSet.has(info.name);
246
279
  let commit;
247
280
  let commitMsg;
248
281
  let commitDate;
@@ -269,17 +302,22 @@ function createInitialState() {
269
302
  marked: false
270
303
  };
271
304
  });
272
- return { packages, searchQuery: "", showHelp: false, activePill: null, dirty: false, viewFilter: "all", sortBy: "default", currentPage: 0 };
305
+ return { packages, searchQuery: "", showHelp: false, activePill: null, dirty: false, viewFilter: "all", sortBy: "default", scrollOffset: 0 };
273
306
  }
274
307
  var StateManager = class {
275
308
  constructor(initial) {
276
309
  this.listeners = /* @__PURE__ */ new Set();
277
310
  this.scheduled = false;
311
+ this.cachedFiltered = null;
312
+ this.cachedFilterKey = "";
278
313
  this.state = initial;
279
314
  }
280
315
  getState() {
281
316
  return this.state;
282
317
  }
318
+ filterKey() {
319
+ return `${this.state.viewFilter}|${this.state.searchQuery}|${this.state.sortBy}`;
320
+ }
283
321
  subscribe(fn) {
284
322
  this.listeners.add(fn);
285
323
  return () => this.listeners.delete(fn);
@@ -299,11 +337,15 @@ var StateManager = class {
299
337
  }
300
338
  });
301
339
  }
340
+ invalidateFilterCache() {
341
+ this.cachedFilterKey = "";
342
+ }
302
343
  setPackageStatus(name, status, extra) {
344
+ this.invalidateFilterCache();
303
345
  this.mutate((s) => {
304
346
  const pkg = s.packages.find((p) => p.info.name === name);
305
347
  if (pkg) {
306
- if (status === "installing" || status === "updating" || status === "uninstalling") {
348
+ if ((status === "installing" || status === "updating" || status === "uninstalling") && pkg.status !== status) {
307
349
  pkg.progressLog = [];
308
350
  }
309
351
  pkg.status = status;
@@ -340,25 +382,25 @@ var StateManager = class {
340
382
  setViewFilter(filter) {
341
383
  this.mutate((s) => {
342
384
  s.viewFilter = filter;
343
- s.currentPage = 0;
385
+ s.scrollOffset = 0;
344
386
  });
345
387
  }
346
388
  cycleViewFilter() {
347
389
  this.mutate((s) => {
348
390
  s.viewFilter = s.viewFilter === "all" ? "installed" : s.viewFilter === "installed" ? "not-installed" : "all";
349
- s.currentPage = 0;
391
+ s.scrollOffset = 0;
350
392
  });
351
393
  }
352
394
  setSortBy(sortBy) {
353
395
  this.mutate((s) => {
354
396
  s.sortBy = sortBy;
355
- s.currentPage = 0;
397
+ s.scrollOffset = 0;
356
398
  });
357
399
  }
358
400
  cycleSortBy() {
359
401
  this.mutate((s) => {
360
402
  s.sortBy = s.sortBy === "default" ? "name" : s.sortBy === "name" ? "status" : s.sortBy === "status" ? "type" : "default";
361
- s.currentPage = 0;
403
+ s.scrollOffset = 0;
362
404
  });
363
405
  }
364
406
  setStatusMessage(msg) {
@@ -379,15 +421,19 @@ var StateManager = class {
379
421
  setSearchQuery(query) {
380
422
  this.mutate((s) => {
381
423
  s.searchQuery = query;
382
- s.currentPage = 0;
424
+ s.scrollOffset = 0;
383
425
  });
384
426
  }
385
- setPage(n) {
427
+ setScrollOffset(n) {
386
428
  this.mutate((s) => {
387
- s.currentPage = n;
429
+ s.scrollOffset = n;
388
430
  });
389
431
  }
390
432
  getFilteredPackages() {
433
+ const key = this.filterKey();
434
+ if (this.cachedFiltered && this.cachedFilterKey === key) {
435
+ return this.cachedFiltered;
436
+ }
391
437
  let pkgs = this.state.packages;
392
438
  if (this.state.viewFilter === "not-installed") {
393
439
  pkgs = pkgs.filter((p) => p.status === "not-installed");
@@ -409,6 +455,8 @@ var StateManager = class {
409
455
  } else if (sortBy === "type") {
410
456
  pkgs = [...pkgs].sort((a, b) => a.info.type.localeCompare(b.info.type));
411
457
  }
458
+ this.cachedFiltered = pkgs;
459
+ this.cachedFilterKey = key;
412
460
  return pkgs;
413
461
  }
414
462
  getPackage(name) {
@@ -429,6 +477,8 @@ var StateManager = class {
429
477
  return this.state.packages.filter((p) => p.marked).map((p) => p.info.name);
430
478
  }
431
479
  refreshPackages() {
480
+ this.invalidateFilterCache();
481
+ const installedSet = getInstalledSet();
432
482
  this.mutate((s) => {
433
483
  const updated = getAllPackages();
434
484
  const oldMap = new Map(s.packages.map((p) => [p.info.name, p]));
@@ -440,7 +490,7 @@ var StateManager = class {
440
490
  }
441
491
  return {
442
492
  info,
443
- status: isInstalled(info.name) ? "installed" : "not-installed",
493
+ status: installedSet.has(info.name) ? "installed" : "not-installed",
444
494
  progressLog: [],
445
495
  expanded: false,
446
496
  logExpanded: false,
@@ -522,16 +572,23 @@ async function downloadSource(info, name, onProgress) {
522
572
  const srcDir = sourceDir(name);
523
573
  const cache = cacheDir(name);
524
574
  const repoUrl = `https://github.com/${info.source.repo}.git`;
525
- const log = (chunk) => onProgress(1, 5, chunk.trim(), "");
575
+ let output = "";
526
576
  if (fs3.existsSync(srcDir)) {
527
577
  onProgress(1, 5, "Updating source...", `git -C ${srcDir} pull`);
578
+ const log = (chunk) => {
579
+ output += chunk;
580
+ onProgress(1, 5, "Updating source...", chunk.trim());
581
+ };
528
582
  await run("git", ["-C", srcDir, "pull"], cache, log);
529
583
  } else {
530
584
  onProgress(1, 5, "Cloning repository...", `git clone --depth=1 ${repoUrl}`);
531
585
  fs3.mkdirSync(cache, { recursive: true });
586
+ const log = (chunk) => onProgress(1, 5, "Cloning repository...", chunk.trim());
532
587
  await run("git", ["clone", "--depth=1", repoUrl, srcDir], cache, log);
533
588
  }
534
- return info.source.subdir ? path3.join(srcDir, info.source.subdir) : srcDir;
589
+ const dir = info.source.subdir ? path3.join(srcDir, info.source.subdir) : srcDir;
590
+ const updated = !output.includes("Already up to date.");
591
+ return { dir, updated };
535
592
  }
536
593
  async function convertSource(inputDir, name, info, onProgress) {
537
594
  const build = buildDir(name);
@@ -593,8 +650,11 @@ async function buildPackage(name, inputDir, info, onProgress) {
593
650
  onProgress(3, 5, "Installing dependencies...", "npm install --legacy-peer-deps");
594
651
  await run("npm", ["install", "--legacy-peer-deps"], build, npmLog);
595
652
  onProgress(3, 5, "Running postinstall...", "npm run postinstall");
596
- await run("npm", ["run", "postinstall", "--if-present"], build, npmLog).catch(() => {
597
- });
653
+ try {
654
+ await run("npm", ["run", "postinstall", "--if-present"], build, npmLog);
655
+ } catch (e) {
656
+ onProgress(3, 5, `Warning: postinstall failed (${e.message})`, "may affect plugin functionality");
657
+ }
598
658
  if (info.pipPackages?.length) {
599
659
  const pipLog = (chunk) => onProgress(3, 5, chunk.trim(), "");
600
660
  const pythonPaths = [
@@ -744,9 +804,9 @@ async function installToCoc(name, onProgress) {
744
804
  const depName = `coc-${name}`;
745
805
  if (!pkg.dependencies[depName]) {
746
806
  pkg.dependencies[depName] = `file:${dest}`;
747
- pkg.lastUpdate = Date.now();
748
- fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
749
807
  }
808
+ pkg.lastUpdate = Date.now();
809
+ fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
750
810
  }
751
811
  function metaPath(name) {
752
812
  return path3.join(cacheDir(name), "meta.json");
@@ -770,13 +830,12 @@ async function installPackage(state, name) {
770
830
  state.setPackageStatus(name, "installing", {
771
831
  progress: `[${step}/${total}] ${msg}`,
772
832
  logEntry: `[${step}/${total}] ${msg}
773
- $ ${cmd}`,
774
- appendLog: true
833
+ $ ${cmd}`
775
834
  });
776
835
  };
777
836
  state.setPackageStatus(name, "installing", { progress: "Starting..." });
778
837
  try {
779
- const input = await downloadSource(info, name, prog);
838
+ const { dir: input } = await downloadSource(info, name, prog);
780
839
  await convertSource(input, name, info, prog);
781
840
  await buildPackage(name, input, info, prog);
782
841
  await installToCoc(name, prog);
@@ -849,13 +908,17 @@ async function updatePackage(state, name) {
849
908
  state.setPackageStatus(name, "updating", {
850
909
  progress: `[${step}/${total}] ${msg}`,
851
910
  logEntry: `[${step}/${total}] ${msg}
852
- $ ${cmd}`,
853
- appendLog: true
911
+ $ ${cmd}`
854
912
  });
855
913
  };
856
914
  state.setPackageStatus(name, "updating", { progress: "Starting..." });
857
915
  try {
858
- const input = await downloadSource(info, name, prog);
916
+ const { dir: input, updated } = await downloadSource(info, name, prog);
917
+ if (!updated) {
918
+ state.setPackageStatus(name, "installed");
919
+ import_coc.window.showInformationMessage(`coc-${name} is already up to date`);
920
+ return;
921
+ }
859
922
  await convertSource(input, name, info, prog);
860
923
  await buildPackage(name, input, info, prog);
861
924
  await installToCoc(name, prog);
@@ -909,41 +972,51 @@ async function runWithOutput(cmd, args, cwd) {
909
972
  async function runConcurrent(items, fn, concurrency = 3) {
910
973
  const pool = /* @__PURE__ */ new Set();
911
974
  for (const item of items) {
912
- const p = fn(item).finally(() => pool.delete(p));
975
+ const p = fn(item).catch(() => {
976
+ }).finally(() => pool.delete(p));
913
977
  pool.add(p);
914
978
  if (pool.size >= concurrency) {
915
979
  await Promise.race(pool);
916
980
  }
917
981
  }
918
- await Promise.all(pool);
982
+ await Promise.allSettled(pool);
919
983
  }
984
+ var checkUpdatesBusy = false;
920
985
  async function checkUpdates(state) {
921
- const s = state.getState();
922
- const results = {};
923
- state.setStatusMessage("Checking for updates...");
924
- for (const pkg of s.packages) {
925
- if (pkg.status !== "installed" || !pkg.commit) continue;
926
- state.setStatusMessage(`Checking ${pkg.info.displayName}...`);
927
- try {
928
- const out = await runWithOutput("git", ["ls-remote", `https://github.com/${pkg.info.source.repo}.git`, "HEAD"], os3.homedir());
929
- const remote = out.split(" ")[0];
930
- if (remote) results[pkg.info.name] = remote.substring(0, 7) !== pkg.commit;
931
- } catch {
986
+ if (checkUpdatesBusy) return;
987
+ checkUpdatesBusy = true;
988
+ try {
989
+ const s = state.getState();
990
+ const results = {};
991
+ state.setStatusMessage("Checking for updates...");
992
+ for (const pkg of s.packages) {
993
+ if (pkg.status !== "installed" || !pkg.commit) continue;
994
+ const live = state.getPackage(pkg.info.name);
995
+ if (!live || live.status !== "installed" || !live.commit) continue;
996
+ state.setStatusMessage(`Checking ${pkg.info.displayName}...`);
997
+ try {
998
+ const out = await runWithOutput("git", ["ls-remote", `https://github.com/${pkg.info.source.repo}.git`, "HEAD"], os3.homedir());
999
+ const remote = out.split(" ")[0];
1000
+ if (remote) results[pkg.info.name] = remote.substring(0, 7) !== live.commit;
1001
+ } catch {
1002
+ }
932
1003
  }
933
- }
934
- const updateCount = Object.values(results).filter(Boolean).length;
935
- state.mutate((s2) => {
936
- for (const p of s2.packages) {
937
- if (results[p.info.name] !== void 0) p.hasUpdate = results[p.info.name];
1004
+ const updateCount = Object.values(results).filter(Boolean).length;
1005
+ state.mutate((s2) => {
1006
+ for (const p of s2.packages) {
1007
+ if (results[p.info.name] !== void 0) p.hasUpdate = results[p.info.name];
1008
+ }
1009
+ s2.statusMessage = void 0;
1010
+ });
1011
+ if (updateCount > 0) {
1012
+ state.setStatusMessage(`Found ${updateCount} package(s) with updates. Use 'u' to update.`);
1013
+ setTimeout(() => state.setStatusMessage(), 5e3);
1014
+ } else {
1015
+ state.setStatusMessage("All packages up to date.");
1016
+ setTimeout(() => state.setStatusMessage(), 3e3);
938
1017
  }
939
- s2.statusMessage = void 0;
940
- });
941
- if (updateCount > 0) {
942
- state.setStatusMessage(`Found ${updateCount} package(s) with updates. Use 'u' to update.`);
943
- setTimeout(() => state.setStatusMessage(), 5e3);
944
- } else {
945
- state.setStatusMessage("All packages up to date.");
946
- setTimeout(() => state.setStatusMessage(), 3e3);
1018
+ } finally {
1019
+ checkUpdatesBusy = false;
947
1020
  }
948
1021
  }
949
1022
 
@@ -973,6 +1046,13 @@ var LineBuffer = class {
973
1046
  currentLine() {
974
1047
  return this.li;
975
1048
  }
1049
+ currentByteLen() {
1050
+ let len = 0;
1051
+ for (const seg of this.lines[this.li]) {
1052
+ len += byteLen(seg.text);
1053
+ }
1054
+ return len;
1055
+ }
976
1056
  highlight(pattern, hlGroup) {
977
1057
  const segs = this.lines[this.li];
978
1058
  let full = "";
@@ -1039,11 +1119,11 @@ var HELP_TEXT = [
1039
1119
  " x Toggle mark",
1040
1120
  " f Cycle filter: all \u2192 installed \u2192 not-installed",
1041
1121
  " s Cycle sort: default \u2192 name \u2192 status \u2192 type",
1042
- " [/] Previous/next page",
1122
+ " j/k Scroll through packages",
1123
+ " / Search filter (then j/k to scroll)",
1043
1124
  " gg Jump to first page",
1044
1125
  " G Jump to last page",
1045
- " <Enter> Toggle expand/collapse details",
1046
- " / Search filter",
1126
+ " <Enter> Open detail popup (description, source, log)",
1047
1127
  " q Close window",
1048
1128
  " <Esc> Help\u2192Search\u2192Marks\u2192Busy guard\u2192Close",
1049
1129
  "",
@@ -1054,7 +1134,7 @@ var HELP_TEXT = [
1054
1134
  " pure-lsp Standard LSP protocol (e.g. Prisma, ESLint)",
1055
1135
  " direct-api Direct coc.nvim API calls (e.g. HTML CSS Support)"
1056
1136
  ];
1057
- var TUI = class {
1137
+ var TUI = class _TUI {
1058
1138
  constructor(state) {
1059
1139
  this.bufnr = 0;
1060
1140
  this.winid = 0;
@@ -1063,11 +1143,20 @@ var TUI = class {
1063
1143
  this.unsubscribe = null;
1064
1144
  this.pkgLineMap = /* @__PURE__ */ new Map();
1065
1145
  this.logLineSet = /* @__PURE__ */ new Set();
1146
+ this.detailWinid = 0;
1147
+ this.detailBufnr = 0;
1148
+ this.detailPkgName = "";
1149
+ this.detailMode = "info";
1150
+ this.windowHeight = 0;
1151
+ this.windowWidth = 0;
1066
1152
  this.keyMap = {
1067
1153
  q: "q",
1068
1154
  esc: "<Esc>",
1069
1155
  question: "?",
1070
1156
  slash: "/",
1157
+ j: "j",
1158
+ k: "k",
1159
+ "close-detail": "close-detail",
1071
1160
  U: "U",
1072
1161
  Z: "Z",
1073
1162
  i: "i",
@@ -1080,16 +1169,20 @@ var TUI = class {
1080
1169
  x: "x",
1081
1170
  D: "D",
1082
1171
  gg: "gg",
1083
- G: "G",
1084
- "[": "pageup",
1085
- "]": "pagedown"
1172
+ G: "G"
1086
1173
  };
1087
1174
  this.rendering = false;
1088
1175
  this.pendingRender = false;
1089
- this.lastPage = -1;
1090
- this.scrollToFirst = false;
1176
+ this.focusIndex = 0;
1177
+ this.focusLineOffset = 0;
1091
1178
  this.state = state;
1092
1179
  }
1180
+ static {
1181
+ this.HEADER_LINES = 6;
1182
+ }
1183
+ static {
1184
+ this.FOOTER_LINES = 3;
1185
+ }
1093
1186
  async open() {
1094
1187
  const nvim = import_coc2.workspace.nvim;
1095
1188
  this.ns = await nvim.createNamespace("coc-loader");
@@ -1107,7 +1200,9 @@ var TUI = class {
1107
1200
  const editorLines = await nvim.call("nvim_get_option", ["lines"]);
1108
1201
  const editorCols = await nvim.call("nvim_get_option", ["columns"]);
1109
1202
  const height = Math.min(Math.floor(editorLines * 0.85), 40);
1203
+ this.windowHeight = height - 2;
1110
1204
  const width = Math.min(Math.floor(editorCols * 0.85), 120);
1205
+ this.windowWidth = width - 2;
1111
1206
  const row = Math.max(Math.floor((editorLines - height) / 2), 0);
1112
1207
  const col = Math.max(Math.floor((editorCols - width) / 2), 0);
1113
1208
  const win = await nvim.openFloatWindow(buf, true, {
@@ -1116,7 +1211,7 @@ var TUI = class {
1116
1211
  height,
1117
1212
  row,
1118
1213
  col,
1119
- border: "none",
1214
+ border: "rounded",
1120
1215
  style: "minimal"
1121
1216
  });
1122
1217
  this.winid = win.id;
@@ -1147,7 +1242,7 @@ var TUI = class {
1147
1242
  const curBuf = await nvim.call("winbufnr", [curWin]);
1148
1243
  const bt = await nvim.call("getbufvar", [curBuf, "&buftype"]);
1149
1244
  if (bt !== "nofile" && bt !== "prompt") {
1150
- this.close();
1245
+ await this.close();
1151
1246
  }
1152
1247
  }
1153
1248
  })
@@ -1162,7 +1257,12 @@ var TUI = class {
1162
1257
  }
1163
1258
  await this.setupKeymaps();
1164
1259
  await this.render();
1165
- updateRegistry().then(() => {
1260
+ this.state.setStatusMessage("Fetching registry...");
1261
+ const onProgress = (msg) => {
1262
+ this.state.setStatusMessage(msg);
1263
+ };
1264
+ updateRegistry(onProgress).then(() => {
1265
+ this.state.setStatusMessage();
1166
1266
  this.state.refreshPackages();
1167
1267
  this.render();
1168
1268
  }).catch(() => {
@@ -1176,14 +1276,23 @@ var TUI = class {
1176
1276
  return cursor[0] - 1;
1177
1277
  }
1178
1278
  async handleKey(id) {
1279
+ if (!this.winid) return;
1179
1280
  const line0 = await this.getCursorLine0();
1180
1281
  const s = this.state.getState();
1181
1282
  if (id === "q") {
1182
- this.close();
1283
+ await this.close();
1183
1284
  return;
1184
1285
  }
1185
1286
  if (id === "I") {
1186
1287
  this.state.setActivePill("I");
1288
+ const marked = this.state.getMarkedNames();
1289
+ if (marked.length === 0) {
1290
+ import_coc2.window.showInformationMessage("No packages marked. Use x to mark packages.");
1291
+ this.state.setActivePill(null);
1292
+ return;
1293
+ }
1294
+ await runConcurrent(marked, (name) => installPackage(this.state, name));
1295
+ this.state.setActivePill(null);
1187
1296
  return;
1188
1297
  }
1189
1298
  if (id === "H") {
@@ -1211,7 +1320,7 @@ var TUI = class {
1211
1320
  import_coc2.window.showInformationMessage("Operation in progress, wait for it to finish");
1212
1321
  return;
1213
1322
  }
1214
- this.close();
1323
+ await this.close();
1215
1324
  return;
1216
1325
  }
1217
1326
  if (id === "question") {
@@ -1228,36 +1337,62 @@ var TUI = class {
1228
1337
  }
1229
1338
  if (id === "f") {
1230
1339
  this.state.cycleViewFilter();
1340
+ this.focusIndex = 0;
1231
1341
  return;
1232
1342
  }
1233
1343
  if (id === "s") {
1234
1344
  this.state.cycleSortBy();
1345
+ this.focusIndex = 0;
1235
1346
  return;
1236
1347
  }
1237
1348
  if (id === "gg") {
1238
- this.state.setPage(0);
1239
- this.scrollToFirst = true;
1349
+ this.state.setScrollOffset(0);
1350
+ this.focusIndex = 0;
1240
1351
  return;
1241
1352
  }
1242
1353
  if (id === "G") {
1243
1354
  const filtered = this.state.getFilteredPackages();
1244
- const totalPages = Math.ceil(filtered.length / PAGE_SIZE) || 1;
1245
- this.state.setPage(totalPages - 1);
1355
+ const visibleCount = Math.max(1, this.windowHeight - _TUI.HEADER_LINES - _TUI.FOOTER_LINES);
1356
+ this.state.setScrollOffset(Math.max(0, filtered.length - visibleCount));
1357
+ this.focusIndex = Math.max(0, filtered.length - 1);
1246
1358
  return;
1247
1359
  }
1248
- if (id === "pagedown") {
1360
+ if (id === "j") {
1249
1361
  const filtered = this.state.getFilteredPackages();
1250
- const totalPages = Math.ceil(filtered.length / PAGE_SIZE) || 1;
1251
- const s2 = this.state.getState();
1252
- if (s2.currentPage < totalPages - 1) {
1253
- this.state.setPage(s2.currentPage + 1);
1362
+ if (this.focusIndex < filtered.length - 1) {
1363
+ this.focusIndex++;
1364
+ this.focusLineOffset = 0;
1365
+ const s2 = this.state.getState();
1366
+ const visibleCount = Math.max(1, this.windowHeight - _TUI.HEADER_LINES - _TUI.FOOTER_LINES);
1367
+ if (this.focusIndex >= s2.scrollOffset + visibleCount) {
1368
+ s2.scrollOffset = Math.min(Math.max(0, filtered.length - visibleCount), s2.scrollOffset + 1);
1369
+ await this.render();
1370
+ return;
1371
+ }
1372
+ const focused = filtered[this.focusIndex];
1373
+ const pkgLine = [...this.pkgLineMap.entries()].find(([l, n]) => n === focused.info.name)?.[0];
1374
+ if (pkgLine !== void 0) {
1375
+ await import_coc2.workspace.nvim.call("nvim_win_set_cursor", [this.winid, [pkgLine + 1, 0]]);
1376
+ }
1254
1377
  }
1255
1378
  return;
1256
1379
  }
1257
- if (id === "pageup") {
1258
- const s2 = this.state.getState();
1259
- if (s2.currentPage > 0) {
1260
- this.state.setPage(s2.currentPage - 1);
1380
+ if (id === "k") {
1381
+ if (this.focusIndex > 0) {
1382
+ this.focusIndex--;
1383
+ this.focusLineOffset = 0;
1384
+ const s2 = this.state.getState();
1385
+ if (this.focusIndex < s2.scrollOffset) {
1386
+ s2.scrollOffset = Math.max(0, s2.scrollOffset - 1);
1387
+ await this.render();
1388
+ return;
1389
+ }
1390
+ const filtered = this.state.getFilteredPackages();
1391
+ const focused = filtered[this.focusIndex];
1392
+ const pkgLine = [...this.pkgLineMap.entries()].find(([l, n]) => n === focused.info.name)?.[0];
1393
+ if (pkgLine !== void 0) {
1394
+ await import_coc2.workspace.nvim.call("nvim_win_set_cursor", [this.winid, [pkgLine + 1, 0]]);
1395
+ }
1261
1396
  }
1262
1397
  return;
1263
1398
  }
@@ -1323,12 +1458,12 @@ var TUI = class {
1323
1458
  await installPackage(this.state, pkgName);
1324
1459
  return;
1325
1460
  }
1461
+ if (id === "close-detail") {
1462
+ this.closeDetailPopup();
1463
+ return;
1464
+ }
1326
1465
  if (id === "cr") {
1327
- if (this.logLineSet.has(line0)) {
1328
- this.state.toggleLog(pkgName);
1329
- } else {
1330
- this.state.toggleExpand(pkgName);
1331
- }
1466
+ if (pkgName) await this.showDetailPopup(pkgName);
1332
1467
  return;
1333
1468
  }
1334
1469
  }
@@ -1352,10 +1487,10 @@ var TUI = class {
1352
1487
  ["s", "s"],
1353
1488
  ["x", "x"],
1354
1489
  ["D", "D"],
1490
+ ["j", "j"],
1491
+ ["k", "k"],
1355
1492
  ["gg", "gg"],
1356
1493
  ["G", "G"],
1357
- ["[", "pageup"],
1358
- ["]", "pagedown"],
1359
1494
  ["<CR>", "cr"]
1360
1495
  ];
1361
1496
  for (const [vimKey, id] of entries) {
@@ -1372,6 +1507,13 @@ var TUI = class {
1372
1507
  d.dispose();
1373
1508
  }
1374
1509
  this.disposables = [];
1510
+ if (this.detailWinid) {
1511
+ try {
1512
+ import_coc2.workspace.nvim.call("nvim_win_close", [this.detailWinid, true]);
1513
+ } catch {
1514
+ }
1515
+ this.detailWinid = 0;
1516
+ }
1375
1517
  if (this.winid) {
1376
1518
  try {
1377
1519
  await import_coc2.workspace.nvim.call("nvim_win_close", [this.winid, true]);
@@ -1396,26 +1538,47 @@ var TUI = class {
1396
1538
  const state = this.state.getState();
1397
1539
  const filtered = this.state.getFilteredPackages();
1398
1540
  const result = state.showHelp ? this.renderHelp() : this.renderPackageList(state, filtered);
1541
+ if (this.windowWidth > 0) {
1542
+ result.highlights = result.highlights.filter((h) => h.colStart < this.windowWidth);
1543
+ for (const h of result.highlights) {
1544
+ if (h.colEnd > this.windowWidth) h.colEnd = this.windowWidth;
1545
+ }
1546
+ }
1399
1547
  nvim.pauseNotification();
1400
- nvim.call("nvim_buf_set_option", [this.bufnr, "modifiable", true], true);
1401
- nvim.call("nvim_buf_clear_namespace", [this.bufnr, this.ns, 0, -1], true);
1402
- nvim.call("nvim_buf_set_lines", [this.bufnr, 0, -1, false, result.lines], true);
1403
- nvim.call("nvim_buf_set_option", [this.bufnr, "modifiable", false], true);
1404
- for (const h of result.highlights) {
1405
- nvim.call("nvim_buf_set_extmark", [this.bufnr, this.ns, h.line, h.colStart, {
1406
- end_col: h.colEnd,
1407
- hl_group: h.hlGroup,
1408
- hl_mode: "combine"
1409
- }], true);
1548
+ try {
1549
+ nvim.call("nvim_buf_set_option", [this.bufnr, "modifiable", true], true);
1550
+ nvim.call("nvim_buf_clear_namespace", [this.bufnr, this.ns, 0, -1], true);
1551
+ nvim.call("nvim_buf_set_lines", [this.bufnr, 0, -1, false, result.lines], true);
1552
+ nvim.call("nvim_buf_set_option", [this.bufnr, "modifiable", false], true);
1553
+ for (const h of result.highlights) {
1554
+ nvim.call("nvim_buf_set_extmark", [this.bufnr, this.ns, h.line, h.colStart, {
1555
+ end_col: h.colEnd,
1556
+ hl_group: h.hlGroup,
1557
+ hl_mode: "combine"
1558
+ }], true);
1559
+ }
1560
+ } finally {
1561
+ await nvim.resumeNotification();
1562
+ }
1563
+ if (this.detailWinid) {
1564
+ this.updateDetailPopup().catch(() => {
1565
+ });
1410
1566
  }
1411
- await nvim.resumeNotification();
1412
- const shouldScroll = this.scrollToFirst || state.currentPage !== this.lastPage;
1413
- this.scrollToFirst = false;
1414
- this.lastPage = state.currentPage;
1415
- if (!state.showHelp && shouldScroll) {
1416
- const firstPkgLine = Math.min(...result.pkgLineMap.keys());
1417
- if (isFinite(firstPkgLine)) {
1418
- await nvim.call("nvim_win_set_cursor", [this.winid, [firstPkgLine + 1, 0]]);
1567
+ if (!state.showHelp && result.pkgLineMap.size > 0) {
1568
+ const visibleCount = Math.max(1, this.windowHeight - _TUI.HEADER_LINES - _TUI.FOOTER_LINES);
1569
+ if (this.focusIndex < state.scrollOffset) {
1570
+ state.scrollOffset = this.focusIndex;
1571
+ } else if (this.focusIndex >= state.scrollOffset + visibleCount) {
1572
+ state.scrollOffset = Math.max(0, this.focusIndex - visibleCount + 1);
1573
+ }
1574
+ const visible = this.state.getFilteredPackages();
1575
+ const idx = Math.min(this.focusIndex, visible.length - 1);
1576
+ const focused = visible[idx];
1577
+ if (focused) {
1578
+ const targetLine = [...result.pkgLineMap.entries()].find(([l, n]) => n === focused.info.name)?.[0];
1579
+ if (targetLine !== void 0) {
1580
+ await nvim.call("nvim_win_set_cursor", [this.winid, [targetLine + 1, 0]]);
1581
+ }
1419
1582
  }
1420
1583
  }
1421
1584
  this.pkgLineMap = result.pkgLineMap;
@@ -1483,19 +1646,16 @@ var TUI = class {
1483
1646
  }
1484
1647
  buf.nl();
1485
1648
  buf.nl();
1486
- const totalPages = Math.ceil(filtered.length / PAGE_SIZE) || 1;
1487
- const page = Math.min(state.currentPage, totalPages - 1);
1488
- const start = page * PAGE_SIZE;
1489
- const pageItems = filtered.slice(start, start + PAGE_SIZE);
1490
- const end = Math.min(start + PAGE_SIZE, filtered.length);
1491
- if (totalPages > 1) {
1492
- buf.append(`Page ${page + 1}/${totalPages} \xB7 ${start + 1}\u2013${end} of ${filtered.length}`, "CocConverterTotal");
1493
- } else {
1494
- buf.append(`${filtered.length} packages`, "CocConverterTotal");
1495
- }
1649
+ const visibleCount = Math.max(1, this.windowHeight - _TUI.HEADER_LINES - _TUI.FOOTER_LINES);
1650
+ const maxOffset = Math.max(0, filtered.length - visibleCount);
1651
+ const start = Math.min(state.scrollOffset, maxOffset);
1652
+ const end = Math.min(start + visibleCount, filtered.length);
1653
+ const visible = filtered.slice(start, end);
1654
+ const indicator = filtered.length > visibleCount ? `${start + 1}\u2013${end} of ${filtered.length}` : `${filtered.length} packages`;
1655
+ buf.append(indicator, "CocConverterTotal");
1496
1656
  buf.nl();
1497
1657
  buf.nl();
1498
- for (const e of pageItems) {
1658
+ for (const e of visible) {
1499
1659
  this.renderEntry(buf, pkgLineMap, logSet, e);
1500
1660
  }
1501
1661
  if (filtered.length === 0 && state.searchQuery) {
@@ -1504,12 +1664,13 @@ var TUI = class {
1504
1664
  buf.nl();
1505
1665
  buf.append(" " + "\u2500".repeat(50), "Comment");
1506
1666
  buf.nl();
1507
- const pageNav = totalPages > 1 ? ` [ prev ] next` : "";
1508
- buf.append(` ${filtered.length} packages \xB7 ${filterLabel} \xB7 ${sortLabel}${pageNav}`, "Comment");
1667
+ const filterLabel2 = state.viewFilter === "all" ? "All" : state.viewFilter === "installed" ? "Installed" : "Available";
1668
+ const sortLabel2 = state.sortBy === "default" ? "Default" : state.sortBy === "name" ? "Name" : state.sortBy === "status" ? "Status" : "Type";
1669
+ buf.append(` ${filtered.length} packages \xB7 ${filterLabel2} \xB7 ${sortLabel2}`, "Comment");
1509
1670
  const result = buf.render(2);
1510
1671
  return { lines: result.lines, pkgLineMap, logLines: logSet, highlights: result.highlights };
1511
1672
  }
1512
- renderEntry(buf, pkgLineMap, logSet, entry) {
1673
+ renderEntry(buf, pkgLineMap, _logSet, entry) {
1513
1674
  const icon = entry.status === "installed" ? "\u25CF" : entry.status === "failed" ? "\u2717" : "\u25CB";
1514
1675
  const iconHl = entry.status === "installed" ? "CocConverterInstalled" : entry.status === "failed" ? "ErrorMsg" : "CocConverterAvailable";
1515
1676
  const pkgLine = buf.currentLine();
@@ -1523,67 +1684,153 @@ var TUI = class {
1523
1684
  buf.append(icon, iconHl);
1524
1685
  buf.append(" ");
1525
1686
  buf.append(entry.info.displayName);
1687
+ let statusText = "";
1688
+ let statusHl = "";
1689
+ if (entry.progress) {
1690
+ statusText = ` ${entry.progress}`;
1691
+ statusHl = "Comment";
1692
+ } else if (entry.status === "failed" && entry.error) {
1693
+ statusText = ` \u2717 ${entry.error}`;
1694
+ statusHl = "ErrorMsg";
1695
+ }
1526
1696
  buf.append(" ");
1527
1697
  buf.append(entry.info.type, "CocConverterType");
1698
+ if (statusText) {
1699
+ buf.append(statusText, statusHl);
1700
+ }
1528
1701
  if (entry.hasUpdate) {
1529
1702
  buf.append(" \u2191", "CocConverterKey");
1530
1703
  }
1531
- if (entry.updated && entry.commit && entry.commitMsg) {
1532
- buf.nl();
1533
- const ln = buf.currentLine();
1534
- buf.append(` ${entry.commit} ${entry.commitMsg}`, "Comment");
1535
- if (entry.commitDate) {
1536
- buf.append(` (${entry.commitDate})`, "Comment");
1704
+ if (entry.commit && entry.commitMsg && entry.status === "installed") {
1705
+ const cr = entry.commitDate ? ` (${entry.commitDate})` : "";
1706
+ let msg = entry.commitMsg;
1707
+ if (this.windowWidth > 0) {
1708
+ const prefixLen = buf.currentByteLen();
1709
+ const commitPrefix = ` ${entry.commit} `;
1710
+ const suffix = cr;
1711
+ const available = this.windowWidth - 2 - prefixLen - Buffer.from(commitPrefix).length - Buffer.from(suffix).length - 3;
1712
+ if (available > 0 && Buffer.from(msg).length > available) {
1713
+ while (Buffer.from(msg).length > available && msg.length > 0) {
1714
+ msg = msg.slice(0, -1);
1715
+ }
1716
+ msg += "\u2026";
1717
+ }
1537
1718
  }
1538
- pkgLineMap.set(ln, entry.info.name);
1539
- }
1540
- if (entry.expanded) {
1541
- buf.nl();
1542
- const extras = [
1543
- entry.info.description,
1544
- `type ${entry.info.type}`,
1545
- entry.commit ? `commit ${entry.commit}` : null,
1546
- `source ${sourceStr(entry.info.source)}`,
1547
- `languages ${entry.info.languages.join(", ")}`,
1548
- `categories ${entry.info.categories.join(", ")}`,
1549
- `homepage ${entry.info.url}`,
1550
- entry.info.serverBinary ? `server ${entry.info.serverBinary.repo} (binary release)` : null
1551
- ];
1552
- for (const text of extras.filter(Boolean)) {
1553
- const ln = buf.currentLine();
1554
- buf.nl(` ${text}`);
1555
- pkgLineMap.set(ln, entry.info.name);
1719
+ buf.append(` ${entry.commit} ${msg}${cr}`, "Comment");
1720
+ }
1721
+ buf.nl();
1722
+ }
1723
+ buildDetailLines(entry, mode = "info") {
1724
+ const lines = [];
1725
+ if (mode === "log") {
1726
+ for (const log of entry.progressLog) {
1727
+ for (const l of log.split("\n")) {
1728
+ lines.push(` ${l}`);
1729
+ }
1556
1730
  }
1731
+ if (entry.error) lines.push("", ` \u2717 ${entry.error}`);
1732
+ return lines;
1557
1733
  }
1558
- if (entry.progress) {
1559
- buf.nl();
1560
- if (entry.logExpanded) {
1561
- const ln = buf.currentLine();
1562
- buf.nl(` \u25BC Install log:`);
1563
- logSet.add(ln);
1564
- pkgLineMap.set(ln, entry.info.name);
1565
- for (const log of entry.progressLog) {
1566
- for (const l of log.split("\n")) {
1567
- const ln2 = buf.currentLine();
1568
- buf.nl(` ${l}`);
1569
- logSet.add(ln2);
1570
- pkgLineMap.set(ln2, entry.info.name);
1571
- }
1734
+ lines.push(
1735
+ ` desc ${entry.info.description}`,
1736
+ ` type ${entry.info.type}`,
1737
+ ` status ${entry.status}`
1738
+ );
1739
+ if (entry.commit) lines.push(` commit ${entry.commit}`);
1740
+ lines.push(
1741
+ ` source ${sourceStr(entry.info.source)}`,
1742
+ ` langs ${entry.info.languages.join(", ")}`,
1743
+ ` cats ${entry.info.categories.join(", ")}`,
1744
+ ` link ${entry.info.url}`
1745
+ );
1746
+ if (entry.info.serverBinary) {
1747
+ lines.push(` server ${entry.info.serverBinary.repo}`);
1748
+ }
1749
+ return lines;
1750
+ }
1751
+ async showDetailPopup(name) {
1752
+ if (this.detailWinid) this.closeDetailPopup();
1753
+ this.detailPkgName = name;
1754
+ const nvim = import_coc2.workspace.nvim;
1755
+ const entry = this.state.getPackage(name);
1756
+ if (!entry) return;
1757
+ this.detailMode = ["installing", "updating", "uninstalling", "failed"].includes(entry.status) ? "log" : "info";
1758
+ const editorLines = await nvim.call("nvim_get_option", ["lines"]);
1759
+ const editorCols = await nvim.call("nvim_get_option", ["columns"]);
1760
+ const lines = this.buildDetailLines(entry, this.detailMode);
1761
+ const height = this.detailMode === "log" ? 20 : Math.min(lines.length, 20);
1762
+ const row = Math.max(0, Math.floor((editorLines - height - 2) / 2));
1763
+ const col = Math.max(0, Math.floor((editorCols - 82) / 2));
1764
+ const buf = await nvim.createNewBuffer(false, true);
1765
+ this.detailBufnr = buf.id;
1766
+ const win = await nvim.openFloatWindow(buf, true, {
1767
+ relative: "editor",
1768
+ width: 78,
1769
+ height,
1770
+ row,
1771
+ col,
1772
+ border: "rounded",
1773
+ style: "minimal",
1774
+ zindex: 100,
1775
+ title: this.detailMode === "log" ? `${entry.info.displayName} \xB7 Log` : entry.info.displayName,
1776
+ title_pos: "left"
1777
+ });
1778
+ this.detailWinid = win.id;
1779
+ await nvim.call("nvim_win_set_option", [this.detailWinid, "wrap", true]);
1780
+ await nvim.call("nvim_buf_set_option", [this.detailBufnr, "bufhidden", "wipe"]);
1781
+ await nvim.call("nvim_buf_set_option", [this.detailBufnr, "buftype", "nofile"]);
1782
+ const keyBuf = nvim.createBuffer(this.detailBufnr);
1783
+ keyBuf.setKeymap("n", "q", '<Cmd>call CocConverterDispatch("close-detail")<CR>', { silent: true, nowait: true });
1784
+ keyBuf.setKeymap("n", "<Esc>", '<Cmd>call CocConverterDispatch("close-detail")<CR>', { silent: true, nowait: true });
1785
+ await this.updateDetailPopup();
1786
+ }
1787
+ async updateDetailPopup() {
1788
+ if (!this.detailWinid || !this.detailPkgName) return;
1789
+ const entry = this.state.getPackage(this.detailPkgName);
1790
+ if (!entry) return;
1791
+ const lines = this.buildDetailLines(entry, this.detailMode);
1792
+ const nvim = import_coc2.workspace.nvim;
1793
+ await nvim.call("nvim_buf_set_lines", [this.detailBufnr, 0, -1, false, lines]);
1794
+ await nvim.call("nvim_buf_clear_namespace", [this.detailBufnr, this.ns, 0, -1]);
1795
+ for (let i = 0; i < lines.length; i++) {
1796
+ const line = lines[i];
1797
+ if (line.startsWith(" [")) {
1798
+ const endBracket = line.indexOf("]");
1799
+ if (endBracket > 0) {
1800
+ nvim.call("nvim_buf_set_extmark", [this.detailBufnr, this.ns, i, 2, { end_col: endBracket + 1, hl_group: "CocConverterKey" }]);
1801
+ nvim.call("nvim_buf_set_extmark", [this.detailBufnr, this.ns, i, endBracket + 1, { end_col: line.length, hl_group: "Comment" }]);
1802
+ }
1803
+ } else if (line.startsWith(" $ ")) {
1804
+ nvim.call("nvim_buf_set_extmark", [this.detailBufnr, this.ns, i, 0, { end_col: line.length, hl_group: "Comment" }]);
1805
+ } else if (line.includes("\u2717") || line.includes("Error:")) {
1806
+ nvim.call("nvim_buf_set_extmark", [this.detailBufnr, this.ns, i, 0, { end_col: line.length, hl_group: "ErrorMsg" }]);
1807
+ } else if (line.match(/^\s{4}at\s/) || line.match(/^\s{4}Node\.js/)) {
1808
+ nvim.call("nvim_buf_set_extmark", [this.detailBufnr, this.ns, i, 0, { end_col: line.length, hl_group: "Comment" }]);
1809
+ } else if (line.match(/^\s{2}\w+\s{3,}/)) {
1810
+ const parts = line.substring(2).split(/\s{2,}/);
1811
+ if (parts.length >= 2 && ["desc", "type", "status", "source", "langs", "cats", "link", "commit", "server"].includes(parts[0])) {
1812
+ const labelEnd = 2 + parts[0].length + line.substring(2 + parts[0].length).match(/^\s*/)[0].length;
1813
+ nvim.call("nvim_buf_set_extmark", [this.detailBufnr, this.ns, i, 2, { end_col: labelEnd, hl_group: "CocConverterKey" }]);
1814
+ nvim.call("nvim_buf_set_extmark", [this.detailBufnr, this.ns, i, labelEnd, { end_col: line.length, hl_group: "Comment" }]);
1572
1815
  }
1573
- } else {
1574
- const ln = buf.currentLine();
1575
- buf.nl(` \u25B6 ${entry.progress}`);
1576
- logSet.add(ln);
1577
- pkgLineMap.set(ln, entry.info.name);
1578
1816
  }
1579
1817
  }
1580
- if (entry.error) {
1581
- buf.nl();
1582
- const ln = buf.currentLine();
1583
- buf.nl(` \u2717 ${entry.error}`);
1584
- pkgLineMap.set(ln, entry.info.name);
1818
+ if (this.detailMode === "log") {
1819
+ await nvim.call("nvim_win_set_cursor", [this.detailWinid, [lines.length, 0]]);
1585
1820
  }
1586
- buf.nl();
1821
+ }
1822
+ closeDetailPopup() {
1823
+ if (!this.detailWinid) return;
1824
+ try {
1825
+ import_coc2.workspace.nvim.call("nvim_win_close", [this.detailWinid, true]);
1826
+ } catch {
1827
+ }
1828
+ this.detailWinid = 0;
1829
+ this.detailBufnr = 0;
1830
+ this.detailPkgName = "";
1831
+ this.detailMode = "info";
1832
+ this.render().catch(() => {
1833
+ });
1587
1834
  }
1588
1835
  isOpen() {
1589
1836
  return this.winid !== 0;
@@ -1641,7 +1888,7 @@ async function activate(context) {
1641
1888
  import_coc3.window.showInformationMessage(`${name} is not installed`);
1642
1889
  return;
1643
1890
  }
1644
- uninstallPackage(state, name);
1891
+ await uninstallPackage(state, name);
1645
1892
  })
1646
1893
  );
1647
1894
  context.subscriptions.push(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coc-vscode-loader",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Run VS Code extensions seamlessly in coc.nvim",
5
5
  "main": "lib/index.js",
6
6
  "keywords": [