@fresh-editor/fresh-editor 0.2.22 → 0.2.24
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/CHANGELOG.md +97 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +521 -381
- package/plugins/audit_mode.ts +3211 -567
- package/plugins/config-schema.json +153 -1
- package/plugins/git_blame.ts +1 -6
- package/plugins/git_explorer.ts +7 -7
- package/plugins/git_log.ts +616 -1025
- package/plugins/lib/fresh.d.ts +94 -4
- package/plugins/lib/git_history.ts +596 -0
- package/plugins/markdown_compose.ts +183 -7
- package/plugins/pkg.ts +151 -397
- package/plugins/search_replace.i18n.json +42 -14
- package/plugins/search_replace.ts +146 -96
- package/plugins/theme_editor.i18n.json +182 -14
- package/plugins/theme_editor.ts +192 -85
- package/plugins/vi_mode.ts +8 -3
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"panel.replace_all_btn": "Replace All (Alt+Ret)",
|
|
37
37
|
"panel.type_pattern": "Type a search pattern above",
|
|
38
38
|
"panel.matches_count": "Matches (%{count} in %{files} files)",
|
|
39
|
-
"panel.matches_title": "Matches"
|
|
39
|
+
"panel.matches_title": "Matches",
|
|
40
|
+
"prompt.confirm_replace": "Replace %{count} match(es) in %{files} file(s)? Undo only covers files still open in this session. Press Enter to confirm, Esc to cancel.",
|
|
41
|
+
"status.replace_cancelled": "Replacement cancelled"
|
|
40
42
|
},
|
|
41
43
|
"cs": {
|
|
42
44
|
"cmd.search_replace": "Hledat a nahradit v projektu",
|
|
@@ -75,7 +77,9 @@
|
|
|
75
77
|
"panel.replace_all_btn": "Nahradit vše (Alt+Ret)",
|
|
76
78
|
"panel.type_pattern": "Zadejte vyhledávací vzor",
|
|
77
79
|
"panel.matches_count": "Shody (%{count} v %{files} souborech)",
|
|
78
|
-
"panel.matches_title": "Shody"
|
|
80
|
+
"panel.matches_title": "Shody",
|
|
81
|
+
"prompt.confirm_replace": "Nahradit %{count} výskyt(ů) v %{files} souboru(ech)? Vrátit zpět lze jen soubory otevřené v této relaci. Enter potvrdí, Esc zruší.",
|
|
82
|
+
"status.replace_cancelled": "Nahrazení zrušeno"
|
|
79
83
|
},
|
|
80
84
|
"de": {
|
|
81
85
|
"cmd.search_replace": "Suchen und Ersetzen im Projekt",
|
|
@@ -114,7 +118,9 @@
|
|
|
114
118
|
"panel.replace_all_btn": "Alle ersetzen (Alt+Ret)",
|
|
115
119
|
"panel.type_pattern": "Suchmuster oben eingeben",
|
|
116
120
|
"panel.matches_count": "Treffer (%{count} in %{files} Dateien)",
|
|
117
|
-
"panel.matches_title": "Treffer"
|
|
121
|
+
"panel.matches_title": "Treffer",
|
|
122
|
+
"prompt.confirm_replace": "%{count} Treffer in %{files} Datei(en) ersetzen? Rückgängig wirkt nur auf Dateien, die in dieser Sitzung geöffnet sind. Enter bestätigt, Esc bricht ab.",
|
|
123
|
+
"status.replace_cancelled": "Ersetzen abgebrochen"
|
|
118
124
|
},
|
|
119
125
|
"es": {
|
|
120
126
|
"cmd.search_replace": "Buscar y Reemplazar en Proyecto",
|
|
@@ -153,7 +159,9 @@
|
|
|
153
159
|
"panel.replace_all_btn": "Reemplazar todo (Alt+Ret)",
|
|
154
160
|
"panel.type_pattern": "Escriba un patrón de búsqueda",
|
|
155
161
|
"panel.matches_count": "Coincidencias (%{count} en %{files} archivos)",
|
|
156
|
-
"panel.matches_title": "Coincidencias"
|
|
162
|
+
"panel.matches_title": "Coincidencias",
|
|
163
|
+
"prompt.confirm_replace": "¿Reemplazar %{count} coincidencia(s) en %{files} archivo(s)? Deshacer solo cubre los archivos abiertos en esta sesión. Intro confirma, Esc cancela.",
|
|
164
|
+
"status.replace_cancelled": "Reemplazo cancelado"
|
|
157
165
|
},
|
|
158
166
|
"fr": {
|
|
159
167
|
"cmd.search_replace": "Rechercher et Remplacer dans le Projet",
|
|
@@ -192,7 +200,9 @@
|
|
|
192
200
|
"panel.replace_all_btn": "Tout remplacer (Alt+Ret)",
|
|
193
201
|
"panel.type_pattern": "Saisissez un motif de recherche",
|
|
194
202
|
"panel.matches_count": "Correspondances (%{count} dans %{files} fichiers)",
|
|
195
|
-
"panel.matches_title": "Correspondances"
|
|
203
|
+
"panel.matches_title": "Correspondances",
|
|
204
|
+
"prompt.confirm_replace": "Remplacer %{count} correspondance(s) dans %{files} fichier(s) ? L'annulation ne concerne que les fichiers ouverts dans cette session. Entrée confirme, Échap annule.",
|
|
205
|
+
"status.replace_cancelled": "Remplacement annulé"
|
|
196
206
|
},
|
|
197
207
|
"it": {
|
|
198
208
|
"cmd.search_replace": "Cerca e sostituisci nel progetto",
|
|
@@ -231,7 +241,9 @@
|
|
|
231
241
|
"panel.replace_all_btn": "Sostituisci tutto (Alt+Ret)",
|
|
232
242
|
"panel.type_pattern": "Digita un modello di ricerca",
|
|
233
243
|
"panel.matches_count": "Corrispondenze (%{count} in %{files} file)",
|
|
234
|
-
"panel.matches_title": "Corrispondenze"
|
|
244
|
+
"panel.matches_title": "Corrispondenze",
|
|
245
|
+
"prompt.confirm_replace": "Sostituire %{count} corrispondenza/e in %{files} file? L'annulla copre solo i file aperti in questa sessione. Invio conferma, Esc annulla.",
|
|
246
|
+
"status.replace_cancelled": "Sostituzione annullata"
|
|
235
247
|
},
|
|
236
248
|
"ja": {
|
|
237
249
|
"cmd.search_replace": "プロジェクト内で検索と置換",
|
|
@@ -270,7 +282,9 @@
|
|
|
270
282
|
"panel.replace_all_btn": "すべて置換 (Alt+Ret)",
|
|
271
283
|
"panel.type_pattern": "検索パターンを入力してください",
|
|
272
284
|
"panel.matches_count": "一致 (%{files}ファイル中%{count}件)",
|
|
273
|
-
"panel.matches_title": "一致"
|
|
285
|
+
"panel.matches_title": "一致",
|
|
286
|
+
"prompt.confirm_replace": "%{files} 個のファイルで %{count} 件の一致を置換しますか?元に戻せるのはこのセッションで開いているファイルのみです。Enterで確認、Escでキャンセル。",
|
|
287
|
+
"status.replace_cancelled": "置換がキャンセルされました"
|
|
274
288
|
},
|
|
275
289
|
"ko": {
|
|
276
290
|
"cmd.search_replace": "프로젝트에서 검색 및 바꾸기",
|
|
@@ -309,7 +323,9 @@
|
|
|
309
323
|
"panel.replace_all_btn": "모두 바꾸기 (Alt+Ret)",
|
|
310
324
|
"panel.type_pattern": "검색 패턴을 입력하세요",
|
|
311
325
|
"panel.matches_count": "일치 (%{files}개 파일에서 %{count}개)",
|
|
312
|
-
"panel.matches_title": "일치"
|
|
326
|
+
"panel.matches_title": "일치",
|
|
327
|
+
"prompt.confirm_replace": "%{files}개 파일에서 %{count}개 일치를 바꾸시겠습니까? 실행 취소는 이 세션에서 열려 있는 파일에만 적용됩니다. Enter 확인, Esc 취소.",
|
|
328
|
+
"status.replace_cancelled": "바꾸기가 취소됨"
|
|
313
329
|
},
|
|
314
330
|
"pt-BR": {
|
|
315
331
|
"cmd.search_replace": "Pesquisar e Substituir no Projeto",
|
|
@@ -348,7 +364,9 @@
|
|
|
348
364
|
"panel.replace_all_btn": "Substituir tudo (Alt+Ret)",
|
|
349
365
|
"panel.type_pattern": "Digite um padrão de pesquisa",
|
|
350
366
|
"panel.matches_count": "Correspondências (%{count} em %{files} arquivos)",
|
|
351
|
-
"panel.matches_title": "Correspondências"
|
|
367
|
+
"panel.matches_title": "Correspondências",
|
|
368
|
+
"prompt.confirm_replace": "Substituir %{count} ocorrência(s) em %{files} arquivo(s)? Desfazer só cobre arquivos abertos nesta sessão. Enter confirma, Esc cancela.",
|
|
369
|
+
"status.replace_cancelled": "Substituição cancelada"
|
|
352
370
|
},
|
|
353
371
|
"ru": {
|
|
354
372
|
"cmd.search_replace": "Поиск и замена в проекте",
|
|
@@ -387,7 +405,9 @@
|
|
|
387
405
|
"panel.replace_all_btn": "Заменить все (Alt+Ret)",
|
|
388
406
|
"panel.type_pattern": "Введите шаблон поиска",
|
|
389
407
|
"panel.matches_count": "Совпадения (%{count} в %{files} файлах)",
|
|
390
|
-
"panel.matches_title": "Совпадения"
|
|
408
|
+
"panel.matches_title": "Совпадения",
|
|
409
|
+
"prompt.confirm_replace": "Заменить %{count} совпадений в %{files} файлах? Отмена применяется только к файлам, открытым в этом сеансе. Enter — подтвердить, Esc — отменить.",
|
|
410
|
+
"status.replace_cancelled": "Замена отменена"
|
|
391
411
|
},
|
|
392
412
|
"th": {
|
|
393
413
|
"cmd.search_replace": "ค้นหาและแทนที่ในโปรเจกต์",
|
|
@@ -426,7 +446,9 @@
|
|
|
426
446
|
"panel.replace_all_btn": "แทนที่ทั้งหมด (Alt+Ret)",
|
|
427
447
|
"panel.type_pattern": "พิมพ์รูปแบบการค้นหา",
|
|
428
448
|
"panel.matches_count": "รายการที่ตรงกัน (%{count} ใน %{files} ไฟล์)",
|
|
429
|
-
"panel.matches_title": "รายการที่ตรงกัน"
|
|
449
|
+
"panel.matches_title": "รายการที่ตรงกัน",
|
|
450
|
+
"prompt.confirm_replace": "แทนที่ %{count} รายการใน %{files} ไฟล์? เลิกทำครอบคลุมเฉพาะไฟล์ที่เปิดอยู่ในเซสชันนี้เท่านั้น กด Enter เพื่อยืนยัน, Esc เพื่อยกเลิก",
|
|
451
|
+
"status.replace_cancelled": "ยกเลิกการแทนที่"
|
|
430
452
|
},
|
|
431
453
|
"uk": {
|
|
432
454
|
"cmd.search_replace": "Пошук та заміна в проекті",
|
|
@@ -465,7 +487,9 @@
|
|
|
465
487
|
"panel.replace_all_btn": "Замінити все (Alt+Ret)",
|
|
466
488
|
"panel.type_pattern": "Введіть шаблон пошуку",
|
|
467
489
|
"panel.matches_count": "Збіги (%{count} у %{files} файлах)",
|
|
468
|
-
"panel.matches_title": "Збіги"
|
|
490
|
+
"panel.matches_title": "Збіги",
|
|
491
|
+
"prompt.confirm_replace": "Замінити %{count} збіг(ів) у %{files} файл(ах)? Скасування охоплює лише файли, відкриті в цьому сеансі. Enter — підтвердити, Esc — скасувати.",
|
|
492
|
+
"status.replace_cancelled": "Заміну скасовано"
|
|
469
493
|
},
|
|
470
494
|
"vi": {
|
|
471
495
|
"cmd.search_replace": "Tìm và Thay thế trong Dự án",
|
|
@@ -504,7 +528,9 @@
|
|
|
504
528
|
"panel.replace_all_btn": "Thay thế tất cả (Alt+Ret)",
|
|
505
529
|
"panel.type_pattern": "Nhập mẫu tìm kiếm",
|
|
506
530
|
"panel.matches_count": "Kết quả (%{count} trong %{files} tệp)",
|
|
507
|
-
"panel.matches_title": "Kết quả"
|
|
531
|
+
"panel.matches_title": "Kết quả",
|
|
532
|
+
"prompt.confirm_replace": "Thay thế %{count} kết quả trong %{files} tệp? Hoàn tác chỉ áp dụng cho các tệp đang mở trong phiên này. Enter để xác nhận, Esc để hủy.",
|
|
533
|
+
"status.replace_cancelled": "Đã hủy thay thế"
|
|
508
534
|
},
|
|
509
535
|
"zh-CN": {
|
|
510
536
|
"cmd.search_replace": "在项目中搜索和替换",
|
|
@@ -543,6 +569,8 @@
|
|
|
543
569
|
"panel.replace_all_btn": "全部替换 (Alt+Ret)",
|
|
544
570
|
"panel.type_pattern": "请输入搜索模式",
|
|
545
571
|
"panel.matches_count": "匹配 (%{files} 个文件中 %{count} 个)",
|
|
546
|
-
"panel.matches_title": "匹配"
|
|
572
|
+
"panel.matches_title": "匹配",
|
|
573
|
+
"prompt.confirm_replace": "在 %{files} 个文件中替换 %{count} 处匹配?撤销仅适用于本次会话中打开的文件。按 Enter 确认,Esc 取消。",
|
|
574
|
+
"status.replace_cancelled": "替换已取消"
|
|
547
575
|
}
|
|
548
576
|
}
|
|
@@ -428,115 +428,106 @@ function buildPanelEntries(): TextPropertyEntry[] {
|
|
|
428
428
|
});
|
|
429
429
|
|
|
430
430
|
// ── Matches tree (virtual-scrolled) ──
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
let selectedLineIdx = -1;
|
|
431
|
+
const flatItems = buildFlatItems();
|
|
432
|
+
const fixedRows = 5;
|
|
433
|
+
const treeVisibleRows = Math.max(3, getViewportHeight() - fixedRows);
|
|
435
434
|
|
|
436
435
|
if (searchPattern && totalMatches === 0) {
|
|
437
|
-
|
|
436
|
+
entries.push({
|
|
438
437
|
text: padStr(" " + editor.t("panel.no_matches"), W) + "\n",
|
|
439
438
|
properties: { type: "empty" },
|
|
440
439
|
style: { fg: C.dim },
|
|
441
440
|
});
|
|
442
441
|
} else if (!searchPattern) {
|
|
443
|
-
|
|
442
|
+
entries.push({
|
|
444
443
|
text: padStr(" " + editor.t("panel.type_pattern"), W) + "\n",
|
|
445
444
|
properties: { type: "empty" },
|
|
446
445
|
style: { fg: C.dim },
|
|
447
446
|
});
|
|
448
447
|
} else {
|
|
449
|
-
let
|
|
450
|
-
for (let fi = 0; fi < fileGroups.length; fi++) {
|
|
451
|
-
const group = fileGroups[fi];
|
|
452
|
-
const isFileSelected = focusPanel === "matches" && panel.matchIndex === flatIdx;
|
|
453
|
-
if (isFileSelected) selectedLineIdx = allTreeLines.length;
|
|
454
|
-
const expandIcon = group.expanded ? "v" : ">";
|
|
455
|
-
const badge = getFileExtBadge(group.relPath);
|
|
456
|
-
const matchCount = group.matches.length;
|
|
457
|
-
const selectedInFile = group.matches.filter(m => m.selected).length;
|
|
458
|
-
const fileLineText = ` ${expandIcon} ${badge} ${group.relPath} (${selectedInFile}/${matchCount})`;
|
|
459
|
-
|
|
460
|
-
const fileOverlays: InlineOverlay[] = [];
|
|
461
|
-
const eiStart = byteLen(" ");
|
|
462
|
-
const eiEnd = eiStart + byteLen(expandIcon);
|
|
463
|
-
fileOverlays.push({ start: eiStart, end: eiEnd, style: { fg: C.expandIcon } });
|
|
464
|
-
const bgStart = eiEnd + byteLen(" ");
|
|
465
|
-
const bgEnd = bgStart + byteLen(badge);
|
|
466
|
-
fileOverlays.push({ start: bgStart, end: bgEnd, style: { fg: C.fileIcon, bold: true } });
|
|
467
|
-
const fpStart = bgEnd + byteLen(" ");
|
|
468
|
-
const fpEnd = fpStart + byteLen(group.relPath);
|
|
469
|
-
fileOverlays.push({ start: fpStart, end: fpEnd, style: { fg: C.filePath } });
|
|
470
|
-
|
|
471
|
-
allTreeLines.push({
|
|
472
|
-
text: padStr(fileLineText, W) + "\n",
|
|
473
|
-
properties: { type: "file-row", fileIndex: fi },
|
|
474
|
-
style: isFileSelected ? { bg: C.selectedBg } : undefined,
|
|
475
|
-
inlineOverlays: fileOverlays,
|
|
476
|
-
});
|
|
477
|
-
flatIdx++;
|
|
478
|
-
|
|
479
|
-
if (group.expanded) {
|
|
480
|
-
for (let mi = 0; mi < group.matches.length; mi++) {
|
|
481
|
-
const result = group.matches[mi];
|
|
482
|
-
const isMatchSelected = focusPanel === "matches" && panel.matchIndex === flatIdx;
|
|
483
|
-
if (isMatchSelected) selectedLineIdx = allTreeLines.length;
|
|
484
|
-
const checkbox = result.selected ? "[v]" : "[ ]";
|
|
485
|
-
const location = `${group.relPath}:${result.match.line}`;
|
|
486
|
-
const context = result.match.context.trim();
|
|
487
|
-
const prefixText = ` ${isMatchSelected ? ">" : " "} ${checkbox} `;
|
|
488
|
-
const maxCtx = W - charLen(prefixText) - charLen(location) - 3;
|
|
489
|
-
const displayCtx = truncate(context, Math.max(10, maxCtx));
|
|
490
|
-
const matchLineText = `${prefixText}${location} - ${displayCtx}`;
|
|
491
|
-
|
|
492
|
-
const inlines: InlineOverlay[] = [];
|
|
493
|
-
const cbStart = byteLen(` ${isMatchSelected ? ">" : " "} `);
|
|
494
|
-
const cbEnd = cbStart + byteLen(checkbox);
|
|
495
|
-
inlines.push({ start: cbStart, end: cbEnd, style: { fg: result.selected ? C.checkOn : C.checkOff } });
|
|
496
|
-
const locStart = cbEnd + byteLen(" ");
|
|
497
|
-
const locEnd = locStart + byteLen(location);
|
|
498
|
-
inlines.push({ start: locStart, end: locEnd, style: { fg: C.lineNum } });
|
|
499
|
-
|
|
500
|
-
if (panel.searchPattern) {
|
|
501
|
-
const ctxStart = locEnd + byteLen(" - ");
|
|
502
|
-
highlightMatches(displayCtx, panel.searchPattern, ctxStart, panel.useRegex, panel.caseSensitive, inlines);
|
|
503
|
-
}
|
|
448
|
+
let selectedLineIdx = focusPanel === "matches" ? panel.matchIndex : -1;
|
|
504
449
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
450
|
+
// Adjust scroll offset to keep selected line visible
|
|
451
|
+
if (selectedLineIdx >= 0) {
|
|
452
|
+
if (selectedLineIdx < panel.scrollOffset) {
|
|
453
|
+
panel.scrollOffset = selectedLineIdx;
|
|
454
|
+
}
|
|
455
|
+
if (selectedLineIdx >= panel.scrollOffset + treeVisibleRows) {
|
|
456
|
+
panel.scrollOffset = selectedLineIdx - treeVisibleRows + 1;
|
|
513
457
|
}
|
|
514
458
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
459
|
+
const maxOffset = Math.max(0, flatItems.length - treeVisibleRows);
|
|
460
|
+
if (panel.scrollOffset > maxOffset) panel.scrollOffset = maxOffset;
|
|
461
|
+
if (panel.scrollOffset < 0) panel.scrollOffset = 0;
|
|
462
|
+
|
|
463
|
+
// ONLY loop through the items that are literally on the screen right now
|
|
464
|
+
for (let i = panel.scrollOffset; i < panel.scrollOffset + treeVisibleRows; i++) {
|
|
465
|
+
if (i >= flatItems.length) break;
|
|
466
|
+
const item = flatItems[i];
|
|
467
|
+
const isSelected = focusPanel === "matches" && panel.matchIndex === i;
|
|
468
|
+
|
|
469
|
+
if (item.type === "file") {
|
|
470
|
+
const group = fileGroups[item.fileIndex];
|
|
471
|
+
const expandIcon = group.expanded ? "v" : ">";
|
|
472
|
+
const badge = getFileExtBadge(group.relPath);
|
|
473
|
+
const matchCount = group.matches.length;
|
|
474
|
+
const selectedInFile = group.matches.filter(m => m.selected).length;
|
|
475
|
+
const fileLineText = ` ${expandIcon} ${badge} ${group.relPath} (${selectedInFile}/${matchCount})`;
|
|
476
|
+
|
|
477
|
+
const fileOverlays: InlineOverlay[] = [];
|
|
478
|
+
const eiStart = byteLen(" ");
|
|
479
|
+
const eiEnd = eiStart + byteLen(expandIcon);
|
|
480
|
+
fileOverlays.push({ start: eiStart, end: eiEnd, style: { fg: C.expandIcon } });
|
|
481
|
+
const bgStart = eiEnd + byteLen(" ");
|
|
482
|
+
const bgEnd = bgStart + byteLen(badge);
|
|
483
|
+
fileOverlays.push({ start: bgStart, end: bgEnd, style: { fg: C.fileIcon, bold: true } });
|
|
484
|
+
const fpStart = bgEnd + byteLen(" ");
|
|
485
|
+
const fpEnd = fpStart + byteLen(group.relPath);
|
|
486
|
+
fileOverlays.push({ start: fpStart, end: fpEnd, style: { fg: C.filePath } });
|
|
487
|
+
|
|
488
|
+
entries.push({
|
|
489
|
+
text: padStr(fileLineText, W) + "\n",
|
|
490
|
+
properties: { type: "file-row", fileIndex: item.fileIndex },
|
|
491
|
+
style: isSelected ? { bg: C.selectedBg } : undefined,
|
|
492
|
+
inlineOverlays: fileOverlays,
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
const group = fileGroups[item.fileIndex];
|
|
496
|
+
const result = group.matches[item.matchIndex!];
|
|
497
|
+
const checkbox = result.selected ? "[v]" : "[ ]";
|
|
498
|
+
const location = `${group.relPath}:${result.match.line}`;
|
|
499
|
+
const context = result.match.context.trim();
|
|
500
|
+
const prefixText = ` ${isSelected ? ">" : " "} ${checkbox} `;
|
|
501
|
+
const maxCtx = W - charLen(prefixText) - charLen(location) - 3;
|
|
502
|
+
const displayCtx = truncate(context, Math.max(10, maxCtx));
|
|
503
|
+
const matchLineText = `${prefixText}${location} - ${displayCtx}`;
|
|
504
|
+
|
|
505
|
+
const inlines: InlineOverlay[] = [];
|
|
506
|
+
const cbStart = byteLen(` ${isSelected ? ">" : " "} `);
|
|
507
|
+
const cbEnd = cbStart + byteLen(checkbox);
|
|
508
|
+
inlines.push({ start: cbStart, end: cbEnd, style: { fg: result.selected ? C.checkOn : C.checkOff } });
|
|
509
|
+
const locStart = cbEnd + byteLen(" ");
|
|
510
|
+
const locEnd = locStart + byteLen(location);
|
|
511
|
+
inlines.push({ start: locStart, end: locEnd, style: { fg: C.lineNum } });
|
|
512
|
+
|
|
513
|
+
if (panel.searchPattern) {
|
|
514
|
+
const ctxStart = locEnd + byteLen(" - ");
|
|
515
|
+
highlightMatches(displayCtx, panel.searchPattern, ctxStart, panel.useRegex, panel.caseSensitive, inlines);
|
|
516
|
+
}
|
|
520
517
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
518
|
+
entries.push({
|
|
519
|
+
text: padStr(matchLineText, W) + "\n",
|
|
520
|
+
properties: { type: "match-row", fileIndex: item.fileIndex, matchIndex: item.matchIndex },
|
|
521
|
+
style: isSelected ? { bg: C.selectedBg } : undefined,
|
|
522
|
+
inlineOverlays: inlines.length > 0 ? inlines : undefined,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
528
525
|
}
|
|
529
526
|
}
|
|
530
|
-
const maxOffset = Math.max(0, allTreeLines.length - treeVisibleRows);
|
|
531
|
-
if (panel.scrollOffset > maxOffset) panel.scrollOffset = maxOffset;
|
|
532
|
-
if (panel.scrollOffset < 0) panel.scrollOffset = 0;
|
|
533
|
-
|
|
534
|
-
const visibleLines = allTreeLines.slice(panel.scrollOffset, panel.scrollOffset + treeVisibleRows);
|
|
535
|
-
for (const line of visibleLines) entries.push(line);
|
|
536
527
|
|
|
537
528
|
// Scroll indicators
|
|
538
529
|
const canScrollUp = panel.scrollOffset > 0;
|
|
539
|
-
const canScrollDown = panel.scrollOffset + treeVisibleRows <
|
|
530
|
+
const canScrollDown = panel.scrollOffset + treeVisibleRows < flatItems.length;
|
|
540
531
|
const scrollHint = canScrollUp || canScrollDown
|
|
541
532
|
? " " + (canScrollUp ? "↑" : " ") + (canScrollDown ? "↓" : " ")
|
|
542
533
|
: "";
|
|
@@ -635,6 +626,8 @@ async function performSearch(pattern: string, silent?: boolean): Promise<SearchR
|
|
|
635
626
|
if (!panel) return [];
|
|
636
627
|
|
|
637
628
|
const generation = ++currentSearchGeneration;
|
|
629
|
+
let lastUiUpdate = Date.now();
|
|
630
|
+
const UI_UPDATE_INTERVAL_MS = 100; // Force maximum 10 UI updates per second
|
|
638
631
|
|
|
639
632
|
try {
|
|
640
633
|
const fixedString = !panel.useRegex;
|
|
@@ -649,17 +642,24 @@ async function performSearch(pattern: string, silent?: boolean): Promise<SearchR
|
|
|
649
642
|
maxResults: MAX_RESULTS,
|
|
650
643
|
wholeWords: panel.wholeWords,
|
|
651
644
|
},
|
|
652
|
-
(matches: GrepMatch[],
|
|
645
|
+
(matches: GrepMatch[], done: boolean) => {
|
|
653
646
|
// Discard if a newer search has started
|
|
654
647
|
if (generation !== currentSearchGeneration || !panel) return;
|
|
655
648
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
649
|
+
if (matches.length > 0) {
|
|
650
|
+
// Use push loop instead of allResults.concat() to save massive memory allocations
|
|
651
|
+
for (const m of matches) {
|
|
652
|
+
allResults.push({ match: m, selected: true });
|
|
653
|
+
}
|
|
660
654
|
panel.searchResults = allResults;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const now = Date.now();
|
|
658
|
+
// Only trigger the expensive UI rebuild if enough time passed or stream finished
|
|
659
|
+
if (done || now - lastUiUpdate > UI_UPDATE_INTERVAL_MS) {
|
|
661
660
|
panel.fileGroups = buildFileGroups(allResults);
|
|
662
661
|
updatePanelContent();
|
|
662
|
+
lastUiUpdate = now;
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
665
|
);
|
|
@@ -1091,7 +1091,7 @@ function search_replace_enter(): void {
|
|
|
1091
1091
|
if (panel) {
|
|
1092
1092
|
panel.focusPanel = "matches";
|
|
1093
1093
|
panel.matchIndex = 0;
|
|
1094
|
-
|
|
1094
|
+
panel.scrollOffset = 0;
|
|
1095
1095
|
updatePanelContent();
|
|
1096
1096
|
}
|
|
1097
1097
|
});
|
|
@@ -1157,12 +1157,33 @@ async function doReplaceAll(): Promise<void> {
|
|
|
1157
1157
|
editor.setStatus(editor.t("status.no_items_selected"));
|
|
1158
1158
|
return;
|
|
1159
1159
|
}
|
|
1160
|
+
// Confirm before applying. Replacements write to disk immediately; Undo
|
|
1161
|
+
// only covers files that remain open in this session (see bug #1 report).
|
|
1162
|
+
const fileCount = new Set(selected.map(r => r.match.file)).size;
|
|
1163
|
+
const confirmed = await editor.prompt(
|
|
1164
|
+
editor.t("prompt.confirm_replace", {
|
|
1165
|
+
count: String(selected.length),
|
|
1166
|
+
files: String(fileCount),
|
|
1167
|
+
}),
|
|
1168
|
+
"",
|
|
1169
|
+
);
|
|
1170
|
+
if (confirmed === null) {
|
|
1171
|
+
editor.setStatus(editor.t("status.replace_cancelled"));
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1160
1174
|
panel.busy = true;
|
|
1161
1175
|
editor.setStatus(editor.t("status.replacing", { count: String(selected.length) }));
|
|
1162
1176
|
const statusMsg = await executeReplacements(selected);
|
|
1163
1177
|
editor.setStatus(statusMsg);
|
|
1164
|
-
|
|
1178
|
+
// Clear stale results before re-searching: the byte offsets in
|
|
1179
|
+
// `panel.searchResults` now point at positions in the pre-replacement
|
|
1180
|
+
// file and must never be re-used (see bug #4 — a second Alt+Enter would
|
|
1181
|
+
// otherwise corrupt files by writing into moved offsets). We also drop
|
|
1182
|
+
// `busy` so rerunSearchQuiet doesn't bail out on its own guard.
|
|
1183
|
+
panel.searchResults = [];
|
|
1184
|
+
panel.fileGroups = [];
|
|
1165
1185
|
panel.busy = false;
|
|
1186
|
+
await rerunSearchQuiet();
|
|
1166
1187
|
updatePanelContent();
|
|
1167
1188
|
}
|
|
1168
1189
|
|
|
@@ -1185,12 +1206,28 @@ async function doReplaceScoped(): Promise<void> {
|
|
|
1185
1206
|
return;
|
|
1186
1207
|
}
|
|
1187
1208
|
|
|
1209
|
+
const fileCount = new Set(toReplace.map(r => r.match.file)).size;
|
|
1210
|
+
const confirmed = await editor.prompt(
|
|
1211
|
+
editor.t("prompt.confirm_replace", {
|
|
1212
|
+
count: String(toReplace.length),
|
|
1213
|
+
files: String(fileCount),
|
|
1214
|
+
}),
|
|
1215
|
+
"",
|
|
1216
|
+
);
|
|
1217
|
+
if (confirmed === null) {
|
|
1218
|
+
editor.setStatus(editor.t("status.replace_cancelled"));
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1188
1222
|
panel.busy = true;
|
|
1189
1223
|
editor.setStatus(editor.t("status.replacing", { count: String(toReplace.length) }));
|
|
1190
1224
|
const statusMsg = await executeReplacements(toReplace);
|
|
1191
1225
|
editor.setStatus(statusMsg);
|
|
1192
|
-
|
|
1226
|
+
// See doReplaceAll — clear stale offsets and drop busy before re-searching.
|
|
1227
|
+
panel.searchResults = [];
|
|
1228
|
+
panel.fileGroups = [];
|
|
1193
1229
|
panel.busy = false;
|
|
1230
|
+
await rerunSearchQuiet();
|
|
1194
1231
|
updatePanelContent();
|
|
1195
1232
|
}
|
|
1196
1233
|
|
|
@@ -1242,6 +1279,19 @@ function onSearchReplacePromptCancelled(args: { prompt_type: string }): boolean
|
|
|
1242
1279
|
registerHandler("onSearchReplacePromptCancelled", onSearchReplacePromptCancelled);
|
|
1243
1280
|
editor.on("prompt_cancelled", "onSearchReplacePromptCancelled");
|
|
1244
1281
|
|
|
1282
|
+
// If the panel's virtual buffer is closed externally (via the × button,
|
|
1283
|
+
// the Close Buffer/Close Tab commands, or anything else), reset the
|
|
1284
|
+
// plugin's internal state so the next invocation of `openPanel` creates
|
|
1285
|
+
// a fresh buffer/split instead of trying to update a buffer that no
|
|
1286
|
+
// longer exists (which silently no-ops and leaves the user with no UI).
|
|
1287
|
+
function onSearchReplaceBufferClosed(args: { buffer_id: number }): void {
|
|
1288
|
+
if (panel && args.buffer_id === panel.resultsBufferId) {
|
|
1289
|
+
panel = null;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
registerHandler("onSearchReplaceBufferClosed", onSearchReplaceBufferClosed);
|
|
1293
|
+
editor.on("buffer_closed", "onSearchReplaceBufferClosed");
|
|
1294
|
+
|
|
1245
1295
|
editor.registerCommand(
|
|
1246
1296
|
"%cmd.search_replace",
|
|
1247
1297
|
"%cmd.search_replace_desc",
|