Skip to content

Commit 682bd94

Browse files
refactor(vscode): cleanup extension client (#5422)
1 parent 3c02ccb commit 682bd94

20 files changed

+243
-674
lines changed

.gitignore

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
out
21
dist
32
node_modules
43
*.tsbuildinfo
54
*.vsix
6-
.vscode-test-web
7-
extensions/*/meta.json
8-
extensions/*/stats.html
9-
extensions/vscode/src/generated-meta.ts
5+
generated-meta.ts
106

117
packages/*/*.d.ts
128
packages/*/*.js

extensions/vscode/.vscodeignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
src
1+
**/*.ts
22
tests
3-
rolldown.config.ts
43
tsconfig.json

extensions/vscode/index.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { activateAutoInsertion, activateDocumentDropEdit, createLabsInfo, middleware } from '@volar/vscode';
2+
import * as lsp from '@volar/vscode/node';
3+
import * as fs from 'node:fs';
4+
import * as path from 'node:path';
5+
import { defineExtension, executeCommand, extensionContext, nextTick, onDeactivate, useActiveTextEditor, useCommand, useOutputChannel, useVisibleTextEditors, watch } from 'reactive-vscode';
6+
import * as vscode from 'vscode';
7+
import { config } from './lib/config';
8+
import { activate as activateSplitEditors } from './lib/splitEditors';
9+
10+
let client: lsp.BaseLanguageClient | undefined;
11+
12+
class _LanguageClient extends lsp.LanguageClient {
13+
fillInitializeParams(params: lsp.InitializeParams) {
14+
// fix https://github.com/vuejs/language-tools/issues/1959
15+
params.locale = vscode.env.language;
16+
}
17+
}
18+
19+
export const { activate, deactivate } = defineExtension(async () => {
20+
await vscode.extensions.getExtension('vscode.typescript-language-features')?.activate();
21+
22+
const context = extensionContext.value!;
23+
const volarLabs = createLabsInfo();
24+
const activeTextEditor = useActiveTextEditor();
25+
const visibleTextEditors = useVisibleTextEditors();
26+
const { stop } = watch(activeTextEditor, () => {
27+
28+
if (!visibleTextEditors.value.some(
29+
editor => config.server.includeLanguages.includes(editor.document.languageId)
30+
)) {
31+
return;
32+
}
33+
34+
nextTick(() => stop());
35+
36+
watch(() => config.server.includeLanguages, async () => {
37+
const reload = await vscode.window.showInformationMessage(
38+
'Please restart extension host to apply the new language settings.',
39+
'Restart Extension Host'
40+
);
41+
if (reload) {
42+
executeCommand('workbench.action.restartExtensionHost');
43+
}
44+
});
45+
46+
// Setup typescript.js in production mode
47+
if (fs.existsSync(path.join(__dirname, 'language-server.js'))) {
48+
fs.writeFileSync(path.join(__dirname, 'typescript.js'), `module.exports = require("${vscode.env.appRoot.replace(/\\/g, '/')}/extensions/node_modules/typescript/lib/typescript.js");`);
49+
}
50+
51+
volarLabs.addLanguageClient(client = launch(context));
52+
53+
const selectors = config.server.includeLanguages;
54+
55+
activateAutoInsertion(selectors, client);
56+
activateDocumentDropEdit(selectors, client);
57+
activateSplitEditors(client);
58+
59+
}, { immediate: true });
60+
61+
useCommand('vue.action.restartServer', async () => {
62+
await executeCommand('typescript.restartTsServer');
63+
await client?.stop();
64+
client?.outputChannel.clear();
65+
await client?.start();
66+
});
67+
68+
onDeactivate(async () => {
69+
await client?.stop();
70+
});
71+
72+
return volarLabs.extensionExports;
73+
});
74+
75+
function launch(context: vscode.ExtensionContext) {
76+
const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'language-server.js');
77+
const client = new _LanguageClient(
78+
'vue',
79+
'Vue',
80+
{
81+
run: {
82+
module: serverModule.fsPath,
83+
transport: lsp.TransportKind.ipc,
84+
options: {},
85+
},
86+
debug: {
87+
module: serverModule.fsPath,
88+
transport: lsp.TransportKind.ipc,
89+
options: { execArgv: ['--nolazy', '--inspect=' + 6009] },
90+
}
91+
},
92+
{
93+
middleware: {
94+
...middleware,
95+
async resolveCodeAction(item, token, next) {
96+
if (item.kind?.value === 'refactor.move.newFile.dumb' && config.codeActions.askNewComponentName) {
97+
const inputName = await vscode.window.showInputBox({ value: (item as any).data.original.data.newName });
98+
if (!inputName) {
99+
return item; // cancel
100+
}
101+
(item as any).data.original.data.newName = inputName;
102+
}
103+
return await (middleware.resolveCodeAction?.(item, token, next) ?? next(item, token));
104+
},
105+
},
106+
documentSelector: config.server.includeLanguages,
107+
markdown: {
108+
isTrusted: true,
109+
supportHtml: true
110+
},
111+
outputChannel: useOutputChannel('Vue Language Server'),
112+
}
113+
);
114+
115+
client.onNotification('tsserver/request', async ([id, command, args]) => {
116+
const tsserver = (globalThis as any).__TSSERVER__?.semantic;
117+
if (!tsserver) {
118+
return;
119+
}
120+
try {
121+
const res = await tsserver.executeImpl(command, args, {
122+
isAsync: true,
123+
expectsResult: true,
124+
lowPriority: true,
125+
requireSemantic: true,
126+
})[0];
127+
client.sendNotification('tsserver/response', [id, res.body]);
128+
} catch {
129+
// noop
130+
}
131+
});
132+
client.start();
133+
134+
return client;
135+
}
136+
137+
try {
138+
const fs = require('node:fs');
139+
const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')!;
140+
const readFileSync = fs.readFileSync;
141+
const extensionJsPath = require.resolve('./dist/extension.js', {
142+
paths: [tsExtension.extensionPath],
143+
});
144+
145+
// @ts-expect-error
146+
fs.readFileSync = (...args) => {
147+
if (args[0] === extensionJsPath) {
148+
let text = readFileSync(...args) as string;
149+
150+
// patch readPlugins
151+
text = text.replace(
152+
'languages:Array.isArray(e.languages)',
153+
[
154+
'languages:',
155+
`e.name==='vue-typescript-plugin-pack'?[${config.server.includeLanguages
156+
.map(lang => `'${lang}'`)
157+
.join(',')}]`,
158+
':Array.isArray(e.languages)'
159+
].join('')
160+
);
161+
162+
// Expose tsserver process in SingleTsServer constructor
163+
text = text.replace(
164+
',this._callbacks.destroy("server errored")}))',
165+
s => s + ',globalThis.__TSSERVER__||={},globalThis.__TSSERVER__[arguments[1]]=this'
166+
);
167+
168+
/**
169+
* VSCode >= 1.87.0
170+
*/
171+
172+
// patch jsTsLanguageModes
173+
text = text.replace(
174+
't.jsTsLanguageModes=[t.javascript,t.javascriptreact,t.typescript,t.typescriptreact]',
175+
s => s + '.concat("vue")'
176+
);
177+
// patch isSupportedLanguageMode
178+
text = text.replace(
179+
'.languages.match([t.typescript,t.typescriptreact,t.javascript,t.javascriptreact]',
180+
s => s + '.concat("vue")'
181+
);
182+
183+
return text;
184+
}
185+
return readFileSync(...args);
186+
};
187+
188+
const loadedModule = require.cache[extensionJsPath];
189+
if (loadedModule) {
190+
delete require.cache[extensionJsPath];
191+
const patchedModule = require(extensionJsPath);
192+
Object.assign(loadedModule.exports, patchedModule);
193+
}
194+
195+
if (tsExtension.isActive) {
196+
vscode.commands.executeCommand('workbench.action.restartExtensionHost');
197+
}
198+
} catch { }
File renamed without changes.

