dsh-vscode-mode 0.7.0 → 0.9.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 +37 -6
- package/assets/icon.svg +7 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +2804 -300
- package/lib/client.js.map +1 -1
- package/lib/index.js +1975 -191
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +4 -2
- package/src/ai/inline.ts +19 -2
- package/src/ai/svnTriage.ts +150 -0
- package/src/capture.ts +225 -51
- package/src/client/addToConversation.ts +203 -1
- package/src/client/compat.ts +134 -4
- package/src/client/imagePreview.ts +76 -2
- package/src/client/index.ts +65 -36
- package/src/client/monaco/lsp/lspClient.ts +46 -0
- package/src/client/monaco/lsp/providers.ts +245 -1
- package/src/client/nativeOpenStore.ts +91 -0
- package/src/client/officialSidebar.ts +11 -45
- package/src/client/outline/index.ts +1 -1
- package/src/client/pdf/pdfPanel.ts +18 -0
- package/src/client/pendingNav.ts +141 -0
- package/src/client/settingsContext.ts +6 -4
- package/src/client/sidebar/panels/FileExplorer.ts +2 -15
- package/src/client/sidebar/panels/SvnPanel.ts +682 -72
- package/src/client/sidebar/panels/index.ts +1 -1
- package/src/client/sidebar/panels/svnListBudget.ts +58 -0
- package/src/client/state/records.ts +7 -1
- package/src/client/styles/editor.css +47 -5
- package/src/client/svnStatus.ts +206 -1
- package/src/client/svnStore.ts +6 -1
- package/src/client/ui/AiSettings.ts +47 -14
- package/src/client/ui/DiffBox.ts +1 -1
- package/src/client/ui/DiffLauncher.ts +6 -4
- package/src/client/ui/EditorView.ts +149 -22
- package/src/client/ui/McpSettings.ts +30 -0
- package/src/client/ui/PerfSettings.ts +4 -1
- package/src/client/ui/SvnAiPlanDialog.ts +293 -0
- package/src/client/ui/icons.ts +91 -0
- package/src/compat.ts +26 -5
- package/src/dshVersion.ts +2 -1
- package/src/fileOpenSettings.ts +274 -49
- package/src/index.ts +18 -1
- package/src/lsp/client.ts +24 -0
- package/src/lsp/rpc.ts +57 -0
- package/src/lsp/server.ts +272 -2
- package/src/model.ts +2 -1
- package/src/perf.ts +90 -4
- package/src/remoteWorkspace.ts +177 -0
- package/src/routes.ts +13 -1
- package/src/rpc.ts +115 -39
- package/src/shared/ai.ts +12 -0
- package/src/shared/lsp.ts +79 -0
- package/src/shared/nativeOpen.ts +91 -0
- package/src/shared/rpc.ts +162 -2
- package/src/shared/svn.ts +296 -10
- package/src/shared/types.ts +5 -1
- package/src/svn.ts +235 -13
package/src/fileOpenSettings.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
|
|
3
3
|
* 依赖守卫:schemastery 动态加载;@deepseek-ai/dsh-settings 仅作 legacy 探测——
|
|
4
4
|
* rc 线导出 installSettingsSection free function,0.1.2-alpha 起移除(改由 settings
|
|
5
|
-
* 服务的 installSection
|
|
5
|
+
* 服务的 installSection 方法承载),0.1.7 再移除 installSection(设置并入 profile
|
|
6
|
+
* 插件 Config schema,见 runSettingsInstall 四策略:legacy/service/forms/none)。
|
|
6
7
|
* 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
|
|
7
8
|
* 全程 try/catch,不产生未捕获 rejection。
|
|
8
|
-
* 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号
|
|
9
|
+
* 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号 / 2026年09月23号
|
|
9
10
|
*/
|
|
10
11
|
import { createRequire } from 'node:module'
|
|
11
12
|
import { pathToFileURL } from 'node:url'
|
|
@@ -15,6 +16,7 @@ import { EDITOR_LIMIT_DEFAULT } from './shared/editorLimit.js'
|
|
|
15
16
|
import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
|
|
16
17
|
import { TORTOISE_DIR_DEFAULT } from './shared/svn.js'
|
|
17
18
|
import type { AiConfigPatch, AiConfigView } from './shared/ai.js'
|
|
19
|
+
import { DEFAULT_NATIVE_CSV } from './shared/nativeOpen.js'
|
|
18
20
|
import { log } from './log.js'
|
|
19
21
|
|
|
20
22
|
export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
|
|
@@ -22,12 +24,15 @@ export const FILE_OPEN_DEFAULT = 'auto'
|
|
|
22
24
|
export interface FileOpenSettings { fileOpenTool: string; integrationBaseUrl: string; svnPath: string; tortoisePath: string }
|
|
23
25
|
export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
|
|
24
26
|
|
|
25
|
-
/** AI
|
|
26
|
-
export const AI_CONFIG_DEFAULT: { enabled: boolean; provider: string; model: string; effort: string } = {
|
|
27
|
+
/** AI 配置默认值(补全默认关;路由空 = 自动;档位空 = 跟随模型默认;任务模型空 = 跟随补全配置)。 */
|
|
28
|
+
export const AI_CONFIG_DEFAULT: { enabled: boolean; provider: string; model: string; effort: string; taskProvider: string; taskModel: string; taskEffort: string } = {
|
|
27
29
|
enabled: false,
|
|
28
30
|
provider: '',
|
|
29
31
|
model: '',
|
|
30
32
|
effort: '',
|
|
33
|
+
taskProvider: '',
|
|
34
|
+
taskModel: '',
|
|
35
|
+
taskEffort: '',
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
type SettingsProvider = {
|
|
@@ -170,11 +175,12 @@ export function loadSettingsDeps(importFn: (specifier: string) => Promise<unknow
|
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
/** 设置 section 安装策略(版本适配机制的观测值)。 */
|
|
173
|
-
export type SettingsInstallStrategy = 'legacy' | 'service' | 'none' | 'unknown'
|
|
178
|
+
export type SettingsInstallStrategy = 'legacy' | 'service' | 'forms' | 'none' | 'unknown'
|
|
174
179
|
|
|
175
180
|
const INSTALL_UNMOUNTED = '设置 section 尚未装配'
|
|
176
181
|
const INSTALL_LEGACY = 'rc 线:dsh-settings.installSettingsSection'
|
|
177
182
|
const INSTALL_SERVICE = '0.1.2-alpha 线:settings 服务 installSection'
|
|
183
|
+
const INSTALL_FORMS = '0.1.7 线:设置并入插件 Config schema(SettingsForms)'
|
|
178
184
|
const INSTALL_NONE = '两路均不可用:设置持久化降级为配置值'
|
|
179
185
|
|
|
180
186
|
interface ObservedInstall { strategy: SettingsInstallStrategy; note: string }
|
|
@@ -201,15 +207,54 @@ export function resetSettingsInstallObserved(): void {
|
|
|
201
207
|
observedInstall = { strategy: 'unknown', note: INSTALL_UNMOUNTED }
|
|
202
208
|
}
|
|
203
209
|
|
|
210
|
+
/**
|
|
211
|
+
* 判定设置写冲突错误(0.1.7 SettingsConflictError:code 常量 / 构造器名双通道)。
|
|
212
|
+
* @author ddj 2026年09月22号
|
|
213
|
+
* @param error 捕获到的错误
|
|
214
|
+
* @returns 是否为 revision 冲突
|
|
215
|
+
*/
|
|
216
|
+
function isConflictError(error: unknown): boolean {
|
|
217
|
+
if (!error || typeof error !== 'object') return false
|
|
218
|
+
const e = error as { code?: unknown; name?: unknown }
|
|
219
|
+
return e.code === 'SETTINGS_CONFLICT' || e.name === 'SettingsConflictError'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 0.1.7 forms 策略:订阅 settings/document-updated 事件,目标 namespace 变化时
|
|
224
|
+
* 把 describe 最新值推给 hooks(setSource + onChange),替代旧 installSection 的
|
|
225
|
+
* 变更回调;disposer 经 ctx.effect 随插件 fiber 卸载(热重载不泄漏)。
|
|
226
|
+
* @author ddj 2026年09月22号
|
|
227
|
+
* @param ctx DSH host 上下文
|
|
228
|
+
* @param provider settings 服务(SettingsForms)
|
|
229
|
+
* @param ns 目标设置命名空间
|
|
230
|
+
* @param hooks 设置源绑定与变更回调
|
|
231
|
+
*/
|
|
232
|
+
function watchDocUpdates(ctx: Ctx, provider: SettingsProvider, ns: string, hooks: { setSource: (source: () => unknown) => void; onChange: () => void }): void {
|
|
233
|
+
try {
|
|
234
|
+
const bus = ctx as { on?: (name: string, handler: (updatedNs: unknown, revision: unknown) => void) => unknown; effect?: (fn: () => unknown) => unknown }
|
|
235
|
+
if (typeof bus.on !== 'function') return
|
|
236
|
+
const off = bus.on('settings/document-updated', (updatedNs: unknown) => {
|
|
237
|
+
if (updatedNs !== ns) return
|
|
238
|
+
hooks.setSource(() => readSectionValue(provider, ns))
|
|
239
|
+
hooks.onChange()
|
|
240
|
+
})
|
|
241
|
+
if (typeof bus.effect === 'function' && typeof off === 'function') bus.effect(() => off)
|
|
242
|
+
} catch (error) {
|
|
243
|
+
log.warn('settings/document-updated 订阅失败:' + String(error))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
204
247
|
/**
|
|
205
248
|
* 设置 section 版本自适应安装(核心策略分派)。
|
|
206
249
|
* legacy:dsh-settings 仍导出 installSettingsSection(rc 线)→ 原样调用,行为与旧版一致。
|
|
207
250
|
* service:该导出已移除(0.1.2-alpha 起)→ 经 ctx.inject(['settings']) 走服务方法
|
|
208
251
|
* provider.installSection(owner, ns, schema, entry, hooks)(等义封装,含
|
|
209
|
-
* base 层与 fiber
|
|
252
|
+
* base 层与 fiber 卸载回退)。
|
|
253
|
+
* forms:0.1.7 起 installSection 移除,设置并入 profile 插件 Config(SettingsForms
|
|
254
|
+
* 的 describe+update 在场即成立)→ 不装 section,改订阅 document-updated。
|
|
210
255
|
* none:两条路由都不存在 → 仅记录并告警,调用方按配置值运行。
|
|
211
256
|
* 全程不抛错、不产生未捕获 rejection。
|
|
212
|
-
* @author ddj 2026年09月02号
|
|
257
|
+
* @author ddj 2026年09月02号(2026年09月22号 增补 forms 策略)
|
|
213
258
|
* @param ctx DSH host 上下文(inject 可选探测)
|
|
214
259
|
* @param ns 设置命名空间
|
|
215
260
|
* @param schema schemastery schema
|
|
@@ -253,6 +298,13 @@ export async function runSettingsInstall(
|
|
|
253
298
|
const provider = typeof sc.get === 'function' ? sc.get('settings') : sc.settings
|
|
254
299
|
const install = (provider as { installSection?: unknown } | undefined)?.installSection
|
|
255
300
|
if (typeof install !== 'function') {
|
|
301
|
+
// 0.1.7:installSection 已移除;SettingsForms(describe+update)在场走 forms 策略
|
|
302
|
+
const forms = provider as SettingsProvider | undefined
|
|
303
|
+
if (typeof forms?.describe === 'function' && typeof forms?.update === 'function') {
|
|
304
|
+
recordInstall('forms', INSTALL_FORMS)
|
|
305
|
+
watchDocUpdates(ctx, forms, ns, hooks)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
256
308
|
log.warn('settings 服务无 installSection(DSH 版本 API 变化),section ' + ns + ' 降级为配置值')
|
|
257
309
|
recordInstall('none', INSTALL_NONE)
|
|
258
310
|
return
|
|
@@ -286,30 +338,214 @@ function keybindingsShape(z: SettingsDeps['z']): Record<string, unknown> {
|
|
|
286
338
|
return shape
|
|
287
339
|
}
|
|
288
340
|
|
|
341
|
+
/**
|
|
342
|
+
* 设置节 describe 项的形状校验:value 须为普通对象。
|
|
343
|
+
* ns 在两代语义不同(旧=自装 section 名,0.1.7=profile entry id),同名撞车时
|
|
344
|
+
* 以形状兜底防误命中非设置数据;0.1.7 下本插件 entry 的 Config 值与旧 section
|
|
345
|
+
* 值同为设置键对象,两形状天然兼容。
|
|
346
|
+
* @author ddj 2026年09月22号
|
|
347
|
+
* @param value describe 项的 value 字段
|
|
348
|
+
* @returns 是否具备设置节形状
|
|
349
|
+
*/
|
|
350
|
+
function isSectionShaped(value: unknown): boolean {
|
|
351
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** describe 项最小形状(读写工具共用)。 */
|
|
355
|
+
export interface SectionDescriptor { ns?: string; value?: unknown; revision?: number }
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 按 ns + 形状校验查找设置节(host 读路径统一入口)。
|
|
359
|
+
* @author ddj 2026年09月22号
|
|
360
|
+
* @param provider settings 服务(可空)
|
|
361
|
+
* @param ns 设置命名空间
|
|
362
|
+
* @returns 命中的 describe 项;未命中返回 undefined
|
|
363
|
+
*/
|
|
364
|
+
export function sectionOf(provider: SettingsProvider | undefined, ns: string): SectionDescriptor | undefined {
|
|
365
|
+
const items = provider?.describe?.({ redactSecrets: true })
|
|
366
|
+
if (!items) return undefined
|
|
367
|
+
return items.find((item) => item.ns === ns && isSectionShaped(item.value))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 读设置节存储值(未就绪/形状不符返回 undefined)。 */
|
|
371
|
+
function readSectionValue(provider: SettingsProvider | undefined, ns: string): unknown {
|
|
372
|
+
return sectionOf(provider, ns)?.value
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 设置写入结果(ok=false 时带原因;conflict=true 表示经冲突重试后成功)。 */
|
|
376
|
+
export interface SectionWriteResult { ok: boolean; conflict?: boolean; error?: string }
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 带冲突自愈的设置写入:0.1.7 SettingsConflictError(revision 拒写)时重读
|
|
380
|
+
* revision 重试一次;其余错误与服务缺失一律结构化返回,绝不抛未捕获异常。
|
|
381
|
+
* @author ddj 2026年09月22号
|
|
382
|
+
* @param provider settings 服务(可空)
|
|
383
|
+
* @param ns 设置命名空间
|
|
384
|
+
* @param patch 写入字段
|
|
385
|
+
* @param expectedRevision 读侧携带的 revision(缺省用 describe 最新值)
|
|
386
|
+
* @returns 写入结果
|
|
387
|
+
*/
|
|
388
|
+
export async function updateSection(provider: SettingsProvider | undefined, ns: string, patch: object, expectedRevision?: number): Promise<SectionWriteResult> {
|
|
389
|
+
if (!provider?.update) return { ok: false, error: 'settings 服务无 update' }
|
|
390
|
+
const first = expectedRevision ?? sectionOf(provider, ns)?.revision
|
|
391
|
+
try {
|
|
392
|
+
await provider.update(ns, patch, first)
|
|
393
|
+
return { ok: true }
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (!isConflictError(error)) return { ok: false, error: String(error) }
|
|
396
|
+
}
|
|
397
|
+
const fresh = sectionOf(provider, ns)?.revision
|
|
398
|
+
try {
|
|
399
|
+
await provider.update(ns, patch, fresh)
|
|
400
|
+
return { ok: true, conflict: true }
|
|
401
|
+
} catch (error) {
|
|
402
|
+
return { ok: false, conflict: true, error: String(error) }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
|
|
408
|
+
* 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
|
|
409
|
+
* aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
|
|
410
|
+
* (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
|
|
411
|
+
* options.volatile:DSH 0.1.7 线的 Config 导出专用——SettingsForms.describe 只下发
|
|
412
|
+
* 含 volatile 字段的 entry(volatileForm 门槛),不标记则 ns 永不出现在 describe、
|
|
413
|
+
* client configForms 恒 unavailable;且字段须位于固定 object 路径(keybindings 整个
|
|
414
|
+
* object 标记、子键不标)。section 安装路径(legacy/service)**不标**:rc/0.1.6 的
|
|
415
|
+
* register→resolve 无该门槛,标了反而让 scope.get() 返回引用污染读取。
|
|
416
|
+
* 标记经 markVolatile 能力守卫:z 无 .volatile()(旧 schemastery)或调用抛错时保持
|
|
417
|
+
* 原字段并记录观测态,模块加载绝不抛错(降级=维持现状,兼容性报告可见)。
|
|
418
|
+
* @author ddj 2026年09月22号(2026年09月23号 增补 options.volatile 与观测)
|
|
419
|
+
* @param z schemastery 命名空间(静态 import 或动态加载均可)
|
|
420
|
+
* @param options volatile:是否按 0.1.7 Config 语义标记字段
|
|
421
|
+
* @returns schemastery object schema
|
|
422
|
+
*/
|
|
423
|
+
export function buildSettingsSchema(z: SettingsDeps['z'], options?: { volatile?: boolean }): unknown {
|
|
424
|
+
const wantVolatile = options?.volatile === true
|
|
425
|
+
let marked = 0
|
|
426
|
+
const withVol = (field: unknown): unknown => {
|
|
427
|
+
if (!wantVolatile) return field
|
|
428
|
+
const result = markVolatile(field)
|
|
429
|
+
if (result.marked) marked += 1
|
|
430
|
+
return result.field
|
|
431
|
+
}
|
|
432
|
+
const shape: Record<string, unknown> = {
|
|
433
|
+
fileOpenTool: withVol(z.string().default(FILE_OPEN_DEFAULT)),
|
|
434
|
+
keybindings: withVol(z.object(keybindingsShape(z)).default({ ...KEYBINDING_DEFAULTS })),
|
|
435
|
+
sidebarMinWidth: withVol(z.number().default(300)),
|
|
436
|
+
// 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
|
|
437
|
+
maxOpenEditors: withVol(z.number().default(EDITOR_LIMIT_DEFAULT)),
|
|
438
|
+
integrationBaseUrl: withVol(z.string().default(INTEGRATION_BASE_DEFAULT)),
|
|
439
|
+
// AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
|
|
440
|
+
aiInline: withVol(z.boolean().default(AI_CONFIG_DEFAULT.enabled)),
|
|
441
|
+
aiProvider: withVol(z.string().default(AI_CONFIG_DEFAULT.provider)),
|
|
442
|
+
aiModel: withVol(z.string().default(AI_CONFIG_DEFAULT.model)),
|
|
443
|
+
aiEffort: withVol(z.string().default(AI_CONFIG_DEFAULT.effort)),
|
|
444
|
+
// AI 任务模型(非补全场景如 AI 智能整理;空 = 跟随补全配置)
|
|
445
|
+
aiTaskProvider: withVol(z.string().default(AI_CONFIG_DEFAULT.taskProvider)),
|
|
446
|
+
aiTaskModel: withVol(z.string().default(AI_CONFIG_DEFAULT.taskModel)),
|
|
447
|
+
aiTaskEffort: withVol(z.string().default(AI_CONFIG_DEFAULT.taskEffort)),
|
|
448
|
+
// SVN 能力:svn CLI 覆盖(空 = PATH)与 TortoiseSVN 目录(Windows 过渡增强)
|
|
449
|
+
svnPath: withVol(z.string().default('')),
|
|
450
|
+
tortoisePath: withVol(z.string().default(TORTOISE_DIR_DEFAULT)),
|
|
451
|
+
// 原生打开范围(逗号分隔后缀;默认 = 让位清单并集,csv/tsv 决策见 shared/nativeOpen.ts)
|
|
452
|
+
nativeOpenExts: withVol(z.string().default(DEFAULT_NATIVE_CSV)),
|
|
453
|
+
}
|
|
454
|
+
if (wantVolatile) configVolatile = { requested: true, marked, total: Object.keys(shape).length }
|
|
455
|
+
return z.object(shape)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Config volatile 标记观测(构建期记录;兼容报告读取,测试可复位)。 */
|
|
459
|
+
export interface ConfigVolatileState { requested: boolean; marked: number; total: number }
|
|
460
|
+
|
|
461
|
+
let configVolatile: ConfigVolatileState = { requested: false, marked: 0, total: 0 }
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 读取 Config volatile 标记观测(0.1.7 设置页可用性的构建期判据)。
|
|
465
|
+
* @author ddj 2026年09月23号
|
|
466
|
+
* @returns 观测态(requested=是否按 Config 语义构建;marked/total=成功标记字段数)
|
|
467
|
+
*/
|
|
468
|
+
export function configVolatileState(): ConfigVolatileState {
|
|
469
|
+
return configVolatile
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 复位 volatile 标记观测(测试隔离用)。
|
|
474
|
+
* @author ddj 2026年09月23号
|
|
475
|
+
*/
|
|
476
|
+
export function resetConfigVolatileState(): void {
|
|
477
|
+
configVolatile = { requested: false, marked: 0, total: 0 }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 为已完成 default 链的 schema 字段追加 volatile 标记(能力守卫)。
|
|
482
|
+
* @author ddj 2026年09月23号
|
|
483
|
+
* @param field schema 字段(两代 schemastery 兼容面,可能没有 volatile 方法)
|
|
484
|
+
* @returns 处理后字段与是否成功标记
|
|
485
|
+
*/
|
|
486
|
+
function markVolatile(field: unknown): { field: unknown; marked: boolean } {
|
|
487
|
+
const target = field as { volatile?: () => unknown } | null | undefined
|
|
488
|
+
if (!target || typeof target.volatile !== 'function') return { field, marked: false }
|
|
489
|
+
try {
|
|
490
|
+
return { field: target.volatile(), marked: true }
|
|
491
|
+
} catch {
|
|
492
|
+
return { field, marked: false }
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** cosmokit volatile 引用写协议符号(Symbol.for 跨拷贝一致,同 cosmokit.isVolatile 判据)。 */
|
|
497
|
+
const VOLATILE_WRITE = Symbol.for('cosmokit.volatile.write')
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* 解引用 0.1.7 volatile 配置引用(普通值直传)。
|
|
501
|
+
* cordis resolveConfig 会把 .volatile() 字段解析成稳定引用,直接读会得到引用对象;
|
|
502
|
+
* 不解引用则配置回退值全部落入默认值分支。
|
|
503
|
+
* @author ddj 2026年09月23号
|
|
504
|
+
* @param value 配置字段原始值
|
|
505
|
+
* @returns 引用的当前快照,或原值
|
|
506
|
+
*/
|
|
507
|
+
export function unref(value: unknown): unknown {
|
|
508
|
+
if (value !== null && typeof value === 'object' && VOLATILE_WRITE in (value as object)) {
|
|
509
|
+
return (value as { get: () => unknown }).get()
|
|
510
|
+
}
|
|
511
|
+
return value
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 读插件组合配置字段并解引用(配置读取唯一入口)。
|
|
516
|
+
* @author ddj 2026年09月23号
|
|
517
|
+
* @param config 插件组合配置(apply 收到的 Config 值)
|
|
518
|
+
* @param key 字段名
|
|
519
|
+
* @returns 字段值(引用已解包;缺失为 undefined)
|
|
520
|
+
*/
|
|
521
|
+
export function configField(config: unknown, key: string): unknown {
|
|
522
|
+
return unref((config as Record<string, unknown> | null | undefined)?.[key])
|
|
523
|
+
}
|
|
524
|
+
|
|
289
525
|
function normalizeValue(value: unknown): string {
|
|
290
526
|
if (typeof value !== 'string' || value.trim() === '') return FILE_OPEN_DEFAULT
|
|
291
527
|
return value.trim()
|
|
292
528
|
}
|
|
293
529
|
|
|
294
530
|
function configValue(config: unknown): string {
|
|
295
|
-
return normalizeValue((config
|
|
531
|
+
return normalizeValue(configField(config, 'fileOpenTool'))
|
|
296
532
|
}
|
|
297
533
|
|
|
298
534
|
/** 配置/设置里的深链基址(缺省/非法回退默认值)。 */
|
|
299
535
|
function baseValueOf(config: unknown): string {
|
|
300
|
-
const raw = (config
|
|
536
|
+
const raw = configField(config, 'integrationBaseUrl')
|
|
301
537
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : INTEGRATION_BASE_DEFAULT
|
|
302
538
|
}
|
|
303
539
|
|
|
304
540
|
/** 配置/设置里的 svn CLI 覆盖(空 = 从 PATH 解析 'svn')。 */
|
|
305
541
|
function svnPathValueOf(config: unknown): string {
|
|
306
|
-
const raw = (config
|
|
542
|
+
const raw = configField(config, 'svnPath')
|
|
307
543
|
return typeof raw === 'string' ? raw.trim() : ''
|
|
308
544
|
}
|
|
309
545
|
|
|
310
546
|
/** 配置/设置里的 TortoiseSVN 目录(空 = 默认安装目录)。 */
|
|
311
547
|
function tortoiseDirValueOf(config: unknown): string {
|
|
312
|
-
const raw = (config
|
|
548
|
+
const raw = configField(config, 'tortoisePath')
|
|
313
549
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : TORTOISE_DIR_DEFAULT
|
|
314
550
|
}
|
|
315
551
|
|
|
@@ -337,37 +573,25 @@ export async function installOpenSettingsSection(
|
|
|
337
573
|
/* 装载失败按缺失处理 */
|
|
338
574
|
}
|
|
339
575
|
if (!deps) return false
|
|
340
|
-
const schema = deps.z
|
|
341
|
-
fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
|
|
342
|
-
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
|
|
343
|
-
sidebarMinWidth: deps.z.number().default(300),
|
|
344
|
-
// 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
|
|
345
|
-
maxOpenEditors: deps.z.number().default(EDITOR_LIMIT_DEFAULT),
|
|
346
|
-
integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
|
|
347
|
-
// AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
|
|
348
|
-
aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
|
|
349
|
-
aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
|
|
350
|
-
aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
|
|
351
|
-
aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort),
|
|
352
|
-
// SVN 能力:svn CLI 覆盖(空 = PATH)与 TortoiseSVN 目录(Windows 过渡增强)
|
|
353
|
-
svnPath: deps.z.string().default(''),
|
|
354
|
-
tortoisePath: deps.z.string().default(TORTOISE_DIR_DEFAULT),
|
|
355
|
-
})
|
|
576
|
+
const schema = buildSettingsSchema(deps.z)
|
|
356
577
|
const strategy = await runSettingsInstall(ctx, ns, schema, entry, {
|
|
357
578
|
setSource: (source) => hooks.setSource(source as () => FileOpenSettings),
|
|
358
579
|
onChange: hooks.onChange,
|
|
359
580
|
}, loader)
|
|
360
|
-
return strategy === 'legacy' || strategy === 'service'
|
|
581
|
+
return strategy === 'legacy' || strategy === 'service' || strategy === 'forms'
|
|
361
582
|
}
|
|
362
583
|
|
|
363
584
|
/** AI 配置脏值读取(settings 未就绪时回退默认)。 */
|
|
364
|
-
function aiValueOf(stored: unknown): { enabled: boolean; provider: string; model: string; effort: string } {
|
|
585
|
+
function aiValueOf(stored: unknown): { enabled: boolean; provider: string; model: string; effort: string; taskProvider: string; taskModel: string; taskEffort: string } {
|
|
365
586
|
const raw = (stored ?? {}) as Record<string, unknown>
|
|
366
587
|
return {
|
|
367
588
|
enabled: raw.aiInline === true,
|
|
368
589
|
provider: typeof raw.aiProvider === 'string' ? raw.aiProvider : AI_CONFIG_DEFAULT.provider,
|
|
369
590
|
model: typeof raw.aiModel === 'string' ? raw.aiModel : AI_CONFIG_DEFAULT.model,
|
|
370
591
|
effort: typeof raw.aiEffort === 'string' ? raw.aiEffort : AI_CONFIG_DEFAULT.effort,
|
|
592
|
+
taskProvider: typeof raw.aiTaskProvider === 'string' ? raw.aiTaskProvider : AI_CONFIG_DEFAULT.taskProvider,
|
|
593
|
+
taskModel: typeof raw.aiTaskModel === 'string' ? raw.aiTaskModel : AI_CONFIG_DEFAULT.taskModel,
|
|
594
|
+
taskEffort: typeof raw.aiTaskEffort === 'string' ? raw.aiTaskEffort : AI_CONFIG_DEFAULT.taskEffort,
|
|
371
595
|
}
|
|
372
596
|
}
|
|
373
597
|
|
|
@@ -385,8 +609,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
|
|
|
385
609
|
let provider: SettingsProvider | undefined
|
|
386
610
|
const notify = (value: unknown): void => { current = normalizeValue(value); onChange(current) }
|
|
387
611
|
const syncRevision = (): void => {
|
|
388
|
-
|
|
389
|
-
revision = descriptor?.revision
|
|
612
|
+
revision = sectionOf(provider, FILE_OPEN_SETTINGS_NS)?.revision
|
|
390
613
|
}
|
|
391
614
|
const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
|
|
392
615
|
const settingsChange = (): void => syncRevision()
|
|
@@ -394,8 +617,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
|
|
|
394
617
|
/** AI 配置当前值(settings 未就绪回退默认)。 */
|
|
395
618
|
let aiCurrent: AiConfigView = { ...AI_CONFIG_DEFAULT }
|
|
396
619
|
const aiSync = (): void => {
|
|
397
|
-
const
|
|
398
|
-
const stored = descriptor?.value
|
|
620
|
+
const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS)
|
|
399
621
|
aiCurrent = stored !== undefined ? aiValueOf(stored) : aiCurrent
|
|
400
622
|
}
|
|
401
623
|
|
|
@@ -409,7 +631,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
|
|
|
409
631
|
}
|
|
410
632
|
/** 读取 settings 存储值(describe 未就绪返回 undefined)。 */
|
|
411
633
|
const storedValue = (): unknown => {
|
|
412
|
-
return provider
|
|
634
|
+
return readSectionValue(provider, FILE_OPEN_SETTINGS_NS)
|
|
413
635
|
}
|
|
414
636
|
|
|
415
637
|
void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
|
|
@@ -428,8 +650,12 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
|
|
|
428
650
|
})
|
|
429
651
|
ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
|
|
430
652
|
provider = settingsCtx.get('settings')
|
|
431
|
-
|
|
432
|
-
|
|
653
|
+
// 初值直读:settings/document-updated 首事件可能先于本订阅建立,只靠事件会漏初值
|
|
654
|
+
const stored = storedValue()
|
|
655
|
+
const section = stored as { fileOpenTool?: unknown } | undefined
|
|
656
|
+
if (section?.fileOpenTool !== undefined) notify(section.fileOpenTool)
|
|
657
|
+
aiCurrent = stored !== undefined ? aiValueOf(stored) : aiCurrent
|
|
658
|
+
svnSync(stored)
|
|
433
659
|
syncRevision()
|
|
434
660
|
})
|
|
435
661
|
|
|
@@ -438,31 +664,30 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
|
|
|
438
664
|
get revision() { return revision },
|
|
439
665
|
update: async (value: string, expectedRevision?: number): Promise<void> => {
|
|
440
666
|
const next = normalizeValue(value)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
const stored = descriptor?.value as { fileOpenTool?: unknown } | undefined
|
|
667
|
+
const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision)
|
|
668
|
+
if (!result.ok) log.warn('fileOpenTool 设置写入失败:' + (result.error ?? '未知原因'))
|
|
669
|
+
const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS) as { fileOpenTool?: unknown } | undefined
|
|
445
670
|
notify(stored?.fileOpenTool ?? next)
|
|
446
671
|
syncRevision()
|
|
447
672
|
},
|
|
448
673
|
ai: () => aiCurrent,
|
|
449
674
|
svn: () => svnCurrent,
|
|
450
675
|
aiUpdate: async (patch: AiConfigPatch, expectedRevision?: number): Promise<AiConfigView> => {
|
|
451
|
-
if (!provider?.update) {
|
|
452
|
-
// settings 不可用:内存态生效(重启回落默认),保持与 fileOpenTool 的降级语义一致
|
|
453
|
-
if (patch.enabled !== undefined) aiCurrent.enabled = patch.enabled
|
|
454
|
-
if (patch.provider !== undefined) aiCurrent.provider = patch.provider
|
|
455
|
-
if (patch.model !== undefined) aiCurrent.model = patch.model
|
|
456
|
-
if (patch.effort !== undefined) aiCurrent.effort = patch.effort
|
|
457
|
-
return aiCurrent
|
|
458
|
-
}
|
|
459
676
|
const stored = { ...aiCurrent }
|
|
460
677
|
const body: Record<string, unknown> = {}
|
|
461
678
|
if (patch.enabled !== undefined) { stored.enabled = patch.enabled; body.aiInline = patch.enabled }
|
|
462
679
|
if (patch.provider !== undefined) { stored.provider = patch.provider; body.aiProvider = patch.provider }
|
|
463
680
|
if (patch.model !== undefined) { stored.model = patch.model; body.aiModel = patch.model }
|
|
464
681
|
if (patch.effort !== undefined) { stored.effort = patch.effort; body.aiEffort = patch.effort }
|
|
465
|
-
|
|
682
|
+
if (patch.taskProvider !== undefined) { stored.taskProvider = patch.taskProvider; body.aiTaskProvider = patch.taskProvider }
|
|
683
|
+
if (patch.taskModel !== undefined) { stored.taskModel = patch.taskModel; body.aiTaskModel = patch.taskModel }
|
|
684
|
+
if (patch.taskEffort !== undefined) { stored.taskEffort = patch.taskEffort; body.aiTaskEffort = patch.taskEffort }
|
|
685
|
+
const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, body, expectedRevision)
|
|
686
|
+
if (!result.ok) {
|
|
687
|
+
// settings 不可用/写入失败:内存态生效(重启回落默认),与 fileOpenTool 降级语义一致
|
|
688
|
+
aiCurrent = stored
|
|
689
|
+
return aiCurrent
|
|
690
|
+
}
|
|
466
691
|
aiSync()
|
|
467
692
|
return aiCurrent
|
|
468
693
|
},
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,9 @@ import { newSearcher } from './search/orchestrator.js'
|
|
|
14
14
|
import { newContentSearcher } from './search/content.js'
|
|
15
15
|
import { installIsolation } from './mcpIsolation.js'
|
|
16
16
|
import { cwdOf } from './registry.js'
|
|
17
|
-
import { setupOpenSettings } from './fileOpenSettings.js'
|
|
17
|
+
import { setupOpenSettings, buildSettingsSchema } from './fileOpenSettings.js'
|
|
18
|
+
import type { SettingsDeps } from './fileOpenSettings.js'
|
|
19
|
+
import z from '@deepseek-ai/schemastery'
|
|
18
20
|
import { shellMenuLifecycle } from './integrate.js'
|
|
19
21
|
import { disposeIndex } from './treeIndex.js'
|
|
20
22
|
import { sweepTreeCache } from './paths.js'
|
|
@@ -36,6 +38,21 @@ import type { Ctx } from './store.js'
|
|
|
36
38
|
export const name = "dsh-vscode-mode"
|
|
37
39
|
export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* 插件 Config schema(cordis 从 module 导出读 `plugin.Config` 做启动校验;
|
|
43
|
+
* DSH 0.1.7 起设置页按此 schema 自动生成表单,设置值 = profile 插件配置)。
|
|
44
|
+
* 字段集与 installOpenSettingsSection 的 section schema 同源(buildSettingsSchema),
|
|
45
|
+
* 全字段带默认值——undefined/空配置经 schemastery 校验自动填充(rc/alpha 两代
|
|
46
|
+
* cordis 均读该导出,实测 4.0.0-rc.8 与 0.1.7 行为一致,校验必过)。
|
|
47
|
+
* volatile: true —— 0.1.7 的 SettingsForms.describe 只下发含 volatile 字段的 entry
|
|
48
|
+
* (volatileForm 门槛),漏标则 ns 不出现 → client configForms 恒 unavailable →
|
|
49
|
+
* 设置页「设置服务暂不可用」;同时 volatile 字段由 Loader 就地提交免重载。
|
|
50
|
+
* 运行时解析:@deepseek-ai/schemastery 经插件自身 dependencies(link/dev/npm 安装
|
|
51
|
+
* 三形态一致,钉 ~3.18.4 —— 3.18.1 起才有 .volatile(),低版本经 markVolatile 守卫降级)。
|
|
52
|
+
* @author ddj 2026年09月22号(2026年09月23号 增补 volatile 标记)
|
|
53
|
+
*/
|
|
54
|
+
export const Config = buildSettingsSchema(z as unknown as SettingsDeps['z'], { volatile: true })
|
|
55
|
+
|
|
39
56
|
/**
|
|
40
57
|
* 装配插件:挂事件监听、注册路由、安装兼容层。
|
|
41
58
|
* @author ddj 2026年08月20号
|
package/src/lsp/client.ts
CHANGED
|
@@ -62,6 +62,9 @@ export function createLspClient(
|
|
|
62
62
|
workspaceSymbol: false,
|
|
63
63
|
hover: false,
|
|
64
64
|
semanticTokens: false,
|
|
65
|
+
completion: false,
|
|
66
|
+
completionResolve: false,
|
|
67
|
+
signatureHelp: false,
|
|
65
68
|
semanticTokenTypes: [],
|
|
66
69
|
semanticTokenModifiers: [],
|
|
67
70
|
}
|
|
@@ -164,6 +167,8 @@ export function createLspClient(
|
|
|
164
167
|
documentSymbolProvider?: unknown
|
|
165
168
|
workspaceSymbolProvider?: unknown
|
|
166
169
|
hoverProvider?: unknown
|
|
170
|
+
completionProvider?: { resolveProvider?: unknown; triggerCharacters?: unknown }
|
|
171
|
+
signatureHelpProvider?: { triggerCharacters?: unknown; retriggerCharacters?: unknown }
|
|
167
172
|
semanticTokensProvider?: {
|
|
168
173
|
legend?: { tokenTypes?: unknown; tokenModifiers?: unknown }
|
|
169
174
|
}
|
|
@@ -185,6 +190,22 @@ export function createLspClient(
|
|
|
185
190
|
references: { dynamicRegistration: false },
|
|
186
191
|
documentSymbol: { dynamicRegistration: false },
|
|
187
192
|
hover: { dynamicRegistration: false },
|
|
193
|
+
// 补全能力声明:snippetSupport 让服务器回 InsertAsSnippet 片段;
|
|
194
|
+
// resolveSupport.properties 声明我方支持 completionItem/resolve 惰性补全
|
|
195
|
+
// (EmmyLua 据此把 documentation 挪到 resolve 阶段,列表载荷显著变小)。
|
|
196
|
+
completion: {
|
|
197
|
+
dynamicRegistration: false,
|
|
198
|
+
contextSupport: true,
|
|
199
|
+
completionItem: {
|
|
200
|
+
snippetSupport: true,
|
|
201
|
+
documentationFormat: ['markdown', 'plaintext'],
|
|
202
|
+
resolveSupport: { properties: ['documentation', 'detail', 'additionalTextEdits'] },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
signatureHelp: {
|
|
206
|
+
dynamicRegistration: false,
|
|
207
|
+
signatureInformation: { documentationFormat: ['markdown', 'plaintext'] },
|
|
208
|
+
},
|
|
188
209
|
semanticTokens: {
|
|
189
210
|
dynamicRegistration: false,
|
|
190
211
|
requests: { range: false, full: { delta: false } },
|
|
@@ -205,6 +226,9 @@ export function createLspClient(
|
|
|
205
226
|
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
206
227
|
hover: Boolean(caps.hoverProvider),
|
|
207
228
|
semanticTokens: Boolean(caps.semanticTokensProvider),
|
|
229
|
+
completion: Boolean(caps.completionProvider),
|
|
230
|
+
completionResolve: Boolean(caps.completionProvider?.resolveProvider),
|
|
231
|
+
signatureHelp: Boolean(caps.signatureHelpProvider),
|
|
208
232
|
semanticTokenTypes: Array.isArray(legend?.tokenTypes)
|
|
209
233
|
? legend.tokenTypes.filter((item): item is string => typeof item === 'string')
|
|
210
234
|
: [...LSP_SEMANTIC_TOKEN_TYPES],
|
package/src/lsp/rpc.ts
CHANGED
|
@@ -436,6 +436,63 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
436
436
|
}
|
|
437
437
|
},
|
|
438
438
|
|
|
439
|
+
'edrv.lsp.completion': async (args) => {
|
|
440
|
+
const lang = langOfPath(args.path)
|
|
441
|
+
if (!lang) return { ok: true, completions: undefined }
|
|
442
|
+
const sc = await rootOf(args.sessionId)
|
|
443
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
444
|
+
try {
|
|
445
|
+
const server = serverOf(sc.root, lang)
|
|
446
|
+
if (!server) return { ok: true, completions: undefined }
|
|
447
|
+
const completions = await server.completion(
|
|
448
|
+
wsPath(sc.root, args.path),
|
|
449
|
+
args.position.line,
|
|
450
|
+
args.position.character,
|
|
451
|
+
args.context,
|
|
452
|
+
)
|
|
453
|
+
if (!completions) return { ok: true, completions: undefined }
|
|
454
|
+
const truncated = completions.items.length > 500
|
|
455
|
+
return {
|
|
456
|
+
ok: true,
|
|
457
|
+
completions: truncated ? { ...completions, items: completions.items.slice(0, 500), truncated: true } : completions,
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return { ok: false, error: 'LSP 补全查询失败:' + String(error) }
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
'edrv.lsp.resolveCompletion': async (args) => {
|
|
465
|
+
const lang = langOfPath(args.path)
|
|
466
|
+
if (!lang) return { ok: true, item: undefined }
|
|
467
|
+
const sc = await rootOf(args.sessionId)
|
|
468
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
469
|
+
try {
|
|
470
|
+
const server = serverOf(sc.root, lang)
|
|
471
|
+
if (!server) return { ok: true, item: args.item }
|
|
472
|
+
const item = await server.resolveCompletion(wsPath(sc.root, args.path), args.item)
|
|
473
|
+
return { ok: true, item: item ?? undefined }
|
|
474
|
+
} catch (error) {
|
|
475
|
+
return { ok: false, error: 'LSP 补全解析失败:' + String(error) }
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
'edrv.lsp.signatureHelp': async (args) => {
|
|
480
|
+
const lang = langOfPath(args.path)
|
|
481
|
+
if (!lang) return { ok: true, signatureHelp: undefined }
|
|
482
|
+
const sc = await rootOf(args.sessionId)
|
|
483
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
484
|
+
try {
|
|
485
|
+
const server = serverOf(sc.root, lang)
|
|
486
|
+
if (!server) return { ok: true, signatureHelp: undefined }
|
|
487
|
+
return {
|
|
488
|
+
ok: true,
|
|
489
|
+
signatureHelp: (await server.signatureHelp(wsPath(sc.root, args.path), args.position.line, args.position.character)) ?? undefined,
|
|
490
|
+
}
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return { ok: false, error: 'LSP 签名帮助查询失败:' + String(error) }
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
|
|
439
496
|
'edrv.lsp.semanticTokens': async (args) => {
|
|
440
497
|
const lang = langOfPath(args.path)
|
|
441
498
|
if (!lang) return { ok: true, tokens: undefined }
|