dsh-vscode-mode 0.15.6 → 0.15.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/lib/index.js CHANGED
@@ -13961,6 +13961,19 @@ function pathToFileUri(absPath) {
13961
13961
  return pathToFileURL(normalized).toString();
13962
13962
  }
13963
13963
  /**
13964
+ * file:// URI → 绝对路径(Windows file:///C:/x → C:\x)。
13965
+ * @author ddj 2026年08月27号
13966
+ * @param uri file:// URI
13967
+ * @returns 绝对路径
13968
+ */
13969
+ function fileUriToPath(fileUri) {
13970
+ try {
13971
+ return fileURLToPath(fileUri);
13972
+ } catch (error) {
13973
+ return String(error);
13974
+ }
13975
+ }
13976
+ /**
13964
13977
  * 是否绝对路径(**平台无关**判定)。
13965
13978
  *
13966
13979
  * ⚠️ 不能用 `node:path` 的 `isAbsolute`:它按**运行平台**解释路径——`isAbsolute('D:/ws/x')`
@@ -14069,6 +14082,130 @@ function deriveDefinitionFromLocations(queryPos, locations, docText, docAbsPath)
14069
14082
  const first = candidates[0];
14070
14083
  return sameRange(first.range, containing.range) ? containing : first;
14071
14084
  }
14085
+ /** 提取文本开头的标识符链(`EventId.ISTaskSystem = {` → ['EventId','ISTaskSystem'])。 */
14086
+ function chainOf(text) {
14087
+ const match = /^\s*([A-Za-z_][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)*)/.exec(text);
14088
+ if (!match) return [];
14089
+ return match[1].replace(/\s+/g, "").split(".");
14090
+ }
14091
+ /**
14092
+ * 判定切片文本是否为查询链的声明形候选:链后紧跟单个 '='(name = ... 赋值,排除 '==' 比较),
14093
+ * 且候选链是查询链的段级前缀(外层表声明 `EventId.ISTaskSystem = {`)或末段一致(叶字段声明
14094
+ * `TaskStateChange = 50000,`)。
14095
+ */
14096
+ function isDeclarationMatch(word, queryChain) {
14097
+ const match = /^\s*([A-Za-z_][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)*)\s*(=)(?!=)/.exec(word);
14098
+ if (!match) return false;
14099
+ const chain = match[1].replace(/\s+/g, "").split(".");
14100
+ if (!chain.length || chain.length > queryChain.length) return false;
14101
+ const prefix = queryChain.slice(0, chain.length);
14102
+ if (chain.every((seg, i) => seg === prefix[i])) return true;
14103
+ if (chain[chain.length - 1] === queryChain[queryChain.length - 1]) return true;
14104
+ return false;
14105
+ }
14106
+ /**
14107
+ * 跨文件声明推导(definition/declaration 均空、同文件推导只给出"点击处自身"时的兜底)。
14108
+ * 背景:EmmyLua 0.9.41 对成员链字段(EventId.ISTaskSystem.TaskStateChange)的 definition 恒返回空,
14109
+ * 但 references(includeDeclaration) 在词中/尾位置包含字段声明位置(声明文件内的 `XXX = {...}` /
14110
+ * `KEY = value,` 赋值行)。同文件推导把跨文件声明过滤掉 → 这里放宽到跨文件,
14111
+ * 用"段级相等/前缀/后缀 + 赋值形(单个 '=')"识别声明,并读取目标文件文本做词级校验。
14112
+ * 文件读取有上限(maxFiles),防止大工作区把跳转拖慢。
14113
+ * @author ddj 2026年10月15号
14114
+ * @param queryPos 查询位置(调用方已 nudge 到词尾)
14115
+ * @param locations references(includeDeclaration) 结果
14116
+ * @param docText 当前文档全文(取点击链)
14117
+ * @param docAbsPath 当前文档绝对路径(file:// URI)
14118
+ * @param lookup 文件文本读取器与读取上限
14119
+ * @returns 跨文件声明位置;未识别到返回 null
14120
+ */
14121
+ async function deriveCrossFileDeclaration(queryPos, locations, docText, docAbsPath, lookup) {
14122
+ if (!locations.length) return null;
14123
+ const docAbs = normAbs(docAbsPath);
14124
+ const containing = locations.filter((loc) => posInLspRange(queryPos, loc.range)).sort((a, b) => rangeSize(a.range) - rangeSize(b.range))[0];
14125
+ if (!containing) return null;
14126
+ const queryChain = chainOf(sliceRange(docText, containing.range));
14127
+ if (!queryChain.length) return null;
14128
+ const maxFiles = lookup.maxFiles ?? 8;
14129
+ const textByKey = /* @__PURE__ */ new Map();
14130
+ let best = null;
14131
+ let bestChainLen = Number.POSITIVE_INFINITY;
14132
+ const ordered = [...locations].sort((a, b) => {
14133
+ const ca = a.range.start.character - b.range.start.character;
14134
+ return ca !== 0 ? ca : a.range.start.line - b.range.start.line;
14135
+ });
14136
+ for (const loc of ordered) {
14137
+ const key = normAbs(loc.uri);
14138
+ if (key === docAbs) continue;
14139
+ let text = textByKey.get(key);
14140
+ if (text === void 0) {
14141
+ if (textByKey.size >= maxFiles) break;
14142
+ text = await lookup.readText(loc.uri);
14143
+ textByKey.set(key, text ?? null);
14144
+ }
14145
+ if (!text) continue;
14146
+ const word = sliceToLineEnd(text, loc.range);
14147
+ const chain = chainOf(word);
14148
+ if (chain.length && chain.length <= bestChainLen && isDeclarationMatch(word, queryChain)) {
14149
+ best = loc;
14150
+ bestChainLen = chain.length;
14151
+ if (bestChainLen === 1) break;
14152
+ }
14153
+ }
14154
+ return best;
14155
+ }
14156
+ /** 从 range.start 到所在行末的切片(声明形判定需覆盖标识符后的 `= value`)。 */
14157
+ function sliceToLineEnd(text, range) {
14158
+ return (text.split(/\r\n|\r|\n/)[Math.max(0, range.start.line)] ?? "").slice(Math.max(0, range.start.character));
14159
+ }
14160
+ /**
14161
+ * 判定"直接 definition 结果是否为同文件误解析(把使用点当定义)"。
14162
+ * 背景:EmmyLua 0.9.41 对跨文件全局(EventId = {} 定义在别的文件)的 textDocument/definition
14163
+ * 常错误返回当前文件最后一个使用点(如 SubHandler:94 的 `{ EventId.NewActivity...`),而非声明文件。
14164
+ * 特征:直接结果全部位于查询文档同文件,且首个结果的切片文本不是点击链的声明形
14165
+ * (local/function 声明或 `链 = ...` 赋值)。命中即应降级走跨文件声明推导。
14166
+ * @author ddj 2026年10月15号
14167
+ * @param docAbsPath 当前文档绝对路径(file:// URI 或绝对路径)
14168
+ * @param locations 直接 definition 结果
14169
+ * @param docText 当前文档全文(取点击链)
14170
+ * @param position 查询位置(调用方已 nudge 到词尾)
14171
+ */
14172
+ function isMisresolvedSameFileUsage(docAbsPath, locations, docText, position) {
14173
+ if (!locations.length) return false;
14174
+ const docAbs = normAbs(docAbsPath);
14175
+ if (locations.some((loc) => normAbs(loc.uri) !== docAbs)) return false;
14176
+ const chainRange = chainRangeAt(docText, position);
14177
+ if (!chainRange) return false;
14178
+ const queryChain = chainOf(sliceRange(docText, chainRange));
14179
+ if (!queryChain.length) return false;
14180
+ return !isDeclarationForm(sliceToLineEnd(docText, locations[0].range), queryChain);
14181
+ }
14182
+ /** 取 position 处的标识符/成员链 range(0-based,取所在行内的链)。 */
14183
+ function chainRangeAt(text, pos) {
14184
+ const line = text.split(/\r\n|\r|\n/)[pos.line] ?? "";
14185
+ const re = /[A-Za-z_][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)*/g;
14186
+ let m;
14187
+ while (m = re.exec(line)) {
14188
+ const start = m.index;
14189
+ const end = start + m[0].length;
14190
+ if (pos.character >= start && pos.character < end) return {
14191
+ start: {
14192
+ line: pos.line,
14193
+ character: start
14194
+ },
14195
+ end: {
14196
+ line: pos.line,
14197
+ character: end
14198
+ }
14199
+ };
14200
+ }
14201
+ return null;
14202
+ }
14203
+ /** 切片文本是否为查询链的"声明形"(local/function 声明或 `链 = ...` 赋值)。 */
14204
+ function isDeclarationForm(word, queryChain) {
14205
+ const t = word.replace(/^\s+/, "");
14206
+ if (/^(local\b|function\b)/.test(t)) return true;
14207
+ return isDeclarationMatch(word, queryChain);
14208
+ }
14072
14209
  //#endregion