extensions/vscode/src/features/splitEditors.ts renamed to extensions/vscode/lib/splitEditors.ts

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { ExecuteCommandRequest, type BaseLanguageClient, type ExecuteCommandPara
22
import type { SFCBlock, SFCParseResult } from '@vue/compiler-sfc';
33
import { executeCommand, useActiveTextEditor, useCommand } from 'reactive-vscode';
44
import * as vscode from 'vscode';
5-
import { config } from '../config';
5+
import { config } from './config';
66

77
export function activate(client: BaseLanguageClient) {
88

9+
const astMap = new WeakMap<vscode.TextDocument, [version: number, Promise<SFCParseResult | undefined>]>();
910
const activeTextEditor = useActiveTextEditor();
10-
const getDocDescriptor = useDocDescriptor(client);
1111

1212
useCommand('vue.action.splitEditors', async () => {
1313
const editor = activeTextEditor.value;
@@ -17,11 +17,22 @@ export function activate(client: BaseLanguageClient) {
1717

1818
const layout = config.splitEditors.layout;
1919
const doc = editor.document;
20-
const descriptor = (await getDocDescriptor(doc.getText()))?.descriptor;
21-
if (!descriptor) {
20+
if (!astMap.has(doc) || astMap.get(doc)![0] !== doc.version) {
21+
astMap.set(doc, [
22+
doc.version,
23+
client.sendRequest(ExecuteCommandRequest.type, {
24+
command: 'vue.parseSfc',
25+
arguments: [doc.getText()],
26+
} satisfies ExecuteCommandParams),
27+
]);
28+
}
29+
const ast = await astMap.get(doc)![1];
30+
if (!ast) {
2231
return;
2332
}
2433

34+
const descriptor = ast.descriptor;
35+
2536
let leftBlocks: SFCBlock[] = [];
2637
let rightBlocks: SFCBlock[] = [];
2738

@@ -65,51 +76,32 @@ export function activate(client: BaseLanguageClient) {
6576
await executeCommand('workbench.action.joinEditorInGroup');
6677

6778
if (activeTextEditor.value === editor) {
68-
await foldingBlocks(leftBlocks);
79+
await foldingBlocks(doc, leftBlocks);
6980
await executeCommand('workbench.action.toggleSplitEditorInGroup');
70-
await foldingBlocks(rightBlocks);
81+
await foldingBlocks(doc, rightBlocks);
7182
}
7283
else {
7384
await executeCommand('editor.unfoldAll');
7485
}
86+
});
7587

76-
async function foldingBlocks(blocks: SFCBlock[]) {
77-
78-
const editor = activeTextEditor.value;
79-
if (!editor) {
80-
return;
81-
}
82-
83-
editor.selections = blocks.length
84-
? blocks.map(block => new vscode.Selection(doc.positionAt(block.loc.start.offset), doc.positionAt(block.loc.start.offset)))
85-
: [new vscode.Selection(doc.positionAt(doc.getText().length), doc.positionAt(doc.getText().length))];
86-
87-
await executeCommand('editor.unfoldAll');
88-
await executeCommand('editor.foldLevel1');
88+
async function foldingBlocks(doc: vscode.TextDocument, blocks: SFCBlock[]) {
8989

90-
const firstBlock = blocks.sort((a, b) => a.loc.start.offset - b.loc.start.offset)[0];
91-
if (firstBlock) {
92-
editor.revealRange(new vscode.Range(doc.positionAt(firstBlock.loc.start.offset), new vscode.Position(editor.document.lineCount, 0)), vscode.TextEditorRevealType.AtTop);
93-
}
90+
const editor = activeTextEditor.value;
91+
if (!editor) {
92+
return;
9493
}
95-
});
96-
}
97-
98-
export function useDocDescriptor(client: BaseLanguageClient) {
9994

100-
let splitDocText: string | undefined;
101-
let splitDocDescriptor: SFCParseResult | undefined;
95+
editor.selections = blocks.length
96+
? blocks.map(block => new vscode.Selection(doc.positionAt(block.loc.start.offset), doc.positionAt(block.loc.start.offset)))
97+
: [new vscode.Selection(doc.positionAt(doc.getText().length), doc.positionAt(doc.getText().length))];
10298

103-
return getDescriptor;
99+
await executeCommand('editor.unfoldAll');
100+
await executeCommand('editor.foldLevel1');
104101

105-
async function getDescriptor(text: string) {
106-
if (text !== splitDocText) {
107-
splitDocText = text;
108-
splitDocDescriptor = await client.sendRequest(ExecuteCommandRequest.type, {
109-
command: 'vue.parseSfc',
110-
arguments: [text],
111-
} satisfies ExecuteCommandParams);
102+
const firstBlock = blocks.sort((a, b) => a.loc.start.offset - b.loc.start.offset)[0];
103+
if (firstBlock) {
104+
editor.revealRange(new vscode.Range(doc.positionAt(firstBlock.loc.start.offset), new vscode.Position(editor.document.lineCount, 0)), vscode.TextEditorRevealType.AtTop);
112105
}
113-
return splitDocDescriptor;
114106
}
115107
}

extensions/vscode/package.json

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"onLanguage:markdown",
2727
"onLanguage:html"
2828
],
29-
"main": "./dist/client.js",
29+
"main": "./dist/extension.js",
3030
"browser": "./web.js",
3131
"capabilities": {
3232
"virtualWorkspaces": {
@@ -279,21 +279,11 @@
279279
],
280280
"markdownDescription": "%configuration.splitEditors.layout.right%"
281281
},
282-
"vue.codeActions.enabled": {
283-
"type": "boolean",
284-
"default": true,
285-
"markdownDescription": "%configuration.codeActions.enabled%"
286-
},
287282
"vue.codeActions.askNewComponentName": {
288283
"type": "boolean",
289284
"default": true,
290285
"markdownDescription": "%configuration.codeActions.askNewComponentName%"
291286
},
292-
"vue.codeLens.enabled": {
293-
"type": "boolean",
294-
"default": true,
295-
"markdownDescription": "%configuration.codeLens.enabled%"
296-
},
297287
"vue.complete.casing.tags": {
298288
"type": "string",
299289
"enum": [
@@ -467,7 +457,7 @@
467457
"devDependencies": {
468458
"@types/node": "^22.10.4",
469459
"@types/semver": "^7.5.3",
470-
"@types/vscode": "^1.82.0",
460+
"@types/vscode": "1.88.0",
471461
"@volar/vscode": "2.4.14",
472462
"@vscode/vsce": "^3.2.1",
473463
"@vue/compiler-sfc": "^3.5.0",

extensions/vscode/package.nls.ja.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
"configuration.splitEditors.icon": "エディターのタイトル領域にエディター分割ボタンを表示します。",
55
"configuration.splitEditors.layout.left": "左側の分割されたエディターに含めるブロック。",
66
"configuration.splitEditors.layout.right": "右側の分割されたエディターに含めるブロック。",
7-
"configuration.codeActions.enabled": "コードアクションを有効にします。",
87
"configuration.codeActions.askNewComponentName": "コンポーネントを抽出する時に新しいコンポーネント名を尋ねます。",
9-
"configuration.codeLens.enabled": "コードレンズを有効にします。",
108
"configuration.complete.casing.tags": "タグ名の命名規則。",
119
"configuration.complete.casing.props": "属性名の命名規則。",
1210
"configuration.complete.defineAssignment": "補完項目 `props` を選択した時に `const props = ` を `defineProps` の前に自動で追加します。(`emit` と `slots` も同様)",

extensions/vscode/package.nls.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
"configuration.splitEditors.icon": "Show split editor icon in title area of editor.",
55
"configuration.splitEditors.layout.left": "Blocks in the left split editor.",
66
"configuration.splitEditors.layout.right": "Blocks in the right split editor.",
7-
"configuration.codeActions.enabled": "Enable code actions.",
87
"configuration.codeActions.askNewComponentName": "Ask for new component name when extract component.",
9-
"configuration.codeLens.enabled": "Enable code lens.",
108
"configuration.complete.casing.tags": "Preferred tag name case.",
119
"configuration.complete.casing.props": "Preferred attr name case.",
1210
"configuration.complete.defineAssignment": "Auto add `const props = ` before `defineProps` when selecting the completion item `props`. (also `emit` and `slots`)",

extensions/vscode/package.nls.ru.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
"configuration.splitEditors.icon": "Отображает иконку разделенного редактора в области заголовка редактора.",
55
"configuration.splitEditors.layout.left": "Блоки в левом разделенном редакторе.",
66
"configuration.splitEditors.layout.right": "Блоки в правом разделенном редакторе.",
7-
"configuration.codeActions.enabled": "Включает код-действия.",
87
"configuration.codeActions.askNewComponentName": "Запрашивает новое имя компонента при извлечении компонента.",
9-
"configuration.codeLens.enabled": "Включает код-линзы.",
108
"configuration.complete.casing.tags": "Предпочитаемый формат имени тега.",
119
"configuration.complete.casing.props": "Предпочитаемый формат имени атрибута.",
1210
"configuration.complete.defineAssignment": "Автоматически добавляет `const props = ` перед `defineProps` при выборе элемента завершения `props`. (также `emit` и `slots`)",

0 commit comments

Comments
 (0)