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/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.5",
42
+ version: "1.2.7",
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) => {
183
- (0, import_child_process.execFile)("curl", ["-sL", url], { encoding: "utf-8", maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
206
+ if (onProgress) onProgress("Downloading registry (curl)...");
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,12 +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
275
  function createInitialState() {
276
+ const installedSet = getInstalledSet();
243
277
  const packages = getAllPackages().map((info) => {
244
- const installed = isInstalled(info.name);
278
+ const installed = installedSet.has(info.name);
245
279
  let commit;
246
280
  let commitMsg;
247
281
  let commitDate;
@@ -268,17 +302,22 @@ function createInitialState() {
268
302
  marked: false
269
303
  };
270
304
  });
271
- return { packages, searchQuery: "", showHelp: false, activePill: null, dirty: false, viewFilter: "all", sortBy: "default" };
305
+ return { packages, searchQuery: "", showHelp: false, activePill: null, dirty: false, viewFilter: "all", sortBy: "default", scrollOffset: 0 };
272
306
  }
273
307
  var StateManager = class {
274
308
  constructor(initial) {
275
309
  this.listeners = /* @__PURE__ */ new Set();
276
310
  this.scheduled = false;
311
+ this.cachedFiltered = null;
312
+ this.cachedFilterKey = "";
277
313
  this.state = initial;
278
314
  }
279
315
  getState() {
280
316
  return this.state;
281
317
  }
318
+ filterKey() {
319
+ return `${this.state.viewFilter}|${this.state.searchQuery}|${this.state.sortBy}`;
320
+ }
282
321
  subscribe(fn) {
283
322
  this.listeners.add(fn);
284
323
  return () => this.listeners.delete(fn);
@@ -298,11 +337,15 @@ var StateManager = class {
298
337
  }
299
338
  });
300
339
  }
340
+ invalidateFilterCache() {
341
+ this.cachedFilterKey = "";
342
+ }
301
343
  setPackageStatus(name, status, extra) {
344
+ this.invalidateFilterCache();
302
345
  this.mutate((s) => {
303
346
  const pkg = s.packages.find((p) => p.info.name === name);
304
347
  if (pkg) {
305
- if (status === "installing" || status === "updating" || status === "uninstalling") {
348
+ if ((status === "installing" || status === "updating" || status === "uninstalling") && pkg.status !== status) {
306
349
  pkg.progressLog = [];
307
350
  }
308
351
  pkg.status = status;
@@ -339,21 +382,25 @@ var StateManager = class {
339
382
  setViewFilter(filter) {
340
383
  this.mutate((s) => {
341
384
  s.viewFilter = filter;
385
+ s.scrollOffset = 0;
342
386
  });
343
387
  }
344
388
  cycleViewFilter() {
345
389
  this.mutate((s) => {
346
390
  s.viewFilter = s.viewFilter === "all" ? "installed" : s.viewFilter === "installed" ? "not-installed" : "all";
391
+ s.scrollOffset = 0;
347
392
  });
348
393
  }
349
394
  setSortBy(sortBy) {
350
395
  this.mutate((s) => {
351
396
  s.sortBy = sortBy;
397
+ s.scrollOffset = 0;
352
398
  });
353
399
  }
354
400
  cycleSortBy() {
355
401
  this.mutate((s) => {
356
402
  s.sortBy = s.sortBy === "default" ? "name" : s.sortBy === "name" ? "status" : s.sortBy === "status" ? "type" : "default";
403
+ s.scrollOffset = 0;
357
404
  });
358
405
  }
359
406
  setStatusMessage(msg) {
@@ -374,9 +421,19 @@ var StateManager = class {
374
421
  setSearchQuery(query) {
375
422
  this.mutate((s) => {
376
423
  s.searchQuery = query;
424
+ s.scrollOffset = 0;
425
+ });
426
+ }
427
+ setScrollOffset(n) {
428
+ this.mutate((s) => {
429
+ s.scrollOffset = n;
377
430
  });
378
431
  }
379
432
  getFilteredPackages() {
433
+ const key = this.filterKey();
434
+ if (this.cachedFiltered && this.cachedFilterKey === key) {
435
+ return this.cachedFiltered;
436
+ }
380
437
  let pkgs = this.state.packages;
381
438
  if (this.state.viewFilter === "not-installed") {
382
439
  pkgs = pkgs.filter((p) => p.status === "not-installed");
@@ -398,6 +455,8 @@ var StateManager = class {
398
455
  } else if (sortBy === "type") {
399
456
  pkgs = [...pkgs].sort((a, b) => a.info.type.localeCompare(b.info.type));
400
457
  }
458
+ this.cachedFiltered = pkgs;
459
+ this.cachedFilterKey = key;
401
460
  return pkgs;
402
461
  }
403
462
  getPackage(name) {
@@ -418,6 +477,8 @@ var StateManager = class {
418
477
  return this.state.packages.filter((p) => p.marked).map((p) => p.info.name);
419
478
  }
420
479
  refreshPackages() {
480
+ this.invalidateFilterCache();
481
+ const installedSet = getInstalledSet();
421
482
  this.mutate((s) => {
422
483
  const updated = getAllPackages();
423
484
  const oldMap = new Map(s.packages.map((p) => [p.info.name, p]));
@@ -429,7 +490,7 @@ var StateManager = class {
429
490
  }
430
491
  return {
431
492
  info,
432
- status: isInstalled(info.name) ? "installed" : "not-installed",
493
+ status: installedSet.has(info.name) ? "installed" : "not-installed",
433
494
  progressLog: [],
434
495
  expanded: false,
435
496
  logExpanded: false,
@@ -511,16 +572,23 @@ async function downloadSource(info, name, onProgress) {
511
572
  const srcDir = sourceDir(name);
512
573
  const cache = cacheDir(name);
513
574
  const repoUrl = `https://github.com/${info.source.repo}.git`;
514
- const log = (chunk) => onProgress(1, 5, chunk.trim(), "");
575
+ let output = "";
515
576
  if (fs3.existsSync(srcDir)) {
516
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
+ };
517
582
  await run("git", ["-C", srcDir, "pull"], cache, log);
518
583
  } else {
519
584
  onProgress(1, 5, "Cloning repository...", `git clone --depth=1 ${repoUrl}`);
520
585
  fs3.mkdirSync(cache, { recursive: true });
586
+ const log = (chunk) => onProgress(1, 5, "Cloning repository...", chunk.trim());
521
587
  await run("git", ["clone", "--depth=1", repoUrl, srcDir], cache, log);
522
588
  }
523
- 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 };
524
592
  }
525
593
  async function convertSource(inputDir, name, info, onProgress) {
526
594
  const build = buildDir(name);
@@ -582,8 +650,11 @@ async function buildPackage(name, inputDir, info, onProgress) {
582
650
  onProgress(3, 5, "Installing dependencies...", "npm install --legacy-peer-deps");
583
651
  await run("npm", ["install", "--legacy-peer-deps"], build, npmLog);
584
652
  onProgress(3, 5, "Running postinstall...", "npm run postinstall");
585
- await run("npm", ["run", "postinstall", "--if-present"], build, npmLog).catch(() => {
586
- });
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
+ }
587
658
  if (info.pipPackages?.length) {
588
659
  const pipLog = (chunk) => onProgress(3, 5, chunk.trim(), "");
589
660
  const pythonPaths = [
@@ -733,9 +804,9 @@ async function installToCoc(name, onProgress) {
733
804
  const depName = `coc-${name}`;
734
805
  if (!pkg.dependencies[depName]) {
735
806
  pkg.dependencies[depName] = `file:${dest}`;
736
- pkg.lastUpdate = Date.now();
737
- fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
738
807
  }
808
+ pkg.lastUpdate = Date.now();
809
+ fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
739
810
  }
740
811
  function metaPath(name) {
741
812
  return path3.join(cacheDir(name), "meta.json");
@@ -759,13 +830,12 @@ async function installPackage(state, name) {
759
830
  state.setPackageStatus(name, "installing", {
760
831
  progress: `[${step}/${total}] ${msg}`,
761
832
  logEntry: `[${step}/${total}] ${msg}
762
- $ ${cmd}`,
763
- appendLog: true
833
+ $ ${cmd}`
764
834
  });
765
835
  };
766
836
  state.setPackageStatus(name, "installing", { progress: "Starting..." });
767
837
  try {
768
- const input = await downloadSource(info, name, prog);
838
+ const { dir: input } = await downloadSource(info, name, prog);
769
839
  await convertSource(input, name, info, prog);
770
840
  await buildPackage(name, input, info, prog);
771
841
  await installToCoc(name, prog);
@@ -838,13 +908,17 @@ async function updatePackage(state, name) {
838
908
  state.setPackageStatus(name, "updating", {
839
909
  progress: `[${step}/${total}] ${msg}`,
840
910
  logEntry: `[${step}/${total}] ${msg}
841
- $ ${cmd}`,
842
- appendLog: true
911
+ $ ${cmd}`
843
912
  });
844
913
  };
845
914
  state.setPackageStatus(name, "updating", { progress: "Starting..." });
846
915
  try {
847
- 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
+ }
848
922
  await convertSource(input, name, info, prog);
849
923
  await buildPackage(name, input, info, prog);
850
924
  await installToCoc(name, prog);
@@ -898,41 +972,51 @@ async function runWithOutput(cmd, args, cwd) {
898
972
  async function runConcurrent(items, fn, concurrency = 3) {
899
973
  const pool = /* @__PURE__ */ new Set();
900
974
  for (const item of items) {
901
- const p = fn(item).finally(() => pool.delete(p));
975
+ const p = fn(item).catch(() => {
976
+ }).finally(() => pool.delete(p));
902
977
  pool.add(p);
903
978
  if (pool.size >= concurrency) {
904
979
  await Promise.race(pool);
905
980
  }
906
981
  }
907
- await Promise.all(pool);
982
+ await Promise.allSettled(pool);
908
983
  }
984
+ var checkUpdatesBusy = false;
909
985
  async function checkUpdates(state) {
910
- const s = state.getState();
911
- const results = {};
912
- state.setStatusMessage("Checking for updates...");
913
- for (const pkg of s.packages) {
914
- if (pkg.status !== "installed" || !pkg.commit) continue;
915
- state.setStatusMessage(`Checking ${pkg.info.displayName}...`);
916
- try {
917
- const out = await runWithOutput("git", ["ls-remote", `https://github.com/${pkg.info.source.repo}.git`, "HEAD"], os3.homedir());
918
- const remote = out.split(" ")[0];
919
- if (remote) results[pkg.info.name] = remote.substring(0, 7) !== pkg.commit;
920
- } 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
+ }
921
1003
  }
922
- }
923
- const updateCount = Object.values(results).filter(Boolean).length;
924
- state.mutate((s2) => {
925
- for (const p of s2.packages) {
926
- 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);
927
1017
  }
928
- s2.statusMessage = void 0;
929
- });
930
- if (updateCount > 0) {
931
- state.setStatusMessage(`Found ${updateCount} package(s) with updates. Use 'u' to update.`);
932
- setTimeout(() => state.setStatusMessage(), 5e3);
933
- } else {
934
- state.setStatusMessage("All packages up to date.");
935
- setTimeout(() => state.setStatusMessage(), 3e3);
1018
+ } finally {
1019
+ checkUpdatesBusy = false;
936
1020
  }
937
1021
  }
938
1022
 
@@ -962,6 +1046,13 @@ var LineBuffer = class {
962
1046
  currentLine() {
963
1047
  return this.li;
964
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
+ }
965
1056
  highlight(pattern, hlGroup) {
966
1057
  const segs = this.lines[this.li];
967
1058
  let full = "";
@@ -1028,10 +1119,11 @@ var HELP_TEXT = [
1028
1119
  " x Toggle mark",
1029
1120
  " f Cycle filter: all \u2192 installed \u2192 not-installed",
1030
1121
  " s Cycle sort: default \u2192 name \u2192 status \u2192 type",
1031
- " gg Jump to first package",
1032
- " G Jump to last package",
1033
- " <Enter> Toggle expand/collapse details",
1034
- " / Search filter",
1122
+ " j/k Scroll through packages",
1123
+ " / Search filter (then j/k to scroll)",
1124
+ " gg Jump to first page",
1125
+ " G Jump to last page",
1126
+ " <Enter> Open detail popup (description, source, log)",
1035
1127
  " q Close window",
1036
1128
  " <Esc> Help\u2192Search\u2192Marks\u2192Busy guard\u2192Close",
1037
1129
  "",
@@ -1042,7 +1134,7 @@ var HELP_TEXT = [
1042
1134
  " pure-lsp Standard LSP protocol (e.g. Prisma, ESLint)",
1043
1135
  " direct-api Direct coc.nvim API calls (e.g. HTML CSS Support)"
1044
1136
  ];
1045
- var TUI = class {
1137
+ var TUI = class _TUI {
1046
1138
  constructor(state) {
1047
1139
  this.bufnr = 0;
1048
1140
  this.winid = 0;
@@ -1051,11 +1143,20 @@ var TUI = class {
1051
1143
  this.unsubscribe = null;
1052
1144
  this.pkgLineMap = /* @__PURE__ */ new Map();
1053
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;
1054
1152
  this.keyMap = {
1055
1153
  q: "q",
1056
1154
  esc: "<Esc>",
1057
1155
  question: "?",
1058
1156
  slash: "/",
1157
+ j: "j",
1158
+ k: "k",
1159
+ "close-detail": "close-detail",
1059
1160
  U: "U",
1060
1161
  Z: "Z",
1061
1162
  i: "i",
@@ -1072,8 +1173,16 @@ var TUI = class {
1072
1173
  };
1073
1174
  this.rendering = false;
1074
1175
  this.pendingRender = false;
1176
+ this.focusIndex = 0;
1177
+ this.focusLineOffset = 0;
1075
1178
  this.state = state;
1076
1179
  }
1180
+ static {
1181
+ this.HEADER_LINES = 6;
1182
+ }
1183
+ static {
1184
+ this.FOOTER_LINES = 3;
1185
+ }
1077
1186
  async open() {
1078
1187
  const nvim = import_coc2.workspace.nvim;
1079
1188
  this.ns = await nvim.createNamespace("coc-loader");
@@ -1091,7 +1200,9 @@ var TUI = class {
1091
1200
  const editorLines = await nvim.call("nvim_get_option", ["lines"]);
1092
1201
  const editorCols = await nvim.call("nvim_get_option", ["columns"]);
1093
1202
  const height = Math.min(Math.floor(editorLines * 0.85), 40);
1203
+ this.windowHeight = height - 2;
1094
1204
  const width = Math.min(Math.floor(editorCols * 0.85), 120);
1205
+ this.windowWidth = width - 2;
1095
1206
  const row = Math.max(Math.floor((editorLines - height) / 2), 0);
1096
1207
  const col = Math.max(Math.floor((editorCols - width) / 2), 0);
1097
1208
  const win = await nvim.openFloatWindow(buf, true, {
@@ -1100,7 +1211,7 @@ var TUI = class {
1100
1211
  height,
1101
1212
  row,
1102
1213
  col,
1103
- border: "none",
1214
+ border: "rounded",
1104
1215
  style: "minimal"
1105
1216
  });
1106
1217
  this.winid = win.id;
@@ -1131,7 +1242,7 @@ var TUI = class {
1131
1242
  const curBuf = await nvim.call("winbufnr", [curWin]);
1132
1243
  const bt = await nvim.call("getbufvar", [curBuf, "&buftype"]);
1133
1244
  if (bt !== "nofile" && bt !== "prompt") {
1134
- this.close();
1245
+ await this.close();
1135
1246
  }
1136
1247
  }
1137
1248
  })
@@ -1146,7 +1257,12 @@ var TUI = class {
1146
1257
  }
1147
1258
  await this.setupKeymaps();
1148
1259
  await this.render();
1149
- 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();
1150
1266
  this.state.refreshPackages();
1151
1267
  this.render();
1152
1268
  }).catch(() => {
@@ -1160,14 +1276,23 @@ var TUI = class {
1160
1276
  return cursor[0] - 1;
1161
1277
  }
1162
1278
  async handleKey(id) {
1279
+ if (!this.winid) return;
1163
1280
  const line0 = await this.getCursorLine0();
1164
1281
  const s = this.state.getState();
1165
1282
  if (id === "q") {
1166
- this.close();
1283
+ await this.close();
1167
1284
  return;
1168
1285
  }
1169
1286
  if (id === "I") {
1170
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);
1171
1296
  return;
1172
1297
  }
1173
1298
  if (id === "H") {
@@ -1195,7 +1320,7 @@ var TUI = class {
1195
1320
  import_coc2.window.showInformationMessage("Operation in progress, wait for it to finish");
1196
1321
  return;
1197
1322
  }
1198
- this.close();
1323
+ await this.close();
1199
1324
  return;
1200
1325
  }
1201
1326
  if (id === "question") {
@@ -1212,23 +1337,62 @@ var TUI = class {
1212
1337
  }
1213
1338
  if (id === "f") {
1214
1339
  this.state.cycleViewFilter();
1340
+ this.focusIndex = 0;
1215
1341
  return;
1216
1342
  }
1217
1343
  if (id === "s") {
1218
1344
  this.state.cycleSortBy();
1345
+ this.focusIndex = 0;
1219
1346
  return;
1220
1347
  }
1221
1348
  if (id === "gg") {
1222
- const firstLine = Math.min(...this.pkgLineMap.keys());
1223
- if (isFinite(firstLine)) {
1224
- await import_coc2.workspace.nvim.call("nvim_win_set_cursor", [this.winid, [firstLine + 1, 0]]);
1225
- }
1349
+ this.state.setScrollOffset(0);
1350
+ this.focusIndex = 0;
1226
1351
  return;
1227
1352
  }
1228
1353
  if (id === "G") {
1229
- const lastLine = Math.max(...this.pkgLineMap.keys());
1230
- if (isFinite(lastLine)) {
1231
- await import_coc2.workspace.nvim.call("nvim_win_set_cursor", [this.winid, [lastLine + 1, 0]]);
1354
+ const filtered = this.state.getFilteredPackages();
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);
1358
+ return;
1359
+ }
1360
+ if (id === "j") {
1361
+ const filtered = this.state.getFilteredPackages();
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
+ }
1377
+ }
1378
+ return;
1379
+ }
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
+ }
1232
1396
  }
1233
1397
  return;
1234
1398
  }
@@ -1294,12 +1458,12 @@ var TUI = class {
1294
1458
  await installPackage(this.state, pkgName);
1295
1459
  return;
1296
1460
  }
1461
+ if (id === "close-detail") {
1462
+ this.closeDetailPopup();
1463
+ return;
1464
+ }
1297
1465
  if (id === "cr") {
1298
- if (this.logLineSet.has(line0)) {
1299
- this.state.toggleLog(pkgName);
1300
- } else {
1301
- this.state.toggleExpand(pkgName);
1302
- }
1466
+ if (pkgName) await this.showDetailPopup(pkgName);
1303
1467
  return;
1304
1468
  }
1305
1469
  }
@@ -1323,6 +1487,8 @@ var TUI = class {
1323
1487
  ["s", "s"],
1324
1488
  ["x", "x"],
1325
1489
  ["D", "D"],
1490
+ ["j", "j"],
1491
+ ["k", "k"],
1326
1492
  ["gg", "gg"],
1327
1493
  ["G", "G"],
1328
1494
  ["<CR>", "cr"]
@@ -1341,6 +1507,13 @@ var TUI = class {
1341
1507
  d.dispose();
1342
1508
  }
1343
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
+ }
1344
1517
  if (this.winid) {
1345
1518
  try {
1346
1519
  await import_coc2.workspace.nvim.call("nvim_win_close", [this.winid, true]);
@@ -1365,19 +1538,49 @@ var TUI = class {
1365
1538
  const state = this.state.getState();
1366
1539
  const filtered = this.state.getFilteredPackages();
1367
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
+ }
1368
1547
  nvim.pauseNotification();
1369
- nvim.call("nvim_buf_set_option", [this.bufnr, "modifiable", true], true);
1370
- nvim.call("nvim_buf_clear_namespace", [this.bufnr, this.ns, 0, -1], true);
1371
- nvim.call("nvim_buf_set_lines", [this.bufnr, 0, -1, false, result.lines], true);
1372
- nvim.call("nvim_buf_set_option", [this.bufnr, "modifiable", false], true);
1373
- for (const h of result.highlights) {
1374
- nvim.call("nvim_buf_set_extmark", [this.bufnr, this.ns, h.line, h.colStart, {
1375
- end_col: h.colEnd,
1376
- hl_group: h.hlGroup,
1377
- hl_mode: "combine"
1378
- }], 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
+ });
1566
+ }
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
+ }
1582
+ }
1379
1583
  }
1380
- await nvim.resumeNotification();
1381
1584
  this.pkgLineMap = result.pkgLineMap;
1382
1585
  this.logLineSet = result.logLines;
1383
1586
  } finally {
@@ -1443,30 +1646,31 @@ var TUI = class {
1443
1646
  }
1444
1647
  buf.nl();
1445
1648
  buf.nl();
1446
- const installed = filtered.filter((e) => e.status !== "not-installed");
1447
- const available = filtered.filter((e) => e.status === "not-installed");
1448
- const section = (title, entries) => {
1449
- if (entries.length === 0) return;
1450
- buf.nl(`${title}`);
1451
- for (const e of entries) {
1452
- this.renderEntry(buf, pkgLineMap, logSet, e);
1453
- }
1454
- buf.nl();
1455
- buf.nl();
1456
- };
1457
- section(`Installed (${installed.length})`, installed);
1458
- section(`Available (${available.length})`, available);
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");
1656
+ buf.nl();
1657
+ buf.nl();
1658
+ for (const e of visible) {
1659
+ this.renderEntry(buf, pkgLineMap, logSet, e);
1660
+ }
1459
1661
  if (filtered.length === 0 && state.searchQuery) {
1460
1662
  buf.nl("no matching packages");
1461
1663
  }
1462
1664
  buf.nl();
1463
1665
  buf.append(" " + "\u2500".repeat(50), "Comment");
1464
1666
  buf.nl();
1465
- buf.append(` ${filtered.length} packages \xB7 ${filterLabel} \xB7 ${sortLabel} order`, "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");
1466
1670
  const result = buf.render(2);
1467
1671
  return { lines: result.lines, pkgLineMap, logLines: logSet, highlights: result.highlights };
1468
1672
  }
1469
- renderEntry(buf, pkgLineMap, logSet, entry) {
1673
+ renderEntry(buf, pkgLineMap, _logSet, entry) {
1470
1674
  const icon = entry.status === "installed" ? "\u25CF" : entry.status === "failed" ? "\u2717" : "\u25CB";
1471
1675
  const iconHl = entry.status === "installed" ? "CocConverterInstalled" : entry.status === "failed" ? "ErrorMsg" : "CocConverterAvailable";
1472
1676
  const pkgLine = buf.currentLine();
@@ -1480,67 +1684,153 @@ var TUI = class {
1480
1684
  buf.append(icon, iconHl);
1481
1685
  buf.append(" ");
1482
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
+ }
1483
1696
  buf.append(" ");
1484
1697
  buf.append(entry.info.type, "CocConverterType");
1698
+ if (statusText) {
1699
+ buf.append(statusText, statusHl);
1700
+ }
1485
1701
  if (entry.hasUpdate) {
1486
1702
  buf.append(" \u2191", "CocConverterKey");
1487
1703
  }
1488
- if (entry.updated && entry.commit && entry.commitMsg) {
1489
- buf.nl();
1490
- const ln = buf.currentLine();
1491
- buf.append(` ${entry.commit} ${entry.commitMsg}`, "Comment");
1492
- if (entry.commitDate) {
1493
- 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
+ }
1494
1718
  }
1495
- pkgLineMap.set(ln, entry.info.name);
1496
- }
1497
- if (entry.expanded) {
1498
- buf.nl();
1499
- const extras = [
1500
- entry.info.description,
1501
- `type ${entry.info.type}`,
1502
- entry.commit ? `commit ${entry.commit}` : null,
1503
- `source ${sourceStr(entry.info.source)}`,
1504
- `languages ${entry.info.languages.join(", ")}`,
1505
- `categories ${entry.info.categories.join(", ")}`,
1506
- `homepage ${entry.info.url}`,
1507
- entry.info.serverBinary ? `server ${entry.info.serverBinary.repo} (binary release)` : null
1508
- ];
1509
- for (const text of extras.filter(Boolean)) {
1510
- const ln = buf.currentLine();
1511
- buf.nl(` ${text}`);
1512
- 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
+ }
1513
1730
  }
1731
+ if (entry.error) lines.push("", ` \u2717 ${entry.error}`);
1732
+ return lines;
1514
1733
  }
1515
- if (entry.progress) {
1516
- buf.nl();
1517
- if (entry.logExpanded) {
1518
- const ln = buf.currentLine();
1519
- buf.nl(` \u25BC Install log:`);
1520
- logSet.add(ln);
1521
- pkgLineMap.set(ln, entry.info.name);
1522
- for (const log of entry.progressLog) {
1523
- for (const l of log.split("\n")) {
1524
- const ln2 = buf.currentLine();
1525
- buf.nl(` ${l}`);
1526
- logSet.add(ln2);
1527
- pkgLineMap.set(ln2, entry.info.name);
1528
- }
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" }]);
1529
1815
  }
1530
- } else {
1531
- const ln = buf.currentLine();
1532
- buf.nl(` \u25B6 ${entry.progress}`);
1533
- logSet.add(ln);
1534
- pkgLineMap.set(ln, entry.info.name);
1535
1816
  }
1536
1817
  }
1537
- if (entry.error) {
1538
- buf.nl();
1539
- const ln = buf.currentLine();
1540
- buf.nl(` \u2717 ${entry.error}`);
1541
- pkgLineMap.set(ln, entry.info.name);
1818
+ if (this.detailMode === "log") {
1819
+ await nvim.call("nvim_win_set_cursor", [this.detailWinid, [lines.length, 0]]);
1542
1820
  }
1543
- 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
+ });
1544
1834
  }
1545
1835
  isOpen() {
1546
1836
  return this.winid !== 0;
@@ -1598,7 +1888,7 @@ async function activate(context) {
1598
1888
  import_coc3.window.showInformationMessage(`${name} is not installed`);
1599
1889
  return;
1600
1890
  }
1601
- uninstallPackage(state, name);
1891
+ await uninstallPackage(state, name);
1602
1892
  })
1603
1893
  );
1604
1894
  context.subscriptions.push(