@involvex/fresh-editor 0.1.76 → 0.1.78
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/bin/CHANGELOG.md +1017 -0
- package/bin/LICENSE +117 -0
- package/bin/README.md +248 -0
- package/bin/fresh.exe +0 -0
- package/bin/plugins/README.md +71 -0
- package/bin/plugins/audit_mode.i18n.json +821 -0
- package/bin/plugins/audit_mode.ts +1810 -0
- package/bin/plugins/buffer_modified.i18n.json +67 -0
- package/bin/plugins/buffer_modified.ts +281 -0
- package/bin/plugins/calculator.i18n.json +93 -0
- package/bin/plugins/calculator.ts +770 -0
- package/bin/plugins/clangd-lsp.ts +168 -0
- package/bin/plugins/clangd_support.i18n.json +223 -0
- package/bin/plugins/clangd_support.md +20 -0
- package/bin/plugins/clangd_support.ts +325 -0
- package/bin/plugins/color_highlighter.i18n.json +145 -0
- package/bin/plugins/color_highlighter.ts +304 -0
- package/bin/plugins/config-schema.json +768 -0
- package/bin/plugins/csharp-lsp.ts +147 -0
- package/bin/plugins/csharp_support.i18n.json +80 -0
- package/bin/plugins/csharp_support.ts +170 -0
- package/bin/plugins/css-lsp.ts +143 -0
- package/bin/plugins/diagnostics_panel.i18n.json +236 -0
- package/bin/plugins/diagnostics_panel.ts +642 -0
- package/bin/plugins/examples/README.md +85 -0
- package/bin/plugins/examples/async_demo.ts +165 -0
- package/bin/plugins/examples/bookmarks.ts +329 -0
- package/bin/plugins/examples/buffer_query_demo.ts +110 -0
- package/bin/plugins/examples/git_grep.ts +262 -0
- package/bin/plugins/examples/hello_world.ts +93 -0
- package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/bin/plugins/find_references.i18n.json +275 -0
- package/bin/plugins/find_references.ts +359 -0
- package/bin/plugins/git_blame.i18n.json +496 -0
- package/bin/plugins/git_blame.ts +707 -0
- package/bin/plugins/git_find_file.i18n.json +314 -0
- package/bin/plugins/git_find_file.ts +300 -0
- package/bin/plugins/git_grep.i18n.json +171 -0
- package/bin/plugins/git_grep.ts +191 -0
- package/bin/plugins/git_gutter.i18n.json +93 -0
- package/bin/plugins/git_gutter.ts +477 -0
- package/bin/plugins/git_log.i18n.json +481 -0
- package/bin/plugins/git_log.ts +1285 -0
- package/bin/plugins/go-lsp.ts +143 -0
- package/bin/plugins/html-lsp.ts +145 -0
- package/bin/plugins/json-lsp.ts +145 -0
- package/bin/plugins/lib/fresh.d.ts +1321 -0
- package/bin/plugins/lib/index.ts +24 -0
- package/bin/plugins/lib/navigation-controller.ts +214 -0
- package/bin/plugins/lib/panel-manager.ts +220 -0
- package/bin/plugins/lib/types.ts +72 -0
- package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
- package/bin/plugins/live_grep.i18n.json +171 -0
- package/bin/plugins/live_grep.ts +422 -0
- package/bin/plugins/markdown_compose.i18n.json +223 -0
- package/bin/plugins/markdown_compose.ts +630 -0
- package/bin/plugins/merge_conflict.i18n.json +821 -0
- package/bin/plugins/merge_conflict.ts +1810 -0
- package/bin/plugins/path_complete.i18n.json +80 -0
- package/bin/plugins/path_complete.ts +165 -0
- package/bin/plugins/python-lsp.ts +162 -0
- package/bin/plugins/rust-lsp.ts +166 -0
- package/bin/plugins/search_replace.i18n.json +405 -0
- package/bin/plugins/search_replace.ts +484 -0
- package/bin/plugins/test_i18n.i18n.json +67 -0
- package/bin/plugins/test_i18n.ts +18 -0
- package/bin/plugins/theme_editor.i18n.json +3746 -0
- package/bin/plugins/theme_editor.ts +2063 -0
- package/bin/plugins/todo_highlighter.i18n.json +184 -0
- package/bin/plugins/todo_highlighter.ts +206 -0
- package/bin/plugins/typescript-lsp.ts +167 -0
- package/bin/plugins/vi_mode.i18n.json +1549 -0
- package/bin/plugins/vi_mode.ts +2747 -0
- package/bin/plugins/welcome.i18n.json +236 -0
- package/bin/plugins/welcome.ts +76 -0
- package/bin/themes/dark.json +102 -0
- package/bin/themes/dracula.json +62 -0
- package/bin/themes/high-contrast.json +102 -0
- package/bin/themes/light.json +102 -0
- package/bin/themes/nord.json +62 -0
- package/bin/themes/nostalgia.json +102 -0
- package/bin/themes/solarized-dark.json +62 -0
- package/binary-install.js +1 -1
- package/dist/bin/fresh.js +9 -0
- package/dist/binary-install.js +149 -0
- package/dist/binary.js +30 -0
- package/dist/fresh-6yhknp07.exe +0 -0
- package/dist/install.js +158 -0
- package/dist/run-fresh.js +43 -0
- package/package.json +7 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
{
|
|
2
|
+
"en": {
|
|
3
|
+
"cmd.live_grep": "Live Grep (Find in Files)",
|
|
4
|
+
"cmd.live_grep_desc": "Search for text across project with live preview",
|
|
5
|
+
"status.ready": "Live Grep ready - use command palette or bind 'start_live_grep'",
|
|
6
|
+
"status.type_to_search": "Type to search (min 2 chars)...",
|
|
7
|
+
"status.found_matches": "Found {count} matches",
|
|
8
|
+
"status.no_matches": "No matches found",
|
|
9
|
+
"status.search_error": "Search error: {error}",
|
|
10
|
+
"status.opened_file": "Opened {file}:{line}",
|
|
11
|
+
"status.no_file_selected": "No file selected",
|
|
12
|
+
"status.cancelled": "Live grep cancelled",
|
|
13
|
+
"prompt.live_grep": "Live grep: "
|
|
14
|
+
},
|
|
15
|
+
"cs": {
|
|
16
|
+
"cmd.live_grep": "Live Grep (Hledat v souborech)",
|
|
17
|
+
"cmd.live_grep_desc": "Vyhledavani textu v projektu s zivy nahledem",
|
|
18
|
+
"status.ready": "Live Grep pripraven - pouzijte paletu prikazu nebo nastavte 'start_live_grep'",
|
|
19
|
+
"status.type_to_search": "Piste pro vyhledavani (min 2 znaky)...",
|
|
20
|
+
"status.found_matches": "Nalezeno {count} shod",
|
|
21
|
+
"status.no_matches": "Zadne shody nenalezeny",
|
|
22
|
+
"status.search_error": "Chyba vyhledavani: {error}",
|
|
23
|
+
"status.opened_file": "Otevreno {file}:{line}",
|
|
24
|
+
"status.no_file_selected": "Zaden soubor nevybran",
|
|
25
|
+
"status.cancelled": "Live grep zrusen",
|
|
26
|
+
"prompt.live_grep": "Live grep: "
|
|
27
|
+
},
|
|
28
|
+
"de": {
|
|
29
|
+
"cmd.live_grep": "Live Grep (Suche in Dateien)",
|
|
30
|
+
"cmd.live_grep_desc": "Text im gesamten Projekt mit Live-Vorschau suchen",
|
|
31
|
+
"status.ready": "Live Grep bereit - Befehlspalette verwenden oder 'start_live_grep' binden",
|
|
32
|
+
"status.type_to_search": "Tippen zum Suchen (min. 2 Zeichen)...",
|
|
33
|
+
"status.found_matches": "{count} Treffer gefunden",
|
|
34
|
+
"status.no_matches": "Keine Treffer gefunden",
|
|
35
|
+
"status.search_error": "Suchfehler: {error}",
|
|
36
|
+
"status.opened_file": "{file}:{line} geoffnet",
|
|
37
|
+
"status.no_file_selected": "Keine Datei ausgewahlt",
|
|
38
|
+
"status.cancelled": "Live Grep abgebrochen",
|
|
39
|
+
"prompt.live_grep": "Live Grep: "
|
|
40
|
+
},
|
|
41
|
+
"es": {
|
|
42
|
+
"cmd.live_grep": "Grep en Vivo (Buscar en Archivos)",
|
|
43
|
+
"cmd.live_grep_desc": "Buscar texto en todo el proyecto con vista previa en vivo",
|
|
44
|
+
"status.ready": "Grep en Vivo listo - usa la paleta de comandos o asigna 'start_live_grep'",
|
|
45
|
+
"status.type_to_search": "Escribe para buscar (min 2 caracteres)...",
|
|
46
|
+
"status.found_matches": "Se encontraron {count} coincidencias",
|
|
47
|
+
"status.no_matches": "No se encontraron coincidencias",
|
|
48
|
+
"status.search_error": "Error de busqueda: {error}",
|
|
49
|
+
"status.opened_file": "Abierto {file}:{line}",
|
|
50
|
+
"status.no_file_selected": "Ningun archivo seleccionado",
|
|
51
|
+
"status.cancelled": "Grep en vivo cancelado",
|
|
52
|
+
"prompt.live_grep": "Grep en vivo: "
|
|
53
|
+
},
|
|
54
|
+
"fr": {
|
|
55
|
+
"cmd.live_grep": "Grep en Direct (Rechercher dans les Fichiers)",
|
|
56
|
+
"cmd.live_grep_desc": "Rechercher du texte dans tout le projet avec apercu en direct",
|
|
57
|
+
"status.ready": "Grep en Direct pret - utilisez la palette de commandes ou liez 'start_live_grep'",
|
|
58
|
+
"status.type_to_search": "Tapez pour rechercher (min 2 caracteres)...",
|
|
59
|
+
"status.found_matches": "{count} correspondances trouvees",
|
|
60
|
+
"status.no_matches": "Aucune correspondance trouvee",
|
|
61
|
+
"status.search_error": "Erreur de recherche : {error}",
|
|
62
|
+
"status.opened_file": "Ouvert {file}:{line}",
|
|
63
|
+
"status.no_file_selected": "Aucun fichier selectionne",
|
|
64
|
+
"status.cancelled": "Grep en direct annule",
|
|
65
|
+
"prompt.live_grep": "Grep en direct : "
|
|
66
|
+
},
|
|
67
|
+
"it": {
|
|
68
|
+
"cmd.live_grep": "Live Grep (Cerca nei file)",
|
|
69
|
+
"cmd.live_grep_desc": "Cerca testo nel progetto con anteprima in tempo reale",
|
|
70
|
+
"status.ready": "Live Grep pronto - usa la tavolozza comandi o associa 'start_live_grep'",
|
|
71
|
+
"status.type_to_search": "Scrivi per cercare (min 2 caratteri)...",
|
|
72
|
+
"status.found_matches": "Trovate {count} corrispondenze",
|
|
73
|
+
"status.no_matches": "Nessuna corrispondenza trovata",
|
|
74
|
+
"status.search_error": "Errore di ricerca: {error}",
|
|
75
|
+
"status.opened_file": "Aperto {file}:{line}",
|
|
76
|
+
"status.no_file_selected": "Nessun file selezionato",
|
|
77
|
+
"status.cancelled": "Live grep annullato",
|
|
78
|
+
"prompt.live_grep": "Live grep: "
|
|
79
|
+
},
|
|
80
|
+
"ja": {
|
|
81
|
+
"cmd.live_grep": "Live Grep (ファイル内検索)",
|
|
82
|
+
"cmd.live_grep_desc": "プロジェクト全体をライブプレビュー付きで検索",
|
|
83
|
+
"status.ready": "Live Grep 準備完了 - コマンドパレットを使用するか 'start_live_grep' をバインド",
|
|
84
|
+
"status.type_to_search": "検索するには入力してください (最小2文字)...",
|
|
85
|
+
"status.found_matches": "{count} 件の一致が見つかりました",
|
|
86
|
+
"status.no_matches": "一致するものが見つかりません",
|
|
87
|
+
"status.search_error": "検索エラー: {error}",
|
|
88
|
+
"status.opened_file": "{file}:{line} を開きました",
|
|
89
|
+
"status.no_file_selected": "ファイルが選択されていません",
|
|
90
|
+
"status.cancelled": "Live grep がキャンセルされました",
|
|
91
|
+
"prompt.live_grep": "Live grep: "
|
|
92
|
+
},
|
|
93
|
+
"ko": {
|
|
94
|
+
"cmd.live_grep": "라이브 Grep (파일에서 찾기)",
|
|
95
|
+
"cmd.live_grep_desc": "실시간 미리보기로 프로젝트 전체에서 텍스트 검색",
|
|
96
|
+
"status.ready": "라이브 Grep 준비됨 - 명령 팔레트를 사용하거나 'start_live_grep' 바인딩",
|
|
97
|
+
"status.type_to_search": "검색하려면 입력하세요 (최소 2자)...",
|
|
98
|
+
"status.found_matches": "{count}개의 일치 항목 발견",
|
|
99
|
+
"status.no_matches": "일치 항목 없음",
|
|
100
|
+
"status.search_error": "검색 오류: {error}",
|
|
101
|
+
"status.opened_file": "{file}:{line} 열림",
|
|
102
|
+
"status.no_file_selected": "선택된 파일 없음",
|
|
103
|
+
"status.cancelled": "라이브 grep 취소됨",
|
|
104
|
+
"prompt.live_grep": "라이브 grep: "
|
|
105
|
+
},
|
|
106
|
+
"pt-BR": {
|
|
107
|
+
"cmd.live_grep": "Grep ao Vivo (Buscar em Arquivos)",
|
|
108
|
+
"cmd.live_grep_desc": "Pesquisar texto no projeto com visualizacao ao vivo",
|
|
109
|
+
"status.ready": "Grep ao Vivo pronto - use a paleta de comandos ou vincule 'start_live_grep'",
|
|
110
|
+
"status.type_to_search": "Digite para pesquisar (min 2 caracteres)...",
|
|
111
|
+
"status.found_matches": "{count} correspondencias encontradas",
|
|
112
|
+
"status.no_matches": "Nenhuma correspondencia encontrada",
|
|
113
|
+
"status.search_error": "Erro de pesquisa: {error}",
|
|
114
|
+
"status.opened_file": "Aberto {file}:{line}",
|
|
115
|
+
"status.no_file_selected": "Nenhum arquivo selecionado",
|
|
116
|
+
"status.cancelled": "Grep ao vivo cancelado",
|
|
117
|
+
"prompt.live_grep": "Grep ao vivo: "
|
|
118
|
+
},
|
|
119
|
+
"ru": {
|
|
120
|
+
"cmd.live_grep": "Live Grep (Поиск в файлах)",
|
|
121
|
+
"cmd.live_grep_desc": "Поиск текста по всему проекту с предпросмотром в реальном времени",
|
|
122
|
+
"status.ready": "Live Grep готов - используйте палитру команд или назначьте 'start_live_grep'",
|
|
123
|
+
"status.type_to_search": "Введите для поиска (мин. 2 символа)...",
|
|
124
|
+
"status.found_matches": "Найдено {count} совпадений",
|
|
125
|
+
"status.no_matches": "Совпадения не найдены",
|
|
126
|
+
"status.search_error": "Ошибка поиска: {error}",
|
|
127
|
+
"status.opened_file": "Открыт {file}:{line}",
|
|
128
|
+
"status.no_file_selected": "Файл не выбран",
|
|
129
|
+
"status.cancelled": "Live grep отменен",
|
|
130
|
+
"prompt.live_grep": "Live grep: "
|
|
131
|
+
},
|
|
132
|
+
"th": {
|
|
133
|
+
"cmd.live_grep": "Live Grep (ค้นหาในไฟล์)",
|
|
134
|
+
"cmd.live_grep_desc": "ค้นหาข้อความในโปรเจกต์พร้อมดูตัวอย่างสด",
|
|
135
|
+
"status.ready": "Live Grep พร้อมแล้ว - ใช้พาเลทคำสั่งหรือผูก 'start_live_grep'",
|
|
136
|
+
"status.type_to_search": "พิมพ์เพื่อค้นหา (อย่างน้อย 2 ตัวอักษร)...",
|
|
137
|
+
"status.found_matches": "พบ {count} รายการที่ตรงกัน",
|
|
138
|
+
"status.no_matches": "ไม่พบรายการที่ตรงกัน",
|
|
139
|
+
"status.search_error": "ข้อผิดพลาดการค้นหา: {error}",
|
|
140
|
+
"status.opened_file": "เปิดแล้ว {file}:{line}",
|
|
141
|
+
"status.no_file_selected": "ไม่ได้เลือกไฟล์",
|
|
142
|
+
"status.cancelled": "ยกเลิก Live grep แล้ว",
|
|
143
|
+
"prompt.live_grep": "Live grep: "
|
|
144
|
+
},
|
|
145
|
+
"uk": {
|
|
146
|
+
"cmd.live_grep": "Live Grep (Пошук у файлах)",
|
|
147
|
+
"cmd.live_grep_desc": "Пошук тексту по всьому проекту з попереднім переглядом у реальному часі",
|
|
148
|
+
"status.ready": "Live Grep готовий - використовуйте палітру команд або прив'яжіть 'start_live_grep'",
|
|
149
|
+
"status.type_to_search": "Введіть для пошуку (мін. 2 символи)...",
|
|
150
|
+
"status.found_matches": "Знайдено {count} збігів",
|
|
151
|
+
"status.no_matches": "Збігів не знайдено",
|
|
152
|
+
"status.search_error": "Помилка пошуку: {error}",
|
|
153
|
+
"status.opened_file": "Відкрито {file}:{line}",
|
|
154
|
+
"status.no_file_selected": "Файл не вибрано",
|
|
155
|
+
"status.cancelled": "Live grep скасовано",
|
|
156
|
+
"prompt.live_grep": "Live grep: "
|
|
157
|
+
},
|
|
158
|
+
"zh-CN": {
|
|
159
|
+
"cmd.live_grep": "实时 Grep (文件内搜索)",
|
|
160
|
+
"cmd.live_grep_desc": "在整个项目中搜索文本并实时预览",
|
|
161
|
+
"status.ready": "实时 Grep 已就绪 - 使用命令面板或绑定 'start_live_grep'",
|
|
162
|
+
"status.type_to_search": "输入以搜索 (最少2个字符)...",
|
|
163
|
+
"status.found_matches": "找到 {count} 个匹配项",
|
|
164
|
+
"status.no_matches": "未找到匹配项",
|
|
165
|
+
"status.search_error": "搜索错误: {error}",
|
|
166
|
+
"status.opened_file": "已打开 {file}:{line}",
|
|
167
|
+
"status.no_file_selected": "未选择文件",
|
|
168
|
+
"status.cancelled": "实时 grep 已取消",
|
|
169
|
+
"prompt.live_grep": "实时 grep: "
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live Grep Plugin
|
|
7
|
+
*
|
|
8
|
+
* Project-wide search with ripgrep and live preview.
|
|
9
|
+
* - Type to search across all files
|
|
10
|
+
* - Navigate results with Up/Down to see preview
|
|
11
|
+
* - Press Enter to open file at location
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface GrepMatch {
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
content: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// State management
|
|
22
|
+
let grepResults: GrepMatch[] = [];
|
|
23
|
+
let previewBufferId: number | null = null;
|
|
24
|
+
let previewSplitId: number | null = null;
|
|
25
|
+
let originalSplitId: number | null = null;
|
|
26
|
+
let lastQuery: string = "";
|
|
27
|
+
let previewCreated: boolean = false;
|
|
28
|
+
let currentSearch: ProcessHandle | null = null;
|
|
29
|
+
let pendingKill: Promise<boolean> | null = null; // Track pending kill globally
|
|
30
|
+
let searchVersion = 0; // Incremented on each input change for debouncing
|
|
31
|
+
|
|
32
|
+
const DEBOUNCE_MS = 150; // Wait 150ms after last keystroke before searching
|
|
33
|
+
|
|
34
|
+
// Parse ripgrep output line
|
|
35
|
+
// Format: file:line:column:content
|
|
36
|
+
function parseRipgrepLine(line: string): GrepMatch | null {
|
|
37
|
+
const match = line.match(/^([^:]+):(\d+):(\d+):(.*)$/);
|
|
38
|
+
if (match) {
|
|
39
|
+
return {
|
|
40
|
+
file: match[1],
|
|
41
|
+
line: parseInt(match[2], 10),
|
|
42
|
+
column: parseInt(match[3], 10),
|
|
43
|
+
content: match[4],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Parse ripgrep output into suggestions
|
|
50
|
+
function parseRipgrepOutput(stdout: string): {
|
|
51
|
+
results: GrepMatch[];
|
|
52
|
+
suggestions: PromptSuggestion[];
|
|
53
|
+
} {
|
|
54
|
+
const results: GrepMatch[] = [];
|
|
55
|
+
const suggestions: PromptSuggestion[] = [];
|
|
56
|
+
|
|
57
|
+
for (const line of stdout.split("\n")) {
|
|
58
|
+
if (!line.trim()) continue;
|
|
59
|
+
const match = parseRipgrepLine(line);
|
|
60
|
+
if (match) {
|
|
61
|
+
results.push(match);
|
|
62
|
+
|
|
63
|
+
// Truncate long content for display
|
|
64
|
+
const displayContent =
|
|
65
|
+
match.content.length > 60
|
|
66
|
+
? match.content.substring(0, 57) + "..."
|
|
67
|
+
: match.content;
|
|
68
|
+
|
|
69
|
+
suggestions.push({
|
|
70
|
+
text: `${match.file}:${match.line}`,
|
|
71
|
+
description: displayContent.trim(),
|
|
72
|
+
value: `${results.length - 1}`, // Store index as value
|
|
73
|
+
disabled: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Limit to 100 results for performance
|
|
77
|
+
if (results.length >= 100) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { results, suggestions };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create or update preview buffer with file content
|
|
87
|
+
async function updatePreview(match: GrepMatch): Promise<void> {
|
|
88
|
+
try {
|
|
89
|
+
// Read the file content
|
|
90
|
+
const content = await editor.readFile(match.file);
|
|
91
|
+
const lines = content.split("\n");
|
|
92
|
+
|
|
93
|
+
// Calculate context window (5 lines before and after)
|
|
94
|
+
const contextBefore = 5;
|
|
95
|
+
const contextAfter = 5;
|
|
96
|
+
const startLine = Math.max(0, match.line - 1 - contextBefore);
|
|
97
|
+
const endLine = Math.min(lines.length, match.line + contextAfter);
|
|
98
|
+
|
|
99
|
+
// Build preview entries with highlighting
|
|
100
|
+
const entries: TextPropertyEntry[] = [];
|
|
101
|
+
|
|
102
|
+
// Header
|
|
103
|
+
entries.push({
|
|
104
|
+
text: ` ${match.file}:${match.line}:${match.column}\n`,
|
|
105
|
+
properties: { type: "header" },
|
|
106
|
+
});
|
|
107
|
+
entries.push({
|
|
108
|
+
text: "─".repeat(60) + "\n",
|
|
109
|
+
properties: { type: "separator" },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Content lines with line numbers
|
|
113
|
+
for (let i = startLine; i < endLine; i++) {
|
|
114
|
+
const lineNum = i + 1;
|
|
115
|
+
const lineContent = lines[i] || "";
|
|
116
|
+
const isMatchLine = lineNum === match.line;
|
|
117
|
+
const prefix = isMatchLine ? "> " : " ";
|
|
118
|
+
const lineNumStr = String(lineNum).padStart(4, " ");
|
|
119
|
+
|
|
120
|
+
entries.push({
|
|
121
|
+
text: `${prefix}${lineNumStr} │ ${lineContent}\n`,
|
|
122
|
+
properties: {
|
|
123
|
+
type: isMatchLine ? "match" : "context",
|
|
124
|
+
line: lineNum,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create or update the preview buffer
|
|
130
|
+
if (previewBufferId === null) {
|
|
131
|
+
// Define mode for preview buffer
|
|
132
|
+
editor.defineMode("live-grep-preview", "special", [["q", "close_buffer"]], true);
|
|
133
|
+
|
|
134
|
+
// Create preview in a split on the right
|
|
135
|
+
const result = await editor.createVirtualBufferInSplit({
|
|
136
|
+
name: "*Preview*",
|
|
137
|
+
mode: "live-grep-preview",
|
|
138
|
+
read_only: true,
|
|
139
|
+
entries,
|
|
140
|
+
ratio: 0.5,
|
|
141
|
+
direction: "vertical",
|
|
142
|
+
panel_id: "live-grep-preview",
|
|
143
|
+
show_line_numbers: false,
|
|
144
|
+
editing_disabled: true,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Extract buffer and split IDs from result
|
|
148
|
+
previewBufferId = result.buffer_id;
|
|
149
|
+
previewSplitId = result.split_id ?? null;
|
|
150
|
+
|
|
151
|
+
// Return focus to original split so prompt stays active
|
|
152
|
+
if (originalSplitId !== null) {
|
|
153
|
+
editor.focusSplit(originalSplitId);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Update existing buffer content
|
|
157
|
+
editor.setVirtualBufferContent(previewBufferId, entries);
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
editor.debug(`Failed to update preview: ${e}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Close preview buffer and its split
|
|
165
|
+
function closePreview(): void {
|
|
166
|
+
// Close the buffer first
|
|
167
|
+
if (previewBufferId !== null) {
|
|
168
|
+
editor.closeBuffer(previewBufferId);
|
|
169
|
+
previewBufferId = null;
|
|
170
|
+
}
|
|
171
|
+
// Then close the split
|
|
172
|
+
if (previewSplitId !== null) {
|
|
173
|
+
editor.closeSplit(previewSplitId);
|
|
174
|
+
previewSplitId = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Run ripgrep search with debouncing
|
|
179
|
+
async function runSearch(query: string): Promise<void> {
|
|
180
|
+
// Increment version to invalidate any pending debounced search
|
|
181
|
+
const thisVersion = ++searchVersion;
|
|
182
|
+
editor.debug(`[live_grep] runSearch called: query="${query}", version=${thisVersion}`);
|
|
183
|
+
|
|
184
|
+
// Kill any existing search immediately (don't wait) to stop wasting CPU
|
|
185
|
+
// Store the kill promise globally so ALL pending searches wait for it
|
|
186
|
+
if (currentSearch) {
|
|
187
|
+
editor.debug(`[live_grep] killing existing search immediately`);
|
|
188
|
+
pendingKill = currentSearch.kill();
|
|
189
|
+
currentSearch = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!query || query.trim().length < 2) {
|
|
193
|
+
// Wait for any pending kill to complete before returning
|
|
194
|
+
if (pendingKill) {
|
|
195
|
+
await pendingKill;
|
|
196
|
+
pendingKill = null;
|
|
197
|
+
}
|
|
198
|
+
editor.debug(`[live_grep] query too short, clearing`);
|
|
199
|
+
editor.setPromptSuggestions([]);
|
|
200
|
+
grepResults = [];
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Debounce: wait a bit to see if user is still typing
|
|
205
|
+
editor.debug(`[live_grep] debouncing for ${DEBOUNCE_MS}ms...`);
|
|
206
|
+
await editor.delay(DEBOUNCE_MS);
|
|
207
|
+
|
|
208
|
+
// Always await any pending kill before continuing - ensures old process is dead
|
|
209
|
+
if (pendingKill) {
|
|
210
|
+
editor.debug(`[live_grep] waiting for previous search to terminate`);
|
|
211
|
+
await pendingKill;
|
|
212
|
+
pendingKill = null;
|
|
213
|
+
editor.debug(`[live_grep] previous search terminated`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// If version changed during delay, a newer search was triggered - abort this one
|
|
217
|
+
if (searchVersion !== thisVersion) {
|
|
218
|
+
editor.debug(`[live_grep] version mismatch after debounce (${thisVersion} vs ${searchVersion}), aborting`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Avoid duplicate searches
|
|
223
|
+
if (query === lastQuery) {
|
|
224
|
+
editor.debug(`[live_grep] duplicate query, skipping`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
lastQuery = query;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const cwd = editor.getCwd();
|
|
231
|
+
editor.debug(`[live_grep] spawning rg for query="${query}" in cwd="${cwd}"`);
|
|
232
|
+
const searchStartTime = Date.now();
|
|
233
|
+
const search = editor.spawnProcess("rg", [
|
|
234
|
+
"--line-number",
|
|
235
|
+
"--column",
|
|
236
|
+
"--no-heading",
|
|
237
|
+
"--color=never",
|
|
238
|
+
"--smart-case",
|
|
239
|
+
"--max-count=100",
|
|
240
|
+
"-g", "!.git",
|
|
241
|
+
"-g", "!node_modules",
|
|
242
|
+
"-g", "!target",
|
|
243
|
+
"-g", "!*.lock",
|
|
244
|
+
"--",
|
|
245
|
+
query,
|
|
246
|
+
], cwd);
|
|
247
|
+
|
|
248
|
+
currentSearch = search;
|
|
249
|
+
editor.debug(`[live_grep] awaiting search result...`);
|
|
250
|
+
const result = await search;
|
|
251
|
+
const searchDuration = Date.now() - searchStartTime;
|
|
252
|
+
editor.debug(`[live_grep] rg completed in ${searchDuration}ms, exit_code=${result.exit_code}, stdout_len=${result.stdout.length}`);
|
|
253
|
+
|
|
254
|
+
// Check if this search was cancelled (a new search started)
|
|
255
|
+
if (currentSearch !== search) {
|
|
256
|
+
editor.debug(`[live_grep] search was superseded, discarding results`);
|
|
257
|
+
return; // Discard stale results
|
|
258
|
+
}
|
|
259
|
+
currentSearch = null;
|
|
260
|
+
|
|
261
|
+
if (result.exit_code === 0) {
|
|
262
|
+
const parseStart = Date.now();
|
|
263
|
+
const { results, suggestions } = parseRipgrepOutput(result.stdout);
|
|
264
|
+
editor.debug(`[live_grep] parsed ${results.length} results in ${Date.now() - parseStart}ms`);
|
|
265
|
+
grepResults = results;
|
|
266
|
+
editor.setPromptSuggestions(suggestions);
|
|
267
|
+
|
|
268
|
+
if (results.length > 0) {
|
|
269
|
+
editor.setStatus(editor.t("status.found_matches", { count: String(results.length) }));
|
|
270
|
+
// Show preview of first result
|
|
271
|
+
await updatePreview(results[0]);
|
|
272
|
+
} else {
|
|
273
|
+
editor.setStatus(editor.t("status.no_matches"));
|
|
274
|
+
}
|
|
275
|
+
} else if (result.exit_code === 1) {
|
|
276
|
+
// No matches
|
|
277
|
+
editor.debug(`[live_grep] no matches (exit_code=1)`);
|
|
278
|
+
grepResults = [];
|
|
279
|
+
editor.setPromptSuggestions([]);
|
|
280
|
+
editor.setStatus(editor.t("status.no_matches"));
|
|
281
|
+
} else if (result.exit_code === -1) {
|
|
282
|
+
// Process was killed, ignore
|
|
283
|
+
editor.debug(`[live_grep] process was killed`);
|
|
284
|
+
} else {
|
|
285
|
+
editor.debug(`[live_grep] search error: ${result.stderr}`);
|
|
286
|
+
editor.setStatus(editor.t("status.search_error", { error: result.stderr }));
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
// Ignore errors from killed processes
|
|
290
|
+
const errorMsg = String(e);
|
|
291
|
+
editor.debug(`[live_grep] caught error: ${errorMsg}`);
|
|
292
|
+
if (!errorMsg.includes("killed") && !errorMsg.includes("not found")) {
|
|
293
|
+
editor.setStatus(editor.t("status.search_error", { error: String(e) }));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Start live grep
|
|
299
|
+
globalThis.start_live_grep = function (): void {
|
|
300
|
+
// Clear previous state
|
|
301
|
+
grepResults = [];
|
|
302
|
+
lastQuery = "";
|
|
303
|
+
previewBufferId = null;
|
|
304
|
+
|
|
305
|
+
// Remember original split to keep focus
|
|
306
|
+
originalSplitId = editor.getActiveSplitId();
|
|
307
|
+
|
|
308
|
+
// Start the prompt
|
|
309
|
+
editor.startPrompt(editor.t("prompt.live_grep"), "live-grep");
|
|
310
|
+
editor.setStatus(editor.t("status.type_to_search"));
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Handle prompt input changes
|
|
314
|
+
globalThis.onLiveGrepPromptChanged = function (args: {
|
|
315
|
+
prompt_type: string;
|
|
316
|
+
input: string;
|
|
317
|
+
}): boolean {
|
|
318
|
+
if (args.prompt_type !== "live-grep") {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
editor.debug(`[live_grep] onPromptChanged: input="${args.input}"`);
|
|
323
|
+
|
|
324
|
+
// runSearch handles debouncing internally
|
|
325
|
+
runSearch(args.input);
|
|
326
|
+
|
|
327
|
+
return true;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Handle selection changes - update preview
|
|
331
|
+
globalThis.onLiveGrepSelectionChanged = function (args: {
|
|
332
|
+
prompt_type: string;
|
|
333
|
+
selected_index: number;
|
|
334
|
+
}): boolean {
|
|
335
|
+
if (args.prompt_type !== "live-grep") {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const match = grepResults[args.selected_index];
|
|
340
|
+
if (match) {
|
|
341
|
+
updatePreview(match);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return true;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Handle prompt confirmation - open file
|
|
348
|
+
globalThis.onLiveGrepPromptConfirmed = function (args: {
|
|
349
|
+
prompt_type: string;
|
|
350
|
+
selected_index: number | null;
|
|
351
|
+
input: string;
|
|
352
|
+
}): boolean {
|
|
353
|
+
if (args.prompt_type !== "live-grep") {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Kill any running search
|
|
358
|
+
if (currentSearch) {
|
|
359
|
+
currentSearch.kill();
|
|
360
|
+
currentSearch = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Close preview first
|
|
364
|
+
closePreview();
|
|
365
|
+
|
|
366
|
+
// Open selected file
|
|
367
|
+
if (args.selected_index !== null && grepResults[args.selected_index]) {
|
|
368
|
+
const selected = grepResults[args.selected_index];
|
|
369
|
+
editor.openFile(selected.file, selected.line, selected.column);
|
|
370
|
+
editor.setStatus(editor.t("status.opened_file", { file: selected.file, line: String(selected.line) }));
|
|
371
|
+
} else {
|
|
372
|
+
editor.setStatus(editor.t("status.no_file_selected"));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Clear state
|
|
376
|
+
grepResults = [];
|
|
377
|
+
originalSplitId = null;
|
|
378
|
+
previewSplitId = null;
|
|
379
|
+
|
|
380
|
+
return true;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Handle prompt cancellation
|
|
384
|
+
globalThis.onLiveGrepPromptCancelled = function (args: {
|
|
385
|
+
prompt_type: string;
|
|
386
|
+
}): boolean {
|
|
387
|
+
if (args.prompt_type !== "live-grep") {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Kill any running search
|
|
392
|
+
if (currentSearch) {
|
|
393
|
+
currentSearch.kill();
|
|
394
|
+
currentSearch = null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Close preview and cleanup
|
|
398
|
+
closePreview();
|
|
399
|
+
grepResults = [];
|
|
400
|
+
originalSplitId = null;
|
|
401
|
+
previewSplitId = null;
|
|
402
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
403
|
+
|
|
404
|
+
return true;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Register event handlers
|
|
408
|
+
editor.on("prompt_changed", "onLiveGrepPromptChanged");
|
|
409
|
+
editor.on("prompt_selection_changed", "onLiveGrepSelectionChanged");
|
|
410
|
+
editor.on("prompt_confirmed", "onLiveGrepPromptConfirmed");
|
|
411
|
+
editor.on("prompt_cancelled", "onLiveGrepPromptCancelled");
|
|
412
|
+
|
|
413
|
+
// Register command
|
|
414
|
+
editor.registerCommand(
|
|
415
|
+
"%cmd.live_grep",
|
|
416
|
+
"%cmd.live_grep_desc",
|
|
417
|
+
"start_live_grep",
|
|
418
|
+
"normal"
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
editor.debug("Live Grep plugin loaded");
|
|
422
|
+
editor.setStatus(editor.t("status.ready"));
|