dsh-chat-link-actions 0.1.0 → 0.2.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/README.md +9 -2
- package/client.js +149 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
长任务跑完,记录里一串路径;想引用回去得手打,想在访达里找到又是另一回事。这个插件把右键菜单接到对话里的文件链接上。
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 三个动作
|
|
8
8
|
|
|
9
9
|
| 右键文件链接 | 做什么 |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| **引用到对话** |
|
|
11
|
+
| **引用到对话** | 插成输入框里的引用芯片(`(</> 文件名)`),和拖文件、`@` 选文件的结果一样 |
|
|
12
12
|
| **在访达中查看** | 打开所在目录并选中这个文件(Windows 用资源管理器;Linux 打开所在目录) |
|
|
13
|
+
| **复制路径** | 绝对路径进剪贴板,直接粘给别的窗口 |
|
|
14
|
+
|
|
15
|
+
## 认哪些链接
|
|
16
|
+
|
|
17
|
+
右键不是只认一种写法,常见渲染都走同一条解析:`href` / `src` 里的 `?path=`(图片、附件卡片)、`data-path` 之类的绝对路径属性、`title` 里的路径(宿主自绘的文件链接、文件夹链接)、以及直接写在正文里的绝对路径(光标落在路径上才算)。Windows 的 `C:\...` 与含空格、中文的路径同样能认。
|
|
13
18
|
|
|
14
19
|
## 装
|
|
15
20
|
|
|
@@ -23,6 +28,8 @@ dsh plugin install dsh-chat-link-actions
|
|
|
23
28
|
|
|
24
29
|
点下菜单之后,如果你又改了输入框或换了会话,插入会停下来不覆盖已写的内容——重新右键一次即可。
|
|
25
30
|
|
|
31
|
+
芯片由宿主渲染:图和视频进的是附件,文件夹显示成 `文件夹名/`,其他文件按扩展名走同一个芯片。
|
|
32
|
+
|
|
26
33
|
## 不做什么
|
|
27
34
|
|
|
28
35
|
- 只认本机存在的绝对路径:网络路径、已删掉的文件,会直接说找不到。
|
package/client.js
CHANGED
|
@@ -33,6 +33,8 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
|
|
34
34
|
/** SessionId -> inputActions for that session's composer. */
|
|
35
35
|
var drafts = new Map();
|
|
36
|
+
/** Client root context, kept for the session-scoped chip event. */
|
|
37
|
+
var rootContext = null;
|
|
36
38
|
/** Session whose composer rendered most recently. */
|
|
37
39
|
var activeSession = null;
|
|
38
40
|
|
|
@@ -61,9 +63,17 @@ window.__ModuleLoader__.load({
|
|
|
61
63
|
|
|
62
64
|
function draftCandidates() {
|
|
63
65
|
var ordered = [];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
66
|
+
var seen = [];
|
|
67
|
+
function add(sessionId) {
|
|
68
|
+
if (sessionId === undefined || sessionId === null) return;
|
|
69
|
+
var actions = drafts.get(sessionId);
|
|
70
|
+
if (actions === undefined || seen.indexOf(actions) !== -1) return;
|
|
71
|
+
seen.push(actions);
|
|
72
|
+
ordered.push({ sessionId: sessionId, actions: actions });
|
|
73
|
+
}
|
|
74
|
+
add(activeSession);
|
|
75
|
+
drafts.forEach(function (_actions, sessionId) {
|
|
76
|
+
add(sessionId);
|
|
67
77
|
});
|
|
68
78
|
return ordered;
|
|
69
79
|
}
|
|
@@ -73,13 +83,70 @@ window.__ModuleLoader__.load({
|
|
|
73
83
|
return /[\s"]/.test(path) ? '@"' + path.replace(/"/g, '\\"') + '"' : "@" + path;
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
/** Directory links carry a trailing separator; files never do. */
|
|
87
|
+
function looksLikeDirectory(path) {
|
|
88
|
+
return path.charAt(path.length - 1) === "/" || path.charAt(path.length - 1) === "\\";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Last path segment, the same label the composer's own reference chips show. */
|
|
92
|
+
function displayLabel(path, directory) {
|
|
93
|
+
var trimmed = path.replace(/[\\/]+$/, "");
|
|
94
|
+
var cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
95
|
+
var name = cut === -1 ? trimmed : trimmed.slice(cut + 1);
|
|
96
|
+
if (name === "") name = trimmed;
|
|
97
|
+
return directory ? name + "/" : name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Insert the reference as the composer's own reference chip (the `(</> name)`
|
|
102
|
+
* node), which the shell only builds for its file-drop and `@`-picker paths.
|
|
103
|
+
* The shell owns that insertion behind a session-scoped event; when the event
|
|
104
|
+
* is not reachable the caller falls back to plain `insertText`.
|
|
105
|
+
*/
|
|
106
|
+
function insertChip(sessionId, actions, path) {
|
|
107
|
+
if (rootContext === null || typeof rootContext.sessions !== "object" || rootContext.sessions === null) return false;
|
|
108
|
+
var binding = null;
|
|
109
|
+
try {
|
|
110
|
+
binding = rootContext.sessions.binding(sessionId);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
var sessionContext = binding === null || binding === undefined ? null : binding.ctx;
|
|
115
|
+
if (sessionContext === null || sessionContext === undefined || typeof sessionContext.emit !== "function") return false;
|
|
116
|
+
var before = actions.captureInsertion();
|
|
117
|
+
if (before === null || before === undefined) return false;
|
|
118
|
+
var directory = looksLikeDirectory(path);
|
|
119
|
+
var mention = referenceText(path);
|
|
120
|
+
var reference = {};
|
|
121
|
+
reference.source = "reference";
|
|
122
|
+
reference.ref = mention;
|
|
123
|
+
reference.label = displayLabel(path, directory);
|
|
124
|
+
reference.appearance = directory ? "folder" : "file";
|
|
125
|
+
reference.clipboardText = mention;
|
|
126
|
+
var request = {};
|
|
127
|
+
request.reference = reference;
|
|
128
|
+
request.span = before;
|
|
129
|
+
var delivered = false;
|
|
130
|
+
try {
|
|
131
|
+
delivered = sessionContext.emit("slash/input-insert-reference", request);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (delivered === false) return false;
|
|
136
|
+
var after = actions.captureInsertion();
|
|
137
|
+
if (after === null || after === undefined) return true;
|
|
138
|
+
return after.start !== before.start || after.end !== before.end;
|
|
139
|
+
}
|
|
140
|
+
|
|
76
141
|
/** Insert the reference into a live composer. Returns true on success. */
|
|
77
142
|
function quoteIntoComposer(path) {
|
|
78
143
|
var candidates = draftCandidates();
|
|
79
144
|
for (var i = 0; i < candidates.length; i += 1) {
|
|
80
|
-
var
|
|
145
|
+
var sessionId = candidates[i].sessionId;
|
|
146
|
+
var actions = candidates[i].actions;
|
|
81
147
|
try {
|
|
82
148
|
if (typeof actions.captureInsertion !== "function") continue;
|
|
149
|
+
if (insertChip(sessionId, actions, path)) return true;
|
|
83
150
|
var span = actions.captureInsertion();
|
|
84
151
|
if (span === null || span === undefined) continue;
|
|
85
152
|
if (actions.insertText(referenceText(path) + " ", span) === true) return true;
|
|
@@ -102,6 +169,9 @@ window.__ModuleLoader__.load({
|
|
|
102
169
|
candidate = candidate.slice(1, -1);
|
|
103
170
|
}
|
|
104
171
|
candidate = candidate.replace(/[.,;:!?,。;:!?]+$/u, "");
|
|
172
|
+
candidate = candidate.replace(/^file:\/\//, "/");
|
|
173
|
+
if (/^[A-Za-z]:(?!\/)/.test(candidate)) candidate = candidate.slice(0, 2) + "/" + candidate.slice(2);
|
|
174
|
+
candidate = candidate.replace(/^([A-Za-z]):\//, "$1:/");
|
|
105
175
|
return looksAbsolute(candidate) ? candidate : null;
|
|
106
176
|
}
|
|
107
177
|
|
|
@@ -116,6 +186,10 @@ window.__ModuleLoader__.load({
|
|
|
116
186
|
var url = new URL(href, document.baseURI);
|
|
117
187
|
var fromQuery = normalizeCandidate(url.searchParams.get("path"));
|
|
118
188
|
if (fromQuery !== null) return fromQuery;
|
|
189
|
+
for (var i = 0; i < ["file", "filepath", "abs", "target"].length; i += 1) {
|
|
190
|
+
var fromNamed = normalizeCandidate(url.searchParams.get(["file", "filepath", "abs", "target"][i]));
|
|
191
|
+
if (fromNamed !== null) return fromNamed;
|
|
192
|
+
}
|
|
119
193
|
if (url.protocol === "file:") return normalizeCandidate(decodeURIComponent(url.pathname));
|
|
120
194
|
if (url.origin === location.origin && looksAbsolute(url.pathname) && /\.[A-Za-z0-9]{1,6}$/.test(url.pathname)) {
|
|
121
195
|
return normalizeCandidate(decodeURIComponent(url.pathname));
|
|
@@ -130,20 +204,71 @@ window.__ModuleLoader__.load({
|
|
|
130
204
|
* Walk up from the clicked node looking for a link that names a file: href
|
|
131
205
|
* first, then an absolute `data-*` value, then the reference chip title.
|
|
132
206
|
*/
|
|
133
|
-
|
|
207
|
+
var PATH_ATTRS = ["href", "src"];
|
|
208
|
+
|
|
209
|
+
/** `data-*` values that may carry a path, in confidence order. */
|
|
210
|
+
function datasetPath(node) {
|
|
211
|
+
var dataset = node.dataset || {};
|
|
212
|
+
var keys = Object.keys(dataset);
|
|
213
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
214
|
+
if (!/path|file|href|src|dir|folder|abs/i.test(keys[i])) continue;
|
|
215
|
+
var candidate = normalizeCandidate(dataset[keys[i]]);
|
|
216
|
+
if (candidate !== null) return candidate;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** An absolute path written as plain text inside the clicked element. */
|
|
222
|
+
var PATH_IN_TEXT = /(?:^|[\s"'“”「」()()\[\]])(@?"?(?:\/|[A-Za-z]:[\\/])[^\s"'“”「」<>,。;:!?]*)/u;
|
|
223
|
+
|
|
224
|
+
/** 正文纯文本里,取光标落点所在的那段路径。 */
|
|
225
|
+
function pathFromCaret(x, y) {
|
|
226
|
+
if (typeof document.caretRangeFromPoint !== "function") return null;
|
|
227
|
+
var range = null;
|
|
228
|
+
try {
|
|
229
|
+
range = document.caretRangeFromPoint(x, y);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
if (range === null || range.startContainer === null || range.startContainer.nodeType !== 3) return null;
|
|
234
|
+
var data = range.startContainer.data;
|
|
235
|
+
var offset = Math.min(range.startOffset, data.length);
|
|
236
|
+
var bounds = [];
|
|
237
|
+
var matcher = new RegExp(PATH_IN_TEXT.source, "gu");
|
|
238
|
+
var hit = null;
|
|
239
|
+
while ((hit = matcher.exec(data)) !== null) {
|
|
240
|
+
bounds.push({ text: hit[0], start: hit.index, end: hit.index + hit[0].length });
|
|
241
|
+
if (hit[0].length === 0) matcher.lastIndex += 1;
|
|
242
|
+
}
|
|
243
|
+
for (var i = 0; i < bounds.length; i += 1) {
|
|
244
|
+
if (offset >= bounds[i].start && offset <= bounds[i].end) {
|
|
245
|
+
return normalizeCandidate(bounds[i].text);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Walk up from the clicked node looking for a file link: href/src first, then
|
|
253
|
+
* an absolute `data-*` value, then a title, then the element's own text. The
|
|
254
|
+
* text pass covers paths the transcript renders as plain prose.
|
|
255
|
+
*/
|
|
256
|
+
function pathFromNode(start, point) {
|
|
134
257
|
for (var node = start; node !== null && node !== undefined && node !== document.body; node = node.parentElement) {
|
|
135
258
|
if (node.nodeType !== 1) continue;
|
|
136
|
-
var
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
var keys = Object.keys(dataset);
|
|
140
|
-
for (var k = 0; k < keys.length; k += 1) {
|
|
141
|
-
var fromData = normalizeCandidate(dataset[keys[k]]);
|
|
142
|
-
if (fromData !== null && /\.[A-Za-z0-9]{1,6}$/.test(fromData)) return fromData;
|
|
259
|
+
for (var a = 0; a < PATH_ATTRS.length; a += 1) {
|
|
260
|
+
var fromHref = decodePathFromHref(node.getAttribute(PATH_ATTRS[a]));
|
|
261
|
+
if (fromHref !== null) return fromHref;
|
|
143
262
|
}
|
|
263
|
+
var fromData = datasetPath(node);
|
|
264
|
+
if (fromData !== null) return fromData;
|
|
144
265
|
var fromTitle = normalizeCandidate(node.getAttribute("title"));
|
|
145
266
|
if (fromTitle !== null) return fromTitle;
|
|
146
267
|
}
|
|
268
|
+
if (point !== null && point !== undefined) {
|
|
269
|
+
var fromCaret = pathFromCaret(point.x, point.y);
|
|
270
|
+
if (fromCaret !== null) return fromCaret;
|
|
271
|
+
}
|
|
147
272
|
return null;
|
|
148
273
|
}
|
|
149
274
|
|
|
@@ -157,7 +282,8 @@ window.__ModuleLoader__.load({
|
|
|
157
282
|
|
|
158
283
|
var STYLE_ID = "dsh-chat-link-actions-style";
|
|
159
284
|
var CSS = [
|
|
160
|
-
".dcla-
|
|
285
|
+
".dcla-layer{position:fixed;inset:0;z-index:1100;pointer-events:none;}",
|
|
286
|
+
".dcla-menu{position:fixed;z-index:1100;pointer-events:auto;box-sizing:border-box;padding:6px;min-width:200px;",
|
|
161
287
|
"max-width:min(320px,calc(100vw - 24px));background:var(--dsw-specific-menu,rgba(30,32,38,.96));",
|
|
162
288
|
"backdrop-filter:var(--dsw-menu-backdrop-filter,blur(20px) saturate(180%));",
|
|
163
289
|
"-webkit-backdrop-filter:var(--dsw-menu-backdrop-filter,blur(20px) saturate(180%));",
|
|
@@ -281,19 +407,19 @@ window.__ModuleLoader__.load({
|
|
|
281
407
|
|
|
282
408
|
function onViewportChange() {
|
|
283
409
|
if (open === null) return;
|
|
284
|
-
placeMenu(open.
|
|
410
|
+
placeMenu(open.panel, open.x, open.y);
|
|
285
411
|
}
|
|
286
412
|
|
|
287
413
|
/** Place the panel at the pointer, flipped inward at the viewport edges. */
|
|
288
|
-
function placeMenu(
|
|
414
|
+
function placeMenu(panel, x, y) {
|
|
289
415
|
var margin = 8;
|
|
290
|
-
var rect =
|
|
416
|
+
var rect = panel.getBoundingClientRect();
|
|
291
417
|
var left = x;
|
|
292
418
|
var top = y;
|
|
293
419
|
if (left + rect.width > window.innerWidth - margin) left = Math.max(margin, x - rect.width);
|
|
294
420
|
if (top + rect.height > window.innerHeight - margin) top = Math.max(margin, y - rect.height);
|
|
295
|
-
|
|
296
|
-
|
|
421
|
+
panel.style.left = Math.round(left) + "px";
|
|
422
|
+
panel.style.top = Math.round(top) + "px";
|
|
297
423
|
}
|
|
298
424
|
|
|
299
425
|
function paintActive() {
|
|
@@ -383,6 +509,7 @@ window.__ModuleLoader__.load({
|
|
|
383
509
|
|
|
384
510
|
var state = {};
|
|
385
511
|
state.layer = layer;
|
|
512
|
+
state.panel = built.panel;
|
|
386
513
|
state.buttons = built.buttons;
|
|
387
514
|
state.items = items;
|
|
388
515
|
state.index = 0;
|
|
@@ -391,7 +518,7 @@ window.__ModuleLoader__.load({
|
|
|
391
518
|
state.y = y;
|
|
392
519
|
state.restoreFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
393
520
|
open = state;
|
|
394
|
-
placeMenu(
|
|
521
|
+
placeMenu(built.panel, x, y);
|
|
395
522
|
document.addEventListener("pointerdown", onPointerDown, true);
|
|
396
523
|
document.addEventListener("keydown", onKeyDown, true);
|
|
397
524
|
document.addEventListener("contextmenu", onContextDuringMenu, true);
|
|
@@ -432,7 +559,7 @@ window.__ModuleLoader__.load({
|
|
|
432
559
|
function onContextMenu(event) {
|
|
433
560
|
if (!(event.target instanceof Element)) return;
|
|
434
561
|
var target = event.target;
|
|
435
|
-
var path = pathFromNode(target);
|
|
562
|
+
var path = pathFromNode(target, { x: event.clientX, y: event.clientY });
|
|
436
563
|
if (path === null) return;
|
|
437
564
|
event.preventDefault();
|
|
438
565
|
event.stopPropagation();
|
|
@@ -441,9 +568,10 @@ window.__ModuleLoader__.load({
|
|
|
441
568
|
|
|
442
569
|
/* ── plugin entry ───────────────────────────────────────────────────── */
|
|
443
570
|
|
|
444
|
-
var inject = ["slots"];
|
|
571
|
+
var inject = ["slots", "sessions"];
|
|
445
572
|
|
|
446
573
|
function apply(ctx) {
|
|
574
|
+
rootContext = ctx;
|
|
447
575
|
ctx.effect(function () {
|
|
448
576
|
document.addEventListener("contextmenu", onContextMenu, true);
|
|
449
577
|
return function () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-chat-link-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Right-click actions on file links in the DSH transcript: quote the path into the composer, or reveal the file in the OS file manager. Menus are drawn in the app's own style, never the system one.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|