dsh-code-server-app 0.2.14 → 0.3.7
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.en.md +85 -0
- package/README.md +96 -0
- package/assets/extensions/dshcs-editor-bridge/extension.js +539 -0
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +241 -0
- package/assets/extensions/dshcs-editor-bridge/lib/context-model.js +204 -0
- package/assets/extensions/dshcs-editor-bridge/lib/diff-model.js +123 -0
- package/assets/extensions/dshcs-editor-bridge/package.json +58 -0
- package/cordis.patch.yml +16 -2
- package/lib/bridge-observe.mjs +184 -0
- package/lib/bridge-session.mjs +142 -0
- package/lib/bridge-tools.mjs +374 -0
- package/lib/bridge.mjs +325 -0
- package/lib/dsh-resolve.mjs +91 -0
- package/lib/index.js +354 -46
- package/package.json +11 -3
- package/vendor/VENDOR.json +1 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
// dshcs-editor-bridge —— 编辑器桥的扩展侧(由 dsh-code-server-app 插件安装)
|
|
2
|
+
//
|
|
3
|
+
// 职责边界(与 host 侧 lib/bridge.mjs / bridge-tools.mjs 的分工):
|
|
4
|
+
// - **本扩展**是唯一知道"用户此刻看到什么"的一方,所以 /context 与 /diagnostics 的数据在这里采集;
|
|
5
|
+
// - **host** 负责鉴权、给 agent 提供工具、观察 agent 的写操作;
|
|
6
|
+
// - 方向 host → 扩展 的事件走 **轮询**(host 推环形缓冲,这里 GET /events?since=N)。
|
|
7
|
+
//
|
|
8
|
+
// 三条硬规则:
|
|
9
|
+
// 1. **只读**:本扩展只读编辑器状态,不写文件、不执行命令、不应用编辑(host 侧命名空间同理只读)。
|
|
10
|
+
// 2. **休眠而不是报错**:读不到 bridge.json(IDE 是用户自己起的旧实例 / 插件关了桥 / 已停止)
|
|
11
|
+
// 就什么都不做。状态栏不显示任何东西,更不弹通知。
|
|
12
|
+
// 3. **绝不覆盖未保存改动**:agent 改了文件而该文档是脏的就只告警 + 给 diff,由用户决定。
|
|
13
|
+
//
|
|
14
|
+
// 只依赖 `vscode` 与 Node 内置模块;纯逻辑在 lib/ 下且不 require('vscode'),便于单测。
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const vscode = require('vscode');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
POLL_INTERVAL_MS,
|
|
24
|
+
CONFIG_REREAD_MS,
|
|
25
|
+
createClient,
|
|
26
|
+
} = require('./lib/bridge-client.js');
|
|
27
|
+
const { createProjector } = require('./lib/context-model.js');
|
|
28
|
+
const { createDiffCache, describeChange } = require('./lib/diff-model.js');
|
|
29
|
+
|
|
30
|
+
/** 状态栏项(仅在桥连通时显示)。 */
|
|
31
|
+
let statusBar = null;
|
|
32
|
+
/** 输出通道(除非用户显式打开,绝不主动弹)。 */
|
|
33
|
+
let output = null;
|
|
34
|
+
/** 桥客户端(整个扩展生命周期一个)。 */
|
|
35
|
+
let client = null;
|
|
36
|
+
/** 轮询定时器。 */
|
|
37
|
+
let pollTimer = null;
|
|
38
|
+
/** 上次成功轮询的时间(状态栏 tooltip 用)。 */
|
|
39
|
+
let lastPollAt = 0;
|
|
40
|
+
/** 是否已经确认过 host 端点可达(避免把"IDE 刚起、扩展先加载"误判为断线)。 */
|
|
41
|
+
let connected = false;
|
|
42
|
+
/** 诊断集合缓存:host 请求时现算,这里只做"有没有变化"的计数上报。 */
|
|
43
|
+
let lastDiagnosticCount = -1;
|
|
44
|
+
/** pathspec → 上次已知的磁盘文本(用于没打开的文件也能给出 old 侧)。 */
|
|
45
|
+
const diffCache = createDiffCache();
|
|
46
|
+
/** diff 左栏(改动前)的文本存放处:URI 里只放 key,文本放这里。
|
|
47
|
+
* 为什么不把文本塞进 URI:一份文件几百 KB,URI 会被 workbench 截断,而且每开一次 diff 都要
|
|
48
|
+
* 重新拼一遍;用 key + TextDocumentContentProvider 又短又稳。 */
|
|
49
|
+
const diffTextStore = new Map();
|
|
50
|
+
/** 同一个文件的 diff tab 不重复开。 */
|
|
51
|
+
const openDiffTabs = new Map();
|
|
52
|
+
|
|
53
|
+
/** 把"改动前的文本"登记进 store,返回它的 docId。 */
|
|
54
|
+
function registerDiffText(text) {
|
|
55
|
+
const docId = crypto.createHash('sha256').update(text ?? '').digest('hex');
|
|
56
|
+
diffTextStore.set(docId, text ?? '');
|
|
57
|
+
// 有界:只保留最近 32 份(够回看几步;超出就丢最旧的)。
|
|
58
|
+
while (diffTextStore.size > 32) {
|
|
59
|
+
const oldest = diffTextStore.keys().next();
|
|
60
|
+
if (oldest.done === true) break;
|
|
61
|
+
diffTextStore.delete(oldest.value);
|
|
62
|
+
}
|
|
63
|
+
return docId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 只读虚拟文档:diff 的左栏。 */
|
|
67
|
+
const oldSideProvider = {
|
|
68
|
+
provideTextDocumentContent(uri) {
|
|
69
|
+
return diffTextStore.get(uri.path) ?? '';
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function log(message) {
|
|
74
|
+
if (output !== null) output.appendLine(`[${new Date().toISOString()}] ${message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 取扩展所在目录(桥配置与它同级:extensionsDir/.dshcs-bridge/)。 */
|
|
78
|
+
function extensionDir() {
|
|
79
|
+
// __dirname = <extensionsDir>/dshcs-editor-bridge/lib → 上溯两级即 extensionsDir
|
|
80
|
+
return path.resolve(__dirname, '..', '..');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 是否落在某个工作区文件夹内(不能把 workspaceFolder 之外的路径喂给 DSH)。 */
|
|
84
|
+
function isInWorkspace(fsPath) {
|
|
85
|
+
try {
|
|
86
|
+
for (const folder of vscode.workspace.workspaceFolders ?? []) {
|
|
87
|
+
const root = folder.uri.fsPath;
|
|
88
|
+
if (typeof root !== 'string' || root === '') continue;
|
|
89
|
+
const rel = path.relative(root, fsPath);
|
|
90
|
+
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) return true;
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// 探测失败 → 保守当作不包含
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const projector = createProjector(isInWorkspace);
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------- 编辑器状态采集
|
|
101
|
+
|
|
102
|
+
/** 一个文档的稳定标识:有磁盘路径用路径,否则用 untitled:<n>。 */
|
|
103
|
+
function documentId(doc) {
|
|
104
|
+
if (doc === null || doc === undefined) return null;
|
|
105
|
+
if (doc.uri === undefined || doc.uri === null) return null;
|
|
106
|
+
if (doc.uri.scheme === 'untitled') return `untitled:${doc.uri.path}`;
|
|
107
|
+
if (doc.uri.scheme !== 'file') return `${doc.uri.scheme}:${doc.uri.path}`;
|
|
108
|
+
return doc.uri.fsPath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 未保存的改动涉及多少行(用于让模型感知"改动量级")。 */
|
|
112
|
+
function countDirtyLines(doc) {
|
|
113
|
+
try {
|
|
114
|
+
// 存盘版本与当前缓冲区逐行比:只数"内容不同的行"这一个廉价近似。
|
|
115
|
+
const current = doc.getText();
|
|
116
|
+
const currentLines = current.split(/\r\n|\r|\n/);
|
|
117
|
+
if (doc.isUntitled === true) return currentLines.length;
|
|
118
|
+
if (doc.isDirty !== true) return 0;
|
|
119
|
+
// 没有磁盘基线可比时,退回"当前行数"作为量级(不是精确值,但足以提示"有未保存内容")。
|
|
120
|
+
return currentLines.length;
|
|
121
|
+
} catch {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 拍平一个文档。 */
|
|
127
|
+
function documentSnapshot(doc) {
|
|
128
|
+
const id = documentId(doc);
|
|
129
|
+
return {
|
|
130
|
+
path: doc.uri.scheme === 'file' ? doc.uri.fsPath : (id ?? null),
|
|
131
|
+
name: id ?? null,
|
|
132
|
+
language: typeof doc.languageId === 'string' ? doc.languageId : null,
|
|
133
|
+
dirty: doc.isDirty === true,
|
|
134
|
+
untitled: doc.isUntitled === true,
|
|
135
|
+
unsavedLines: countDirtyLines(doc),
|
|
136
|
+
version: Number.isSafeInteger(doc.version) ? doc.version : null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 拍平工作区里全部诊断(`languages.getDiagnostics()`,核心已去抖 50ms)。 */
|
|
141
|
+
function diagnosticsSnapshot() {
|
|
142
|
+
const groups = [];
|
|
143
|
+
for (const [uri, items] of vscode.languages.getDiagnostics()) {
|
|
144
|
+
if (uri.scheme !== 'file') continue; // 虚拟文档/untitled 的诊断没有可交出去的文件路径
|
|
145
|
+
const fsPath = uri.fsPath;
|
|
146
|
+
if (!isInWorkspace(fsPath)) continue;
|
|
147
|
+
groups.push({
|
|
148
|
+
path: fsPath,
|
|
149
|
+
items: (items ?? []).map((item) => ({
|
|
150
|
+
line: item.range.start.line + 1, // VS Code 0 基 → 对外统一 1 基
|
|
151
|
+
column: item.range.start.character + 1,
|
|
152
|
+
severity: item.severity,
|
|
153
|
+
message: typeof item.message === 'string' ? item.message : '',
|
|
154
|
+
source: typeof item.source === 'string' ? item.source : undefined,
|
|
155
|
+
code: item.code === undefined || item.code === null
|
|
156
|
+
? undefined
|
|
157
|
+
: (typeof item.code === 'object' ? item.code.value : item.code),
|
|
158
|
+
})),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return groups;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 活动编辑器快照(含选区与选中文本)。 */
|
|
165
|
+
function activeSnapshot() {
|
|
166
|
+
const editor = vscode.window.activeTextEditor;
|
|
167
|
+
if (editor === undefined || editor === null) return null;
|
|
168
|
+
const doc = editor.document;
|
|
169
|
+
const base = documentSnapshot(doc);
|
|
170
|
+
const selection = editor.selection;
|
|
171
|
+
const result = {
|
|
172
|
+
path: base.path,
|
|
173
|
+
name: base.name,
|
|
174
|
+
language: base.language,
|
|
175
|
+
dirty: base.dirty,
|
|
176
|
+
selection: null,
|
|
177
|
+
selectedText: '',
|
|
178
|
+
};
|
|
179
|
+
if (selection !== undefined && selection !== null && selection.isEmpty === false) {
|
|
180
|
+
result.selection = {
|
|
181
|
+
startLine: selection.start.line + 1, // 1 基
|
|
182
|
+
startColumn: selection.start.character + 1,
|
|
183
|
+
endLine: selection.end.line + 1,
|
|
184
|
+
endColumn: selection.end.character + 1,
|
|
185
|
+
};
|
|
186
|
+
try {
|
|
187
|
+
result.selectedText = doc.getText(selection);
|
|
188
|
+
} catch {
|
|
189
|
+
result.selectedText = '';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------- host → 扩展:事件
|
|
196
|
+
|
|
197
|
+
/** 活动编辑器快照(含选区与选中文本)。 */
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* agent 改了一个文件:给出 old/new 两侧并开 diff。
|
|
201
|
+
*
|
|
202
|
+
* 时序很关键:事件到达时 VS Code 的磁盘 watcher 可能还没把新内容灌进缓冲区,
|
|
203
|
+
* 所以**先同步取缓冲区文本**(= 改动前),再去读磁盘(= 改动后)。
|
|
204
|
+
*/
|
|
205
|
+
async function handleAgentEdit(event) {
|
|
206
|
+
const target = typeof event.path === 'string' && event.path !== '' ? event.path : null;
|
|
207
|
+
if (target === null) return;
|
|
208
|
+
if (!isInWorkspace(target)) {
|
|
209
|
+
log(`跳过工作区外的改动:${target}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const uri = vscode.Uri.file(target);
|
|
213
|
+
|
|
214
|
+
// 1) old 侧:优先"此刻的缓冲区"(还未被磁盘改动刷新);没有打开的文档就退回缓存。
|
|
215
|
+
let oldText = diffCache.recall(target);
|
|
216
|
+
let dirty = false;
|
|
217
|
+
const open = vscode.workspace.textDocuments.find((doc) => documentId(doc) === target);
|
|
218
|
+
if (open !== undefined) {
|
|
219
|
+
try {
|
|
220
|
+
oldText = open.getText();
|
|
221
|
+
} catch {
|
|
222
|
+
// 保留缓存值
|
|
223
|
+
}
|
|
224
|
+
dirty = open.isDirty === true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2) new 侧:磁盘内容。
|
|
228
|
+
let newText = null;
|
|
229
|
+
try {
|
|
230
|
+
const bytes = await vscode.workspace.fs.readFile(uri);
|
|
231
|
+
newText = Buffer.from(bytes).toString('utf8');
|
|
232
|
+
} catch {
|
|
233
|
+
newText = null; // 可能被删了
|
|
234
|
+
}
|
|
235
|
+
diffCache.remember(target, newText === null ? '' : newText, Date.now());
|
|
236
|
+
|
|
237
|
+
const decision = describeChange(oldText, newText);
|
|
238
|
+
if (decision.show === false) {
|
|
239
|
+
log(`agent 改动 ${target}:${decision.reason} → 不打扰`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 3) 开 diff(**左 = 改动前的虚拟文档,右 = 真实的磁盘文件**)。
|
|
244
|
+
// 右栏刻意用 file: URI:这样用户在 diff 里按"撤销/编辑"落到的是真文件,VS Code 的
|
|
245
|
+
// 常规编辑与撤销栈全部生效,我们不需要自己做任何写回。
|
|
246
|
+
const docId = registerDiffText(oldText ?? '');
|
|
247
|
+
const left = vscode.Uri.from({ scheme: 'dshcs-old', path: docId });
|
|
248
|
+
const right = uri;
|
|
249
|
+
// 行数统计可能是 null(只有一侧时无从得知)—— 那时只显示原因,不显示数字。
|
|
250
|
+
const delta = (decision.added === null || decision.removed === null)
|
|
251
|
+
? ''
|
|
252
|
+
: `${decision.added > 0 ? ` +${decision.added}` : ''}${decision.removed > 0 ? ` -${decision.removed}` : ''}`;
|
|
253
|
+
const title = `${path.basename(target)} ← DSH 改动 (${decision.reason}${delta})`;
|
|
254
|
+
const previewTab = dirty !== true; // 脏缓冲区时用正式 tab(用户要长时间对照)
|
|
255
|
+
try {
|
|
256
|
+
const existing = openDiffTabs.get(target);
|
|
257
|
+
if (existing !== undefined) {
|
|
258
|
+
try {
|
|
259
|
+
existing.dispose();
|
|
260
|
+
} catch {
|
|
261
|
+
// 已被用户关掉
|
|
262
|
+
}
|
|
263
|
+
openDiffTabs.delete(target);
|
|
264
|
+
}
|
|
265
|
+
await vscode.commands.executeCommand('vscode.diff', left, right, title, { preview: previewTab, preserveFocus: true });
|
|
266
|
+
// 记下这次开的 tab(下次同文件先关旧的,避免堆一屏同名 diff);有界,避免长会话里泄漏。
|
|
267
|
+
try {
|
|
268
|
+
const tabs = vscode.window.tabGroups.all.flatMap((group) => group.tabs);
|
|
269
|
+
const ours = tabs.find((tab) => {
|
|
270
|
+
const input = tab.input;
|
|
271
|
+
return input !== undefined && input !== null
|
|
272
|
+
&& input.original !== undefined && input.original.scheme === 'dshcs-old'
|
|
273
|
+
&& input.modified !== undefined && input.modified.fsPath === target;
|
|
274
|
+
});
|
|
275
|
+
if (ours !== undefined) {
|
|
276
|
+
openDiffTabs.set(target, ours);
|
|
277
|
+
while (openDiffTabs.size > 8) {
|
|
278
|
+
const oldest = openDiffTabs.keys().next();
|
|
279
|
+
if (oldest.done === true) break;
|
|
280
|
+
openDiffTabs.delete(oldest.value);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// 拿不到 tab 列表(版本差异)不影响 diff 已经打开这件事
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
log(`打开 diff 失败(${target}):${error && error.message ? error.message : error}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 4) 脏缓冲区:只告警,绝不覆盖。
|
|
291
|
+
if (dirty === true) {
|
|
292
|
+
const choice = await vscode.window.showWarningMessage(
|
|
293
|
+
`${path.basename(target)} 在编辑器里有未保存的改动,而 DSH 刚改了磁盘上的同名文件。`,
|
|
294
|
+
{ modal: false },
|
|
295
|
+
'查看差异',
|
|
296
|
+
'忽略',
|
|
297
|
+
);
|
|
298
|
+
if (choice === '查看差异') {
|
|
299
|
+
try {
|
|
300
|
+
await vscode.commands.executeCommand('vscode.diff', left, right, title, { preview: false });
|
|
301
|
+
} catch {
|
|
302
|
+
// 已经开过了
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 处理一条 host 事件。 */
|
|
309
|
+
async function handleEvent(event) {
|
|
310
|
+
if (event === null || typeof event !== 'object') return;
|
|
311
|
+
switch (event.kind) {
|
|
312
|
+
case 'agent-edit':
|
|
313
|
+
await handleAgentEdit(event);
|
|
314
|
+
break;
|
|
315
|
+
case 'diagnostics-changed':
|
|
316
|
+
// 目前 host 不推这条;留着是为了将来"改完提示 agent 复查"。
|
|
317
|
+
log('诊断有变化(host 上报)');
|
|
318
|
+
break;
|
|
319
|
+
default:
|
|
320
|
+
log(`未知事件 ${String(event.kind)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------- 轮询
|
|
325
|
+
|
|
326
|
+
async function pollOnce() {
|
|
327
|
+
if (client === null) return;
|
|
328
|
+
if (client.isDormant()) {
|
|
329
|
+
// 休眠:读不到配置(桥关着 / IDE 是自己起的旧实例)。静默,状态栏也收起来。
|
|
330
|
+
if (connected) {
|
|
331
|
+
connected = false;
|
|
332
|
+
log('桥配置消失 → 进入休眠');
|
|
333
|
+
}
|
|
334
|
+
updateStatusBar();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// 一趟来回:推状态 + 取事件。诊断只在有文档打开时才现算(关掉面板时不做无用功)。
|
|
338
|
+
const docs = vscode.workspace.textDocuments;
|
|
339
|
+
const hasEditor = docs.length > 0;
|
|
340
|
+
const diagnostics = hasEditor ? diagnosticsSnapshot() : [];
|
|
341
|
+
const payload = {
|
|
342
|
+
context: projector.context({
|
|
343
|
+
active: activeSnapshot(),
|
|
344
|
+
documents: docs.map((doc) => documentSnapshot(doc)),
|
|
345
|
+
diagnostics,
|
|
346
|
+
}),
|
|
347
|
+
diagnostics,
|
|
348
|
+
at: Date.now(),
|
|
349
|
+
};
|
|
350
|
+
const result = await client.sync(payload);
|
|
351
|
+
if (result.ok !== true) {
|
|
352
|
+
if (connected) {
|
|
353
|
+
// 401 = 令牌轮换(host 重启);503/0 = IDE 停了。都只降级显示,不弹窗。
|
|
354
|
+
connected = false;
|
|
355
|
+
log(`同步失败(${result.status ?? 0}):${result.error ?? '未知'};等待恢复`);
|
|
356
|
+
updateStatusBar();
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!connected) {
|
|
361
|
+
connected = true;
|
|
362
|
+
log(`已连接宿主 ${client.config.url}`);
|
|
363
|
+
updateStatusBar();
|
|
364
|
+
}
|
|
365
|
+
lastPollAt = Date.now();
|
|
366
|
+
for (const event of result.events ?? []) {
|
|
367
|
+
await handleEvent(event);
|
|
368
|
+
}
|
|
369
|
+
if ((result.events ?? []).length > 0) client.persist();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function startPolling(context) {
|
|
373
|
+
if (pollTimer !== null) return;
|
|
374
|
+
// 首次延迟:让 IDE 的扩展宿主把工作区索引起来,免得一上来就报一堆诊断。
|
|
375
|
+
pollTimer = setInterval(() => {
|
|
376
|
+
pollOnce().catch((error) => log(`轮询异常:${error && error.message ? error.message : error}`));
|
|
377
|
+
}, POLL_INTERVAL_MS);
|
|
378
|
+
context.subscriptions.push({ dispose() { if (pollTimer !== null) { clearInterval(pollTimer); pollTimer = null; } } });
|
|
379
|
+
// 立刻跑一次,别等 600ms。
|
|
380
|
+
setTimeout(() => { pollOnce().catch(() => {}); }, 1200);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function updateStatusBar() {
|
|
384
|
+
if (statusBar === null) return;
|
|
385
|
+
if (connected && client !== null && client.config !== null) {
|
|
386
|
+
statusBar.text = '$(plug) DSH';
|
|
387
|
+
statusBar.tooltip = `编辑器桥已连接:${client.config.url}\n上次轮询:${lastPollAt === 0 ? '—' : new Date(lastPollAt).toLocaleTimeString()}\n点击查看日志`;
|
|
388
|
+
statusBar.command = 'dsh-code-server.showBridgeLog';
|
|
389
|
+
statusBar.show();
|
|
390
|
+
} else {
|
|
391
|
+
statusBar.hide();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------- 命令
|
|
396
|
+
|
|
397
|
+
/** 选中内容 → DSH(编辑器→DSH 的主入口)。 */
|
|
398
|
+
async function askAboutSelection() {
|
|
399
|
+
const editor = vscode.window.activeTextEditor;
|
|
400
|
+
if (editor === undefined || editor === null) {
|
|
401
|
+
vscode.window.showInformationMessage('没有活动的编辑器。');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (client === null || client.isDormant()) {
|
|
405
|
+
vscode.window.showInformationMessage('编辑器桥未启用:请在 DSH 里打开 Code Server 标签后重试。');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const doc = editor.document;
|
|
409
|
+
const selection = editor.selection;
|
|
410
|
+
const hasSelection = selection !== undefined && selection !== null && selection.isEmpty === false;
|
|
411
|
+
const selectedText = hasSelection ? doc.getText(selection) : '';
|
|
412
|
+
const question = await vscode.window.showInputBox({
|
|
413
|
+
title: hasSelection ? '问 DSH(带选中内容)' : '问 DSH(当前文件)',
|
|
414
|
+
prompt: `${path.basename(doc.fileName)}${hasSelection ? ` 第 ${selection.start.line + 1}-${selection.end.line + 1} 行` : ''}`,
|
|
415
|
+
placeHolder: '例如:这段逻辑有什么问题?',
|
|
416
|
+
ignoreFocusOut: true,
|
|
417
|
+
});
|
|
418
|
+
if (question === undefined || question.trim() === '') return;
|
|
419
|
+
try {
|
|
420
|
+
const result = await client.ask({
|
|
421
|
+
text: question.trim(),
|
|
422
|
+
file: doc.uri.scheme === 'file' ? doc.uri.fsPath : null,
|
|
423
|
+
lineStart: hasSelection ? selection.start.line + 1 : (doc.isDirty ? null : editor.selection.active.line + 1),
|
|
424
|
+
lineEnd: hasSelection ? selection.end.line + 1 : null,
|
|
425
|
+
selection: selectedText === '' ? null : selectedText,
|
|
426
|
+
languageId: doc.languageId ?? null,
|
|
427
|
+
});
|
|
428
|
+
if (result.ok === true) {
|
|
429
|
+
vscode.window.setStatusBarMessage('$(check) 已发送给 DSH', 3000);
|
|
430
|
+
log(`已投递编辑器消息(session=${result.sessionId ?? '?'})`);
|
|
431
|
+
} else {
|
|
432
|
+
vscode.window.showWarningMessage(`未投递:${result.error ?? '未知原因'}`);
|
|
433
|
+
}
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const status = error && error.status;
|
|
436
|
+
if (status === 409) vscode.window.showWarningMessage('DSH 里没有可投递的会话:请先打开或新建一个会话。');
|
|
437
|
+
else if (status === 401 || status === 503) vscode.window.showWarningMessage('编辑器桥尚未就绪,请稍后重试。');
|
|
438
|
+
else vscode.window.showWarningMessage(`投递失败:${error && error.message ? error.message : error}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** 整个文件 → DSH(不需要选中)。 */
|
|
443
|
+
async function askAboutFile() {
|
|
444
|
+
await askAboutSelection();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function showBridgeLog() {
|
|
448
|
+
if (output !== null) output.show(true);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---------------------------------------------------------------- 激活
|
|
452
|
+
|
|
453
|
+
function activate(context) {
|
|
454
|
+
output = vscode.window.createOutputChannel('DSH Editor Bridge');
|
|
455
|
+
context.subscriptions.push(output);
|
|
456
|
+
|
|
457
|
+
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
|
|
458
|
+
context.subscriptions.push(statusBar);
|
|
459
|
+
|
|
460
|
+
// diff 左栏的只读虚拟文档(`dshcs-old:`)。必须注册,否则 vscode.diff 左栏是空的。
|
|
461
|
+
context.subscriptions.push(
|
|
462
|
+
vscode.workspace.registerTextDocumentContentProvider('dshcs-old', oldSideProvider),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
client = createClient({ extensionsDir: extensionDir() });
|
|
466
|
+
if (client.refresh() === null) {
|
|
467
|
+
log('未找到桥配置(休眠)。DSH 插件启用编辑器桥并启动 IDE 后,这里会自动连上。');
|
|
468
|
+
} else {
|
|
469
|
+
client.restore();
|
|
470
|
+
log(`发现桥配置:${client.config.url}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// host 请求时才现算,这里只维护"上次计数",用于日志与将来的变化上报。
|
|
474
|
+
context.subscriptions.push(vscode.languages.onDidChangeDiagnostics(() => {
|
|
475
|
+
try {
|
|
476
|
+
const total = diagnosticsSnapshot().reduce((sum, group) => sum + group.items.length, 0);
|
|
477
|
+
if (lastDiagnosticCount >= 0 && total !== lastDiagnosticCount) {
|
|
478
|
+
log(`诊断变化:${lastDiagnosticCount} → ${total}`);
|
|
479
|
+
}
|
|
480
|
+
lastDiagnosticCount = total;
|
|
481
|
+
} catch {
|
|
482
|
+
// 忽略
|
|
483
|
+
}
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
// 文档关闭时把最后内容留在缓存里(下次 agent 改它就能给出 old 侧)。
|
|
487
|
+
context.subscriptions.push(vscode.workspace.onDidCloseTextDocument((doc) => {
|
|
488
|
+
const id = documentId(doc);
|
|
489
|
+
if (id === null || doc.uri.scheme !== 'file') return;
|
|
490
|
+
try {
|
|
491
|
+
diffCache.remember(id, doc.getText(), Date.now());
|
|
492
|
+
} catch {
|
|
493
|
+
// 忽略
|
|
494
|
+
}
|
|
495
|
+
}));
|
|
496
|
+
|
|
497
|
+
// **方向说明(改之前先读)**:扩展宿主里**没有** HTTP 服务器 —— 它是 VS Code server 的
|
|
498
|
+
// 一个子进程,不监听任何端口。所以 host **不能**反向请求本扩展拿编辑器状态。
|
|
499
|
+
// 实际方向是:本扩展在每次轮询里 `POST /api/code-server/bridge/sync`,把状态推上去、
|
|
500
|
+
// 同时取回 host 的待处理事件(agent 改了哪个文件)。host 侧缓存状态供 agent 工具读。
|
|
501
|
+
// 见 lib/bridge-client.js 的 sync() 与 lib/bridge.mjs 顶部的通道说明。
|
|
502
|
+
|
|
503
|
+
// 命令
|
|
504
|
+
context.subscriptions.push(vscode.commands.registerCommand('dsh-code-server.askAboutSelection', () => {
|
|
505
|
+
askAboutSelection().catch((error) => log(`askAboutSelection 异常:${error && error.message ? error.message : error}`));
|
|
506
|
+
}));
|
|
507
|
+
context.subscriptions.push(vscode.commands.registerCommand('dsh-code-server.askAboutFile', () => {
|
|
508
|
+
askAboutFile().catch((error) => log(`askAboutFile 异常:${error && error.message ? error.message : error}`));
|
|
509
|
+
}));
|
|
510
|
+
context.subscriptions.push(vscode.commands.registerCommand('dsh-code-server.showBridgeLog', () => showBridgeLog()));
|
|
511
|
+
|
|
512
|
+
// 定期重读配置(令牌/端口轮换后最多 CONFIG_REREAD_MS 恢复)。
|
|
513
|
+
const refreshTimer = setInterval(() => {
|
|
514
|
+
if (client === null) return;
|
|
515
|
+
const before = client.config === null ? null : client.config.url;
|
|
516
|
+
const next = client.refresh();
|
|
517
|
+
const after = next === null ? null : next.url;
|
|
518
|
+
if (before !== after) {
|
|
519
|
+
log(`桥目标变化:${before ?? '(休眠)'} → ${after ?? '(休眠)'}`);
|
|
520
|
+
if (after !== null) client.restore();
|
|
521
|
+
updateStatusBar();
|
|
522
|
+
}
|
|
523
|
+
}, CONFIG_REREAD_MS);
|
|
524
|
+
context.subscriptions.push({ dispose() { clearInterval(refreshTimer); } });
|
|
525
|
+
|
|
526
|
+
startPolling(context);
|
|
527
|
+
updateStatusBar();
|
|
528
|
+
log('dshcs-editor-bridge 已激活');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function deactivate() {
|
|
532
|
+
if (pollTimer !== null) {
|
|
533
|
+
clearInterval(pollTimer);
|
|
534
|
+
pollTimer = null;
|
|
535
|
+
}
|
|
536
|
+
if (client !== null) client.persist();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
module.exports = { activate, deactivate };
|