dsh-vscode-mode 0.1.44 → 0.1.46

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/lib/index.js CHANGED
@@ -112,15 +112,18 @@ async function refreshMcp(ctx, id) {
112
112
  /**
113
113
  * dsh-vscode-mode shared — 快捷键命令目录与默认键位(双面契约)。
114
114
  * 纯数据模块:host(settings schema 默认值)与 client(执行匹配/设置页)共用。
115
- * 键位格式:修饰符 + 主键,`+` 连接,如 `Ctrl+Shift+F`;空串 = 未绑定。
115
+ * 键位格式:修饰符 + 主键,`+` 连接,如 `Ctrl+Shift+F`;空串 = 未绑定;
116
+ * `|` 连接多个候选(任一命中即触发),如 `Alt+ArrowLeft|Ctrl+Alt+-`。
116
117
  * 作者 ddj 2026年08月26号
117
118
  */
118
- /** 命令 id → 默认键位。命令目录以此为准,新增命令只需加一项。 */
119
+ /** 命令 id → 默认键位(可为多候选)。命令目录以此为准,新增命令只需加一项。 */
119
120
  const KEYBINDING_DEFAULTS = {
120
121
  "edrv.save": "Ctrl+S",
121
122
  "edrv.quickOpen": "Ctrl+P",
122
123
  "edrv.toggleSidebar": "Ctrl+B",
123
- "edrv.searchInFiles": "Ctrl+Shift+F"
124
+ "edrv.searchInFiles": "Ctrl+Shift+F",
125
+ "edrv.navigateBack": "Alt+ArrowLeft|Ctrl+Alt+-",
126
+ "edrv.navigateForward": "Alt+ArrowRight|Ctrl+Shift+-"
124
127
  };
125
128
  //#endregion
126
129
  //#region src/fileOpenSettings.ts
@@ -5757,6 +5760,7 @@ function createLspClient(transport, events = {}) {
5757
5760
  const pending = /* @__PURE__ */ new Map();
5758
5761
  let serverCapabilities = {
5759
5762
  definition: false,
5763
+ declaration: false,
5760
5764
  references: false,
5761
5765
  documentSymbol: false,
5762
5766
  workspaceSymbol: false,
@@ -5891,6 +5895,7 @@ function createLspClient(transport, events = {}) {
5891
5895
  didSave: true
5892
5896
  },
5893
5897
  definition: { dynamicRegistration: false },
5898
+ declaration: { dynamicRegistration: false },
5894
5899
  references: { dynamicRegistration: false },
5895
5900
  documentSymbol: { dynamicRegistration: false },
5896
5901
  hover: { dynamicRegistration: false },
@@ -5910,6 +5915,7 @@ function createLspClient(transport, events = {}) {
5910
5915
  const legend = caps.semanticTokensProvider?.legend;
5911
5916
  serverCapabilities = {
5912
5917
  definition: Boolean(caps.definitionProvider),
5918
+ declaration: Boolean(caps.declarationProvider),
5913
5919
  references: Boolean(caps.referencesProvider),
5914
5920
  documentSymbol: Boolean(caps.documentSymbolProvider),
5915
5921
  workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
@@ -5963,6 +5969,73 @@ function pathToFileUri(absPath) {
5963
5969
  return pathToFileURL(normalized).toString();
5964
5970
  }
5965
5971
  //#endregion
5972
+ //#region src/lsp/derive.ts
5973
+ /** file:// URI(或裸路径)→ 归一化绝对路径(小写、/ 分隔)。 */
5974
+ function normAbs(uri) {
5975
+ const path = String(uri ?? "").replace(/^file:\/\/\/?/, "");
5976
+ return decodeURIComponent(path).replace(/\\/g, "/").replace(/^\/([A-Za-z]:)/, "$1").toLowerCase();
5977
+ }
5978
+ /** 取文档 text 在 LSP range(0-based)内的切片。 */
5979
+ function sliceRange(text, range) {
5980
+ const lines = text.split(/\r\n|\r|\n/);
5981
+ const start = range.start;
5982
+ const end = range.end;
5983
+ const startLine = Math.max(0, start.line);
5984
+ const endLine = Math.max(startLine, end.line);
5985
+ if (startLine >= lines.length || endLine >= lines.length) return "";
5986
+ const startChar = Math.max(0, start.character);
5987
+ if (startLine === endLine) return lines[startLine].slice(startChar, Math.max(startChar, end.character));
5988
+ const first = lines[startLine].slice(startChar);
5989
+ const middle = lines.slice(startLine + 1, endLine);
5990
+ const last = lines[endLine].slice(0, Math.max(0, end.character));
5991
+ return [
5992
+ first,
5993
+ ...middle,
5994
+ last
5995
+ ].join("\n");
5996
+ }
5997
+ /** LSP position 是否落在 range 内(start 包含、end 排除;零宽按点匹配)。 */
5998
+ function posInLspRange(pos, range) {
5999
+ const start = range.start;
6000
+ const end = range.end ?? start;
6001
+ if (start.line === end.line && start.character === end.character) return pos.line === start.line && pos.character === start.character;
6002
+ if (pos.line < start.line || pos.line > end.line) return false;
6003
+ if (pos.line === start.line && pos.character < start.character) return false;
6004
+ if (pos.line === end.line && pos.character >= end.character) return false;
6005
+ return true;
6006
+ }
6007
+ /** 两个 range 是否完全一致。 */
6008
+ function sameRange(a, b) {
6009
+ return a.start.line === b.start.line && a.start.character === b.start.character && a.end.line === b.end.line && a.end.character === b.end.character;
6010
+ }
6011
+ /** range 长度(行/列均按 0-based 差估算,仅用于取"最小包裹"排序)。 */
6012
+ function rangeSize(r) {
6013
+ return (r.end.line - r.start.line) * 1e6 + (r.end.line === r.start.line ? r.end.character - r.start.character : r.end.character);
6014
+ }
6015
+ /**
6016
+ * 从引用列表推导定义位置。
6017
+ * @author ddj 2026年09月02号
6018
+ * @param queryPos 查询位置(LSP 0-based)
6019
+ * @param locations references(includeDeclaration) 结果
6020
+ * @param docText 当前文档全文(词文本一致校验)
6021
+ * @param docAbsPath 当前文档绝对路径(root+相对路径,与 loc.uri 归一化后比较)
6022
+ * @returns 推导出的定义位置;无法确定返回 null
6023
+ */
6024
+ function deriveDefinitionFromLocations(queryPos, locations, docText, docAbsPath) {
6025
+ if (!locations.length) return null;
6026
+ const docAbs = normAbs(docAbsPath);
6027
+ const containing = locations.filter((loc) => posInLspRange(queryPos, loc.range)).sort((a, b) => rangeSize(a.range) - rangeSize(b.range))[0];
6028
+ if (!containing) return null;
6029
+ const queryWord = sliceRange(docText, containing.range);
6030
+ if (!queryWord) return null;
6031
+ const candidates = locations.filter((loc) => {
6032
+ return normAbs(loc.uri) === docAbs && sliceRange(docText, loc.range) === queryWord;
6033
+ });
6034
+ if (!candidates.length) return null;
6035
+ const first = candidates[0];
6036
+ return sameRange(first.range, containing.range) ? containing : first;
6037
+ }
6038
+ //#endregion
5966
6039
  //#region src/lsp/server.ts
5967
6040
  /**
5968
6041
  * dsh-vscode-mode host — 单语言服务器会话(绑定 工作区根 + 语言)。
@@ -5987,6 +6060,7 @@ function createLspServer(spec, root, languageId, logger) {
5987
6060
  let client = null;
5988
6061
  let capabilities = {
5989
6062
  definition: false,
6063
+ declaration: false,
5990
6064
  references: false,
5991
6065
  documentSymbol: false,
5992
6066
  workspaceSymbol: false,
@@ -6170,13 +6244,42 @@ function createLspServer(spec, root, languageId, logger) {
6170
6244
  async definition(path, line, character) {
6171
6245
  const doc = docs.get(path);
6172
6246
  if (!doc || !await readyWait) return [];
6173
- return normalizeLocations(await ensureClient().request("textDocument/definition", {
6247
+ const client = ensureClient();
6248
+ const position = {
6249
+ line,
6250
+ character
6251
+ };
6252
+ const direct = normalizeLocations(await client.request("textDocument/definition", {
6174
6253
  textDocument: { uri: doc.uri },
6175
- position: {
6176
- line,
6177
- character
6178
- }
6254
+ position
6179
6255
  }));
6256
+ if (direct.length) return direct;
6257
+ if (capabilities.declaration) try {
6258
+ const decl = normalizeLocations(await client.request("textDocument/declaration", {
6259
+ textDocument: { uri: doc.uri },
6260
+ position
6261
+ }));
6262
+ if (decl.length) {
6263
+ log("definition fallback → declaration (" + path + ":" + line + ":" + character + ")");
6264
+ return decl;
6265
+ }
6266
+ } catch (error) {
6267
+ log("declaration request failed: " + String(error));
6268
+ }
6269
+ try {
6270
+ const derived = deriveDefinitionFromLocations(position, normalizeLocations(await client.request("textDocument/references", {
6271
+ textDocument: { uri: doc.uri },
6272
+ position,
6273
+ context: { includeDeclaration: true }
6274
+ })), doc.text, doc.uri);
6275
+ if (derived) {
6276
+ log("definition fallback → references-derived (" + path + ":" + line + ":" + character + ")");
6277
+ return [derived];
6278
+ }
6279
+ } catch (error) {
6280
+ log("references fallback failed: " + String(error));
6281
+ }
6282
+ return [];
6180
6283
  },
6181
6284
  async references(path, line, character, includeDeclaration) {
6182
6285
  const doc = docs.get(path);