@ryuu-64/dsh-find-all 0.1.5 → 0.1.6
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 +13 -0
- package/README.md +7 -4
- package/lib/client.js +292 -83
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
Fix text matching, history feedback and search state after the 0.1.5 compatibility release.
|
|
6
|
+
|
|
7
|
+
- Match phrases across inline text and syntax-highlighting spans within one text block, while keeping separate messages, block elements, `<br>` elements and excluded content apart.
|
|
8
|
+
- Preserve original UTF-16 offsets during case-insensitive Unicode matching, so characters such as `İ` no longer shift later highlights. Matching remains literal, without locale-specific folding or normalization.
|
|
9
|
+
- Report “已搜索当前可用历史” when the host stops offering earlier pages. Recheck readiness and the final results before showing that status; it does not guarantee that the host's history is complete.
|
|
10
|
+
- Keep the last query in the active plugin instance so reopening the bar can restore it. A valid new text selection takes priority; disabling the plugin, refreshing or restarting clears the query.
|
|
11
|
+
- Preserve a verifiable current match when history is prepended or the page rescans, without scrolling automatically. Restore it across replaced text nodes only when the original block text is unchanged; otherwise clamp the previous result index. A new query or conversation selects the first result.
|
|
12
|
+
- Add regression coverage for observer cleanup, conversation and embedded-panel isolation, cross-node text matching, history feedback, query retention and stable selection.
|
|
13
|
+
|
|
14
|
+
The supported host-version list is unchanged. This patch does not add new Web or Desktop host-matrix acceptance evidence; the 0.1.5 compatibility notes and their documented limits still apply.
|
|
15
|
+
|
|
3
16
|
## 0.1.5
|
|
4
17
|
|
|
5
18
|
Fix compatibility with newer DSH hosts while preserving the existing find controls and matching behavior.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-find-all
|
|
2
2
|
|
|
3
|
-
DSH Desktop 的 **⌘F / Ctrl+F
|
|
3
|
+
DSH Desktop 的 **⌘F / Ctrl+F 查找**插件。它有一个和浏览器查找不一样的地方:**会向宿主请求更早的历史,不只搜索当前已经显示出来的部分。**
|
|
4
4
|
|
|
5
5
|
## 为什么需要它
|
|
6
6
|
|
|
@@ -8,17 +8,18 @@ DSH 桌面窗口是 Electron 壳,本身没有浏览器那种查找栏。而会
|
|
|
8
8
|
|
|
9
9
|
所以拿普通查找条搜一个较早的词,结果永远是 0,哪怕这段会话里确实有。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
这个插件在你输入关键词之后,通过会话服务把更早的历史一页页加载进来,每加载一页就重搜一次。宿主不再提供更早页时,最后重搜并提示“已搜索当前可用历史”。宿主公开接口不能证明历史完整性,插件不会把终止信号当作“全部搜完”。
|
|
12
12
|
|
|
13
13
|
## 功能
|
|
14
14
|
|
|
15
|
-
- **⌘F / Ctrl+F**
|
|
15
|
+
- **⌘F / Ctrl+F** 唤起查找栏,优先带入有效的选中文字;没有新选词时恢复上次关键词。关键词仅保留在当前插件实例内存中,禁用、刷新或重启后清空
|
|
16
16
|
- **范围切换**:`整段`(默认)↔ `本页`,点那个胶囊按钮切换
|
|
17
17
|
- **边搜边加载**:整段模式下自动分页加载历史,高亮与计数随加载实时更新
|
|
18
18
|
- **进度可见**:显示「正在加载更早的历史… 已 3 页」,**点一下这段文字即可停止加载**
|
|
19
19
|
- **安全上限**:最多加载 400 页(约 2 万条消息);历史不再增长时会自行停止
|
|
20
20
|
- **导航**:Enter / Shift+Enter 上下一个;⌘G(Ctrl+G)/ F3 下一个,加 Shift 反向;Esc 关闭查找栏并清除高亮
|
|
21
|
-
-
|
|
21
|
+
- 切换会话时清除旧结果,并在新会话里重搜同一关键词
|
|
22
|
+
- 后台重扫首次找到结果时选中第一项;前插历史时优先保留可确认的当前命中,不自动滚动。原节点可对应,或同一正文块仍在且原文完全未变时,可在 Text/span 换树后恢复当前项;无法确认时把旧序号夹到新结果范围,换词或换会话则选第一项
|
|
22
23
|
|
|
23
24
|
## 安装
|
|
24
25
|
|
|
@@ -65,6 +66,8 @@ dsh plugin --profile desktop remove dsh-find-bar
|
|
|
65
66
|
- 单次搜索最多记 5000 个命中
|
|
66
67
|
- 折叠起来的工具调用和思考块,正文不在页面里,搜不到——先展开再搜
|
|
67
68
|
- 整段模式要真的翻历史,所以很长的会话首次搜索需要等几秒
|
|
69
|
+
- 宿主尚未就绪、公开报错、无进展或到达上限时,保留已找到的结果并提示未完成;再次打开查找或输入关键词可手动重试。停止、关闭、换词或切换会话会取消插件任务,但不会中断宿主已经发出的请求
|
|
70
|
+
- “已搜索当前可用历史”不保证历史完整。宿主可能在正常末页和不连续页错误时都不再提供更早页;此时重新查找也不保证恢复缺失历史
|
|
68
71
|
|
|
69
72
|
## 卸载 / 回退
|
|
70
73
|
|
package/lib/client.js
CHANGED
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
// * In whole-conversation mode a search pages the older history in through
|
|
22
22
|
// the session service (ctx.sessions.binding(id).session.loadOlder()),
|
|
23
23
|
// re-running the match pass after every page — so the count and the
|
|
24
|
-
// highlight set
|
|
24
|
+
// highlight set cover the history the host currently makes available.
|
|
25
25
|
// * Progress is visible ("正在加载更早的历史… 已 3 页"), clickable to stop,
|
|
26
26
|
// capped at MAX_PAGES, and it stops by itself when the history stops
|
|
27
|
-
// growing
|
|
27
|
+
// growing. Host termination does not establish historical completeness.
|
|
28
28
|
//
|
|
29
29
|
// How it works without fighting React (unchanged from the fork):
|
|
30
30
|
// * Matches are collected as DOM Range objects (start/end offsets inside
|
|
31
|
-
// text nodes)
|
|
31
|
+
// text nodes) from block-local text snapshots, including shadow roots.
|
|
32
32
|
// * Highlights use the CSS Custom Highlight API (CSS.highlights + Highlight)
|
|
33
33
|
// which paints ranges WITHOUT mutating the DOM — React keeps full
|
|
34
34
|
// ownership of the tree. Chromium/Electron support this since 105.
|
|
@@ -59,6 +59,16 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
/** How often the open bar notices that the user switched session. */
|
|
60
60
|
var SESSION_POLL_MS = 600;
|
|
61
61
|
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1 };
|
|
62
|
+
// Structural boundaries, not a computed-style visibility policy. In
|
|
63
|
+
// particular, host message divs and code-content/toolbars stay separate.
|
|
64
|
+
var BLOCK_TAGS = {
|
|
65
|
+
ADDRESS: 1, ARTICLE: 1, ASIDE: 1, BLOCKQUOTE: 1, DD: 1, DETAILS: 1,
|
|
66
|
+
DIALOG: 1, DIV: 1, DL: 1, DT: 1, FIELDSET: 1, FIGCAPTION: 1, FIGURE: 1,
|
|
67
|
+
FOOTER: 1, FORM: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1,
|
|
68
|
+
HEADER: 1, HR: 1, LI: 1, MAIN: 1, NAV: 1, OL: 1, P: 1, PRE: 1,
|
|
69
|
+
SECTION: 1, SUMMARY: 1, TABLE: 1, TBODY: 1, TD: 1, TFOOT: 1,
|
|
70
|
+
TH: 1, THEAD: 1, TR: 1, UL: 1
|
|
71
|
+
};
|
|
62
72
|
|
|
63
73
|
var ZH = typeof navigator !== "undefined" && /^zh/i.test(navigator.language || "");
|
|
64
74
|
var L = ZH ? {
|
|
@@ -72,9 +82,8 @@ window.__ModuleLoader__.load({
|
|
|
72
82
|
scopeToPage: "搜索范围:整段会话。点击改为只搜当前页面",
|
|
73
83
|
scopeToWhole: "搜索范围:仅当前页面。点击改为搜索整段会话",
|
|
74
84
|
loading: "正在加载更早的历史… 已 {n} 页",
|
|
75
|
-
|
|
76
|
-
capped: "
|
|
77
|
-
stalled: "没有更早的历史了",
|
|
85
|
+
available: "已搜索当前可用历史;宿主未提供更早页,无法确认历史完整性",
|
|
86
|
+
capped: "历史尚未搜完:已到加载上限({n} 页)",
|
|
78
87
|
stopTip: "点击停止加载",
|
|
79
88
|
noService: "会话接口不可用,仅保留已显示的部分结果",
|
|
80
89
|
noTarget: "请先选择可见的会话,再打开查找",
|
|
@@ -92,9 +101,8 @@ window.__ModuleLoader__.load({
|
|
|
92
101
|
scopeToPage: "Scope: the whole conversation. Click to search only the rendered page",
|
|
93
102
|
scopeToWhole: "Scope: the rendered page only. Click to search the whole conversation",
|
|
94
103
|
loading: "Loading earlier history… {n} pages",
|
|
95
|
-
|
|
96
|
-
capped: "
|
|
97
|
-
stalled: "No earlier history",
|
|
104
|
+
available: "Searched currently available history; the host offers no earlier pages, so completeness cannot be confirmed",
|
|
105
|
+
capped: "History search incomplete: page cap reached ({n})",
|
|
98
106
|
stopTip: "Click to stop loading",
|
|
99
107
|
noService: "Session service unavailable; rendered results only",
|
|
100
108
|
noTarget: "Select a visible conversation before opening find",
|
|
@@ -105,22 +113,30 @@ window.__ModuleLoader__.load({
|
|
|
105
113
|
|
|
106
114
|
//#region pure helpers (exported for tests)
|
|
107
115
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
116
|
+
// Literal, locale-independent Unicode simple folding on the ORIGINAL
|
|
117
|
+
// text. RegExp returns UTF-16 offsets, exactly as DOM Range requires.
|
|
118
|
+
// No normalization, full case folding, or transformed-string offsets.
|
|
119
|
+
function findMatches(text, query) {
|
|
111
120
|
if (!query) return [];
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
// This is the entire pattern (not interpolated into another regex).
|
|
122
|
+
// Escape regex syntax only; '-' and '/' are literal in this context.
|
|
123
|
+
// Do not require the newer RegExp.escape() API in supported hosts.
|
|
124
|
+
var literal = String(query).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
125
|
+
if (!literal) return [];
|
|
126
|
+
var haystack = String(text);
|
|
127
|
+
var matcher = new RegExp(literal, "giu");
|
|
114
128
|
var out = [];
|
|
115
|
-
var
|
|
116
|
-
while (
|
|
117
|
-
out.push(
|
|
118
|
-
if (out.length >= MAX_MATCHES) break;
|
|
119
|
-
i = haystack.indexOf(needle, i + needle.length);
|
|
129
|
+
var match;
|
|
130
|
+
while (out.length < MAX_MATCHES && (match = matcher.exec(haystack)) !== null) {
|
|
131
|
+
out.push({ start: match.index, end: match.index + match[0].length });
|
|
120
132
|
}
|
|
121
133
|
return out;
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
function findIndices(text, query) {
|
|
137
|
+
return findMatches(text, query).map(function (match) { return match.start; });
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
// Keyboard routing, isolated so tests can drive it with spies.
|
|
125
141
|
function createHandlers(ops) {
|
|
126
142
|
return function onKeydown(event) {
|
|
@@ -151,41 +167,44 @@ window.__ModuleLoader__.load({
|
|
|
151
167
|
};
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* Every dependency is injected, so the loop is testable without a DOM or
|
|
158
|
-
* a host: the caller supplies how to read `hasMore`, how to load a page,
|
|
159
|
-
* how to measure the rendered history, and how to wait for that measure
|
|
160
|
-
* to change.
|
|
161
|
-
*
|
|
162
|
-
* @param opts - { maxPages, isCancelled, hasMore, loadOlder, signature,
|
|
163
|
-
* waitChange, onProgress }.
|
|
164
|
-
* `hasMore` returns true/false/null (null = the host cannot say, so the
|
|
165
|
-
* pager keeps going until a page changes nothing).
|
|
166
|
-
* @returns { pages, reason } where reason is one of done | cancelled |
|
|
167
|
-
* capped | stalled | error.
|
|
170
|
+
/** Page only while the public host state is ready; false is not completeness.
|
|
171
|
+
* Dependencies supply cancellable, bounded readiness/load/DOM waits.
|
|
172
|
+
* @returns { pages, reason } with done, cancelled, capped, stalled, unknown or error.
|
|
168
173
|
*/
|
|
169
174
|
async function runPageIn(opts) {
|
|
170
175
|
var pages = 0;
|
|
171
|
-
|
|
176
|
+
function result(reason) { return { pages: pages, reason: reason }; }
|
|
172
177
|
for (;;) {
|
|
173
|
-
if (opts.isCancelled())
|
|
174
|
-
|
|
175
|
-
if (
|
|
178
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
179
|
+
var ready = await opts.waitReady();
|
|
180
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
181
|
+
if (ready.reason) return result(ready.reason);
|
|
182
|
+
if (ready.hasMore === false) return result("done");
|
|
183
|
+
if (pages >= opts.maxPages) return result("capped");
|
|
176
184
|
var before = opts.signature();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
var loaded;
|
|
186
|
+
try { loaded = await opts.loadOlder(); }
|
|
187
|
+
catch (error) { loaded = { reason: "error" }; }
|
|
188
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
189
|
+
ready = await opts.waitReady(!!(loaded && loaded.reason));
|
|
190
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
191
|
+
if (loaded && loaded.reason) return result(loaded.reason);
|
|
192
|
+
if (ready.reason) return result(ready.reason);
|
|
193
|
+
if (loaded && loaded.deferred) continue;
|
|
194
|
+
if (ready.hasMore !== false) await opts.waitChange(before);
|
|
195
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
196
|
+
ready = await opts.waitReady();
|
|
197
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
198
|
+
if (ready.reason) return result(ready.reason);
|
|
199
|
+
var changed = opts.signature() !== before;
|
|
200
|
+
if (changed) {
|
|
201
|
+
pages += 1;
|
|
202
|
+
if (opts.onProgress) opts.onProgress(pages);
|
|
182
203
|
}
|
|
183
|
-
if (opts.isCancelled())
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
if (opts.onProgress) opts.onProgress(pages);
|
|
204
|
+
if (opts.isCancelled()) return result("cancelled");
|
|
205
|
+
if (ready.hasMore === false) return result("done");
|
|
206
|
+
if (!changed) return result("stalled");
|
|
187
207
|
}
|
|
188
|
-
return { pages: pages, reason: reason };
|
|
189
208
|
}
|
|
190
209
|
//#endregion
|
|
191
210
|
|
|
@@ -221,8 +240,8 @@ window.__ModuleLoader__.load({
|
|
|
221
240
|
return typeof CSS !== "undefined" && CSS.highlights && typeof Highlight === "function";
|
|
222
241
|
}
|
|
223
242
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
243
|
+
// Keep the existing pager signature collector unchanged. Matching below
|
|
244
|
+
// uses separate block snapshots; page-settlement semantics are out of scope.
|
|
226
245
|
function collectTextNodes(root, into) {
|
|
227
246
|
if (!root) return into;
|
|
228
247
|
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
@@ -247,17 +266,62 @@ window.__ModuleLoader__.load({
|
|
|
247
266
|
return into;
|
|
248
267
|
}
|
|
249
268
|
|
|
269
|
+
// Join inline Text nodes only within one structural block and DOM root.
|
|
270
|
+
// These snapshots live for one scan. Preserve every original code unit,
|
|
271
|
+
// including whitespace-only nodes and code newlines; never trim or pad.
|
|
272
|
+
function collectTextBlocks(root) {
|
|
273
|
+
var blocks = [];
|
|
274
|
+
var text = "";
|
|
275
|
+
var segments = [];
|
|
276
|
+
function flush() {
|
|
277
|
+
if (segments.length) blocks.push({ text: text, segments: segments });
|
|
278
|
+
text = "";
|
|
279
|
+
segments = [];
|
|
280
|
+
}
|
|
281
|
+
function visit(node) {
|
|
282
|
+
if (node.nodeType === 3) {
|
|
283
|
+
if (node.data.length) {
|
|
284
|
+
segments.push({ node: node, start: text.length, end: text.length + node.data.length });
|
|
285
|
+
text += node.data;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (node.nodeType !== 1 && node.nodeType !== 11) return;
|
|
290
|
+
if (SKIP_TAGS[node.tagName] || node.id === BAR_ID || node.tagName === "BR") {
|
|
291
|
+
// Excluded subtrees and br are hard boundaries, not empty text.
|
|
292
|
+
flush();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
var boundary = BLOCK_TAGS[node.tagName] || node.nodeType === 11 || node.shadowRoot;
|
|
296
|
+
if (boundary) flush();
|
|
297
|
+
for (var child = node.firstChild; child; child = child.nextSibling) visit(child);
|
|
298
|
+
if (boundary) flush();
|
|
299
|
+
// No Range may span a shadow root and its surrounding light DOM.
|
|
300
|
+
if (node.shadowRoot) visit(node.shadowRoot);
|
|
301
|
+
}
|
|
302
|
+
visit(root);
|
|
303
|
+
flush();
|
|
304
|
+
return blocks;
|
|
305
|
+
}
|
|
306
|
+
|
|
250
307
|
function collectRanges(query, root) {
|
|
251
308
|
var ranges = [];
|
|
252
309
|
if (!query || !root || typeof document === "undefined") return ranges;
|
|
253
|
-
var
|
|
254
|
-
for (var i = 0; i <
|
|
255
|
-
var
|
|
256
|
-
var
|
|
257
|
-
|
|
310
|
+
var blocks = collectTextBlocks(root);
|
|
311
|
+
for (var i = 0; i < blocks.length && ranges.length < MAX_MATCHES; i++) {
|
|
312
|
+
var block = blocks[i];
|
|
313
|
+
var matches = findMatches(block.text, query);
|
|
314
|
+
var start = 0, end = 0;
|
|
315
|
+
for (var j = 0; j < matches.length && ranges.length < MAX_MATCHES; j++) {
|
|
316
|
+
var match = matches[j];
|
|
317
|
+
// At a node junction, start at the following node and end at
|
|
318
|
+
// the preceding node. Non-overlapping matches move forwards.
|
|
319
|
+
while (match.start >= block.segments[start].end) start++;
|
|
320
|
+
while (match.end > block.segments[end].end) end++;
|
|
321
|
+
var first = block.segments[start], last = block.segments[end];
|
|
258
322
|
var range = document.createRange();
|
|
259
|
-
range.setStart(node,
|
|
260
|
-
range.setEnd(node,
|
|
323
|
+
range.setStart(first.node, match.start - first.start);
|
|
324
|
+
range.setEnd(last.node, match.end - last.start);
|
|
261
325
|
ranges.push(range);
|
|
262
326
|
}
|
|
263
327
|
}
|
|
@@ -448,7 +512,7 @@ window.__ModuleLoader__.load({
|
|
|
448
512
|
return root ? collectTextNodes(root, []).length : 0;
|
|
449
513
|
}
|
|
450
514
|
|
|
451
|
-
function
|
|
515
|
+
function waitForPage(check, isCancelled) {
|
|
452
516
|
return new Promise(function (resolve) {
|
|
453
517
|
var started = Date.now();
|
|
454
518
|
function finish(value) {
|
|
@@ -457,24 +521,53 @@ window.__ModuleLoader__.load({
|
|
|
457
521
|
state.settleCancel = null;
|
|
458
522
|
resolve(value);
|
|
459
523
|
}
|
|
460
|
-
state.settleCancel = function () { finish(
|
|
524
|
+
state.settleCancel = function () { finish({ reason: "cancelled" }); };
|
|
461
525
|
(function poll() {
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
if (
|
|
526
|
+
if (isCancelled()) { finish({ reason: "cancelled" }); return; }
|
|
527
|
+
var value = check();
|
|
528
|
+
if (value) { finish(value); return; }
|
|
529
|
+
if (Date.now() - started >= PAGE_SETTLE_MS) { finish({ reason: "stalled" }); return; }
|
|
465
530
|
state.settleTimer = setTimeout(poll, PAGE_POLL_MS);
|
|
466
531
|
})();
|
|
467
532
|
});
|
|
468
533
|
}
|
|
469
534
|
|
|
470
|
-
function
|
|
535
|
+
function facePageState(face) {
|
|
471
536
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return
|
|
477
|
-
|
|
537
|
+
var snapshot = typeof face.getSnapshot === "function" && face.getSnapshot();
|
|
538
|
+
if (!snapshot) return { reason: "unknown" };
|
|
539
|
+
if (snapshot.openError != null || snapshot.openState === "error") return { reason: "error" };
|
|
540
|
+
if (snapshot.openState === "cold" || snapshot.openState === "loading") return null;
|
|
541
|
+
if (snapshot.openState !== "open" || snapshot.openError !== null) return { reason: "unknown" };
|
|
542
|
+
if (snapshot.loadingOlder === true) return null;
|
|
543
|
+
if (snapshot.loadingOlder !== false) return { reason: "unknown" };
|
|
544
|
+
return { hasMore: typeof snapshot.hasMore === "boolean" ? snapshot.hasMore : null };
|
|
545
|
+
} catch (error) { return { reason: "unknown" }; }
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// A cancelled UI task cannot abort SessionFace.loadOlder(). Keep its record
|
|
549
|
+
// until settlement so a later query/enable never starts a concurrent request.
|
|
550
|
+
var pendingLoads = new WeakMap();
|
|
551
|
+
async function loadPage(face, isCancelled) {
|
|
552
|
+
var status = facePageState(face);
|
|
553
|
+
if (!status || status.reason || status.hasMore === false || pendingLoads.has(face)) return { deferred: true };
|
|
554
|
+
var pending = { done: false, reason: null };
|
|
555
|
+
pendingLoads.set(face, pending);
|
|
556
|
+
var promise;
|
|
557
|
+
try { promise = Promise.resolve(face.loadOlder()); }
|
|
558
|
+
catch (error) { promise = Promise.reject(error); }
|
|
559
|
+
promise.then(function () {
|
|
560
|
+
pending.done = true;
|
|
561
|
+
pendingLoads.delete(face);
|
|
562
|
+
}, function () {
|
|
563
|
+
pending.done = true;
|
|
564
|
+
pending.reason = "error";
|
|
565
|
+
pendingLoads.delete(face);
|
|
566
|
+
});
|
|
567
|
+
await Promise.resolve();
|
|
568
|
+
return waitForPage(function () {
|
|
569
|
+
return pending.done ? { reason: pending.reason } : null;
|
|
570
|
+
}, isCancelled);
|
|
478
571
|
}
|
|
479
572
|
//#endregion
|
|
480
573
|
|
|
@@ -494,6 +587,7 @@ window.__ModuleLoader__.load({
|
|
|
494
587
|
query: "",
|
|
495
588
|
ranges: [],
|
|
496
589
|
index: -1,
|
|
590
|
+
selection: null,
|
|
497
591
|
capped: false,
|
|
498
592
|
observer: null,
|
|
499
593
|
debounce: 0,
|
|
@@ -529,6 +623,7 @@ window.__ModuleLoader__.load({
|
|
|
529
623
|
function setStatus(text, busy) {
|
|
530
624
|
if (!state.status) return;
|
|
531
625
|
state.status.textContent = text || "";
|
|
626
|
+
state.status.title = busy ? L.stopTip : (text || "");
|
|
532
627
|
if (busy) state.status.setAttribute("data-busy", "1");
|
|
533
628
|
else state.status.removeAttribute("data-busy");
|
|
534
629
|
updateCount();
|
|
@@ -547,12 +642,104 @@ window.__ModuleLoader__.load({
|
|
|
547
642
|
return state.target || (state.adapter && state.adapter.scopeUnavailable()) ? L.noRoot : L.noTarget;
|
|
548
643
|
}
|
|
549
644
|
|
|
645
|
+
// The code content wrapper survives the host's plain-pre -> Shiki tree
|
|
646
|
+
// swap (0.2.0-rc.2 CodeBlock). Otherwise use the nearest block boundary.
|
|
647
|
+
function selectionBlock(range, root) {
|
|
648
|
+
var nearest = null;
|
|
649
|
+
for (var node = range.startContainer.parentNode; node; node = node.parentNode) {
|
|
650
|
+
if (node === root || node.nodeType === 11) return nearest || node;
|
|
651
|
+
if (node.hasAttribute && node.hasAttribute("data-code-block-content")) return node;
|
|
652
|
+
if (!nearest && BLOCK_TAGS[node.tagName]) nearest = node;
|
|
653
|
+
}
|
|
654
|
+
return nearest;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function positionInBlocks(range, blocks) {
|
|
658
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
659
|
+
var start = null, end = null;
|
|
660
|
+
for (var j = 0; j < blocks[i].segments.length; j++) {
|
|
661
|
+
var segment = blocks[i].segments[j];
|
|
662
|
+
if (segment.node === range.startContainer) start = segment.start + range.startOffset;
|
|
663
|
+
if (segment.node === range.endContainer) end = segment.start + range.endOffset;
|
|
664
|
+
}
|
|
665
|
+
if (start !== null && end !== null) return { run: i, start: start, end: end };
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function matchNodes(blocks, position) {
|
|
671
|
+
if (!position) return [];
|
|
672
|
+
return blocks[position.run].segments.filter(function (segment) {
|
|
673
|
+
return segment.end > position.start && segment.start < position.end;
|
|
674
|
+
}).map(function (segment) {
|
|
675
|
+
return {
|
|
676
|
+
node: segment.node, text: segment.node.data,
|
|
677
|
+
start: Math.max(position.start, segment.start) - segment.start,
|
|
678
|
+
end: Math.min(position.end, segment.end) - segment.start
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function rememberSelection() {
|
|
684
|
+
var range = state.ranges[state.index];
|
|
685
|
+
state.selection = null;
|
|
686
|
+
if (!range || !state.target) return;
|
|
687
|
+
var block = selectionBlock(range, state.target.root);
|
|
688
|
+
var blocks = block ? collectTextBlocks(block) : [];
|
|
689
|
+
var position = positionInBlocks(range, blocks);
|
|
690
|
+
var nodes = matchNodes(blocks, position);
|
|
691
|
+
state.selection = {
|
|
692
|
+
target: state.target, query: state.query, root: state.target.root,
|
|
693
|
+
startNode: range.startContainer, startOffset: range.startOffset,
|
|
694
|
+
endNode: range.endContainer, endOffset: range.endOffset,
|
|
695
|
+
block: block, position: position, nodes: nodes,
|
|
696
|
+
texts: blocks.map(function (item) { return item.text; })
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function retainedIndex(saved) {
|
|
701
|
+
if (!saved || saved.target !== state.target || saved.query !== state.query || saved.root !== state.target.root) return -1;
|
|
702
|
+
if (!saved.block || !saved.block.isConnected || !saved.position) return -1;
|
|
703
|
+
var blocks = collectTextBlocks(saved.block);
|
|
704
|
+
var i, range;
|
|
705
|
+
for (i = 0; i < state.ranges.length; i++) {
|
|
706
|
+
range = state.ranges[i];
|
|
707
|
+
if (range.startContainer !== saved.startNode || range.startOffset !== saved.startOffset ||
|
|
708
|
+
range.endContainer !== saved.endNode || range.endOffset !== saved.endOffset) continue;
|
|
709
|
+
// Compare the new match's actual ordered participants. Old nodes can
|
|
710
|
+
// remain connected with unchanged text after moving outside this hit.
|
|
711
|
+
var nodes = matchNodes(blocks, positionInBlocks(range, blocks));
|
|
712
|
+
if (nodes.length > 0 && nodes.length === saved.nodes.length && nodes.every(function (item, index) {
|
|
713
|
+
var previous = saved.nodes[index];
|
|
714
|
+
return item.node === previous.node && item.text === previous.text && item.start === previous.start && item.end === previous.end;
|
|
715
|
+
})) return i;
|
|
716
|
+
}
|
|
717
|
+
if (blocks.length !== saved.texts.length || blocks.some(function (item, index) { return item.text !== saved.texts[index]; })) return -1;
|
|
718
|
+
var segments = blocks[saved.position.run].segments;
|
|
719
|
+
var first = null, last = null;
|
|
720
|
+
for (i = 0; i < segments.length; i++) {
|
|
721
|
+
if (saved.position.start >= segments[i].start && saved.position.start < segments[i].end) first = segments[i];
|
|
722
|
+
if (saved.position.end > segments[i].start && saved.position.end <= segments[i].end) last = segments[i];
|
|
723
|
+
}
|
|
724
|
+
if (!first || !last) return -1;
|
|
725
|
+
for (i = 0; i < state.ranges.length; i++) {
|
|
726
|
+
range = state.ranges[i];
|
|
727
|
+
if (range.startContainer === first.node && range.startOffset === saved.position.start - first.start &&
|
|
728
|
+
range.endContainer === last.node && range.endOffset === saved.position.end - last.start) return i;
|
|
729
|
+
}
|
|
730
|
+
return -1;
|
|
731
|
+
}
|
|
732
|
+
|
|
550
733
|
function runSearch(keepIndex) {
|
|
551
734
|
var valid = state.adapter && state.adapter.valid(state.target);
|
|
552
735
|
state.ranges = valid ? collectRanges(state.query, state.target.root) : [];
|
|
553
736
|
if (!valid) setStatus(unavailableStatus(), false);
|
|
554
737
|
state.capped = state.ranges.length >= MAX_MATCHES;
|
|
555
|
-
|
|
738
|
+
var retained = keepIndex && valid ? retainedIndex(state.selection) : -1;
|
|
739
|
+
if (!state.ranges.length) state.index = -1;
|
|
740
|
+
else if (!keepIndex) state.index = 0;
|
|
741
|
+
else state.index = retained >= 0 ? retained : Math.max(0, Math.min(state.index, state.ranges.length - 1));
|
|
742
|
+
rememberSelection();
|
|
556
743
|
paint();
|
|
557
744
|
updateCount();
|
|
558
745
|
|
|
@@ -563,6 +750,7 @@ window.__ModuleLoader__.load({
|
|
|
563
750
|
var n = state.ranges.length;
|
|
564
751
|
if (n === 0) return;
|
|
565
752
|
state.index = ((state.index + delta) % n + n) % n;
|
|
753
|
+
rememberSelection();
|
|
566
754
|
paint();
|
|
567
755
|
updateCount();
|
|
568
756
|
scrollToRange(state.ranges[state.index]);
|
|
@@ -611,7 +799,8 @@ window.__ModuleLoader__.load({
|
|
|
611
799
|
|
|
612
800
|
function startPageIn() {
|
|
613
801
|
if (state.scope !== "whole" || !state.query || !isOpen()) return;
|
|
614
|
-
|
|
802
|
+
cancelPageIn();
|
|
803
|
+
var token = state.pageToken;
|
|
615
804
|
var target = state.target;
|
|
616
805
|
if (!state.adapter || !state.adapter.valid(target)) {
|
|
617
806
|
setStatus(unavailableStatus(), false);
|
|
@@ -623,33 +812,51 @@ window.__ModuleLoader__.load({
|
|
|
623
812
|
return;
|
|
624
813
|
}
|
|
625
814
|
state.sessionId = target.id;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
815
|
+
function isCancelled() {
|
|
816
|
+
return token !== state.pageToken || !isOpen() || state.target !== target || !state.adapter || state.adapter.face(target) !== face;
|
|
817
|
+
}
|
|
818
|
+
function waitReady(failFast) {
|
|
819
|
+
return waitForPage(function () {
|
|
820
|
+
var status = facePageState(face);
|
|
821
|
+
if (status && status.reason) return status;
|
|
822
|
+
if (pendingLoads.has(face) || !status) return failFast ? { reason: "stalled" } : null;
|
|
823
|
+
return status;
|
|
824
|
+
}, isCancelled);
|
|
629
825
|
}
|
|
630
826
|
state.paging = true;
|
|
631
827
|
state.pages = 0;
|
|
632
828
|
setStatus(L.loading.replace("{n}", "0"), true);
|
|
633
829
|
runPageIn({
|
|
634
830
|
maxPages: MAX_PAGES,
|
|
635
|
-
isCancelled:
|
|
636
|
-
|
|
637
|
-
loadOlder: function () { return
|
|
831
|
+
isCancelled: isCancelled,
|
|
832
|
+
waitReady: waitReady,
|
|
833
|
+
loadOlder: function () { return loadPage(face, isCancelled); },
|
|
638
834
|
signature: function () { return domSignature(target.root); },
|
|
639
|
-
waitChange: function (before) {
|
|
835
|
+
waitChange: function (before) {
|
|
836
|
+
return waitForPage(function () {
|
|
837
|
+
var status = facePageState(face);
|
|
838
|
+
return domSignature(target.root) !== before || !status || status.reason || status.hasMore === false ? {} : null;
|
|
839
|
+
}, isCancelled);
|
|
840
|
+
},
|
|
640
841
|
onProgress: function (pages) {
|
|
641
|
-
if (
|
|
842
|
+
if (isCancelled()) return;
|
|
642
843
|
state.pages = pages;
|
|
643
844
|
setStatus(L.loading.replace("{n}", String(pages)), true);
|
|
644
845
|
runSearch(true);
|
|
645
846
|
}
|
|
646
847
|
}).then(function (result) {
|
|
647
|
-
if (
|
|
848
|
+
if (isCancelled()) {
|
|
849
|
+
// Face replacement cancels this task even if the DOM target survived.
|
|
850
|
+
if (token === state.pageToken) { state.paging = false; setStatus(L.incomplete, false); }
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
var terminal = facePageState(face);
|
|
854
|
+
if (result.reason === "done" && (!terminal || terminal.reason || terminal.hasMore !== false || pendingLoads.has(face))) result.reason = "stalled";
|
|
648
855
|
state.paging = false;
|
|
649
856
|
state.pages = result.pages;
|
|
650
857
|
if (result.reason === "capped") setStatus(L.capped.replace("{n}", String(result.pages)), false);
|
|
651
858
|
else if (result.reason !== "done") setStatus(L.incomplete, false);
|
|
652
|
-
else setStatus(
|
|
859
|
+
else setStatus(L.available, false);
|
|
653
860
|
runSearch(true);
|
|
654
861
|
});
|
|
655
862
|
}
|
|
@@ -783,9 +990,9 @@ window.__ModuleLoader__.load({
|
|
|
783
990
|
var selection = "";
|
|
784
991
|
try { selection = String(window.getSelection()).trim(); } catch (error) {}
|
|
785
992
|
if (state.target && selection && selection.length <= 200 && selection !== state.query) {
|
|
786
|
-
state.input.value = selection;
|
|
787
993
|
state.query = selection;
|
|
788
994
|
}
|
|
995
|
+
state.input.value = state.query;
|
|
789
996
|
state.input.focus();
|
|
790
997
|
state.input.select();
|
|
791
998
|
runSearch(false);
|
|
@@ -798,10 +1005,9 @@ window.__ModuleLoader__.load({
|
|
|
798
1005
|
cancelPageIn();
|
|
799
1006
|
stopWatchingSession();
|
|
800
1007
|
if (state.bar) state.bar.style.display = "none";
|
|
801
|
-
state.query = "";
|
|
802
|
-
if (state.input) state.input.value = "";
|
|
803
1008
|
state.ranges = [];
|
|
804
1009
|
state.index = -1;
|
|
1010
|
+
state.selection = null;
|
|
805
1011
|
setStatus("", false);
|
|
806
1012
|
paint(); // clears registered highlights
|
|
807
1013
|
updateCount();
|
|
@@ -850,6 +1056,7 @@ window.__ModuleLoader__.load({
|
|
|
850
1056
|
window.removeEventListener("focusin", focus, true);
|
|
851
1057
|
window.removeEventListener("pointerdown", focus, true);
|
|
852
1058
|
close();
|
|
1059
|
+
state.query = "";
|
|
853
1060
|
if (state.bar) state.bar.remove();
|
|
854
1061
|
var style = document.querySelector('style[data-plugin-css="dsh-find-all/bar.css"]');
|
|
855
1062
|
if (style) style.remove();
|
|
@@ -865,6 +1072,8 @@ window.__ModuleLoader__.load({
|
|
|
865
1072
|
exports.resolveScope = resolveScope;
|
|
866
1073
|
exports.createHostAdapter = createHostAdapter;
|
|
867
1074
|
exports.findIndices = findIndices; // for tests
|
|
1075
|
+
exports.findMatches = findMatches; // for tests
|
|
1076
|
+
exports.collectRanges = collectRanges; // for tests
|
|
868
1077
|
exports.createHandlers = createHandlers; // for tests
|
|
869
1078
|
exports.runPageIn = runPageIn; // for tests
|
|
870
1079
|
return module.exports;
|
package/package.json
CHANGED