dsh-locator 0.8.1 → 0.9.0
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 +25 -0
- package/README-es.md +21 -5
- package/README-hi.md +22 -5
- package/README-pt.md +22 -5
- package/README.en.md +22 -5
- package/README.md +22 -5
- package/client.js +968 -54
- package/lib/combined.d.ts +20 -3
- package/lib/combined.d.ts.map +1 -1
- package/lib/combined.js +79 -13
- package/lib/combined.js.map +1 -1
- package/lib/content-search-worker.d.ts +3 -3
- package/lib/content-search-worker.js +11 -6
- package/lib/content-search-worker.js.map +1 -1
- package/lib/content-search.d.ts +81 -7
- package/lib/content-search.d.ts.map +1 -1
- package/lib/content-search.js +454 -36
- package/lib/content-search.js.map +1 -1
- package/lib/content.d.ts +20 -0
- package/lib/content.d.ts.map +1 -1
- package/lib/content.js +71 -21
- package/lib/content.js.map +1 -1
- package/lib/find.d.ts.map +1 -1
- package/lib/find.js +2 -1
- package/lib/find.js.map +1 -1
- package/lib/http.d.ts.map +1 -1
- package/lib/http.js +9 -2
- package/lib/http.js.map +1 -1
- package/package.json +1 -1
- package/src/combined.ts +97 -15
- package/src/content-search-worker.ts +15 -8
- package/src/content-search.ts +559 -36
- package/src/content.ts +81 -18
- package/src/find.ts +3 -2
- package/src/http.ts +11 -2
package/client.js
CHANGED
|
@@ -123,8 +123,39 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
123
123
|
background:var(--dsw-alias-bg-layer-2,rgba(30,32,38,.92));color:var(--dsw-alias-label-primary,#fff);
|
|
124
124
|
border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));box-shadow:0 8px 24px rgba(0,0,0,.25);
|
|
125
125
|
animation:dsh-locator-in 120ms ease-out}
|
|
126
|
-
::highlight(dsh-locator-find){background-color:
|
|
127
|
-
::highlight(dsh-locator-find-active){background-color:
|
|
126
|
+
::highlight(dsh-locator-find){background-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 28%,transparent);color:inherit}
|
|
127
|
+
::highlight(dsh-locator-find-active){background-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 50%,transparent);color:inherit;
|
|
128
|
+
text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:2px}
|
|
129
|
+
::highlight(dsh-locator-find-scope){background-color:color-mix(in srgb,var(--dsw-alias-bg-document-selection,rgba(90,120,255,.25)) 35%,transparent)}
|
|
130
|
+
.dsh-locator-findbar{position:fixed;z-index:70;display:flex;flex-direction:column;width:min(560px,calc(100vw - 24px));
|
|
131
|
+
border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-base,rgba(120,125,135,.28)));
|
|
132
|
+
border-radius:var(--dsw-radius-md,12px);background:var(--dsw-menu-surface-fill,var(--dsw-alias-bg-base,#fff));
|
|
133
|
+
color:var(--dsw-alias-label-primary,inherit);box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.24));
|
|
134
|
+
font:13px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif;animation:dsh-locator-in 120ms ease-out}
|
|
135
|
+
@media (prefers-reduced-motion:reduce){.dsh-locator-findbar{animation:none}}
|
|
136
|
+
.dsh-locator-findbar-main{display:flex;align-items:center;gap:6px;padding:6px 8px}
|
|
137
|
+
.dsh-locator-findbar-glyph{font-size:14px;opacity:.6;padding:0 2px}
|
|
138
|
+
.dsh-locator-findbar-input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:inherit;font:inherit;font-size:13px;padding:3px 0}
|
|
139
|
+
.dsh-locator-findbar-count{font-variant-numeric:tabular-nums;font-size:12px;opacity:.75;white-space:nowrap;min-width:52px;text-align:right}
|
|
140
|
+
.dsh-locator-findbar-btn{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;padding:0 5px;
|
|
141
|
+
border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:13px}
|
|
142
|
+
.dsh-locator-findbar-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(120,125,135,.14))}
|
|
143
|
+
.dsh-locator-findbar-btn:disabled{opacity:.4;cursor:default}
|
|
144
|
+
.dsh-locator-findbar-btn[aria-pressed=true]{background:var(--dsw-alias-interactive-bg-active,rgba(64,116,255,.18));
|
|
145
|
+
color:var(--dsw-alias-state-business-primary,#4f7cff)}
|
|
146
|
+
.dsh-locator-findbar-opts{display:flex;align-items:center;gap:8px;padding:5px 9px 7px;flex-wrap:wrap;
|
|
147
|
+
border-top:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-base,rgba(120,125,135,.16)));font-size:11.5px}
|
|
148
|
+
.dsh-locator-findbar-opts select{border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));border-radius:5px;
|
|
149
|
+
background:transparent;color:inherit;font:inherit;font-size:11.5px;padding:1px 3px}
|
|
150
|
+
.dsh-locator-findbar-label{display:inline-flex;align-items:center;gap:4px;opacity:.8;white-space:nowrap}
|
|
151
|
+
.dsh-locator-findbar-status{display:flex;align-items:center;gap:8px;padding:4px 10px 6px;font-size:11.5px;opacity:.75;flex-wrap:wrap}
|
|
152
|
+
.dsh-locator-findbar-status .grow{flex:1}
|
|
153
|
+
.dsh-locator-findbar-error{padding:4px 10px 6px;font-size:11.5px;color:var(--dsw-alias-state-error-primary,#d33);
|
|
154
|
+
background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d33) 9%,transparent)}
|
|
155
|
+
.dsh-locator-findbar-chip{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:999px;
|
|
156
|
+
background:var(--dsw-alias-bg-layer-2,rgba(120,125,135,.14));font-size:11px;white-space:nowrap}
|
|
157
|
+
.dsh-locator-findbar-chip button{border:0;background:transparent;color:inherit;cursor:pointer;padding:0 1px;font:inherit;font-size:12px}
|
|
158
|
+
.dsh-locator-findbar kbd{border:1px solid var(--dsw-alias-border-base,rgba(120,125,135,.3));border-radius:4px;padding:0 3px;font-size:10px;opacity:.6}
|
|
128
159
|
`
|
|
129
160
|
function ensureStyles() {
|
|
130
161
|
if (typeof document === 'undefined') return
|
|
@@ -177,6 +208,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
177
208
|
launcher: 'Local file search (Ctrl+Shift+F)',
|
|
178
209
|
justNow: 'just now', minutesAgo: '{n} min ago', hoursAgo: '{n} h ago',
|
|
179
210
|
phraseNotFound: 'No match in the conversation (it may not be loaded yet)',
|
|
211
|
+
findOpen: 'Find in conversation', findTitle: 'Find in conversation', findAria: 'Find in conversation',
|
|
212
|
+
findPlaceholder: 'Find in conversation…', findPlaceholderSelection: 'Find in selection…',
|
|
213
|
+
findPrev: 'Previous match (Shift+F3)', findNext: 'Next match (F3)', findClose: 'Close (Esc)',
|
|
214
|
+
findCase: 'Match case', findWhole: 'Whole word', findWrap: 'Wrap around', findReverse: 'Search backwards',
|
|
215
|
+
findMode: 'Mode', modeNormal: 'Normal', modeExtended: 'Extended', modeRegex: 'Regex',
|
|
216
|
+
findScope: 'In selection', findScopeChip: 'selection {n} chars', findSessionChip: 'this conversation',
|
|
217
|
+
findNoMatch: 'No matches', findEmpty: 'Type to find in this conversation',
|
|
218
|
+
findInvalidRegex: 'Invalid regular expression: {message}', findInvalidEscape: 'Invalid extended escape: {token}',
|
|
219
|
+
findTimeout: 'Regex timed out; simplify the expression', findWrappedTop: 'Wrapped to top',
|
|
220
|
+
findWrappedBottom: 'Wrapped to bottom', findNoWrapEnd: 'Reached the last match (wrap is off)',
|
|
221
|
+
findNoWrapStart: 'Reached the first match (wrap is off)', findOlder: 'Load earlier messages and retry',
|
|
222
|
+
findLoadingOlder: 'Loading earlier messages…', findLoadedOnly: 'loaded window only · earlier history not loaded',
|
|
223
|
+
findCapped: 'capped', findCopied: 'Match copied', findSelectionLost: 'Selection expired; searching the whole conversation',
|
|
224
|
+
findNoSession: 'Open a conversation first', findOptions: 'Options',
|
|
225
|
+
findExtendedHelp: 'Extended: \\n \\r \\t \\0 \\xHH \\\\',
|
|
226
|
+
panelTotal: 'selected {index}/{total}', panelRangeAll: 'All indexed', panelRangeSession: 'This conversation',
|
|
227
|
+
panelSessionHint: 'This scope searches indexed session messages only; press Ctrl+F for live in-conversation find',
|
|
228
|
+
panelRegexPaused: 'Content scope paused in regex mode', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
229
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Word', panelModeLabel: 'Mode',
|
|
180
230
|
},
|
|
181
231
|
zh: {
|
|
182
232
|
scopeContent: '内容', scopeFiles: '文件', scopeDirs: '目录',
|
|
@@ -202,6 +252,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
202
252
|
launcher: '本地文件搜索 (Ctrl+Shift+F)',
|
|
203
253
|
justNow: '刚刚', minutesAgo: '{n} 分钟前', hoursAgo: '{n} 小时前',
|
|
204
254
|
phraseNotFound: '会话里没有找到匹配内容(可能还没加载)',
|
|
255
|
+
findOpen: '在会话中查找', findTitle: '在会话中查找', findAria: '在会话中查找',
|
|
256
|
+
findPlaceholder: '在会话中查找…', findPlaceholderSelection: '在选区内查找…',
|
|
257
|
+
findPrev: '上一个匹配 (Shift+F3)', findNext: '下一个匹配 (F3)', findClose: '关闭 (Esc)',
|
|
258
|
+
findCase: '区分大小写', findWhole: '全词匹配', findWrap: '循环查找', findReverse: '反向查找',
|
|
259
|
+
findMode: '模式', modeNormal: '普通', modeExtended: '扩展', modeRegex: '正则',
|
|
260
|
+
findScope: '仅在选区内', findScopeChip: '选区 {n} 字', findSessionChip: '本会话',
|
|
261
|
+
findNoMatch: '没有匹配', findEmpty: '输入关键词后在此会话中查找',
|
|
262
|
+
findInvalidRegex: '正则无效:{message}', findInvalidEscape: '扩展转义无效:{token}',
|
|
263
|
+
findTimeout: '正则执行超时,请简化表达式', findWrappedTop: '已回到顶部',
|
|
264
|
+
findWrappedBottom: '已到底部,回到开头', findNoWrapEnd: '已到最后一个匹配(循环查找已关闭)',
|
|
265
|
+
findNoWrapStart: '已到第一个匹配(循环查找已关闭)', findOlder: '加载更早消息后重试',
|
|
266
|
+
findLoadingOlder: '正在加载更早消息…', findLoadedOnly: '仅已加载窗口 · 更早历史未加载',
|
|
267
|
+
findCapped: '已截断', findCopied: '已复制匹配', findSelectionLost: '选区已失效,已切回整个会话',
|
|
268
|
+
findNoSession: '请先打开一个会话', findOptions: '选项',
|
|
269
|
+
findExtendedHelp: '扩展:\\n \\r \\t \\0 \\xHH \\\\',
|
|
270
|
+
panelTotal: '已选 {index}/{total}', panelRangeAll: '全部索引', panelRangeSession: '当前会话',
|
|
271
|
+
panelSessionHint: '此范围只查已索引的会话消息;实时查找请按 Ctrl+F',
|
|
272
|
+
panelRegexPaused: '正则模式下内容范围暂停', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
273
|
+
panelCaseShort: 'Aa', panelWholeShort: '全词', panelModeLabel: '模式',
|
|
205
274
|
},
|
|
206
275
|
es: {
|
|
207
276
|
scopeContent: 'Contenido', scopeFiles: 'Archivos', scopeDirs: 'Carpetas',
|
|
@@ -227,6 +296,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
227
296
|
launcher: 'Búsqueda local (Ctrl+Shift+F)',
|
|
228
297
|
justNow: 'ahora', minutesAgo: 'hace {n} min', hoursAgo: 'hace {n} h',
|
|
229
298
|
phraseNotFound: 'No se encontró en la conversación (quizá no está cargada)',
|
|
299
|
+
findOpen: 'Buscar en la conversación', findTitle: 'Buscar en la conversación', findAria: 'Buscar en la conversación',
|
|
300
|
+
findPlaceholder: 'Buscar en la conversación…', findPlaceholderSelection: 'Buscar en la selección…',
|
|
301
|
+
findPrev: 'Coincidencia anterior (Shift+F3)', findNext: 'Siguiente coincidencia (F3)', findClose: 'Cerrar (Esc)',
|
|
302
|
+
findCase: 'Distinguir mayúsculas', findWhole: 'Palabra completa', findWrap: 'Continuar al inicio', findReverse: 'Buscar hacia atrás',
|
|
303
|
+
findMode: 'Modo', modeNormal: 'Normal', modeExtended: 'Extendido', modeRegex: 'Regex',
|
|
304
|
+
findScope: 'En la selección', findScopeChip: 'selección {n} caracteres', findSessionChip: 'esta conversación',
|
|
305
|
+
findNoMatch: 'Sin coincidencias', findEmpty: 'Escribe para buscar en esta conversación',
|
|
306
|
+
findInvalidRegex: 'Expresión regular no válida: {message}', findInvalidEscape: 'Escape extendido no válido: {token}',
|
|
307
|
+
findTimeout: 'La regex agotó el tiempo; simplifícala', findWrappedTop: 'Vuelto al inicio',
|
|
308
|
+
findWrappedBottom: 'Vuelto al final', findNoWrapEnd: 'Última coincidencia (continuación desactivada)',
|
|
309
|
+
findNoWrapStart: 'Primera coincidencia (continuación desactivada)', findOlder: 'Cargar mensajes anteriores y reintentar',
|
|
310
|
+
findLoadingOlder: 'Cargando mensajes anteriores…', findLoadedOnly: 'solo ventana cargada · historial anterior sin cargar',
|
|
311
|
+
findCapped: 'limitado', findCopied: 'Coincidencia copiada', findSelectionLost: 'La selección caducó; buscando en toda la conversación',
|
|
312
|
+
findNoSession: 'Abre primero una conversación', findOptions: 'Opciones',
|
|
313
|
+
findExtendedHelp: 'Extendido: \\n \\r \\t \\0 \\xHH \\\\',
|
|
314
|
+
panelTotal: 'seleccionado {index}/{total}', panelRangeAll: 'Todo el índice', panelRangeSession: 'Esta conversación',
|
|
315
|
+
panelSessionHint: 'Este ámbito solo busca mensajes indexados; pulsa Ctrl+F para buscar en vivo',
|
|
316
|
+
panelRegexPaused: 'Ámbito de contenido en pausa en modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
317
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Palabra', panelModeLabel: 'Modo',
|
|
230
318
|
},
|
|
231
319
|
pt: {
|
|
232
320
|
scopeContent: 'Conteúdo', scopeFiles: 'Arquivos', scopeDirs: 'Pastas',
|
|
@@ -252,6 +340,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
252
340
|
launcher: 'Busca local (Ctrl+Shift+F)',
|
|
253
341
|
justNow: 'agora', minutesAgo: 'há {n} min', hoursAgo: 'há {n} h',
|
|
254
342
|
phraseNotFound: 'Não encontrado na conversa (talvez ainda não carregada)',
|
|
343
|
+
findOpen: 'Buscar na conversa', findTitle: 'Buscar na conversa', findAria: 'Buscar na conversa',
|
|
344
|
+
findPlaceholder: 'Buscar na conversa…', findPlaceholderSelection: 'Buscar na seleção…',
|
|
345
|
+
findPrev: 'Ocorrência anterior (Shift+F3)', findNext: 'Próxima ocorrência (F3)', findClose: 'Fechar (Esc)',
|
|
346
|
+
findCase: 'Diferenciar maiúsculas', findWhole: 'Palavra inteira', findWrap: 'Continuar do início', findReverse: 'Buscar para trás',
|
|
347
|
+
findMode: 'Modo', modeNormal: 'Normal', modeExtended: 'Estendido', modeRegex: 'Regex',
|
|
348
|
+
findScope: 'Na seleção', findScopeChip: 'seleção {n} caracteres', findSessionChip: 'esta conversa',
|
|
349
|
+
findNoMatch: 'Sem resultados', findEmpty: 'Digite para buscar nesta conversa',
|
|
350
|
+
findInvalidRegex: 'Expressão regular inválida: {message}', findInvalidEscape: 'Escape estendido inválido: {token}',
|
|
351
|
+
findTimeout: 'A regex excedeu o tempo; simplifique', findWrappedTop: 'Voltou ao início',
|
|
352
|
+
findWrappedBottom: 'Voltou ao fim', findNoWrapEnd: 'Última ocorrência (continuação desativada)',
|
|
353
|
+
findNoWrapStart: 'Primeira ocorrência (continuação desativada)', findOlder: 'Carregar mensagens anteriores e tentar novamente',
|
|
354
|
+
findLoadingOlder: 'Carregando mensagens anteriores…', findLoadedOnly: 'apenas janela carregada · histórico anterior não carregado',
|
|
355
|
+
findCapped: 'limitado', findCopied: 'Ocorrência copiada', findSelectionLost: 'A seleção expirou; buscando em toda a conversa',
|
|
356
|
+
findNoSession: 'Abra uma conversa primeiro', findOptions: 'Opções',
|
|
357
|
+
findExtendedHelp: 'Estendido: \\n \\r \\t \\0 \\xHH \\\\',
|
|
358
|
+
panelTotal: 'selecionado {index}/{total}', panelRangeAll: 'Todo o índice', panelRangeSession: 'Esta conversa',
|
|
359
|
+
panelSessionHint: 'Este escopo busca apenas mensagens indexadas; use Ctrl+F para buscar ao vivo',
|
|
360
|
+
panelRegexPaused: 'Escopo de conteúdo pausado no modo regex', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
361
|
+
panelCaseShort: 'Aa', panelWholeShort: 'Palavra', panelModeLabel: 'Modo',
|
|
255
362
|
},
|
|
256
363
|
hi: {
|
|
257
364
|
scopeContent: 'सामग्री', scopeFiles: 'फ़ाइलें', scopeDirs: 'फ़ोल्डर',
|
|
@@ -277,6 +384,25 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
277
384
|
launcher: 'स्थानीय फ़ाइल खोज (Ctrl+Shift+F)',
|
|
278
385
|
justNow: 'अभी', minutesAgo: '{n} मिनट पहले', hoursAgo: '{n} घंटे पहले',
|
|
279
386
|
phraseNotFound: 'बातचीत में नहीं मिला (शायद अभी लोड नहीं हुआ)',
|
|
387
|
+
findOpen: 'बातचीत में खोजें', findTitle: 'बातचीत में खोजें', findAria: 'बातचीत में खोजें',
|
|
388
|
+
findPlaceholder: 'बातचीत में खोजें…', findPlaceholderSelection: 'चयन में खोजें…',
|
|
389
|
+
findPrev: 'पिछला मेल (Shift+F3)', findNext: 'अगला मेल (F3)', findClose: 'बंद करें (Esc)',
|
|
390
|
+
findCase: 'केस मिलाएँ', findWhole: 'पूरा शब्द', findWrap: 'चक्रीय खोज', findReverse: 'उल्टा खोजें',
|
|
391
|
+
findMode: 'मोड', modeNormal: 'सामान्य', modeExtended: 'विस्तृत', modeRegex: 'रेगेक्स',
|
|
392
|
+
findScope: 'चयन में', findScopeChip: 'चयन {n} अक्षर', findSessionChip: 'यह बातचीत',
|
|
393
|
+
findNoMatch: 'कोई मेल नहीं', findEmpty: 'इस बातचीत में खोजने के लिए लिखें',
|
|
394
|
+
findInvalidRegex: 'अमान्य रेगुलर एक्सप्रेशन: {message}', findInvalidEscape: 'अमान्य विस्तृत एस्केप: {token}',
|
|
395
|
+
findTimeout: 'रेगेक्स का समय समाप्त; सरल बनाएँ', findWrappedTop: 'शीर्ष पर लौटा',
|
|
396
|
+
findWrappedBottom: 'अंत पर लौटा', findNoWrapEnd: 'अंतिम मेल (चक्र बंद)',
|
|
397
|
+
findNoWrapStart: 'पहला मेल (चक्र बंद)', findOlder: 'पुराने संदेश लोड कर पुन: प्रयास',
|
|
398
|
+
findLoadingOlder: 'पुराने संदेश लोड हो रहे हैं…', findLoadedOnly: 'केवल लोडेड विंडो · पुराना इतिहास लोड नहीं',
|
|
399
|
+
findCapped: 'सीमित', findCopied: 'मेल कॉपी हुआ', findSelectionLost: 'चयन समाप्त; पूरी बातचीत में खोज',
|
|
400
|
+
findNoSession: 'पहले एक बातचीत खोलें', findOptions: 'विकल्प',
|
|
401
|
+
findExtendedHelp: 'विस्तृत: \\n \\r \\t \\0 \\xHH \\\\',
|
|
402
|
+
panelTotal: 'चयनित {index}/{total}', panelRangeAll: 'पूरा इंडेक्स', panelRangeSession: 'यह बातचीत',
|
|
403
|
+
panelSessionHint: 'यह क्षेत्र केवल इंडेक्स किए संदेश खोजता है; लाइव खोज के लिए Ctrl+F दबाएँ',
|
|
404
|
+
panelRegexPaused: 'रेगेक्स मोड में सामग्री क्षेत्र रुका है', panelCountBounded: '≥ {n}', panelCountExact: '{n}',
|
|
405
|
+
panelCaseShort: 'Aa', panelWholeShort: 'शब्द', panelModeLabel: 'मोड',
|
|
280
406
|
},
|
|
281
407
|
}
|
|
282
408
|
const LANG = (() => {
|
|
@@ -382,7 +508,8 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
382
508
|
const PREFS_KEY = 'dsh-locator.panel.v1'
|
|
383
509
|
const SORTS = ['mtime', 'name', 'size', 'path']
|
|
384
510
|
function readPrefs() {
|
|
385
|
-
const fallback = { scopes: { content: true, files: true, dirs: true }, sort: 'mtime', desc: true
|
|
511
|
+
const fallback = { scopes: { content: true, files: true, dirs: true }, sort: 'mtime', desc: true,
|
|
512
|
+
matchMode: 'normal', caseSensitive: false, wholeWord: false }
|
|
386
513
|
try {
|
|
387
514
|
const raw = window.localStorage.getItem(PREFS_KEY)
|
|
388
515
|
if (raw === null) return fallback
|
|
@@ -397,6 +524,9 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
397
524
|
},
|
|
398
525
|
sort: SORTS.includes(value.sort) ? value.sort : 'mtime',
|
|
399
526
|
desc: typeof value.desc === 'boolean' ? value.desc : true,
|
|
527
|
+
matchMode: ['normal', 'extended', 'regex'].includes(value.matchMode) ? value.matchMode : 'normal',
|
|
528
|
+
caseSensitive: typeof value.caseSensitive === 'boolean' ? value.caseSensitive : false,
|
|
529
|
+
wholeWord: typeof value.wholeWord === 'boolean' ? value.wholeWord : false,
|
|
400
530
|
}
|
|
401
531
|
} catch {
|
|
402
532
|
return fallback
|
|
@@ -424,37 +554,57 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
424
554
|
legacyTargets.clear()
|
|
425
555
|
}
|
|
426
556
|
|
|
427
|
-
|
|
557
|
+
const HL_SCOPE = 'dsh-locator-find-scope'
|
|
558
|
+
let highlightOwner = null
|
|
559
|
+
|
|
560
|
+
function clearFindHighlight(owner) {
|
|
561
|
+
if (owner && highlightOwner !== null && owner !== highlightOwner) return
|
|
562
|
+
highlightOwner = null
|
|
428
563
|
clearLegacyTargets()
|
|
429
564
|
if (!highlightsAvailable()) return
|
|
430
565
|
CSS.highlights.delete(HL_BASE)
|
|
431
566
|
CSS.highlights.delete(HL_ACTIVE)
|
|
567
|
+
CSS.highlights.delete(HL_SCOPE)
|
|
432
568
|
}
|
|
433
569
|
|
|
434
|
-
function setFindHighlight(ranges) {
|
|
570
|
+
function setFindHighlight(ranges, activeIndex, owner) {
|
|
571
|
+
clearFindHighlight()
|
|
572
|
+
if (ranges.length === 0) return
|
|
573
|
+
highlightOwner = owner || null
|
|
574
|
+
const active = ranges[Math.max(0, Math.min(ranges.length - 1, activeIndex || 0))]
|
|
435
575
|
if (!highlightsAvailable()) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (row !== null) { row.classList.add('dsh-locator-target'); legacyTargets.add(row) }
|
|
441
|
-
}
|
|
576
|
+
const node = active.startContainer
|
|
577
|
+
const element = node instanceof Element ? node : node.parentElement
|
|
578
|
+
const row = element === null ? null : element.closest('[data-chat-node-key]')
|
|
579
|
+
if (row !== null) { row.classList.add('dsh-locator-target'); legacyTargets.add(row) }
|
|
442
580
|
return
|
|
443
581
|
}
|
|
444
|
-
if (ranges.length === 0) return
|
|
445
582
|
CSS.highlights.set(HL_BASE, new Highlight(...ranges))
|
|
446
|
-
CSS.highlights.set(HL_ACTIVE, new Highlight(
|
|
583
|
+
CSS.highlights.set(HL_ACTIVE, new Highlight(active))
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function setFindScope(range) {
|
|
587
|
+
if (!highlightsAvailable()) return
|
|
588
|
+
CSS.highlights.delete(HL_SCOPE)
|
|
589
|
+
if (range !== null) CSS.highlights.set(HL_SCOPE, new Highlight(range))
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function isVisible(element) {
|
|
593
|
+
if (element.getClientRects().length === 0) return false
|
|
594
|
+
return getComputedStyle(element).visibility !== 'hidden'
|
|
447
595
|
}
|
|
448
596
|
|
|
449
597
|
function conversationScope(sessionId) {
|
|
450
598
|
const id = sessionId === undefined || sessionId === null ? '' : String(sessionId)
|
|
451
|
-
let
|
|
599
|
+
let visibleMain = null
|
|
600
|
+
let visibleEmbedded = null
|
|
452
601
|
for (const element of document.querySelectorAll('[data-conversation-content][data-conversation-session]')) {
|
|
453
602
|
if (id !== '' && element.getAttribute('data-conversation-session') !== id) continue
|
|
454
|
-
if (element
|
|
455
|
-
|
|
603
|
+
if (!isVisible(element)) continue
|
|
604
|
+
if (element.closest('[data-sidebar-chat]') === null) visibleMain = element
|
|
605
|
+
else visibleEmbedded = element
|
|
456
606
|
}
|
|
457
|
-
return
|
|
607
|
+
return visibleMain ?? visibleEmbedded
|
|
458
608
|
}
|
|
459
609
|
|
|
460
610
|
function chatFlow(scope) {
|
|
@@ -674,7 +824,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
674
824
|
let activeHighlight = null
|
|
675
825
|
|
|
676
826
|
function clearJumpHighlight() {
|
|
677
|
-
clearFindHighlight()
|
|
827
|
+
clearFindHighlight('jump')
|
|
678
828
|
if (activeHighlight !== null) {
|
|
679
829
|
const clear = activeHighlight
|
|
680
830
|
activeHighlight = null
|
|
@@ -728,7 +878,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
728
878
|
}
|
|
729
879
|
if (stale()) return false
|
|
730
880
|
if (target === null) { showNotice(t('phraseNotFound')); return false }
|
|
731
|
-
setFindHighlight(target.ranges)
|
|
881
|
+
setFindHighlight(target.ranges, 0, 'jump')
|
|
732
882
|
centerRange(target.ranges[0])
|
|
733
883
|
const start = target.ranges[0].startContainer
|
|
734
884
|
const chain = scrollChain(start instanceof Element ? start : start.parentElement)
|
|
@@ -746,9 +896,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
746
896
|
const fn = highlightCleanup.pop()
|
|
747
897
|
try { fn() } catch { /* ignore */ }
|
|
748
898
|
}
|
|
749
|
-
clearFindHighlight()
|
|
899
|
+
clearFindHighlight('jump')
|
|
750
900
|
}
|
|
751
|
-
|
|
901
|
+
// Cancelling must drop the scroll lock too, not only the paint: the lock
|
|
902
|
+
// owns timers and scroll/pointer listeners that would otherwise outlive
|
|
903
|
+
// the highlight.
|
|
904
|
+
activeHighlight = () => { unlock(); clearHighlight() }
|
|
752
905
|
if (scroller !== null) {
|
|
753
906
|
let writing = false
|
|
754
907
|
let expected = scroller.scrollTop
|
|
@@ -784,8 +937,13 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
784
937
|
document.addEventListener('keydown', onKey, true)
|
|
785
938
|
highlightCleanup.push(() => document.removeEventListener('keydown', onKey, true))
|
|
786
939
|
const sessionWatch = setInterval(() => {
|
|
787
|
-
|
|
788
|
-
if (
|
|
940
|
+
if (conversationScope(hit.sessionId) === null) { cancelJump(); return }
|
|
941
|
+
if (target.ranges[0].startContainer.isConnected) return
|
|
942
|
+
const next = finder()
|
|
943
|
+
if (next === null) { cancelJump(); return }
|
|
944
|
+
target = next
|
|
945
|
+
setFindHighlight(target.ranges, 0, 'jump')
|
|
946
|
+
centerRange(target.ranges[0])
|
|
789
947
|
}, 1_000)
|
|
790
948
|
highlightCleanup.push(() => clearInterval(sessionWatch))
|
|
791
949
|
showNotice(t('jumped'))
|
|
@@ -797,6 +955,640 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
797
955
|
const PAGE_NAME = 50
|
|
798
956
|
const PAGE_CONTENT = 30
|
|
799
957
|
|
|
958
|
+
// ── in-conversation find bar ────────────────────────────────────────────
|
|
959
|
+
const FIND_MAX_MATCHES = 2000
|
|
960
|
+
const FIND_MAX_CHARS = 4_000_000
|
|
961
|
+
const FIND_BLOCK_SELECTOR = 'p,li,pre,td,th,h1,h2,h3,h4,h5,h6,blockquote,dd,dt'
|
|
962
|
+
const FIND_EXCLUDED = '[hidden]:not([hidden="until-found"]),[data-chat-history],[data-text-interaction-surface],'
|
|
963
|
+
+ '[data-composer-seat],[data-text-interaction-exclude],script,style,button,input,textarea,select,'
|
|
964
|
+
+ '.sr-only,.katex-mathml,[aria-hidden="true"]:not(.katex-html)'
|
|
965
|
+
|
|
966
|
+
function createFindStore() {
|
|
967
|
+
const listeners = new Set()
|
|
968
|
+
let state = {
|
|
969
|
+
open: false, sessionId: '', scope: 'session', selection: null, query: '',
|
|
970
|
+
mode: 'normal', caseSensitive: false, wholeWord: false, wrap: true, direction: 1,
|
|
971
|
+
optionsOpen: false, count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '',
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
get: () => state,
|
|
975
|
+
subscribe(listener) {
|
|
976
|
+
listeners.add(listener)
|
|
977
|
+
return () => listeners.delete(listener)
|
|
978
|
+
},
|
|
979
|
+
set(patch) {
|
|
980
|
+
state = { ...state, ...patch }
|
|
981
|
+
for (const listener of listeners) listener()
|
|
982
|
+
},
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const findStore = createFindStore()
|
|
986
|
+
const findApi = { move: () => false, cancel: () => {}, focus: () => {} }
|
|
987
|
+
function useFindState() {
|
|
988
|
+
return useSyncExternalStore(findStore.subscribe, findStore.get, findStore.get)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function selectionSnapshot(scope) {
|
|
992
|
+
if (scope === null || typeof window.getSelection !== 'function') return null
|
|
993
|
+
const selection = window.getSelection()
|
|
994
|
+
if (selection === null || selection.rangeCount !== 1 || selection.isCollapsed) return null
|
|
995
|
+
const range = selection.getRangeAt(0).cloneRange()
|
|
996
|
+
if (!scope.contains(range.commonAncestorContainer)) return null
|
|
997
|
+
const startEl = range.startContainer instanceof Element ? range.startContainer : range.startContainer.parentElement
|
|
998
|
+
if (startEl !== null && startEl.closest('[data-composer-seat]') !== null) return null
|
|
999
|
+
const text = String(selection.toString() || '').replace(/\s+/g, ' ').trim()
|
|
1000
|
+
if (text === '') return null
|
|
1001
|
+
return { range, text: text.slice(0, 500), chars: text.length }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function openFind(options) {
|
|
1005
|
+
const opts = options || {}
|
|
1006
|
+
let scope = opts.sessionId ? conversationScope(opts.sessionId) : null
|
|
1007
|
+
if (scope === null) scope = conversationScope('')
|
|
1008
|
+
if (scope === null) { showNotice(t('findNoSession')); return false }
|
|
1009
|
+
const sessionId = scope.getAttribute('data-conversation-session') || opts.sessionId || ''
|
|
1010
|
+
const selection = opts.selection === false ? null : selectionSnapshot(scope)
|
|
1011
|
+
const useSelection = opts.selection === true && selection !== null
|
|
1012
|
+
findApi.cancel()
|
|
1013
|
+
cancelJump()
|
|
1014
|
+
if (store.isOpen()) store.set(false)
|
|
1015
|
+
findStore.set({
|
|
1016
|
+
open: true, sessionId, scope: useSelection ? 'selection' : 'session',
|
|
1017
|
+
selection: useSelection ? selection : null, query: '', mode: 'normal',
|
|
1018
|
+
caseSensitive: false, wholeWord: false, wrap: true, direction: 1,
|
|
1019
|
+
optionsOpen: false, count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '',
|
|
1020
|
+
})
|
|
1021
|
+
return true
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function closeFind() {
|
|
1025
|
+
if (!findStore.get().open) return
|
|
1026
|
+
findStore.set({ open: false, selection: null, query: '', error: '', hint: '', status: 'idle' })
|
|
1027
|
+
findApi.cancel()
|
|
1028
|
+
clearFindHighlight('find')
|
|
1029
|
+
setFindScope(null)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const FIND_WORKER_SRC = `
|
|
1033
|
+
self.onmessage = event => {
|
|
1034
|
+
const { id, pattern, flags, texts } = event.data
|
|
1035
|
+
try {
|
|
1036
|
+
const re = new RegExp(pattern, flags)
|
|
1037
|
+
const out = []
|
|
1038
|
+
for (let ti = 0; ti < texts.length; ti++) {
|
|
1039
|
+
const text = texts[ti]
|
|
1040
|
+
re.lastIndex = 0
|
|
1041
|
+
let match
|
|
1042
|
+
while ((match = re.exec(text)) !== null) {
|
|
1043
|
+
if (match[0].length === 0) { re.lastIndex++; continue }
|
|
1044
|
+
out.push([ti, match.index, match.index + match[0].length])
|
|
1045
|
+
if (out.length >= 2000) break
|
|
1046
|
+
}
|
|
1047
|
+
if (out.length >= 2000) break
|
|
1048
|
+
}
|
|
1049
|
+
self.postMessage({ id, ok: true, matches: out, capped: out.length >= 2000 })
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
self.postMessage({ id, ok: false, error: String(error && error.message || error) })
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
`
|
|
1055
|
+
|
|
1056
|
+
function scanRegexInWorker(texts, source, flags) {
|
|
1057
|
+
if (typeof Worker !== 'function' || typeof Blob !== 'function' || typeof URL.createObjectURL !== 'function') {
|
|
1058
|
+
return Promise.resolve(null)
|
|
1059
|
+
}
|
|
1060
|
+
return new Promise(resolve => {
|
|
1061
|
+
let worker = null
|
|
1062
|
+
let url = ''
|
|
1063
|
+
let done = false
|
|
1064
|
+
const finish = value => {
|
|
1065
|
+
if (done) return
|
|
1066
|
+
done = true
|
|
1067
|
+
clearTimeout(timer)
|
|
1068
|
+
try { if (worker !== null) worker.terminate() } catch { /* ignore */ }
|
|
1069
|
+
try { if (url !== '') URL.revokeObjectURL(url) } catch { /* ignore */ }
|
|
1070
|
+
resolve(value)
|
|
1071
|
+
}
|
|
1072
|
+
const timer = setTimeout(() => finish({ timeout: true }), 500)
|
|
1073
|
+
try {
|
|
1074
|
+
const blob = new Blob([FIND_WORKER_SRC], { type: 'text/javascript' })
|
|
1075
|
+
url = URL.createObjectURL(blob)
|
|
1076
|
+
worker = new Worker(url)
|
|
1077
|
+
worker.onmessage = event => finish(event.data)
|
|
1078
|
+
worker.onerror = () => finish(null)
|
|
1079
|
+
worker.postMessage({ id: 1, pattern: source, flags, texts })
|
|
1080
|
+
} catch {
|
|
1081
|
+
finish(null)
|
|
1082
|
+
}
|
|
1083
|
+
})
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function expandExtended(input) {
|
|
1087
|
+
let out = ''
|
|
1088
|
+
for (let index = 0; index < input.length; index++) {
|
|
1089
|
+
const char = input[index]
|
|
1090
|
+
if (char !== '\\') { out += char; continue }
|
|
1091
|
+
const next = input[index + 1]
|
|
1092
|
+
if (next === undefined) { out += '\\'; continue }
|
|
1093
|
+
index++
|
|
1094
|
+
if (next === 'n') out += '\n'
|
|
1095
|
+
else if (next === 'r') out += '\r'
|
|
1096
|
+
else if (next === 't') out += '\t'
|
|
1097
|
+
else if (next === '0') out += '\u0000'
|
|
1098
|
+
else if (next === '\\') out += '\\'
|
|
1099
|
+
else if (next === 'x') {
|
|
1100
|
+
const hex = input.slice(index + 1, index + 3)
|
|
1101
|
+
if (!/^[0-9a-fA-F]{2}$/u.test(hex)) return { error: '\\x' }
|
|
1102
|
+
out += String.fromCharCode(parseInt(hex, 16))
|
|
1103
|
+
index += 2
|
|
1104
|
+
} else if (next === 'u') {
|
|
1105
|
+
const hex = input.slice(index + 1, index + 5)
|
|
1106
|
+
if (!/^[0-9a-fA-F]{4}$/u.test(hex)) return { error: '\\u' }
|
|
1107
|
+
out += String.fromCharCode(parseInt(hex, 16))
|
|
1108
|
+
index += 4
|
|
1109
|
+
} else out += '\\' + next
|
|
1110
|
+
}
|
|
1111
|
+
return { value: out }
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function compileFind(query, mode, caseSensitive) {
|
|
1115
|
+
let source = String(query || '')
|
|
1116
|
+
if (mode === 'extended') {
|
|
1117
|
+
const expanded = expandExtended(source)
|
|
1118
|
+
if (expanded.error !== undefined) return { error: t('findInvalidEscape', { token: expanded.error }) }
|
|
1119
|
+
source = expanded.value
|
|
1120
|
+
}
|
|
1121
|
+
if (mode !== 'regex') source = escapeRegExp(source)
|
|
1122
|
+
const flags = 'g' + (caseSensitive ? '' : 'i')
|
|
1123
|
+
try {
|
|
1124
|
+
return { source, flags: /[\u{10000}-\u{10FFFF}]/u.test(source) ? flags + 'u' : flags, re: new RegExp(source, flags) }
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (mode === 'regex') {
|
|
1127
|
+
try { return { source, flags: flags + 'u', re: new RegExp(source, flags + 'u') } } catch { /* fall through */ }
|
|
1128
|
+
}
|
|
1129
|
+
return { error: t('findInvalidRegex', { message: String(error && error.message || error).slice(0, 140) }) }
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function isWordCharacter(char) {
|
|
1134
|
+
return typeof char === 'string' && char !== '' && /^[A-Za-z0-9_]$/u.test(char)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function wholeWordAt(text, start, end) {
|
|
1138
|
+
return !isWordCharacter(text[start - 1]) && !isWordCharacter(text[end])
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function collectFindGroups(root) {
|
|
1142
|
+
if (root === null) return []
|
|
1143
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
1144
|
+
const groups = new Map()
|
|
1145
|
+
let node = walker.nextNode()
|
|
1146
|
+
while (node !== null) {
|
|
1147
|
+
const parent = node.parentElement
|
|
1148
|
+
if (parent !== null && parent.closest(FIND_EXCLUDED) === null) {
|
|
1149
|
+
const value = node.nodeValue || ''
|
|
1150
|
+
if (value !== '') {
|
|
1151
|
+
const block = parent.closest(FIND_BLOCK_SELECTOR)
|
|
1152
|
+
|| parent.closest('[data-text-interaction-source]')
|
|
1153
|
+
|| parent.closest('[data-chat-node-key]')
|
|
1154
|
+
|| root
|
|
1155
|
+
let group = groups.get(block)
|
|
1156
|
+
if (group === undefined) { group = { block, nodes: [], text: '', starts: [] }; groups.set(block, group) }
|
|
1157
|
+
group.starts.push(group.text.length)
|
|
1158
|
+
group.nodes.push(node)
|
|
1159
|
+
group.text += value
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
node = walker.nextNode()
|
|
1163
|
+
}
|
|
1164
|
+
return [...groups.values()]
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function clipForGroup(group, range) {
|
|
1168
|
+
if (range === null) return null
|
|
1169
|
+
let start = null
|
|
1170
|
+
let end = null
|
|
1171
|
+
let position = 0
|
|
1172
|
+
for (const node of group.nodes) {
|
|
1173
|
+
const length = (node.nodeValue || '').length
|
|
1174
|
+
let nodeStart = 0
|
|
1175
|
+
let nodeEnd = length
|
|
1176
|
+
try {
|
|
1177
|
+
if (!range.intersectsNode(node)) { position += length; continue }
|
|
1178
|
+
} catch { position += length; continue }
|
|
1179
|
+
if (node === range.startContainer) nodeStart = range.startOffset
|
|
1180
|
+
if (node === range.endContainer) nodeEnd = range.endOffset
|
|
1181
|
+
if (nodeStart < nodeEnd) {
|
|
1182
|
+
if (start === null) start = position + nodeStart
|
|
1183
|
+
end = position + nodeEnd
|
|
1184
|
+
}
|
|
1185
|
+
position += length
|
|
1186
|
+
}
|
|
1187
|
+
if (start === null || end === null || start >= end) return null
|
|
1188
|
+
return [start, end]
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function rangeForGroupOffsets(group, start, end) {
|
|
1192
|
+
if (group.nodes.length === 0) return null
|
|
1193
|
+
const startOffset = offsetInGroup(group, start)
|
|
1194
|
+
const endOffset = offsetInGroup(group, end)
|
|
1195
|
+
const range = document.createRange()
|
|
1196
|
+
try {
|
|
1197
|
+
range.setStart(startOffset[0], startOffset[1])
|
|
1198
|
+
range.setEnd(endOffset[0], endOffset[1])
|
|
1199
|
+
return range
|
|
1200
|
+
} catch {
|
|
1201
|
+
return null
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function findHistoryButton(scope) {
|
|
1206
|
+
return scope === null ? null : scope.querySelector('[data-chat-history] button')
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function FindToggle(props) {
|
|
1210
|
+
const onClick = () => { openFind({ sessionId: props && props.sessionId ? props.sessionId : '', selection: false }) }
|
|
1211
|
+
return h('button', {
|
|
1212
|
+
className: 'dsh-locator-launch',
|
|
1213
|
+
title: t('findOpen') + ' (Ctrl+F)',
|
|
1214
|
+
'aria-label': t('findOpen'),
|
|
1215
|
+
onClick,
|
|
1216
|
+
}, '⌕')
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function FindBar() {
|
|
1220
|
+
const state = useFindState()
|
|
1221
|
+
const inputRef = useRef(null)
|
|
1222
|
+
const barRef = useRef(null)
|
|
1223
|
+
const scopeRef = useRef(null)
|
|
1224
|
+
const matchesRef = useRef([])
|
|
1225
|
+
const activeRef = useRef(-1)
|
|
1226
|
+
const sequenceRef = useRef(0)
|
|
1227
|
+
const debounceRef = useRef(0)
|
|
1228
|
+
const observerRef = useRef(null)
|
|
1229
|
+
const revealTimersRef = useRef([])
|
|
1230
|
+
const returnFocusRef = useRef(null)
|
|
1231
|
+
const [anchor, setAnchor] = useState(null)
|
|
1232
|
+
const composingRef = useRef(false)
|
|
1233
|
+
|
|
1234
|
+
const applyHighlight = useCallback((index) => {
|
|
1235
|
+
const matches = matchesRef.current
|
|
1236
|
+
if (matches.length === 0) { clearFindHighlight('find'); setFindScope(null); return }
|
|
1237
|
+
const safe = Math.max(0, Math.min(matches.length - 1, index))
|
|
1238
|
+
activeRef.current = safe
|
|
1239
|
+
setFindHighlight(matches.map(match => match.range), safe, 'find')
|
|
1240
|
+
setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null)
|
|
1241
|
+
}, [state.scope, state.selection])
|
|
1242
|
+
|
|
1243
|
+
const revealActive = useCallback(() => {
|
|
1244
|
+
const match = matchesRef.current[activeRef.current]
|
|
1245
|
+
if (!match || !match.range.startContainer.isConnected) return false
|
|
1246
|
+
expandHidden(match.range.startContainer)
|
|
1247
|
+
const run = () => {
|
|
1248
|
+
if (!match.range.startContainer.isConnected) return
|
|
1249
|
+
centerRange(match.range)
|
|
1250
|
+
}
|
|
1251
|
+
requestAnimationFrame(run)
|
|
1252
|
+
for (const delay of [80, 240]) {
|
|
1253
|
+
const timer = setTimeout(run, delay)
|
|
1254
|
+
revealTimersRef.current.push(timer)
|
|
1255
|
+
}
|
|
1256
|
+
return true
|
|
1257
|
+
}, [])
|
|
1258
|
+
|
|
1259
|
+
const move = useCallback(delta => {
|
|
1260
|
+
const matches = matchesRef.current
|
|
1261
|
+
if (matches.length === 0) return false
|
|
1262
|
+
const direction = delta === 0 ? (findStore.get().direction === -1 ? -1 : 1) : delta
|
|
1263
|
+
if (!findStore.get().wrap && (activeRef.current + direction >= matches.length || activeRef.current + direction < 0)) {
|
|
1264
|
+
findStore.set({ hint: direction > 0 ? t('findNoWrapEnd') : t('findNoWrapStart') })
|
|
1265
|
+
return false
|
|
1266
|
+
}
|
|
1267
|
+
let next = activeRef.current + direction
|
|
1268
|
+
if (next >= matches.length) next = 0
|
|
1269
|
+
if (next < 0) next = matches.length - 1
|
|
1270
|
+
const wrapped = (activeRef.current + direction >= matches.length) || (activeRef.current + direction < 0)
|
|
1271
|
+
activeRef.current = next
|
|
1272
|
+
applyHighlight(next)
|
|
1273
|
+
findStore.set({
|
|
1274
|
+
index: next,
|
|
1275
|
+
hint: wrapped ? (direction > 0 ? t('findWrappedTop') : t('findWrappedBottom')) : '',
|
|
1276
|
+
})
|
|
1277
|
+
revealActive()
|
|
1278
|
+
return true
|
|
1279
|
+
}, [applyHighlight, revealActive])
|
|
1280
|
+
|
|
1281
|
+
const runScan = useCallback(async (keepActive) => {
|
|
1282
|
+
const token = ++sequenceRef.current
|
|
1283
|
+
const scope = scopeRef.current || conversationScope(state.sessionId) || conversationScope('')
|
|
1284
|
+
if (scope === null) { return }
|
|
1285
|
+
scopeRef.current = scope
|
|
1286
|
+
if (state.query === '') {
|
|
1287
|
+
matchesRef.current = []
|
|
1288
|
+
activeRef.current = -1
|
|
1289
|
+
clearFindHighlight('find')
|
|
1290
|
+
setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null)
|
|
1291
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'idle', error: '', hint: '' })
|
|
1292
|
+
return
|
|
1293
|
+
}
|
|
1294
|
+
const compiled = compileFind(state.query, state.mode, state.caseSensitive)
|
|
1295
|
+
if (compiled.error !== undefined) {
|
|
1296
|
+
matchesRef.current = []
|
|
1297
|
+
activeRef.current = -1
|
|
1298
|
+
clearFindHighlight('find')
|
|
1299
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: compiled.error })
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
let selectionRange = null
|
|
1303
|
+
if (state.scope === 'selection' && state.selection) {
|
|
1304
|
+
selectionRange = state.selection.range
|
|
1305
|
+
if (!selectionRange.startContainer.isConnected) {
|
|
1306
|
+
findStore.set({ scope: 'session', selection: null, hint: t('findSelectionLost') })
|
|
1307
|
+
return
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const groups = collectFindGroups(scope)
|
|
1311
|
+
const texts = []
|
|
1312
|
+
const clips = []
|
|
1313
|
+
let chars = 0
|
|
1314
|
+
for (const group of groups) {
|
|
1315
|
+
const clip = selectionRange === null ? null : clipForGroup(group, selectionRange)
|
|
1316
|
+
if (selectionRange !== null && clip === null) { texts.push(null); clips.push(null); continue }
|
|
1317
|
+
const start = clip === null ? 0 : clip[0]
|
|
1318
|
+
const end = clip === null ? group.text.length : clip[1]
|
|
1319
|
+
const text = group.text.slice(start, end)
|
|
1320
|
+
chars += text.length
|
|
1321
|
+
if (chars > FIND_MAX_CHARS) { texts.push(null); clips.push(null); continue }
|
|
1322
|
+
texts.push(text)
|
|
1323
|
+
clips.push(clip === null ? [0, group.text.length] : clip)
|
|
1324
|
+
}
|
|
1325
|
+
let rawMatches = null
|
|
1326
|
+
let capped = false
|
|
1327
|
+
if (state.mode === 'regex') {
|
|
1328
|
+
try {
|
|
1329
|
+
const result = await scanRegexInWorker(texts.filter(text => text !== null), compiled.source, compiled.flags)
|
|
1330
|
+
if (token !== sequenceRef.current) return
|
|
1331
|
+
if (result !== null) {
|
|
1332
|
+
if (result.timeout === true) {
|
|
1333
|
+
matchesRef.current = []
|
|
1334
|
+
clearFindHighlight('find')
|
|
1335
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findTimeout') })
|
|
1336
|
+
return
|
|
1337
|
+
}
|
|
1338
|
+
if (result.ok === false) {
|
|
1339
|
+
matchesRef.current = []
|
|
1340
|
+
clearFindHighlight('find')
|
|
1341
|
+
findStore.set({ count: 0, index: -1, capped: false, status: 'error', error: t('findInvalidRegex', { message: String(result.error || '') }) })
|
|
1342
|
+
return
|
|
1343
|
+
}
|
|
1344
|
+
const compact = []
|
|
1345
|
+
let cursor = 0
|
|
1346
|
+
for (let groupIndex = 0; groupIndex < texts.length; groupIndex++) {
|
|
1347
|
+
if (texts[groupIndex] === null) continue
|
|
1348
|
+
compact.push(groupIndex)
|
|
1349
|
+
cursor++
|
|
1350
|
+
}
|
|
1351
|
+
rawMatches = []
|
|
1352
|
+
for (const item of result.matches) {
|
|
1353
|
+
const groupIndex = compact[item[0]]
|
|
1354
|
+
if (groupIndex === undefined) continue
|
|
1355
|
+
rawMatches.push({ groupIndex, start: item[1] + clips[groupIndex][0], end: item[2] + clips[groupIndex][0] })
|
|
1356
|
+
}
|
|
1357
|
+
capped = result.capped === true
|
|
1358
|
+
}
|
|
1359
|
+
} catch { /* fall back below */ }
|
|
1360
|
+
}
|
|
1361
|
+
if (rawMatches === null) {
|
|
1362
|
+
rawMatches = []
|
|
1363
|
+
const re = new RegExp(compiled.source, compiled.flags)
|
|
1364
|
+
const started = performance.now()
|
|
1365
|
+
for (let groupIndex = 0; groupIndex < texts.length; groupIndex++) {
|
|
1366
|
+
const text = texts[groupIndex]
|
|
1367
|
+
if (text === null) continue
|
|
1368
|
+
re.lastIndex = 0
|
|
1369
|
+
let match
|
|
1370
|
+
while ((match = re.exec(text)) !== null) {
|
|
1371
|
+
if (match[0].length === 0) { re.lastIndex++; continue }
|
|
1372
|
+
const start = clips[groupIndex][0] + match.index
|
|
1373
|
+
const end = start + match[0].length
|
|
1374
|
+
if (state.wholeWord && !wholeWordAt(groups[groupIndex].text, start, end)) continue
|
|
1375
|
+
rawMatches.push({ groupIndex, start, end })
|
|
1376
|
+
if (rawMatches.length >= FIND_MAX_MATCHES) break
|
|
1377
|
+
}
|
|
1378
|
+
if (rawMatches.length >= FIND_MAX_MATCHES) break
|
|
1379
|
+
if (performance.now() - started > 1500) { capped = true; break }
|
|
1380
|
+
}
|
|
1381
|
+
capped = capped || rawMatches.length >= FIND_MAX_MATCHES
|
|
1382
|
+
} else if (state.wholeWord) {
|
|
1383
|
+
rawMatches = rawMatches.filter(raw => {
|
|
1384
|
+
const group = groups[raw.groupIndex]
|
|
1385
|
+
return group !== undefined && wholeWordAt(group.text, raw.start, raw.end)
|
|
1386
|
+
})
|
|
1387
|
+
capped = capped || rawMatches.length >= FIND_MAX_MATCHES
|
|
1388
|
+
}
|
|
1389
|
+
if (token !== sequenceRef.current) return
|
|
1390
|
+
const matches = []
|
|
1391
|
+
for (const raw of rawMatches) {
|
|
1392
|
+
const group = groups[raw.groupIndex]
|
|
1393
|
+
if (group === undefined) continue
|
|
1394
|
+
const range = rangeForGroupOffsets(group, raw.start, raw.end)
|
|
1395
|
+
if (range === null) continue
|
|
1396
|
+
const row = group.block.closest('[data-chat-node-key]')
|
|
1397
|
+
matches.push({ range, rowKey: row === null ? '' : (row.getAttribute('data-chat-node-key') || '') })
|
|
1398
|
+
}
|
|
1399
|
+
if (token !== sequenceRef.current) return
|
|
1400
|
+
matchesRef.current = matches
|
|
1401
|
+
const next = matches.length === 0 ? -1 : (keepActive && activeRef.current >= 0 ? Math.min(activeRef.current, matches.length - 1) : 0)
|
|
1402
|
+
activeRef.current = next
|
|
1403
|
+
const history = findHistoryButton(scopeRef.current)
|
|
1404
|
+
findStore.set({
|
|
1405
|
+
count: matches.length,
|
|
1406
|
+
index: next,
|
|
1407
|
+
capped,
|
|
1408
|
+
status: matches.length === 0 ? 'none' : 'idle',
|
|
1409
|
+
error: '',
|
|
1410
|
+
hint: matches.length === 0 && history !== null && !history.disabled ? t('findLoadedOnly') : '',
|
|
1411
|
+
})
|
|
1412
|
+
if (next >= 0) { applyHighlight(next); revealActive() }
|
|
1413
|
+
else { clearFindHighlight('find'); setFindScope(state.scope === 'selection' && state.selection ? state.selection.range : null) }
|
|
1414
|
+
}, [applyHighlight, revealActive, state.caseSensitive, state.mode, state.query, state.scope, state.selection, state.sessionId, state.wholeWord])
|
|
1415
|
+
const runScanRef = useRef(runScan)
|
|
1416
|
+
runScanRef.current = runScan
|
|
1417
|
+
|
|
1418
|
+
useEffect(() => {
|
|
1419
|
+
if (!state.open) return undefined
|
|
1420
|
+
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
1421
|
+
const measure = () => {
|
|
1422
|
+
const scope = conversationScope(findStore.get().sessionId) || conversationScope('')
|
|
1423
|
+
if (scope === null) { closeFind(); return }
|
|
1424
|
+
scopeRef.current = scope
|
|
1425
|
+
const rect = scope.getBoundingClientRect()
|
|
1426
|
+
const width = Math.max(280, Math.min(560, rect.width - 24))
|
|
1427
|
+
setAnchor({ top: Math.max(8, rect.top + 8), right: Math.max(8, window.innerWidth - rect.right + 12), width })
|
|
1428
|
+
}
|
|
1429
|
+
measure()
|
|
1430
|
+
const timer = setTimeout(() => {
|
|
1431
|
+
if (inputRef.current !== null) { inputRef.current.focus(); inputRef.current.select() }
|
|
1432
|
+
}, 0)
|
|
1433
|
+
const onResize = () => measure()
|
|
1434
|
+
window.addEventListener('resize', onResize)
|
|
1435
|
+
let observer = null
|
|
1436
|
+
const schedule = () => {
|
|
1437
|
+
clearTimeout(debounceRef.current)
|
|
1438
|
+
debounceRef.current = setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 400)
|
|
1439
|
+
}
|
|
1440
|
+
const scope = scopeRef.current
|
|
1441
|
+
if (scope !== null) {
|
|
1442
|
+
try {
|
|
1443
|
+
observer = new MutationObserver(schedule)
|
|
1444
|
+
observer.observe(scope, { childList: true, subtree: true, characterData: true })
|
|
1445
|
+
} catch { /* observer unavailable */ }
|
|
1446
|
+
}
|
|
1447
|
+
return () => {
|
|
1448
|
+
clearTimeout(timer)
|
|
1449
|
+
window.removeEventListener('resize', onResize)
|
|
1450
|
+
clearTimeout(debounceRef.current)
|
|
1451
|
+
if (observer !== null) observer.disconnect()
|
|
1452
|
+
for (const id of revealTimersRef.current) clearTimeout(id)
|
|
1453
|
+
revealTimersRef.current = []
|
|
1454
|
+
sequenceRef.current++
|
|
1455
|
+
clearFindHighlight('find')
|
|
1456
|
+
setFindScope(null)
|
|
1457
|
+
const target = returnFocusRef.current
|
|
1458
|
+
if (target !== null && target.isConnected) target.focus({ preventScroll: true })
|
|
1459
|
+
}
|
|
1460
|
+
}, [state.open])
|
|
1461
|
+
|
|
1462
|
+
useEffect(() => {
|
|
1463
|
+
if (!state.open) return undefined
|
|
1464
|
+
const timer = setTimeout(() => { void runScan(false) }, 150)
|
|
1465
|
+
return () => clearTimeout(timer)
|
|
1466
|
+
}, [state.open, state.query, state.mode, state.caseSensitive, state.wholeWord, state.scope, state.sessionId, state.selection, runScan])
|
|
1467
|
+
|
|
1468
|
+
// Close when the conversation is switched, hidden, or the panel opens.
|
|
1469
|
+
useEffect(() => {
|
|
1470
|
+
if (!state.open) return undefined
|
|
1471
|
+
const timer = setInterval(() => {
|
|
1472
|
+
if (!findStore.get().open) return
|
|
1473
|
+
if (store.isOpen()) { closeFind(); return }
|
|
1474
|
+
if (conversationScope(findStore.get().sessionId) === null) closeFind()
|
|
1475
|
+
}, 800)
|
|
1476
|
+
return () => clearInterval(timer)
|
|
1477
|
+
}, [state.open])
|
|
1478
|
+
|
|
1479
|
+
useEffect(() => {
|
|
1480
|
+
findApi.move = delta => move(delta)
|
|
1481
|
+
findApi.cancel = () => { sequenceRef.current++ }
|
|
1482
|
+
findApi.focus = () => { if (inputRef.current !== null) { inputRef.current.focus(); inputRef.current.select() } }
|
|
1483
|
+
return () => { findApi.move = () => false; findApi.cancel = () => {}; findApi.focus = () => {} }
|
|
1484
|
+
}, [move])
|
|
1485
|
+
|
|
1486
|
+
if (!state.open) return null
|
|
1487
|
+
const onInput = event => { findStore.set({ query: event.target.value, error: '', hint: '' }) }
|
|
1488
|
+
const onKeyDown = event => {
|
|
1489
|
+
if (composingRef.current || event.isComposing) return
|
|
1490
|
+
if (event.key === 'Enter') { event.preventDefault(); move(event.shiftKey ? -1 : 0); return }
|
|
1491
|
+
if (event.key === 'Escape') { event.preventDefault(); closeFind(); return }
|
|
1492
|
+
if ((event.ctrlKey || event.metaKey) && event.shiftKey && String(event.key).toLowerCase() === 'c') {
|
|
1493
|
+
const match = matchesRef.current[activeRef.current]
|
|
1494
|
+
const text = match && match.range.startContainer.isConnected ? match.range.toString() : findStore.get().query
|
|
1495
|
+
if (text !== '') { event.preventDefault(); copyText(text).then(() => findStore.set({ hint: t('findCopied') })).catch(() => {}) }
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const pickMode = event => { findStore.set({ mode: event.target.value }) }
|
|
1499
|
+
const toggle = key => { findStore.set({ [key]: !findStore.get()[key] }) }
|
|
1500
|
+
const toggleScope = () => {
|
|
1501
|
+
if (state.scope === 'selection') findStore.set({ scope: 'session', selection: null, hint: '' })
|
|
1502
|
+
else {
|
|
1503
|
+
const scope = scopeRef.current
|
|
1504
|
+
const selection = scope === null ? null : selectionSnapshot(scope)
|
|
1505
|
+
if (selection === null) { findStore.set({ hint: t('findPlaceholderSelection') }); return }
|
|
1506
|
+
findStore.set({ scope: 'selection', selection })
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const countText = state.query === '' ? '—'
|
|
1510
|
+
: (state.count === 0 ? '0/0' : (state.index < 0 ? '…' : (state.index + 1) + '/' + state.count + (state.capped ? '+' : '')))
|
|
1511
|
+
const history = scopeRef.current === null ? null : findHistoryButton(scopeRef.current)
|
|
1512
|
+
const canLoadOlder = history !== null && !history.disabled
|
|
1513
|
+
const loadOlder = () => {
|
|
1514
|
+
if (scopeRef.current === null || !revealOlder(scopeRef.current)) return
|
|
1515
|
+
findStore.set({ hint: t('findLoadingOlder') })
|
|
1516
|
+
setTimeout(() => { if (findStore.get().open) void runScanRef.current(true) }, 700)
|
|
1517
|
+
}
|
|
1518
|
+
return h('div', {
|
|
1519
|
+
ref: barRef,
|
|
1520
|
+
className: 'dsh-locator-findbar',
|
|
1521
|
+
role: 'search',
|
|
1522
|
+
'aria-label': t('findAria'),
|
|
1523
|
+
style: anchor === null ? { top: 12, right: 12, width: 'min(560px,calc(100vw - 24px))' } : { top: anchor.top, right: anchor.right, width: anchor.width },
|
|
1524
|
+
},
|
|
1525
|
+
h('div', { className: 'dsh-locator-findbar-main' },
|
|
1526
|
+
h('span', { className: 'dsh-locator-findbar-glyph', 'aria-hidden': 'true' }, '⌕'),
|
|
1527
|
+
h('input', {
|
|
1528
|
+
ref: inputRef,
|
|
1529
|
+
className: 'dsh-locator-findbar-input',
|
|
1530
|
+
value: state.query,
|
|
1531
|
+
placeholder: state.scope === 'selection' ? t('findPlaceholderSelection') : t('findPlaceholder'),
|
|
1532
|
+
'aria-label': t('findAria'),
|
|
1533
|
+
'aria-invalid': state.error !== '' ? 'true' : undefined,
|
|
1534
|
+
spellCheck: false,
|
|
1535
|
+
autoComplete: 'off',
|
|
1536
|
+
onInput,
|
|
1537
|
+
onKeyDown,
|
|
1538
|
+
onCompositionStart: () => { composingRef.current = true },
|
|
1539
|
+
onCompositionEnd: () => { composingRef.current = false; void runScan(false) },
|
|
1540
|
+
}),
|
|
1541
|
+
h('span', { className: 'dsh-locator-findbar-count', 'aria-hidden': 'true' }, countText),
|
|
1542
|
+
h('span', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, role: 'status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
1543
|
+
state.error !== '' ? state.error
|
|
1544
|
+
: state.query === '' ? ''
|
|
1545
|
+
: state.count === 0 ? t('findNoMatch')
|
|
1546
|
+
: (state.index + 1) + '/' + state.count),
|
|
1547
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findPrev'), 'aria-label': t('findPrev'),
|
|
1548
|
+
disabled: state.count === 0, onMouseDown: event => event.preventDefault(), onClick: () => move(-1) }, '↑'),
|
|
1549
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findNext'), 'aria-label': t('findNext'),
|
|
1550
|
+
disabled: state.count === 0, onMouseDown: event => event.preventDefault(), onClick: () => move(1) }, '↓'),
|
|
1551
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findClose'), 'aria-label': t('findClose'),
|
|
1552
|
+
onClick: () => closeFind() }, '×'),
|
|
1553
|
+
),
|
|
1554
|
+
h('div', { className: 'dsh-locator-findbar-opts' },
|
|
1555
|
+
h('label', { className: 'dsh-locator-findbar-label' }, t('findMode'),
|
|
1556
|
+
h('select', { value: state.mode, onChange: pickMode, 'aria-label': t('findMode') },
|
|
1557
|
+
h('option', { value: 'normal' }, t('modeNormal')),
|
|
1558
|
+
h('option', { value: 'extended' }, t('modeExtended')),
|
|
1559
|
+
h('option', { value: 'regex' }, t('modeRegex')),
|
|
1560
|
+
)),
|
|
1561
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findCase'), 'aria-label': t('findCase'),
|
|
1562
|
+
'aria-pressed': state.caseSensitive ? 'true' : 'false', onClick: () => toggle('caseSensitive') }, t('panelCaseShort')),
|
|
1563
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findWhole'), 'aria-label': t('findWhole'),
|
|
1564
|
+
'aria-pressed': state.wholeWord ? 'true' : 'false', onClick: () => toggle('wholeWord') }, t('panelWholeShort')),
|
|
1565
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findWrap'), 'aria-label': t('findWrap'),
|
|
1566
|
+
'aria-pressed': state.wrap ? 'true' : 'false', onClick: () => toggle('wrap') }, '↻'),
|
|
1567
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findReverse'), 'aria-label': t('findReverse'),
|
|
1568
|
+
'aria-pressed': state.direction === -1 ? 'true' : 'false', onClick: () => findStore.set({ direction: state.direction === -1 ? 1 : -1 }) }, '⇅'),
|
|
1569
|
+
h('button', { className: 'dsh-locator-findbar-btn', title: t('findScope'), 'aria-label': t('findScope'),
|
|
1570
|
+
'aria-pressed': state.scope === 'selection' ? 'true' : 'false', onClick: toggleScope }, '⛶'),
|
|
1571
|
+
state.scope === 'selection' && state.selection
|
|
1572
|
+
? h('span', { className: 'dsh-locator-findbar-chip' }, t('findScopeChip', { n: state.selection.chars }),
|
|
1573
|
+
h('button', { onClick: () => findStore.set({ scope: 'session', selection: null, hint: '' }), 'aria-label': t('findClose') }, '×'))
|
|
1574
|
+
: h('span', { className: 'dsh-locator-findbar-chip' }, t('findSessionChip')),
|
|
1575
|
+
state.mode === 'extended' ? h('span', { className: 'dsh-locator-findbar-label', style: { opacity: .55 } }, t('findExtendedHelp')) : null,
|
|
1576
|
+
),
|
|
1577
|
+
state.error !== '' ? h('div', { className: 'dsh-locator-findbar-error', role: 'alert' }, state.error) : null,
|
|
1578
|
+
h('div', { className: 'dsh-locator-findbar-status' },
|
|
1579
|
+
h('span', null, state.query === '' ? t('findEmpty') : state.count === 0 ? t('findNoMatch') : (state.capped ? t('findCapped') : '')),
|
|
1580
|
+
state.hint !== '' ? h('span', null, state.hint) : null,
|
|
1581
|
+
h('span', { className: 'grow' }),
|
|
1582
|
+
canLoadOlder && state.query !== '' && state.count === 0
|
|
1583
|
+
? h('button', { className: 'dsh-locator-dirbtn', onClick: loadOlder }, t('findOlder'))
|
|
1584
|
+
: null,
|
|
1585
|
+
h('kbd', null, 'Enter'), h('span', null, '↓'),
|
|
1586
|
+
h('kbd', null, 'Shift+Enter'), h('span', null, '↑'),
|
|
1587
|
+
h('kbd', null, 'Esc'),
|
|
1588
|
+
),
|
|
1589
|
+
)
|
|
1590
|
+
}
|
|
1591
|
+
|
|
800
1592
|
function Panel() {
|
|
801
1593
|
const open = useOpen()
|
|
802
1594
|
const prefs = useMemo(readPrefs, [])
|
|
@@ -810,6 +1602,10 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
810
1602
|
const [took, setTook] = useState(0)
|
|
811
1603
|
const [status, setStatus] = useState(null)
|
|
812
1604
|
const [availableContent, setAvailableContent] = useState(true)
|
|
1605
|
+
const [matchMode, setMatchMode] = useState(prefs.matchMode || 'normal')
|
|
1606
|
+
const [caseSensitive, setCaseSensitive] = useState(prefs.caseSensitive === true)
|
|
1607
|
+
const [wholeWord, setWholeWord] = useState(prefs.wholeWord === true)
|
|
1608
|
+
const [sessionOnly, setSessionOnly] = useState(false)
|
|
813
1609
|
const [selected, setSelected] = useState(0)
|
|
814
1610
|
const [toast, setToast] = useState('')
|
|
815
1611
|
const [refreshTick, setRefreshTick] = useState(0)
|
|
@@ -819,14 +1615,20 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
819
1615
|
const listRef = useRef(null)
|
|
820
1616
|
|
|
821
1617
|
const close = useCallback(() => store.set(false), [])
|
|
822
|
-
const
|
|
1618
|
+
const activeScope = conversationScope('')
|
|
1619
|
+
const activeSessionId = activeScope === null ? '' : (activeScope.getAttribute('data-conversation-session') || '')
|
|
1620
|
+
const effectiveScopes = sessionOnly
|
|
1621
|
+
? { content: scopes.content, files: false, dirs: false }
|
|
1622
|
+
: { content: scopes.content, files: scopes.files, dirs: scopes.dirs }
|
|
1623
|
+
const anyScope = effectiveScopes.content || effectiveScopes.files || effectiveScopes.dirs
|
|
823
1624
|
|
|
824
1625
|
useEffect(() => {
|
|
825
|
-
writePrefs({ scopes, sort, desc })
|
|
826
|
-
}, [scopes, sort, desc])
|
|
1626
|
+
writePrefs({ scopes, sort, desc, matchMode, caseSensitive, wholeWord })
|
|
1627
|
+
}, [scopes, sort, desc, matchMode, caseSensitive, wholeWord])
|
|
827
1628
|
|
|
828
1629
|
useEffect(() => {
|
|
829
1630
|
if (!open) return undefined
|
|
1631
|
+
closeFind()
|
|
830
1632
|
const previous = document.activeElement
|
|
831
1633
|
setError('')
|
|
832
1634
|
const timer = setTimeout(() => {
|
|
@@ -879,13 +1681,18 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
879
1681
|
setError('')
|
|
880
1682
|
post('/dsh-locator/combined', {
|
|
881
1683
|
query,
|
|
882
|
-
content:
|
|
883
|
-
files:
|
|
884
|
-
dirs:
|
|
1684
|
+
content: effectiveScopes.content,
|
|
1685
|
+
files: effectiveScopes.files,
|
|
1686
|
+
dirs: effectiveScopes.dirs,
|
|
885
1687
|
limit: PAGE_NAME,
|
|
886
1688
|
contentLimit: PAGE_CONTENT,
|
|
887
1689
|
sort,
|
|
888
1690
|
desc,
|
|
1691
|
+
matchMode,
|
|
1692
|
+
caseSensitive,
|
|
1693
|
+
wholeWord,
|
|
1694
|
+
sessionId: sessionOnly ? activeSessionId : '',
|
|
1695
|
+
count: matchMode === 'regex' ? 'none' : 'exact',
|
|
889
1696
|
}, controller.signal).then(data => {
|
|
890
1697
|
if (controller.signal.aborted) return
|
|
891
1698
|
setGroups({
|
|
@@ -903,7 +1710,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
903
1710
|
})
|
|
904
1711
|
}, 140)
|
|
905
1712
|
return () => clearTimeout(timer)
|
|
906
|
-
}, [open, query, sort, desc, scopes.content, scopes.files, scopes.dirs, refreshTick, anyScope])
|
|
1713
|
+
}, [open, query, sort, desc, scopes.content, scopes.files, scopes.dirs, matchMode, caseSensitive, wholeWord, sessionOnly, activeSessionId, refreshTick, anyScope, effectiveScopes.content, effectiveScopes.files, effectiveScopes.dirs])
|
|
907
1714
|
|
|
908
1715
|
const contentBuilding = groups !== null && groups.content !== undefined && groups.content.building === true
|
|
909
1716
|
useEffect(() => {
|
|
@@ -916,17 +1723,23 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
916
1723
|
if (groups === null) return []
|
|
917
1724
|
const out = []
|
|
918
1725
|
for (const group of ['content', 'files', 'dirs']) {
|
|
919
|
-
if (!
|
|
1726
|
+
if (!effectiveScopes[group]) continue
|
|
920
1727
|
const hits = groups[group] && Array.isArray(groups[group].hits) ? groups[group].hits : []
|
|
921
1728
|
for (const hit of hits) out.push({ group, hit })
|
|
922
1729
|
}
|
|
923
1730
|
return out
|
|
924
|
-
}, [groups,
|
|
1731
|
+
}, [groups, effectiveScopes.content, effectiveScopes.files, effectiveScopes.dirs])
|
|
925
1732
|
|
|
926
1733
|
useEffect(() => {
|
|
927
1734
|
setSelected(value => Math.min(Math.max(0, value), Math.max(0, rows.length - 1)))
|
|
928
1735
|
}, [rows.length])
|
|
929
1736
|
|
|
1737
|
+
useEffect(() => {
|
|
1738
|
+
if (!open) return
|
|
1739
|
+
const row = document.getElementById('dsh-locator-opt-' + selected)
|
|
1740
|
+
if (row !== null) row.scrollIntoView({ block: 'nearest' })
|
|
1741
|
+
}, [open, selected, rows.length])
|
|
1742
|
+
|
|
930
1743
|
const notify = useCallback(message => {
|
|
931
1744
|
setToast(message)
|
|
932
1745
|
setTimeout(() => setToast(''), 1600)
|
|
@@ -940,12 +1753,12 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
940
1753
|
const reveal = useCallback(hit => {
|
|
941
1754
|
if (!hit || loading) return
|
|
942
1755
|
post('/dsh-locator/reveal', { path: hit.path }).then(() => notify(t('revealed'))).catch(() => copy(hit))
|
|
943
|
-
}, [copy, notify])
|
|
1756
|
+
}, [copy, loading, notify])
|
|
944
1757
|
|
|
945
1758
|
const openFile = useCallback(hit => {
|
|
946
1759
|
if (!hit || hit.dir || loading) return
|
|
947
1760
|
post('/dsh-locator/open', { path: hit.path }).then(() => notify(t('opened'))).catch(() => copy(hit))
|
|
948
|
-
}, [copy, notify])
|
|
1761
|
+
}, [copy, loading, notify])
|
|
949
1762
|
|
|
950
1763
|
const activate = useCallback(hit => {
|
|
951
1764
|
if (!hit || loading) return
|
|
@@ -955,17 +1768,17 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
955
1768
|
return
|
|
956
1769
|
}
|
|
957
1770
|
reveal(hit)
|
|
958
|
-
}, [close, notify, query, reveal])
|
|
1771
|
+
}, [close, loading, notify, query, reveal])
|
|
959
1772
|
|
|
960
1773
|
const insert = useCallback(hit => {
|
|
961
|
-
if (!hit || hit.kind === 'session') return
|
|
1774
|
+
if (!hit || hit.kind === 'session' || loading) return
|
|
962
1775
|
const mention = '@"' + hit.path.replace(/"/g, '') + '" '
|
|
963
1776
|
close()
|
|
964
1777
|
setTimeout(() => {
|
|
965
1778
|
if (insertIntoComposer(mention)) notify(t('inserted'))
|
|
966
1779
|
else copyText(mention).then(() => notify(t('insertFailed'))).catch(() => {})
|
|
967
1780
|
}, 80)
|
|
968
|
-
}, [close, notify])
|
|
1781
|
+
}, [close, loading, notify])
|
|
969
1782
|
|
|
970
1783
|
const toggleScope = useCallback(key => {
|
|
971
1784
|
if (key === 'content' && !availableContent) return
|
|
@@ -1001,6 +1814,11 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1001
1814
|
toggleScope(scopeDigit === '1' ? 'content' : scopeDigit === '2' ? 'files' : 'dirs')
|
|
1002
1815
|
return
|
|
1003
1816
|
}
|
|
1817
|
+
if (event.key === 'F3') {
|
|
1818
|
+
event.preventDefault()
|
|
1819
|
+
if (rows.length > 0) setSelected(value => (value + (event.shiftKey ? -1 : 1) + rows.length) % rows.length)
|
|
1820
|
+
return
|
|
1821
|
+
}
|
|
1004
1822
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1005
1823
|
event.preventDefault()
|
|
1006
1824
|
setSelected(value => Math.max(0, Math.min(rows.length - 1, value + (event.key === 'ArrowDown' ? 1 : -1))))
|
|
@@ -1056,23 +1874,35 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1056
1874
|
const content = groups !== null ? groups.content : undefined
|
|
1057
1875
|
const files = groups !== null ? groups.files : undefined
|
|
1058
1876
|
const dirs = groups !== null ? groups.dirs : undefined
|
|
1877
|
+
const countLabel = (group, fallback) => {
|
|
1878
|
+
const count = group && group.count
|
|
1879
|
+
if (count && typeof count.value === 'number' && Number.isFinite(count.value)) {
|
|
1880
|
+
return (count.kind === 'bounded' ? '≥ ' : '') + String(count.value)
|
|
1881
|
+
}
|
|
1882
|
+
return String(fallback)
|
|
1883
|
+
}
|
|
1059
1884
|
const contentCount = content === undefined ? 0 : (content.returned ?? 0)
|
|
1060
1885
|
const filesCount = files === undefined ? 0 : (files.returned ?? files.hits?.length ?? 0)
|
|
1061
1886
|
const dirsCount = dirs === undefined ? 0 : (dirs.returned ?? dirs.hits?.length ?? 0)
|
|
1887
|
+
const contentCountText = content === undefined ? '' : countLabel(content, contentCount)
|
|
1888
|
+
const filesCountText = files === undefined ? '' : countLabel(files, filesCount)
|
|
1889
|
+
const dirsCountText = dirs === undefined ? '' : countLabel(dirs, dirsCount)
|
|
1062
1890
|
const totalRows = rows.length
|
|
1063
1891
|
|
|
1064
1892
|
const scopeBox = (key, label, count) => h('label', {
|
|
1065
|
-
className: 'dsh-locator-check' + (key === 'content' && !availableContent ? ' disabled' : '')
|
|
1066
|
-
|
|
1893
|
+
className: 'dsh-locator-check' + (key === 'content' && !availableContent ? ' disabled' : '')
|
|
1894
|
+
+ (sessionOnly && key !== 'content' ? ' disabled' : ''),
|
|
1895
|
+
title: key === 'content' && !availableContent ? t('contentDisabled')
|
|
1896
|
+
: sessionOnly && key !== 'content' ? t('panelSessionHint') : label,
|
|
1067
1897
|
},
|
|
1068
1898
|
h('input', {
|
|
1069
1899
|
type: 'checkbox',
|
|
1070
1900
|
checked: scopes[key],
|
|
1071
|
-
disabled: key === 'content' && !availableContent,
|
|
1901
|
+
disabled: (key === 'content' && !availableContent) || (sessionOnly && key !== 'content'),
|
|
1072
1902
|
onChange: () => toggleScope(key),
|
|
1073
1903
|
}),
|
|
1074
1904
|
label,
|
|
1075
|
-
count
|
|
1905
|
+
count ? h('span', { className: 'dsh-locator-count' }, count) : null,
|
|
1076
1906
|
)
|
|
1077
1907
|
|
|
1078
1908
|
const groupHead = (key, label, count, note) => h('div', {
|
|
@@ -1124,23 +1954,26 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1124
1954
|
const hits = Array.isArray(group.hits) ? group.hits : []
|
|
1125
1955
|
const count = typeof group.returned === 'number' ? group.returned : hits.length
|
|
1126
1956
|
const note = key === 'content'
|
|
1127
|
-
? (
|
|
1128
|
-
:
|
|
1957
|
+
? (matchMode === 'regex' ? t('panelRegexPaused')
|
|
1958
|
+
: contentBuilding ? t('contentBuilding', { files: content?.files ?? 0, messages: content?.messages ?? 0 })
|
|
1959
|
+
: (content?.capped ? t('contentCapped', { n: count }) : (query.trim() !== '' ? t('relevance') : null)))
|
|
1129
1960
|
: (group.deduped > 0 ? t('deduped', { n: group.deduped }) : (group.truncated ? t('showing', { n: hits.length }) : null))
|
|
1130
1961
|
if (typeof group.error === 'string' && group.error !== '') {
|
|
1962
|
+
const message = group.error === 'content-regex-not-supported' ? t('panelRegexPaused') : group.error
|
|
1131
1963
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1132
1964
|
groupHead(key, label, count, null),
|
|
1133
|
-
h('div', { className: 'dsh-locator-groupempty', role: 'alert' }, t('errorPrefix') +
|
|
1965
|
+
h('div', { className: 'dsh-locator-groupempty', role: 'alert' }, t('errorPrefix') + message,
|
|
1134
1966
|
h('button', { className: 'dsh-locator-dirbtn dsh-locator-retry', onClick: () => setRefreshTick(v => v + 1) }, t('retry'))),
|
|
1135
1967
|
)
|
|
1136
1968
|
}
|
|
1137
1969
|
return h('div', { className: 'dsh-locator-group', role: 'group', 'aria-labelledby': 'dsh-locator-group-' + key },
|
|
1138
|
-
groupHead(key, label, count, note),
|
|
1970
|
+
groupHead(key, label, countLabel(group, count), note),
|
|
1139
1971
|
extra === undefined ? null : extra,
|
|
1140
1972
|
hits.length === 0
|
|
1141
1973
|
? h('div', { className: 'dsh-locator-groupempty' },
|
|
1142
1974
|
key === 'content' && scopes.content
|
|
1143
|
-
? (query.trim().length
|
|
1975
|
+
? (sessionOnly && query.trim().length >= 2 ? t('panelSessionHint')
|
|
1976
|
+
: query.trim().length < 2 ? t('contentShort') : (contentBuilding ? t('loading') : t('emptyResults')))
|
|
1144
1977
|
: (query.trim() === '' ? t('emptyQuery') : t('emptyResults')))
|
|
1145
1978
|
: hits.map(hit => { index++; return rowFor(key, hit, index) }),
|
|
1146
1979
|
)
|
|
@@ -1174,14 +2007,32 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1174
2007
|
'aria-autocomplete': 'list',
|
|
1175
2008
|
'aria-activedescendant': totalRows > 0 ? 'dsh-locator-opt-' + selected : undefined,
|
|
1176
2009
|
}),
|
|
2010
|
+
h('span', { className: 'dsh-locator-count', 'aria-hidden': 'true' },
|
|
2011
|
+
loading ? t('loading') : totalRows > 0 ? t('panelTotal', { index: selected + 1, total: totalRows }) : ''),
|
|
1177
2012
|
h('span', { className: 'dsh-locator-kbd' }, 'Ctrl+Shift+F'),
|
|
1178
2013
|
h('button', { className: 'dsh-locator-close', onClick: close, title: t('closeTitle'), 'aria-label': t('closeLabel') }, '×'),
|
|
1179
2014
|
),
|
|
1180
2015
|
h('div', { className: 'dsh-locator-bar' },
|
|
1181
|
-
scopeBox('content', t('scopeContent'),
|
|
1182
|
-
scopeBox('files', t('scopeFiles'),
|
|
1183
|
-
scopeBox('dirs', t('scopeDirs'),
|
|
2016
|
+
scopeBox('content', t('scopeContent'), contentCountText),
|
|
2017
|
+
scopeBox('files', t('scopeFiles'), filesCountText),
|
|
2018
|
+
scopeBox('dirs', t('scopeDirs'), dirsCountText),
|
|
1184
2019
|
h('span', { className: 'grow' }),
|
|
2020
|
+
h('label', { className: 'dsh-locator-check' + (activeSessionId === '' ? ' disabled' : ''),
|
|
2021
|
+
title: activeSessionId === '' ? t('findNoSession') : t('panelSessionHint') },
|
|
2022
|
+
h('input', { type: 'checkbox', checked: sessionOnly, disabled: activeSessionId === '',
|
|
2023
|
+
onChange: () => setSessionOnly(value => !value) }),
|
|
2024
|
+
t('panelRangeSession')),
|
|
2025
|
+
h('label', { className: 'dsh-locator-check' }, t('panelModeLabel'),
|
|
2026
|
+
h('select', { className: 'dsh-locator-sort', value: matchMode,
|
|
2027
|
+
onChange: event => setMatchMode(['normal', 'extended', 'regex'].includes(event.target.value) ? event.target.value : 'normal') },
|
|
2028
|
+
h('option', { value: 'normal' }, t('modeNormal')),
|
|
2029
|
+
h('option', { value: 'extended' }, t('modeExtended')),
|
|
2030
|
+
h('option', { value: 'regex' }, t('modeRegex')),
|
|
2031
|
+
)),
|
|
2032
|
+
h('button', { className: 'dsh-locator-dirbtn', title: t('findCase'),
|
|
2033
|
+
'aria-pressed': caseSensitive ? 'true' : 'false', onClick: () => setCaseSensitive(value => !value) }, t('panelCaseShort')),
|
|
2034
|
+
h('button', { className: 'dsh-locator-dirbtn', title: t('findWhole'),
|
|
2035
|
+
'aria-pressed': wholeWord ? 'true' : 'false', onClick: () => setWholeWord(value => !value) }, t('panelWholeShort')),
|
|
1185
2036
|
h('label', { className: 'dsh-locator-check' }, t('sortLabel'),
|
|
1186
2037
|
h('select', {
|
|
1187
2038
|
className: 'dsh-locator-sort',
|
|
@@ -1209,6 +2060,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1209
2060
|
roots: (status.roots || []).length,
|
|
1210
2061
|
})),
|
|
1211
2062
|
),
|
|
2063
|
+
sessionOnly ? h('div', { className: 'dsh-locator-groupempty' }, t('panelSessionHint')) : null,
|
|
1212
2064
|
h('div', { id: 'dsh-locator-results', className: 'dsh-locator-scroll', ref: listRef, role: 'listbox', 'aria-busy': loading ? 'true' : 'false' },
|
|
1213
2065
|
error !== ''
|
|
1214
2066
|
? h('div', { className: 'dsh-locator-empty', role: 'alert' }, t('errorPrefix') + error,
|
|
@@ -1230,7 +2082,7 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1230
2082
|
+ (contentBuilding ? ' · ' + t('indexing') : '')),
|
|
1231
2083
|
h('span', { className: 'grow' }),
|
|
1232
2084
|
toast === '' ? null : h('span', { className: 'dsh-locator-toast' }, toast),
|
|
1233
|
-
h('span', null, t('footer')),
|
|
2085
|
+
h('span', null, t('footer') + ' · Ctrl+F ' + t('findOpen')),
|
|
1234
2086
|
),
|
|
1235
2087
|
h('div', { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, role: 'status', 'aria-live': 'polite' },
|
|
1236
2088
|
t('results', { content: contentCount, files: filesCount, dirs: dirsCount, ms: Math.round(took) })),
|
|
@@ -1252,25 +2104,87 @@ body[data-ds-dark-theme] .dsh-locator-overlay{background:var(--dsw-alias-bg-mask
|
|
|
1252
2104
|
inject: ['slots', 'uiWorkspace'],
|
|
1253
2105
|
apply(ctx) {
|
|
1254
2106
|
workspaceService = ctx.uiWorkspace ?? (typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null)
|
|
1255
|
-
|
|
2107
|
+
ctx.effect(() => {
|
|
2108
|
+
ensureStyles()
|
|
2109
|
+
return () => {
|
|
2110
|
+
const tag = document.querySelector('style[data-plugin-css="' + NS + '"]')
|
|
2111
|
+
if (tag !== null) tag.remove()
|
|
2112
|
+
}
|
|
2113
|
+
}, 'dsh-locator: styles')
|
|
1256
2114
|
ctx.effect(() => {
|
|
1257
2115
|
const onKey = event => {
|
|
1258
2116
|
if ((event.metaKey || event.ctrlKey) && event.shiftKey && String(event.key).toLowerCase() === 'f') {
|
|
1259
2117
|
event.preventDefault()
|
|
1260
2118
|
event.stopPropagation()
|
|
2119
|
+
closeFind()
|
|
1261
2120
|
store.toggle()
|
|
1262
2121
|
}
|
|
1263
2122
|
}
|
|
1264
2123
|
document.addEventListener('keydown', onKey, true)
|
|
1265
2124
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
1266
2125
|
}, 'dsh-locator: search hotkey')
|
|
1267
|
-
ctx.effect(() =>
|
|
2126
|
+
ctx.effect(() => {
|
|
2127
|
+
const onFindKey = event => {
|
|
2128
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
2129
|
+
const target = event.target
|
|
2130
|
+
if (target instanceof Element && target.closest('.xterm,.monaco-editor,[data-code-editor]') !== null) return
|
|
2131
|
+
const inFind = target instanceof Element && target.closest('.dsh-locator-findbar') !== null
|
|
2132
|
+
const ctrl = event.ctrlKey || event.metaKey
|
|
2133
|
+
if (ctrl && !event.shiftKey && !event.altKey && event.code === 'KeyF') {
|
|
2134
|
+
if (store.isOpen()) {
|
|
2135
|
+
event.preventDefault()
|
|
2136
|
+
event.stopPropagation()
|
|
2137
|
+
const panelInput = document.querySelector('.dsh-locator-input')
|
|
2138
|
+
if (panelInput !== null) { panelInput.focus(); panelInput.select() }
|
|
2139
|
+
return
|
|
2140
|
+
}
|
|
2141
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]') !== null && !inFind) return
|
|
2142
|
+
if (findStore.get().open) { event.preventDefault(); event.stopPropagation(); findApi.focus(); return }
|
|
2143
|
+
if (conversationScope('') === null) return
|
|
2144
|
+
event.preventDefault()
|
|
2145
|
+
event.stopPropagation()
|
|
2146
|
+
openFind({ selection: true })
|
|
2147
|
+
return
|
|
2148
|
+
}
|
|
2149
|
+
if (!ctrl && !event.altKey && event.code === 'F3') {
|
|
2150
|
+
if (!findStore.get().open) return
|
|
2151
|
+
event.preventDefault()
|
|
2152
|
+
event.stopPropagation()
|
|
2153
|
+
findApi.move(event.shiftKey ? -1 : 1)
|
|
2154
|
+
return
|
|
2155
|
+
}
|
|
2156
|
+
if (event.key === 'Escape' && findStore.get().open) {
|
|
2157
|
+
event.preventDefault()
|
|
2158
|
+
event.stopPropagation()
|
|
2159
|
+
closeFind()
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
document.addEventListener('keydown', onFindKey, true)
|
|
2163
|
+
return () => document.removeEventListener('keydown', onFindKey, true)
|
|
2164
|
+
}, 'dsh-locator: find hotkeys')
|
|
2165
|
+
ctx.effect(() => () => { closeFind(); cancelJump() }, 'dsh-locator: find cleanup')
|
|
1268
2166
|
try {
|
|
1269
|
-
ctx.slots.inject('shell.overlay', () =>
|
|
1270
|
-
|
|
2167
|
+
ctx.slots.inject('shell.overlay', () => {
|
|
2168
|
+
const disposers = [
|
|
2169
|
+
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-findbar', order: 59 }, FindBar),
|
|
2170
|
+
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-panel', order: 60 }, Panel),
|
|
2171
|
+
ctx.slots.register({ name: 'shell.overlay', id: 'dsh-locator-notice', order: 61 }, Notice),
|
|
2172
|
+
]
|
|
2173
|
+
return () => {
|
|
2174
|
+
for (const dispose of disposers.reverse()) {
|
|
2175
|
+
if (typeof dispose === 'function') dispose()
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
})
|
|
1271
2179
|
} catch (error) {
|
|
1272
2180
|
console.error('dsh-locator: overlay slot unavailable', error)
|
|
1273
2181
|
}
|
|
2182
|
+
try {
|
|
2183
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
2184
|
+
ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'dsh-locator-find-toggle', order: 30 }, FindToggle))
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
console.error('dsh-locator: session header slot unavailable', error)
|
|
2187
|
+
}
|
|
1274
2188
|
try {
|
|
1275
2189
|
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dsh-locator-launch', order: 36 }, Launcher))
|
|
1276
2190
|
} catch (error) {
|