14073
14210
  //#region src/lsp/server.ts
14074
14211
  /**
@@ -14090,6 +14227,72 @@ function lspDocVersion(incoming, previous) {
14090
14227
  return Number.isInteger(incoming) && incoming >= next && incoming <= MAX_DOC_VERSION ? incoming : next;
14091
14228
  }
14092
14229
  /**
14230
+ * 把查询位置挪到光标所在 Lua 标识符词的末尾字符。
14231
+ *
14232
+ * 背景:EmmyLua 0.9.41 存在词边界怪癖——查询位置落在标识符 token 前半时,definition/hover/references
14233
+ * 一律返回空 / "unknown" / [],落在 token 后半/末尾才正常解析(已对 EventId/ISTaskSystem/
14234
+ * TaskStateChange 逐字符实测确认)。把位置挪到词尾即可让基标识符(如 `EventId`)正常解析。
14235
+ * 仅当位置严格落在标识符词内时才挪;点号/空白/字符串/注释位置原样返回,不影响正常符号。
14236
+ * @author ddj 2026年10月15号
14237
+ * @param text 文档全文
14238
+ * @param pos 查询位置(0-based)
14239
+ * @returns 词末字符位置;不在词内则原样返回
14240
+ */
14241
+ function nudgeToWordEnd(text, pos) {
14242
+ const line = text.split(/\r\n|\r|\n/)[pos.line];
14243
+ if (line === void 0) return pos;
14244
+ const identifier = /[A-Za-z_][A-Za-z0-9_]*/g;
14245
+ let match;
14246
+ while ((match = identifier.exec(line)) !== null) {
14247
+ const start = match.index;
14248
+ const end = start + match[0].length;
14249
+ if (pos.character >= start && pos.character < end) return {
14250
+ line: pos.line,
14251
+ character: end - 1
14252
+ };
14253
+ if (pos.character < start) break;
14254
+ }
14255
+ return pos;
14256
+ }
14257
+ /** hover 内容是否"空/unknown"(EmmyLua 对未解析符号、成员链字段 hover 的原生输出)。 */
14258
+ function isEmptyHover(contents) {
14259
+ return contents.every((c) => {
14260
+ const t = c.trim();
14261
+ return !t || t === "unknown";
14262
+ });
14263
+ }
14264
+ /**
14265
+ * 取光标所在成员链的基标识符范围(`EventId.ISTaskSystem.TaskStateChange` → `EventId`)。
14266
+ * 链中无成员访问(无 '.')时返回 null——基标识符回查无意义。
14267
+ */
14268
+ function chainBaseRange(line, character) {
14269
+ const chain = /[A-Za-z_][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)*/g;
14270
+ let match;
14271
+ while ((match = chain.exec(line)) !== null) {
14272
+ const start = match.index;
14273
+ const end = start + match[0].length;
14274
+ if (character >= start && character < end) {
14275
+ const dot = match[0].indexOf(".");
14276
+ if (dot === -1) return null;
14277
+ return {
14278
+ start,
14279
+ end: start + dot
14280
+ };
14281
+ }
14282
+ if (character < start) break;
14283
+ }
14284
+ return null;
14285
+ }
14286
+ /** LSP position 是否落在 range 内(start 包含、end 排除)。 */
14287
+ function rangeContains(pos, range) {
14288
+ const start = range.start;
14289
+ const end = range.end ?? start;
14290
+ if (pos.line < start.line || pos.line > end.line) return false;
14291
+ if (pos.line === start.line && pos.character < start.character) return false;
14292
+ if (pos.line === end.line && pos.character >= end.character) return false;
14293
+ return true;
14294
+ }
14295
+ /**
14093
14296
  * 创建语言服务器会话。
14094
14297
  * @author ddj 2026年08月27号
14095
14298
  * @param spec provider 规格(argv/cwd)
@@ -14115,6 +14318,20 @@ function createLspServer(spec, root, languageId, logger) {
14115
14318
  const rel = toWorkspacePath(root, path);
14116
14319
  return isAbsolutePath(rel) ? pathToFileUri(rel) : pathToFileUri(root + "/" + rel.split("/").join("/"));
14117
14320
  };
14321
+ /**
14322
+ * 按 file:// URI 读取文件文本:已打开文档走内存(与语言服务器索引一致),其余读盘。
14323
+ * @author ddj 2026年10月15号
14324
+ * @param uri 目标文件 file:// URI
14325
+ * @returns 文件文本;读取失败(文件不存在/非 UTF-8 等)返回 null
14326
+ */
14327
+ const readTextByUri = async (uri) => {
14328
+ for (const doc of docs.values()) if (doc.uri === uri) return doc.text;
14329
+ try {
14330
+ return await readFile(fileUriToPath(uri), "utf8");
14331
+ } catch {
14332
+ return null;
14333
+ }
14334
+ };
14118
14335
  let phase = "idle";
