From 9965d56b7bb295c99201bcd4f2f4a6babd28a2cf Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 4 Apr 2024 19:00:15 +0800 Subject: [PATCH] feat(vscode): automatically enable Hybrid Mode (#4206) --- extensions/vscode/package.json | 7 +- extensions/vscode/src/common.ts | 157 ++++++++++++++++++++--- extensions/vscode/src/config.ts | 2 +- extensions/vscode/src/features/doctor.ts | 21 --- extensions/vscode/src/nodeClientMain.ts | 5 +- 5 files changed, 150 insertions(+), 42 deletions(-) diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 74d4b5edf6..f9a33f814c 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -221,7 +221,12 @@ }, "vue.server.hybridMode": { "type": "boolean", - "default": false, + "default": "auto", + "enum": [ + "auto", + true, + false + ], "description": "Vue language server only handles CSS and HTML language support, and tsserver takes over TS language support via TS plugin." }, "vue.server.maxFileSize": { diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 31d8f03a60..ab63b7bb76 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -5,6 +5,9 @@ import { config } from './config'; import * as doctor from './features/doctor'; import * as nameCasing from './features/nameCasing'; import * as splitEditors from './features/splitEditors'; +import * as semver from 'semver'; +import * as fs from 'fs'; +import * as path from 'path'; let client: lsp.BaseLanguageClient; @@ -17,8 +20,6 @@ type CreateLanguageClient = ( outputChannel: vscode.OutputChannel, ) => lsp.BaseLanguageClient; -const beginHybridMode = config.server.hybridMode; - export async function activate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { const stopCheck = vscode.window.onDidChangeActiveTextEditor(tryActivate); @@ -36,17 +37,137 @@ export async function activate(context: vscode.ExtensionContext, createLc: Creat } } +export const currentHybridModeStatus = getCurrentHybridModeStatus(); + +function getCurrentHybridModeStatus(report = false) { + if (config.server.hybridMode === 'auto') { + const unknownExtensions: string[] = []; + for (const extension of vscode.extensions.all) { + const hasTsPlugin = !!extension.packageJSON?.contributes?.typescriptServerPlugins; + if (hasTsPlugin) { + if ( + extension.id === 'Vue.volar' + || extension.id === 'unifiedjs.vscode-mdx' + || extension.id === 'astro-build.astro-vscode' + || extension.id === 'ije.esm-vscode' + || extension.id === 'johnsoncodehk.vscode-tsslint' + || extension.id === 'VisualStudioExptTeam.vscodeintellicode' + ) { + continue; + } + else { + unknownExtensions.push(extension.id); + } + } + } + if (unknownExtensions.length) { + if (report) { + vscode.window.showInformationMessage( + `Hybrid Mode is disabled automatically because there is a potentially incompatible ${unknownExtensions.join(', ')} TypeScript plugin installed.`, + 'Open Settings', + 'Report a false positive', + ).then(value => { + if (value === 'Open Settings') { + vscode.commands.executeCommand('workbench.action.openSettings', 'vue.server.hybridMode'); + } + else if (value == 'Report a false positive') { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/vuejs/language-tools/pull/4206')); + } + }); + } + return false; + } + const vscodeTsdkVersion = getVScodeTsdkVersion(); + const workspaceTsdkVersion = getWorkspaceTsdkVersion(); + if ( + (vscodeTsdkVersion && !semver.gte(vscodeTsdkVersion, '5.3.0')) + || (workspaceTsdkVersion && !semver.gte(workspaceTsdkVersion, '5.3.0')) + ) { + if (report) { + let msg = `Hybrid Mode is disabled automatically because TSDK >= 5.3.0 is required (VSCode TSDK: ${vscodeTsdkVersion}`; + if (workspaceTsdkVersion) { + msg += `, Workspace TSDK: ${workspaceTsdkVersion}`; + } + msg += `).`; + vscode.window.showInformationMessage(msg, 'Open Settings').then(value => { + if (value === 'Open Settings') { + vscode.commands.executeCommand('workbench.action.openSettings', 'vue.server.hybridMode'); + } + }); + } + return false; + } + return true; + } + else { + return config.server.hybridMode; + } + + function getVScodeTsdkVersion() { + const nightly = vscode.extensions.getExtension('ms-vscode.vscode-typescript-next'); + if (nightly) { + const libPath = path.join( + nightly.extensionPath.replace(/\\/g, '/'), + 'node_modules/typescript/lib', + ); + return getTsVersion(libPath); + } + + if (vscode.env.appRoot) { + const libPath = path.join( + vscode.env.appRoot.replace(/\\/g, '/'), + 'extensions/node_modules/typescript/lib', + ); + return getTsVersion(libPath); + } + } + + function getWorkspaceTsdkVersion() { + const libPath = vscode.workspace.getConfiguration('typescript').get('tsdk')?.replace(/\\/g, '/'); + if (libPath) { + return getTsVersion(libPath); + } + } + + function getTsVersion(libPath: string): string | undefined { + + const p = libPath.toString().split('/'); + const p2 = p.slice(0, -1); + const modulePath = p2.join('/'); + const filePath = modulePath + '/package.json'; + const contents = fs.readFileSync(filePath, 'utf-8'); + + if (contents === undefined) { + return; + } + + let desc: any = null; + try { + desc = JSON.parse(contents); + } catch (err) { + return; + } + if (!desc || !desc.version) { + return; + } + + return desc.version; + } +} + async function doActivate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { - vscode.commands.executeCommand('setContext', 'vue.activated', true); + getCurrentHybridModeStatus(true); const outputChannel = vscode.window.createOutputChannel('Vue Language Server'); + vscode.commands.executeCommand('setContext', 'vue.activated', true); + client = createLc( 'vue', 'Vue', getDocumentSelector(), - await getInitializationOptions(context), + await getInitializationOptions(context, currentHybridModeStatus), 6009, outputChannel ); @@ -72,20 +193,20 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang lsp.activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); lsp.activateServerSys(client); - if (!config.server.hybridMode) { + if (!currentHybridModeStatus) { lsp.activateTsConfigStatusItem(selectors, 'vue.tsconfig', client); lsp.activateTsVersionStatusItem(selectors, 'vue.tsversion', context, client, text => 'TS ' + text); } const hybridModeStatus = vscode.languages.createLanguageStatusItem('vue-hybrid-mode', selectors); hybridModeStatus.text = 'Hybrid Mode'; - hybridModeStatus.detail = config.server.hybridMode ? 'Enabled' : 'Disabled'; + hybridModeStatus.detail = (currentHybridModeStatus ? 'Enabled' : 'Disabled') + (config.server.hybridMode === 'auto' ? ' (Auto)' : ''); hybridModeStatus.command = { title: 'Open Setting', command: 'workbench.action.openSettings', arguments: ['vue.server.hybridMode'], }; - if (!config.server.hybridMode) { + if (!currentHybridModeStatus) { hybridModeStatus.severity = vscode.LanguageStatusSeverity.Warning; } @@ -122,12 +243,15 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang function activateConfigWatcher() { context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('vue.server.hybridMode') && config.server.hybridMode !== beginHybridMode) { - requestReloadVscode( - config.server.hybridMode - ? 'Please reload VSCode to enable Hybrid Mode.' - : 'Please reload VSCode to disable Hybrid Mode.' - ); + if (e.affectsConfiguration('vue.server.hybridMode')) { + const newStatus = getCurrentHybridModeStatus(); + if (newStatus !== currentHybridModeStatus) { + requestReloadVscode( + newStatus + ? 'Please reload VSCode to enable Hybrid Mode.' + : 'Please reload VSCode to disable Hybrid Mode.' + ); + } } else if (e.affectsConfiguration('vue')) { vscode.commands.executeCommand('vue.action.restartServer', false); @@ -139,7 +263,7 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang context.subscriptions.push(vscode.commands.registerCommand('vue.action.restartServer', async (restartTsServer: boolean = true) => { await client.stop(); outputChannel.clear(); - client.clientOptions.initializationOptions = await getInitializationOptions(context); + client.clientOptions.initializationOptions = await getInitializationOptions(context, currentHybridModeStatus); await client.start(); nameCasing.activate(context, client, selectors); if (restartTsServer) { @@ -167,6 +291,7 @@ export function getDocumentSelector(): lsp.DocumentFilter[] { async function getInitializationOptions( context: vscode.ExtensionContext, + hybridMode: boolean, ): Promise { return { // volar @@ -174,11 +299,11 @@ async function getInitializationOptions( typescript: { tsdk: (await lsp.getTsdk(context)).tsdk }, maxFileSize: config.server.maxFileSize, semanticTokensLegend: { - tokenTypes: ['component'], + tokenTypes: [], tokenModifiers: [], }, vue: { - hybridMode: beginHybridMode, + hybridMode, additionalExtensions: [ ...config.server.additionalExtensions, ...!config.server.petiteVue.supportHtmlFile ? [] : ['html'], diff --git a/extensions/vscode/src/config.ts b/extensions/vscode/src/config.ts index 826bcf4a84..bad43b03a1 100644 --- a/extensions/vscode/src/config.ts +++ b/extensions/vscode/src/config.ts @@ -16,7 +16,7 @@ export const config = { return _config().get('doctor')!; }, get server(): Readonly<{ - hybridMode: boolean; + hybridMode: 'auto' | boolean; maxOldSpaceSize: number; maxFileSize: number; diagnosticModel: 'push' | 'pull'; diff --git a/extensions/vscode/src/features/doctor.ts b/extensions/vscode/src/features/doctor.ts index 2d79630625..4a4e5b34b9 100644 --- a/extensions/vscode/src/features/doctor.ts +++ b/extensions/vscode/src/features/doctor.ts @@ -231,27 +231,6 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan }); } - if (config.server.hybridMode) { - // #3942, https://github.com/microsoft/TypeScript/issues/57633 - for (const extId of [ - 'svelte.svelte-vscode', - 'styled-components.vscode-styled-components', - 'Divlo.vscode-styled-jsx-languageserver', - ]) { - const ext = vscode.extensions.getExtension(extId); - if (ext) { - problems.push({ - title: `Recommended to disable "${ext.packageJSON.displayName || extId}" in Vue workspace`, - message: [ - `This extension's TypeScript Plugin and Vue's TypeScript Plugin are known to cause some conflicts. Until the problem is resolved, it is recommended that you temporarily disable the this extension in the Vue workspace.`, - '', - 'Issues: https://github.com/vuejs/language-tools/issues/3942, https://github.com/microsoft/TypeScript/issues/57633', - ].join('\n'), - }); - } - } - } - // check outdated vue language plugins // check node_modules has more than one vue versions // check ESLint, Prettier... diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 1d2d9d36b7..85f2ae3561 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -3,7 +3,7 @@ import * as serverLib from '@vue/language-server'; import * as fs from 'fs'; import * as vscode from 'vscode'; import * as lsp from '@volar/vscode/node'; -import { activate as commonActivate, deactivate as commonDeactivate } from './common'; +import { activate as commonActivate, deactivate as commonDeactivate, currentHybridModeStatus } from './common'; import { config } from './config'; import { middleware } from './middleware'; @@ -133,7 +133,6 @@ try { const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')!; const readFileSync = fs.readFileSync; const extensionJsPath = require.resolve('./dist/extension.js', { paths: [tsExtension.extensionPath] }); - const { hybridMode } = config.server; // @ts-expect-error fs.readFileSync = (...args) => { @@ -141,7 +140,7 @@ try { // @ts-expect-error let text = readFileSync(...args) as string; - if (!hybridMode) { + if (!currentHybridModeStatus) { // patch readPlugins text = text.replace( 'languages:Array.isArray(e.languages)',