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/README.md +4 -3
- package/lib/client.js +66 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +281 -16
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/monaco/lsp/providers.ts +68 -1
- package/src/client/ui/EditorView.ts +3 -1
- package/src/lsp/derive.ts +157 -0
- package/src/lsp/server.ts +159 -10
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)
|
|
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
|
|
14576
|
+
const refs = normalizeLocations(await client.request("textDocument/references", {
|
|
14338
14577
|
textDocument: { uri: doc.uri },
|
|
14339
14578
|
position,
|
|
14340
14579
|
context: { includeDeclaration: true }
|
|
14341
|
-
}))
|
|
14342
|
-
|
|
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
|
|
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
|
-
|
|
14387
|
-
|
|
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)。
|