14119
14336
  let transport = null;
14120
14337
  let client = null;
@@ -14312,15 +14529,37 @@ function createLspServer(spec, root, languageId, logger) {
14312
14529
  const doc = docs.get(path);
14313
14530
  if (!doc || !await readyWait) return [];
14314
14531
  const client = ensureClient();
14315
- const position = {
14532
+ const position = nudgeToWordEnd(doc.text, {
14316
14533
  line,
14317
14534
  character
14535
+ });
14536
+ const crossFromRefs = async () => {
14537
+ try {
14538
+ const refs = normalizeLocations(await client.request("textDocument/references", {
14539
+ textDocument: { uri: doc.uri },
14540
+ position,
14541
+ context: { includeDeclaration: true }
14542
+ }));
14543
+ return await deriveCrossFileDeclaration(position, refs, doc.text, doc.uri, { readText: readTextByUri });
14544
+ } catch (error) {
14545
+ log("references fallback failed: " + String(error));
14546
+ return null;
14547
+ }
14318
14548
  };
14319
14549
  const direct = normalizeLocations(await client.request("textDocument/definition", {
14320
14550
  textDocument: { uri: doc.uri },
14321
14551
  position
14322
14552
  }));
14323
- if (direct.length) return direct;
14553
+ if (direct.length) {
14554
+ if (isMisresolvedSameFileUsage(doc.uri, direct, doc.text, position)) {
14555
+ const cross = await crossFromRefs();
14556
+ if (cross) {
14557
+ log("definition direct was same-file misresolution → cross-file declaration (" + path + ":" + line + ":" + character + ")");
14558
+ return [cross];
14559
+ }
14560
+ }
14561
+ return direct;
14562
+ }
14324
14563
  if (capabilities.declaration) try {
14325
14564
  const decl = normalizeLocations(await client.request("textDocument/declaration", {
14326
14565
  textDocument: { uri: doc.uri },
@@ -14334,15 +14573,24 @@ function createLspServer(spec, root, languageId, logger) {
14334
14573
  log("declaration request failed: " + String(error));
14335
14574
  }
14336
14575
  try {
14337
- const derived = deriveDefinitionFromLocations(position, normalizeLocations(await client.request("textDocument/references", {
14576
+ const refs = normalizeLocations(await client.request("textDocument/references", {
14338
14577
  textDocument: { uri: doc.uri },
14339
14578
  position,
14340
14579
  context: { includeDeclaration: true }
14341
- })), doc.text, doc.uri);
14342
- if (derived) {
14580
+ }));
14581
+ const derived = deriveDefinitionFromLocations(position, refs, doc.text, doc.uri);
14582
+ const selfDerived = derived ? rangeContains(position, derived.range) : false;
14583
+ const misDerived = derived ? isMisresolvedSameFileUsage(doc.uri, [derived], doc.text, position) : false;
14584
+ if (derived && !selfDerived && !misDerived) {
14343
14585
  log("definition fallback → references-derived (" + path + ":" + line + ":" + character + ")");
14344
14586
  return [derived];
14345
14587
  }
14588
+ const cross = await deriveCrossFileDeclaration(position, refs, doc.text, doc.uri, { readText: readTextByUri });
14589
+ if (cross) {
14590
+ log("definition fallback → cross-file declaration (" + path + ":" + line + ":" + character + ")");
14591
+ return [cross];
14592
+ }
14593
+ if (derived) return [derived];
14346
14594
  } catch (error) {
14347
14595
  log("references fallback failed: " + String(error));
14348
14596
  }
@@ -14351,12 +14599,13 @@ function createLspServer(spec, root, languageId, logger) {
14351
14599
  async references(path, line, character, includeDeclaration) {
14352
14600
  const doc = docs.get(path);
14353
14601
  if (!doc || !await readyWait) return [];
14602
+ const position = nudgeToWordEnd(doc.text, {
14603
+ line,
14604
+ character
14605
+ });
14354
14606
  return normalizeLocations(await ensureClient().request("textDocument/references", {
14355
14607
  textDocument: { uri: doc.uri },
14356
- position: {
14357
- line,
14358
- character
14359
- },
14608
+ position,
14360
14609
  context: { includeDeclaration }
14361
14610
  }));
14362
14611
  },
@@ -14376,15 +14625,31 @@ function createLspServer(spec, root, languageId, logger) {
14376
14625
  async hover(path, line, character) {
14377
14626
  const doc = docs.get(path);
14378
14627
  if (!doc || !await readyWait) return null;
14379
- const result = await ensureClient().request("textDocument/hover", {
14628
+ const client = ensureClient();
14629
+ const position = nudgeToWordEnd(doc.text, {
14630
+ line,
14631
+ character
14632
+ });
14633
+ const result = await client.request("textDocument/hover", {
14380
14634
  textDocument: { uri: doc.uri },
14381
- position: {
14382
- line,
14383
- character
14384
- }
14635
+ position
14385
14636
  });
14386
- if (!result || !result.contents) return null;
14387
- return { contents: stringifyHoverContents(result.contents) };
14637
+ const contents = result?.contents ? stringifyHoverContents(result.contents) : [];
14638
+ if (isEmptyHover(contents)) {
14639
+ const base = chainBaseRange(doc.text.split(/\r\n|\r|\n/)[position.line] ?? "", position.character);
14640
+ if (base) {
14641
+ const baseResult = await client.request("textDocument/hover", {
14642
+ textDocument: { uri: doc.uri },
14643
+ position: {
14644
+ line: position.line,
14645
+ character: base.end - 1
14646
+ }
14647
+ });
14648
+ const baseContents = baseResult?.contents ? stringifyHoverContents(baseResult.contents) : [];
14649
+ if (!isEmptyHover(baseContents)) return { contents: baseContents };
14650
+ }
14651
+ }
14652
+ return contents.length ? { contents } : null;
14388
14653
  },
14389
14654
  /**
14390
14655
  * 查询补全列表(textDocument/completion)。