From 598bef353174933f15451ba0eddcc97a323995b8 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Sat, 26 Oct 2024 12:57:00 +0800 Subject: [PATCH 01/10] refactor: wip --- extensions/vscode/package.json | 3 + extensions/vscode/src/common.ts | 712 +++++++++--------- extensions/vscode/src/config.ts | 44 +- extensions/vscode/src/features/doctor.ts | 42 +- extensions/vscode/src/features/nameCasing.ts | 252 +++---- .../vscode/src/features/splitEditors.ts | 32 +- extensions/vscode/src/middleware.ts | 6 +- extensions/vscode/src/nodeClientMain.ts | 158 ++-- 8 files changed, 615 insertions(+), 634 deletions(-) diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index e20627d251..49802bbc52 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -561,5 +561,8 @@ "esbuild-visualizer": "latest", "semver": "^7.5.4", "vscode-tmlanguage-snapshot": "latest" + }, + "dependencies": { + "reactive-vscode": "0.2.6" } } diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index d45926e992..c086f9808f 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as semver from 'semver'; import * as vscode from 'vscode'; +import { computed, executeCommand, ref, useAllExtensions, watchEffect, useActiveTextEditor, useVisibleTextEditors, watch, useOutputChannel, useCommand } from 'reactive-vscode'; import { config } from './config'; import * as doctor from './features/doctor'; import * as nameCasing from './features/nameCasing'; @@ -21,428 +22,380 @@ type CreateLanguageClient = ( outputChannel: vscode.OutputChannel, ) => lsp.BaseLanguageClient; -export function activate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { - - const stopCheck = vscode.window.onDidChangeActiveTextEditor(tryActivate); - tryActivate(); - - function tryActivate() { - if (vscode.window.visibleTextEditors.some(editor => config.server.includeLanguages.includes(editor.document.languageId))) { - doActivate(context, createLc); - stopCheck.dispose(); - } +export const enabledHybridMode = ref(true); +export const enabledTypeScriptPlugin = computed(() => { + return enabledHybridMode.value || config.server.value.hybridMode === 'typeScriptPluginOnly'; +}); + +const extensions = useAllExtensions(); + +const incompatibleExtensions = computed(() => { + return extensions.value + .filter((ext) => isExtensionCompatibleWithHybridMode(ext) === false) + .map((ext) => ext.id); +}); + +const unknownExtensions = computed(() => { + return extensions.value + .filter((ext) => isExtensionCompatibleWithHybridMode(ext) === undefined && !!ext.packageJSON?.contributes?.typescriptServerPlugins) + .map((ext) => ext.id); +}); + +const vscodeTsdkVersion = computed(() => { + const nightly = extensions.value.find(({ id }) => id === 'ms-vscode.vscode-typescript-next'); + if (nightly) { + const libPath = path.join( + nightly.extensionPath.replace(/\\/g, '/'), + 'node_modules/typescript/lib' + ); + return getTsVersion(libPath); } -} - -export const enabledHybridMode = getCurrentHybridModeStatus(); -export const enabledTypeScriptPlugin = getCurrentTypeScriptPluginStatus(enabledHybridMode); - -vscode.commands.executeCommand('setContext', 'vueHybridMode', enabledHybridMode); - -function getCurrentTypeScriptPluginStatus(enabledHybridMode: boolean) { - return enabledHybridMode || config.server.hybridMode === 'typeScriptPluginOnly'; -} - -function isExtensionCompatibleWithHybridMode(extension: vscode.Extension) { - 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' - || extension.id === 'bierner.lit-html' - || extension.id === 'jenkey2011.string-highlight' - || extension.id === 'mxsdev.typescript-explorer' - || extension.id === 'miaonster.vscode-tsx-arrow-definition' - || extension.id === 'runem.lit-plugin' - || extension.id === 'kimuson.ts-type-expand' - || extension.id === 'p42ai.refactor' - || extension.id === 'styled-components.vscode-styled-components' - || extension.id === 'Divlo.vscode-styled-jsx-languageserver' - || extension.id === 'nrwl.angular-console' - || extension.id === 'ShenQingchuan.vue-vine-extension' - || extension.id === 'ms-dynamics-smb.al' - ) { - return true; - } - if (extension.id === 'denoland.vscode-deno') { - return !vscode.workspace.getConfiguration('deno').get('enable'); + if (vscode.env.appRoot) { + const libPath = path.join( + vscode.env.appRoot.replace(/\\/g, '/'), + 'extensions/node_modules/typescript/lib' + ); + return getTsVersion(libPath); } - if (extension.id === 'svelte.svelte-vscode') { - return semver.gte(extension.packageJSON.version, '108.4.0'); - } -} +}); -function getCurrentHybridModeStatus(report = false) { - - const incompatibleExtensions: string[] = []; - const unknownExtensions: string[] = []; - - for (const extension of vscode.extensions.all) { - const compatible = isExtensionCompatibleWithHybridMode(extension); - if (compatible === false) { - incompatibleExtensions.push(extension.id); - } - else if (compatible === undefined) { - const hasTsPlugin = !!extension.packageJSON?.contributes?.typescriptServerPlugins; - if (hasTsPlugin) { - unknownExtensions.push(extension.id); - } - } +const workspaceTsdkVersion = computed(() => { + const libPath = vscode.workspace.getConfiguration('typescript').get('tsdk')?.replace(/\\/g, '/'); + if (libPath) { + return getTsVersion(libPath); } +}); - if (config.server.hybridMode === 'typeScriptPluginOnly') { - return false; - } - else if (config.server.hybridMode === 'auto') { - if (incompatibleExtensions.length || unknownExtensions.length) { - if (report) { +watchEffect(() => { + switch (config.server.value.hybridMode) { + case 'typeScriptPluginOnly': { + enabledHybridMode.value = false; + break; + } + case 'auto': { + if (incompatibleExtensions.value.length || unknownExtensions.value.length) { vscode.window.showInformationMessage( - `Hybrid Mode is disabled automatically because there is a potentially incompatible ${[...incompatibleExtensions, ...unknownExtensions].join(', ')} TypeScript plugin installed.`, + `Hybrid Mode is disabled automatically because there is a potentially incompatible ${[...incompatibleExtensions.value, ...unknownExtensions.value].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'); + 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')); } }); + enabledHybridMode.value = false; } - 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}`; + else if ( + (vscodeTsdkVersion.value && !semver.gte(vscodeTsdkVersion.value, '5.3.0')) + || (workspaceTsdkVersion.value && !semver.gte(workspaceTsdkVersion.value, '5.3.0')) + ) { + let msg = `Hybrid Mode is disabled automatically because TSDK >= 5.3.0 is required (VSCode TSDK: ${vscodeTsdkVersion.value}`; + if (workspaceTsdkVersion.value) { + msg += `, Workspace TSDK: ${workspaceTsdkVersion.value}`; } msg += `).`; vscode.window.showInformationMessage(msg, 'Open Settings').then(value => { if (value === 'Open Settings') { - vscode.commands.executeCommand('workbench.action.openSettings', 'vue.server.hybridMode'); + executeCommand('workbench.action.openSettings', 'vue.server.hybridMode'); } }); + enabledHybridMode.value = false; + } + else { + enabledHybridMode.value = true; } - return false; + break; } - return true; - } - else { - if (config.server.hybridMode && incompatibleExtensions.length && report) { - vscode.window.showWarningMessage( - `You have explicitly enabled Hybrid Mode, but you have installed known incompatible extensions: ${incompatibleExtensions.join(', ')}. You may want to change vue.server.hybridMode to "auto" to avoid compatibility issues.`, - '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')); - } - }); + default: { + if (config.server.value.hybridMode && incompatibleExtensions.value.length) { + vscode.window.showWarningMessage( + `You have explicitly enabled Hybrid Mode, but you have installed known incompatible extensions: ${incompatibleExtensions.value.join(', ')}. You may want to change vue.server.hybridMode to "auto" to avoid compatibility issues.`, + 'Open Settings', + 'Report a false positive' + ).then(value => { + if (value === 'Open Settings') { + 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')); + } + }); + } + enabledHybridMode.value = config.server.value.hybridMode; } - 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); - } +export function activate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { + const activeTextEditor = useActiveTextEditor(); + const visibleTextEditors = useVisibleTextEditors(); + const outputChannel = useOutputChannel('Vue Language Server'); - if (vscode.env.appRoot) { - const libPath = path.join( - vscode.env.appRoot.replace(/\\/g, '/'), - 'extensions/node_modules/typescript/lib' - ); - return getTsVersion(libPath); - } - } + executeCommand('setContext', 'vueHybridMode', enabledHybridMode.value); - function getWorkspaceTsdkVersion() { - const libPath = vscode.workspace.getConfiguration('typescript').get('tsdk')?.replace(/\\/g, '/'); - if (libPath) { - return getTsVersion(libPath); + const { stop } = watch(activeTextEditor, async () => { + if (visibleTextEditors.value.every(editor => !config.server.value.includeLanguages.includes(editor.document.languageId))) { + return; } - } - function getTsVersion(libPath: string): string | undefined { - try { - 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; + executeCommand('setContext', 'vue.activated', true); + + const selectors = config.server.value.includeLanguages; + + client = createLc( + 'vue', + 'Vue', + selectors, + await getInitializationOptions(context, enabledHybridMode.value), + 6009, + outputChannel + ); + + watch([enabledHybridMode, enabledTypeScriptPlugin], (newValues, oldValues) => { + if (newValues[0] !== oldValues[0]) { + requestReloadVSCode( + newValues[0] + ? 'Please reload VSCode to enable Hybrid Mode.' + : 'Please reload VSCode to disable Hybrid Mode.' + ); } - if (!desc || !desc.version) { - return; + else if (newValues[1] !== oldValues[1]) { + requestReloadVSCode( + newValues[1] + ? 'Please reload VSCode to enable Vue TypeScript Plugin.' + : 'Please reload VSCode to disable Vue TypeScript Plugin.' + ); } + }); - return desc.version; - } catch { } - } -} - -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'); - const selectors = config.server.includeLanguages; - - client = createLc( - 'vue', - 'Vue', - selectors, - await getInitializationOptions(context, enabledHybridMode), - 6009, - outputChannel - ); + watch(() => config.server.value.includeLanguages, () => { + if (enabledHybridMode.value) { + requestReloadVSCode('Please reload VSCode to apply the new language settings.'); + } + }); - activateConfigWatcher(); - activateRestartRequest(); + watch(config.server, () => { + if (!enabledHybridMode.value) { + executeCommand('vue.action.restartServer', false); + } + }); - nameCasing.activate(context, client, selectors); - splitEditors.register(context, client); - doctor.register(context, client); + watch(Object.values(config).filter((conf) => conf !== config.server), () => { + executeCommand('vue.action.restartServer', false); + }); - lsp.activateAutoInsertion(selectors, client); - lsp.activateDocumentDropEdit(selectors, client); - lsp.activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); + useCommand('vue.action.restartServer', async (restartTsServer: boolean = true) => { + if (restartTsServer) { + await executeCommand('typescript.restartTsServer'); + } + await client.stop(); + outputChannel.clear(); + client.clientOptions.initializationOptions = await getInitializationOptions(context, enabledHybridMode.value); + await client.start(); + nameCasing.activate(client, selectors); + }); - if (!enabledHybridMode) { - lsp.activateTsConfigStatusItem(selectors, 'vue.tsconfig', client); - lsp.activateTsVersionStatusItem(selectors, 'vue.tsversion', context, text => 'TS ' + text); - lsp.activateFindFileReferences('vue.findAllFileReferences', client); - } + doctor.register(context, client); + nameCasing.activate(client, selectors); + splitEditors.register(client); - const hybridModeStatus = vscode.languages.createLanguageStatusItem('vue-hybrid-mode', selectors); - hybridModeStatus.text = 'Hybrid Mode'; - hybridModeStatus.detail = (enabledHybridMode ? 'Enabled' : 'Disabled') + (config.server.hybridMode === 'auto' ? ' (Auto)' : ''); - hybridModeStatus.command = { - title: 'Open Setting', - command: 'workbench.action.openSettings', - arguments: ['vue.server.hybridMode'], - }; - if (!enabledHybridMode) { - hybridModeStatus.severity = vscode.LanguageStatusSeverity.Warning; - } + lsp.activateAutoInsertion(selectors, client); + lsp.activateDocumentDropEdit(selectors, client); + lsp.activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); - const item = vscode.languages.createLanguageStatusItem('vue-insider', 'vue'); - item.text = 'Checking for Updates...'; - item.busy = true; - let succeed = false; - for (const url of [ - 'https://raw.githubusercontent.com/vuejs/language-tools/HEAD/insiders.json', - 'https://cdn.jsdelivr.net/gh/vuejs/language-tools/insiders.json', - ]) { - try { - const res = await fetch(url); - onJson(await res.json() as any); - succeed = true; - break; - } catch { } - } - item.busy = false; - if (!succeed) { - item.text = 'Failed to Fetch Versions'; - item.severity = vscode.LanguageStatusSeverity.Error; - } + if (!enabledHybridMode.value) { + lsp.activateTsConfigStatusItem(selectors, 'vue.tsconfig', client); + lsp.activateTsVersionStatusItem(selectors, 'vue.tsversion', context, text => 'TS ' + text); + lsp.activateFindFileReferences('vue.findAllFileReferences', client); + } - function onJson(json: { - latest: string; - versions: { - version: string; - date: string; - downloads: { - GitHub: string; - AFDIAN: string; - }; - }[]; - }) { - item.detail = undefined; - item.command = { - title: 'Select Version', - command: 'vue-insiders.update', + const hybridModeStatus = vscode.languages.createLanguageStatusItem('vue-hybrid-mode', selectors); + hybridModeStatus.text = 'Hybrid Mode'; + hybridModeStatus.detail = (enabledHybridMode.value ? 'Enabled' : 'Disabled') + (config.server.value.hybridMode === 'auto' ? ' (Auto)' : ''); + hybridModeStatus.command = { + title: 'Open Setting', + command: 'workbench.action.openSettings', + arguments: ['vue.server.hybridMode'], }; - if (json.versions.some(version => version.version === context.extension.packageJSON.version)) { - item.text = 'πŸš€ Insiders Edition'; - item.severity = vscode.LanguageStatusSeverity.Information; + if (!enabledHybridMode.value) { + hybridModeStatus.severity = vscode.LanguageStatusSeverity.Warning; + } - if (context.extension.packageJSON.version !== json.latest) { - item.detail = 'New Version Available!'; - item.severity = vscode.LanguageStatusSeverity.Warning; - vscode.window - .showInformationMessage('New Insiders Version Available!', 'Download') - .then(download => { - if (download) { - vscode.commands.executeCommand('vue-insiders.update'); - } - }); - } + const item = vscode.languages.createLanguageStatusItem('vue-insider', 'vue'); + item.text = 'Checking for Updates...'; + item.busy = true; + let succeed = false; + for (const url of [ + 'https://raw.githubusercontent.com/vuejs/language-tools/HEAD/insiders.json', + 'https://cdn.jsdelivr.net/gh/vuejs/language-tools/insiders.json', + ]) { + try { + const res = await fetch(url); + onJson(await res.json() as any); + succeed = true; + break; + } catch { } } - else { - item.text = '✨ Get Insiders Edition'; - item.severity = vscode.LanguageStatusSeverity.Warning; + item.busy = false; + if (!succeed) { + item.text = 'Failed to Fetch Versions'; + item.severity = vscode.LanguageStatusSeverity.Error; } - vscode.commands.registerCommand('vue-insiders.update', async () => { - const quickPickItems: { [version: string]: vscode.QuickPickItem; } = {}; - for (const { version, date } of json.versions) { - let description = date; - if (context.extension.packageJSON.version === version) { - description += ' (current)'; - } - quickPickItems[version] = { - label: version, - description, + + function onJson(json: { + latest: string; + versions: { + version: string; + date: string; + downloads: { + GitHub: string; + AFDIAN: string; }; - } - const version = await quickPick([quickPickItems, { - learnMore: { - label: 'Learn more about Insiders Edition', - }, - joinViaGitHub: { - label: 'Join via GitHub Sponsors', - }, - joinViaAFDIAN: { - label: 'Join via AFDIAN (ηˆ±ε‘η”΅)', - }, - }]); - if (version === 'learnMore') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition')); - } - else if (version === 'joinViaGitHub') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/sponsors/johnsoncodehk')); - } - else if (version === 'joinViaAFDIAN') { - vscode.env.openExternal(vscode.Uri.parse('https://afdian.net/a/johnsoncodehk')); + }[]; + }) { + item.detail = undefined; + item.command = { + title: 'Select Version', + command: 'vue-insiders.update', + }; + if (json.versions.some(version => version.version === context.extension.packageJSON.version)) { + item.text = 'πŸš€ Insiders Edition'; + item.severity = vscode.LanguageStatusSeverity.Information; + + if (context.extension.packageJSON.version !== json.latest) { + item.detail = 'New Version Available!'; + item.severity = vscode.LanguageStatusSeverity.Warning; + vscode.window + .showInformationMessage('New Insiders Version Available!', 'Download') + .then(download => { + if (download) { + executeCommand('vue-insiders.update'); + } + }); + } } else { - const downloads = json.versions.find(v => v.version === version)?.downloads; - if (downloads) { - const quickPickItems: { [key: string]: vscode.QuickPickItem; } = { - GitHub: { - label: `${version} - GitHub Releases`, - description: 'Access via GitHub Sponsors', - detail: downloads.GitHub, - }, - AFDIAN: { - label: `${version} - Insiders η”΅εœˆ`, - description: 'Access via AFDIAN (ηˆ±ε‘η”΅)', - detail: downloads.AFDIAN, - }, - }; - const otherItems: { [key: string]: vscode.QuickPickItem; } = { - learnMore: { - label: 'Learn more about Insiders Edition', - }, - joinViaGitHub: { - label: 'Join via GitHub Sponsors', - }, - joinViaAFDIAN: { - label: 'Join via AFDIAN (ηˆ±ε‘η”΅)', - }, - }; - const option = await quickPick([quickPickItems, otherItems]); - if (option === 'learnMore') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition')); - } - else if (option === 'joinViaGitHub') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/sponsors/johnsoncodehk')); - } - else if (option === 'joinViaAFDIAN') { - vscode.env.openExternal(vscode.Uri.parse('https://afdian.net/a/johnsoncodehk')); - } - else if (option) { - vscode.env.openExternal(vscode.Uri.parse(downloads[option as keyof typeof downloads])); + item.text = '✨ Get Insiders Edition'; + item.severity = vscode.LanguageStatusSeverity.Warning; + } + useCommand('vue-insiders.update', async () => { + const quickPickItems: { [version: string]: vscode.QuickPickItem; } = {}; + for (const { version, date } of json.versions) { + let description = date; + if (context.extension.packageJSON.version === version) { + description += ' (current)'; } + quickPickItems[version] = { + label: version, + description, + }; } - } - }); - } - - async function requestReloadVscode(msg: string) { - const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); - if (reload === undefined) { - return; // cancel - } - vscode.commands.executeCommand('workbench.action.reloadWindow'); - } - - function activateConfigWatcher() { - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('vue.server.hybridMode')) { - const newHybridModeStatus = getCurrentHybridModeStatus(); - const newTypeScriptPluginStatus = getCurrentTypeScriptPluginStatus(newHybridModeStatus); - if (newHybridModeStatus !== enabledHybridMode) { - requestReloadVscode( - newHybridModeStatus - ? 'Please reload VSCode to enable Hybrid Mode.' - : 'Please reload VSCode to disable Hybrid Mode.' - ); + const version = await quickPick([quickPickItems, { + learnMore: { + label: 'Learn more about Insiders Edition', + }, + joinViaGitHub: { + label: 'Join via GitHub Sponsors', + }, + joinViaAFDIAN: { + label: 'Join via AFDIAN (ηˆ±ε‘η”΅)', + }, + }]); + if (version === 'learnMore') { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition')); } - else if (newTypeScriptPluginStatus !== enabledTypeScriptPlugin) { - requestReloadVscode( - newTypeScriptPluginStatus - ? 'Please reload VSCode to enable Vue TypeScript Plugin.' - : 'Please reload VSCode to disable Vue TypeScript Plugin.' - ); + else if (version === 'joinViaGitHub') { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/sponsors/johnsoncodehk')); } - } - else if (e.affectsConfiguration('vue.server')) { - if (enabledHybridMode) { - if (e.affectsConfiguration('vue.server.includeLanguages')) { - requestReloadVscode('Please reload VSCode to apply the new language settings.'); - } + else if (version === 'joinViaAFDIAN') { + vscode.env.openExternal(vscode.Uri.parse('https://afdian.net/a/johnsoncodehk')); } else { - vscode.commands.executeCommand('vue.action.restartServer', false); + const downloads = json.versions.find(v => v.version === version)?.downloads; + if (downloads) { + const quickPickItems: { [key: string]: vscode.QuickPickItem; } = { + GitHub: { + label: `${version} - GitHub Releases`, + description: 'Access via GitHub Sponsors', + detail: downloads.GitHub, + }, + AFDIAN: { + label: `${version} - Insiders η”΅εœˆ`, + description: 'Access via AFDIAN (ηˆ±ε‘η”΅)', + detail: downloads.AFDIAN, + }, + }; + const otherItems: { [key: string]: vscode.QuickPickItem; } = { + learnMore: { + label: 'Learn more about Insiders Edition', + }, + joinViaGitHub: { + label: 'Join via GitHub Sponsors', + }, + joinViaAFDIAN: { + label: 'Join via AFDIAN (ηˆ±ε‘η”΅)', + }, + }; + const option = await quickPick([quickPickItems, otherItems]); + if (option === 'learnMore') { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition')); + } + else if (option === 'joinViaGitHub') { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/sponsors/johnsoncodehk')); + } + else if (option === 'joinViaAFDIAN') { + vscode.env.openExternal(vscode.Uri.parse('https://afdian.net/a/johnsoncodehk')); + } + else if (option) { + vscode.env.openExternal(vscode.Uri.parse(downloads[option as keyof typeof downloads])); + } + } } - } - else if (e.affectsConfiguration('vue')) { - vscode.commands.executeCommand('vue.action.restartServer', false); - } - })); - } + }); + } - async function activateRestartRequest() { - context.subscriptions.push(vscode.commands.registerCommand('vue.action.restartServer', async (restartTsServer: boolean = true) => { - if (restartTsServer) { - await vscode.commands.executeCommand('typescript.restartTsServer'); + async function requestReloadVSCode(msg: string) { + const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); + if (reload === undefined) { + return; // cancel } - await client.stop(); - outputChannel.clear(); - client.clientOptions.initializationOptions = await getInitializationOptions(context, enabledHybridMode); - await client.start(); - nameCasing.activate(context, client, selectors); - })); - } + executeCommand('workbench.action.reloadWindow'); + } + + stop(); + }, { + immediate: true + }) +} + +function getTsVersion(libPath: string): string | undefined { + try { + 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; + } catch { } } export function deactivate(): Thenable | undefined { @@ -458,3 +411,34 @@ async function getInitializationOptions( vue: { hybridMode }, }; }; + +function isExtensionCompatibleWithHybridMode(extension: vscode.Extension) { + 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' + || extension.id === 'bierner.lit-html' + || extension.id === 'jenkey2011.string-highlight' + || extension.id === 'mxsdev.typescript-explorer' + || extension.id === 'miaonster.vscode-tsx-arrow-definition' + || extension.id === 'runem.lit-plugin' + || extension.id === 'kimuson.ts-type-expand' + || extension.id === 'p42ai.refactor' + || extension.id === 'styled-components.vscode-styled-components' + || extension.id === 'Divlo.vscode-styled-jsx-languageserver' + || extension.id === 'nrwl.angular-console' + || extension.id === 'ShenQingchuan.vue-vine-extension' + || extension.id === 'ms-dynamics-smb.al' + ) { + return true; + } + if (extension.id === 'denoland.vscode-deno') { + return !vscode.workspace.getConfiguration('deno').get('enable'); + } + if (extension.id === 'svelte.svelte-vscode') { + return semver.gte(extension.packageJSON.version, '108.4.0'); + } +} \ No newline at end of file diff --git a/extensions/vscode/src/config.ts b/extensions/vscode/src/config.ts index 6637590a90..b8c16cdc81 100644 --- a/extensions/vscode/src/config.ts +++ b/extensions/vscode/src/config.ts @@ -1,49 +1,35 @@ -import * as vscode from 'vscode'; +import { defineConfigs } from 'reactive-vscode'; -const _config = () => vscode.workspace.getConfiguration('vue'); - -export const config = { - update: (section: string, value: any) => _config().update(section, value), - get splitEditors(): Readonly<{ +export const config = defineConfigs('vue', { + splitEditors: {} as { icon: boolean; - layout: { left: string[], right: string[]; }; - }> { - return _config().get('splitEditors')!; + layout: { + left: string[]; + right: string[]; + } }, - get doctor(): Readonly<{ + doctor: {} as { status: boolean; - }> { - return _config().get('doctor')!; }, - get server(): Readonly<{ + server: {} as { includeLanguages: string[]; hybridMode: 'auto' | 'typeScriptPluginOnly' | boolean; maxOldSpaceSize: number; - }> { - return _config().get('server')!; }, - get updateImportsOnFileMove(): Readonly<{ + updateImportsOnFileMove: {} as { enabled: boolean; - }> { - return _config().get('updateImportsOnFileMove')!; }, - get codeActions(): Readonly<{ + codeActions: {} as { enabled: boolean; askNewComponentName: boolean; - }> { - return _config().get('codeActions')!; }, - get codeLens(): Readonly<{ + codeLens: {} as { enabled: boolean; - }> { - return _config().get('codeLens')!; }, - get complete(): Readonly<{ + complete: {} as { casing: { props: 'autoKebab' | 'autoCamel' | 'kebab' | 'camel'; tags: 'autoKebab' | 'autoPascal' | 'kebab' | 'pascal'; }; - }> { - return _config().get('complete')!; - }, -}; + } +}); \ No newline at end of file diff --git a/extensions/vscode/src/features/doctor.ts b/extensions/vscode/src/features/doctor.ts index 8857211503..c90609b1d8 100644 --- a/extensions/vscode/src/features/doctor.ts +++ b/extensions/vscode/src/features/doctor.ts @@ -4,6 +4,7 @@ import { commands } from '@vue/language-server/lib/types'; import * as semver from 'semver'; import * as vscode from 'vscode'; import { config } from '../config'; +import { executeCommand, useActiveTextEditor, useCommand, useEventEmitter, useStatusBarItem, watch } from 'reactive-vscode'; const scheme = 'vue-doctor'; const knownValidSyntaxHighlightExtensions = { @@ -14,15 +15,32 @@ const knownValidSyntaxHighlightExtensions = { export async function register(context: vscode.ExtensionContext, client: BaseLanguageClient) { - const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); - item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); - item.command = 'vue.action.doctor'; + const item = useStatusBarItem({ + alignment: vscode.StatusBarAlignment.Right, + backgroundColor: new vscode.ThemeColor('statusBarItem.warningBackground'), + command: 'vue.action.doctor' + }); - const docChangeEvent = new vscode.EventEmitter(); + const activeTextEditor = useActiveTextEditor(); + const docChangeEvent = useEventEmitter(); - updateStatusBar(vscode.window.activeTextEditor); + watch(activeTextEditor, () => { + updateStatusBar(activeTextEditor.value); + }, { + immediate: true + }); + + useCommand('vue.action.doctor', () => { + const doc = activeTextEditor.value?.document; + if ( + doc + && (doc.languageId === 'vue' || doc.uri.toString().endsWith('.vue')) + && doc.uri.scheme === 'file' + ) { + executeCommand('markdown.showPreviewToSide', getDoctorUri(doc.uri)); + } + }); - context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(updateStatusBar)); context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider( scheme, { @@ -49,16 +67,6 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan } } )); - context.subscriptions.push(vscode.commands.registerCommand('vue.action.doctor', () => { - const doc = vscode.window.activeTextEditor?.document; - if ( - doc - && (doc.languageId === 'vue' || doc.uri.toString().endsWith('.vue')) - && doc.uri.scheme === 'file' - ) { - vscode.commands.executeCommand('markdown.showPreviewToSide', getDoctorUri(doc.uri)); - } - })); function getDoctorUri(fileUri: vscode.Uri) { return fileUri.with({ scheme, path: fileUri.path + '/Doctor.md' }); @@ -66,7 +74,7 @@ export async function register(context: vscode.ExtensionContext, client: BaseLan async function updateStatusBar(editor: vscode.TextEditor | undefined) { if ( - config.doctor.status + config.doctor.value.status && editor && (editor.document.languageId === 'vue' || editor.document.uri.toString().endsWith('.vue')) && editor.document.uri.scheme === 'file' diff --git a/extensions/vscode/src/features/nameCasing.ts b/extensions/vscode/src/features/nameCasing.ts index 599045f12c..a4534927ed 100644 --- a/extensions/vscode/src/features/nameCasing.ts +++ b/extensions/vscode/src/features/nameCasing.ts @@ -1,46 +1,84 @@ -import { BaseLanguageClient, ExecuteCommandParams, ExecuteCommandRequest, State, TextEdit } from '@volar/vscode'; +import { BaseLanguageClient, ExecuteCommandParams, ExecuteCommandRequest, TextEdit } from '@volar/vscode'; import { quickPick } from '@volar/vscode/lib/common'; import { AttrNameCasing, TagNameCasing, commands } from '@vue/language-server/lib/types'; import * as vscode from 'vscode'; import { config } from '../config'; +import { ref, useActiveTextEditor, useCommand, watch } from 'reactive-vscode'; -export const attrNameCasings = new Map(); -export const tagNameCasings = new Map(); +export const attrNameCasings = ref(new Map()); +export const tagNameCasings = ref(new Map()); -export async function activate(_context: vscode.ExtensionContext, client: BaseLanguageClient, selector: vscode.DocumentSelector) { +export async function activate(client: BaseLanguageClient, selector: vscode.DocumentSelector) { await client.start(); - const disposes: vscode.Disposable[] = []; + const activeTextEditor = useActiveTextEditor(); + const statusBar = vscode.languages.createLanguageStatusItem('vue-name-casing', selector); statusBar.command = { title: 'Open Menu', command: 'vue.action.nameCasing', }; - update(vscode.window.activeTextEditor?.document); - - disposes.push(vscode.window.onDidChangeActiveTextEditor(e => { - update(e?.document); - })); - disposes.push(vscode.workspace.onDidChangeConfiguration(() => { - attrNameCasings.clear(); - tagNameCasings.clear(); - update(vscode.window.activeTextEditor?.document); - })); - disposes.push(vscode.workspace.onDidCloseTextDocument(doc => { - attrNameCasings.delete(doc.uri.toString()); - tagNameCasings.delete(doc.uri.toString()); - })); - disposes.push(vscode.commands.registerCommand('vue.action.nameCasing', async () => { - - if (!vscode.window.activeTextEditor?.document) { + watch([attrNameCasings, tagNameCasings], () => { + const document = activeTextEditor.value?.document; + if (!document) { + return ''; + } + + const attrNameCasing = attrNameCasings.value.get(document.uri.toString()); + const tagNameCasing = tagNameCasings.value.get(document.uri.toString()); + + let text = `<`; + if (tagNameCasing === TagNameCasing.Kebab) { + text += `tag-name `; + } + else if (tagNameCasing === TagNameCasing.Pascal) { + text += `TagName `; + } + else { + text += '? '; + } + if (attrNameCasing === AttrNameCasing.Kebab) { + text += `prop-name`; + } + else if (attrNameCasing === AttrNameCasing.Camel) { + text += `propName`; + } + else { + text += '?'; + } + text += ' />'; + statusBar.text = text; + }, { + deep: true + }); + + watch(activeTextEditor, () => { + initialize(activeTextEditor.value?.document); + }, { + immediate: true + }); + + watch(config.complete, () => { + attrNameCasings.value.clear(); + tagNameCasings.value.clear(); + initialize(activeTextEditor.value?.document); + }); + + vscode.workspace.onDidCloseTextDocument(doc => { + attrNameCasings.value.delete(doc.uri.toString()); + tagNameCasings.value.delete(doc.uri.toString()); + }) + + useCommand('vue.action.nameCasing', async () => { + if (!activeTextEditor.value?.document) { return; } - const document = vscode.window.activeTextEditor.document; - const currentAttrNameCasing = attrNameCasings.get(document.uri.toString()); - const currentTagNameCasing = tagNameCasings.get(document.uri.toString()); + const document = activeTextEditor.value.document; + const currentAttrNameCasing = attrNameCasings.value.get(document.uri.toString()); + const currentTagNameCasing = tagNameCasings.value.get(document.uri.toString()); const select = await quickPick([ { '1': { label: (currentTagNameCasing === TagNameCasing.Kebab ? 'β€’ ' : '') + 'Component Name Using kebab-case' }, @@ -61,39 +99,84 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa } // tag if (select === '1') { - tagNameCasings.set(document.uri.toString(), TagNameCasing.Kebab); + tagNameCasings.value.set(document.uri.toString(), TagNameCasing.Kebab); } if (select === '2') { - tagNameCasings.set(document.uri.toString(), TagNameCasing.Pascal); + tagNameCasings.value.set(document.uri.toString(), TagNameCasing.Pascal); } if (select === '3') { - await convertTag(vscode.window.activeTextEditor, TagNameCasing.Kebab); + await convertTag(activeTextEditor.value, TagNameCasing.Kebab); } if (select === '4') { - await convertTag(vscode.window.activeTextEditor, TagNameCasing.Pascal); + await convertTag(activeTextEditor.value, TagNameCasing.Pascal); } // attr if (select === '5') { - attrNameCasings.set(document.uri.toString(), AttrNameCasing.Kebab); + attrNameCasings.value.set(document.uri.toString(), AttrNameCasing.Kebab); } if (select === '6') { - attrNameCasings.set(document.uri.toString(), AttrNameCasing.Camel); + attrNameCasings.value.set(document.uri.toString(), AttrNameCasing.Camel); } if (select === '7') { - await convertAttr(vscode.window.activeTextEditor, AttrNameCasing.Kebab); + await convertAttr(activeTextEditor.value, AttrNameCasing.Kebab); } if (select === '8') { - await convertAttr(vscode.window.activeTextEditor, AttrNameCasing.Camel); + await convertAttr(activeTextEditor.value, AttrNameCasing.Camel); } - updateStatusBarText(); - })); + }); - client.onDidChangeState(e => { - if (e.newState === State.Stopped) { - disposes.forEach(d => d.dispose()); - statusBar.dispose(); + async function initialize(document: vscode.TextDocument | undefined) { + if (!document || !vscode.languages.match(selector, document)) { + return; + } + + let detected: Awaited> | undefined; + let attrNameCasing = attrNameCasings.value.get(document.uri.toString()); + let tagNameCasing = tagNameCasings.value.get(document.uri.toString()); + + if (!attrNameCasing) { + const attrNameCasingSetting = config.complete.value.casing.props; + if (attrNameCasingSetting === 'kebab') { + attrNameCasing = AttrNameCasing.Kebab; + } + else if (attrNameCasingSetting === 'camel') { + attrNameCasing = AttrNameCasing.Camel; + } + else { + detected ??= await detect(document); + if (detected?.attr.length === 1) { + attrNameCasing = detected.attr[0]; + } + else if (attrNameCasingSetting === 'autoCamel') { + attrNameCasing = AttrNameCasing.Camel; + } + else { + attrNameCasing = AttrNameCasing.Kebab; + } + } + attrNameCasings.value.set(document.uri.toString(), attrNameCasing); } - }); + + if (!tagNameCasing) { + const tagMode = config.complete.value.casing.tags; + if (tagMode === 'kebab') { + tagNameCasing = TagNameCasing.Kebab; + } + else if (tagMode === 'pascal') { + tagNameCasing = TagNameCasing.Pascal; + } + else { + detected ??= await detect(document); + if (detected?.tag.length === 1) { + tagNameCasing = detected.tag[0]; + } + else { + tagNameCasing = TagNameCasing.Pascal; + } + } + tagNameCasings.value.set(document.uri.toString(), tagNameCasing); + } + } async function convertTag(editor: vscode.TextEditor, casing: TagNameCasing) { @@ -114,8 +197,7 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa }); } - tagNameCasings.set(editor.document.uri.toString(), casing); - updateStatusBarText(); + tagNameCasings.value.set(editor.document.uri.toString(), casing); } async function convertAttr(editor: vscode.TextEditor, casing: AttrNameCasing) { @@ -137,61 +219,7 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa }); } - attrNameCasings.set(editor.document.uri.toString(), casing); - updateStatusBarText(); - } - - async function update(document: vscode.TextDocument | undefined) { - if (document && vscode.languages.match(selector, document)) { - let detected: Awaited> | undefined; - let attrNameCasing = attrNameCasings.get(document.uri.toString()); - let tagNameCasing = tagNameCasings.get(document.uri.toString()); - - if (!attrNameCasing) { - const attrNameCasingSetting = config.complete.casing.props; - if (attrNameCasingSetting === 'kebab') { - attrNameCasing = AttrNameCasing.Kebab; - } - else if (attrNameCasingSetting === 'camel') { - attrNameCasing = AttrNameCasing.Camel; - } - else { - detected ??= await detect(document); - if (detected?.attr.length === 1) { - attrNameCasing = detected.attr[0]; - } - else if (attrNameCasingSetting === 'autoCamel') { - attrNameCasing = AttrNameCasing.Camel; - } - else { - attrNameCasing = AttrNameCasing.Kebab; - } - } - attrNameCasings.set(document.uri.toString(), attrNameCasing); - } - - if (!tagNameCasing) { - const tagMode = config.complete.casing.tags; - if (tagMode === 'kebab') { - tagNameCasing = TagNameCasing.Kebab; - } - else if (tagMode === 'pascal') { - tagNameCasing = TagNameCasing.Pascal; - } - else { - detected ??= await detect(document); - if (detected?.tag.length === 1) { - tagNameCasing = detected.tag[0]; - } - else { - tagNameCasing = TagNameCasing.Pascal; - } - } - tagNameCasings.set(document.uri.toString(), tagNameCasing); - } - - updateStatusBarText(); - } + attrNameCasings.value.set(editor.document.uri.toString(), casing); } function detect(document: vscode.TextDocument): Promise<{ @@ -203,34 +231,4 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa arguments: [client.code2ProtocolConverter.asUri(document.uri)], } satisfies ExecuteCommandParams); } - - function updateStatusBarText() { - const document = vscode.window.activeTextEditor?.document; - if (!document) { - return; - } - const attrNameCasing = attrNameCasings.get(document.uri.toString()); - const tagNameCasing = tagNameCasings.get(document.uri.toString()); - let text = `<`; - if (tagNameCasing === TagNameCasing.Kebab) { - text += `tag-name `; - } - else if (tagNameCasing === TagNameCasing.Pascal) { - text += `TagName `; - } - else { - text += '? '; - } - if (attrNameCasing === AttrNameCasing.Kebab) { - text += `prop-name`; - } - else if (attrNameCasing === AttrNameCasing.Camel) { - text += `propName`; - } - else { - text += '?'; - } - text += ' />'; - statusBar.text = text; - } } diff --git a/extensions/vscode/src/features/splitEditors.ts b/extensions/vscode/src/features/splitEditors.ts index 64a509f524..3836933059 100644 --- a/extensions/vscode/src/features/splitEditors.ts +++ b/extensions/vscode/src/features/splitEditors.ts @@ -3,28 +3,28 @@ import type { SFCParseResult } from '@vue/language-server'; import { commands } from '@vue/language-server/lib/types'; import * as vscode from 'vscode'; import { config } from '../config'; +import { executeCommand, useActiveTextEditor, useCommand } from 'reactive-vscode'; type SFCBlock = SFCParseResult['descriptor']['customBlocks'][number]; -export function register(context: vscode.ExtensionContext, client: BaseLanguageClient) { +export function register(client: BaseLanguageClient) { + const activeTextEditor = useActiveTextEditor(); const getDocDescriptor = useDocDescriptor(client); - - context.subscriptions.push(vscode.commands.registerCommand('vue.action.splitEditors', onSplit)); - - async function onSplit() { - - const editor = vscode.window.activeTextEditor; + + useCommand('vue.action.splitEditors', async () => { + const editor = activeTextEditor.value; if (!editor) { return; } - const layout = config.splitEditors.layout; + const layout = config.splitEditors.value.layout; const doc = editor.document; const descriptor = (await getDocDescriptor(doc.getText()))?.descriptor; if (!descriptor) { return; } + let leftBlocks: SFCBlock[] = []; let rightBlocks: SFCBlock[] = []; @@ -65,20 +65,20 @@ export function register(context: vscode.ExtensionContext, client: BaseLanguageC rightBlocks = rightBlocks.concat(descriptor.customBlocks); } - await vscode.commands.executeCommand('workbench.action.joinEditorInGroup'); + await executeCommand('workbench.action.joinEditorInGroup'); - if (vscode.window.activeTextEditor === editor) { + if (activeTextEditor.value === editor) { await foldingBlocks(leftBlocks); - await vscode.commands.executeCommand('workbench.action.toggleSplitEditorInGroup'); + await executeCommand('workbench.action.toggleSplitEditorInGroup'); await foldingBlocks(rightBlocks); } else { - await vscode.commands.executeCommand('editor.unfoldAll'); + await executeCommand('editor.unfoldAll'); } async function foldingBlocks(blocks: SFCBlock[]) { - const editor = vscode.window.activeTextEditor; + const editor = activeTextEditor.value; if (!editor) { return; } @@ -87,15 +87,15 @@ export function register(context: vscode.ExtensionContext, client: BaseLanguageC ? blocks.map(block => new vscode.Selection(doc.positionAt(block.loc.start.offset), doc.positionAt(block.loc.start.offset))) : [new vscode.Selection(doc.positionAt(doc.getText().length), doc.positionAt(doc.getText().length))]; - await vscode.commands.executeCommand('editor.unfoldAll'); - await vscode.commands.executeCommand('editor.foldLevel1'); + await executeCommand('editor.unfoldAll'); + await executeCommand('editor.foldLevel1'); const firstBlock = blocks.sort((a, b) => a.loc.start.offset - b.loc.start.offset)[0]; if (firstBlock) { editor.revealRange(new vscode.Range(doc.positionAt(firstBlock.loc.start.offset), new vscode.Position(editor.document.lineCount, 0)), vscode.TextEditorRevealType.AtTop); } } - } + }); } export function useDocDescriptor(client: BaseLanguageClient) { diff --git a/extensions/vscode/src/middleware.ts b/extensions/vscode/src/middleware.ts index d4b22781cb..6820dd5e0f 100644 --- a/extensions/vscode/src/middleware.ts +++ b/extensions/vscode/src/middleware.ts @@ -7,7 +7,7 @@ import { config } from './config'; export const middleware: lsp.Middleware = { ...lsp.middleware, async resolveCodeAction(item, token, next) { - if (item.kind?.value === 'refactor.move.newFile.dumb' && config.codeActions.askNewComponentName) { + if (item.kind?.value === 'refactor.move.newFile.dumb' && config.codeActions.value.askNewComponentName) { const inputName = await vscode.window.showInputBox({ value: (item as any).data.original.data.newName }); if (!inputName) { return item; // cancel @@ -22,7 +22,7 @@ export const middleware: lsp.Middleware = { return params.items.map(item => { if (item.scopeUri) { if (item.section === 'vue.complete.casing.tags') { - const tagNameCasing = tagNameCasings.get(item.scopeUri); + const tagNameCasing = tagNameCasings.value.get(item.scopeUri); if (tagNameCasing === TagNameCasing.Kebab) { return 'kebab'; } @@ -31,7 +31,7 @@ export const middleware: lsp.Middleware = { } } else if (item.section === 'vue.complete.casing.props') { - const attrCase = attrNameCasings.get(item.scopeUri); + const attrCase = attrNameCasings.value.get(item.scopeUri); if (attrCase === AttrNameCasing.Kebab) { return 'kebab'; } diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index c4aa8e0b24..7ee9e97462 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -3,75 +3,14 @@ import * as protocol from '@vue/language-server/protocol'; import * as fs from 'fs'; import * as vscode from 'vscode'; import * as lsp from '@volar/vscode/node'; +import { defineExtension, executeCommand, onActivate, onDeactivate } from 'reactive-vscode'; import { activate as commonActivate, deactivate as commonDeactivate, enabledHybridMode, enabledTypeScriptPlugin } from './common'; import { config } from './config'; import { middleware } from './middleware'; -export async function activate(context: vscode.ExtensionContext) { - +export const { activate, deactivate } = defineExtension(async () => { const volarLabs = createLabsInfo(protocol); - await commonActivate(context, ( - id, - name, - documentSelector, - initOptions, - port, - outputChannel - ) => { - - class _LanguageClient extends lsp.LanguageClient { - fillInitializeParams(params: lsp.InitializeParams) { - // fix https://github.com/vuejs/language-tools/issues/1959 - params.locale = vscode.env.language; - } - } - - let serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); - - const runOptions: lsp.ForkOptions = {}; - if (config.server.maxOldSpaceSize) { - runOptions.execArgv ??= []; - runOptions.execArgv.push("--max-old-space-size=" + config.server.maxOldSpaceSize); - } - const debugOptions: lsp.ForkOptions = { execArgv: ['--nolazy', '--inspect=' + port] }; - const serverOptions: lsp.ServerOptions = { - run: { - module: serverModule.fsPath, - transport: lsp.TransportKind.ipc, - options: runOptions - }, - debug: { - module: serverModule.fsPath, - transport: lsp.TransportKind.ipc, - options: debugOptions - }, - }; - const clientOptions: lsp.LanguageClientOptions = { - middleware, - documentSelector: documentSelector, - initializationOptions: initOptions, - markdown: { - isTrusted: true, - supportHtml: true, - }, - outputChannel, - }; - const client = new _LanguageClient( - id, - name, - serverOptions, - clientOptions, - ); - client.start(); - - volarLabs.addLanguageClient(client); - - updateProviders(client); - - return client; - }); - const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features'); const vueTsPluginExtension = vscode.extensions.getExtension('Vue.vscode-typescript-vue-plugin'); @@ -82,9 +21,9 @@ export async function activate(context: vscode.ExtensionContext) { vscode.window.showWarningMessage( 'Takeover mode is no longer needed since v2. Please enable the "TypeScript and JavaScript Language Features" extension.', 'Show Extension' - ).then(selected => { + ).then((selected) => { if (selected) { - vscode.commands.executeCommand('workbench.extensions.search', '@builtin typescript-language-features'); + executeCommand('workbench.extensions.search', '@builtin typescript-language-features'); } }); } @@ -93,19 +32,82 @@ export async function activate(context: vscode.ExtensionContext) { vscode.window.showWarningMessage( `The "${vueTsPluginExtension.packageJSON.displayName}" extension is no longer needed since v2. Please uninstall it.`, 'Show Extension' - ).then(selected => { + ).then((selected) => { if (selected) { - vscode.commands.executeCommand('workbench.extensions.search', vueTsPluginExtension.id); + executeCommand('workbench.extensions.search', vueTsPluginExtension.id); } }); } - return volarLabs.extensionExports; -} + onActivate((context) => { + commonActivate(context, ( + id, + name, + documentSelector, + initOptions, + port, + outputChannel + ) => { + + class _LanguageClient extends lsp.LanguageClient { + fillInitializeParams(params: lsp.InitializeParams) { + // fix https://github.com/vuejs/language-tools/issues/1959 + params.locale = vscode.env.language; + } + } -export function deactivate(): Thenable | undefined { - return commonDeactivate(); -} + let serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); + + const runOptions: lsp.ForkOptions = {}; + if (config.server.value.maxOldSpaceSize) { + runOptions.execArgv ??= []; + runOptions.execArgv.push("--max-old-space-size=" + config.server.value.maxOldSpaceSize); + } + const debugOptions: lsp.ForkOptions = { execArgv: ['--nolazy', '--inspect=' + port] }; + const serverOptions: lsp.ServerOptions = { + run: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: runOptions + }, + debug: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: debugOptions + }, + }; + const clientOptions: lsp.LanguageClientOptions = { + middleware, + documentSelector: documentSelector, + initializationOptions: initOptions, + markdown: { + isTrusted: true, + supportHtml: true, + }, + outputChannel, + }; + const client = new _LanguageClient( + id, + name, + serverOptions, + clientOptions, + ); + client.start(); + + volarLabs.addLanguageClient(client); + + updateProviders(client); + + return client; + }); + }); + + onDeactivate(() => { + commonDeactivate(); + }); + + return volarLabs.extensionExports; +}); function updateProviders(client: lsp.LanguageClient) { @@ -114,13 +116,13 @@ function updateProviders(client: lsp.LanguageClient) { (client as any).initializeFeatures = (...args: any) => { const capabilities = (client as any)._capabilities as lsp.ServerCapabilities; - if (!config.codeActions.enabled) { + if (!config.codeActions.value.enabled) { capabilities.codeActionProvider = undefined; } - if (!config.codeLens.enabled) { + if (!config.codeLens.value.enabled) { capabilities.codeLensProvider = undefined; } - if (!config.updateImportsOnFileMove.enabled && capabilities.workspace?.fileOperations?.willRename) { + if (!config.updateImportsOnFileMove.value.enabled && capabilities.workspace?.fileOperations?.willRename) { capabilities.workspace.fileOperations.willRename = undefined; } @@ -139,19 +141,19 @@ try { // @ts-expect-error let text = readFileSync(...args) as string; - if (!enabledTypeScriptPlugin) { + if (!enabledTypeScriptPlugin.value) { text = text.replace( 'for(const e of n.contributes.typescriptServerPlugins', s => s + `.filter(p=>p.name!=='typescript-vue-plugin-bundle')` ); } - else if (enabledHybridMode) { + else if (enabledHybridMode.value) { // patch readPlugins text = text.replace( 'languages:Array.isArray(e.languages)', [ 'languages:', - `e.name==='typescript-vue-plugin-bundle'?[${config.server.includeLanguages.map(lang => `"${lang}"`).join(',')}]`, + `e.name==='typescript-vue-plugin-bundle'?[${config.server.value.includeLanguages.map(lang => `"${lang}"`).join(',')}]`, ':Array.isArray(e.languages)', ].join('') ); From 306d3c92455f036626486ce81c9515a6a6854d4a Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Sat, 26 Oct 2024 13:02:34 +0800 Subject: [PATCH 02/10] chore: refresh lockfile --- pnpm-lock.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f6cc195fd..472d81bea0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,10 @@ importers: version: 2.1.3(@types/node@22.8.1) extensions/vscode: + dependencies: + reactive-vscode: + specifier: 0.2.6 + version: 0.2.6(@types/vscode@1.94.0) devDependencies: '@types/semver': specifier: ^7.5.3 @@ -918,6 +922,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@reactive-vscode/reactivity@0.2.6': + resolution: {integrity: sha512-qAzhi+8YbLJpylLr6n5D498280TyDkvNpJFG68FuZMZy2tXMqDzW7drR2yJ7xgagQ/zKlVlxzIPgzyBX4v2K5w==} + '@rollup/rollup-android-arm-eabi@4.24.0': resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} cpu: [arm] @@ -2728,6 +2735,11 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + reactive-vscode@0.2.6: + resolution: {integrity: sha512-JT1wiMpUS0txRtrcOYyI7UoGpVB3XmlCAdczeCL2NSJtRjwV3zm3n0aJaIEPq51cVKpmaQu9byx2guxj4bMi/Q==} + peerDependencies: + '@types/vscode': ^1.89.0 + read-cmd-shim@4.0.0: resolution: {integrity: sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4142,6 +4154,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@reactive-vscode/reactivity@0.2.6': {} + '@rollup/rollup-android-arm-eabi@4.24.0': optional: true @@ -6128,6 +6142,11 @@ snapshots: strip-json-comments: 2.0.1 optional: true + reactive-vscode@0.2.6(@types/vscode@1.94.0): + dependencies: + '@reactive-vscode/reactivity': 0.2.6 + '@types/vscode': 1.94.0 + read-cmd-shim@4.0.0: {} read-package-json-fast@3.0.2: From 423bb6b1ea8ae0fa78c70b27921f241d4ba4efaa Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Sat, 26 Oct 2024 16:40:45 +0800 Subject: [PATCH 03/10] refactor: split Co-authored-by: _Kerman --- extensions/vscode/src/common.ts | 522 ++++-------------- extensions/vscode/src/compatibility.ts | 49 ++ extensions/vscode/src/features/doctor.ts | 4 +- extensions/vscode/src/features/nameCasing.ts | 2 +- .../vscode/src/features/splitEditors.ts | 4 +- extensions/vscode/src/hybridMode.ts | 195 +++++++ extensions/vscode/src/insiders.ts | 158 ++++++ extensions/vscode/src/nodeClientMain.ts | 88 +-- 8 files changed, 581 insertions(+), 441 deletions(-) create mode 100644 extensions/vscode/src/compatibility.ts create mode 100644 extensions/vscode/src/hybridMode.ts create mode 100644 extensions/vscode/src/insiders.ts diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index c086f9808f..c537979ad9 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -1,15 +1,22 @@ -import * as lsp from '@volar/vscode'; -import { quickPick } from '@volar/vscode/lib/common'; -import type { VueInitializationOptions } from '@vue/language-server'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as semver from 'semver'; -import * as vscode from 'vscode'; -import { computed, executeCommand, ref, useAllExtensions, watchEffect, useActiveTextEditor, useVisibleTextEditors, watch, useOutputChannel, useCommand } from 'reactive-vscode'; -import { config } from './config'; -import * as doctor from './features/doctor'; -import * as nameCasing from './features/nameCasing'; -import * as splitEditors from './features/splitEditors'; +import * as lsp from "@volar/vscode"; +import type { VueInitializationOptions } from "@vue/language-server"; +import { + executeCommand, + nextTick, + useActiveTextEditor, + useVisibleTextEditors, + useOutputChannel, + useCommand, + useVscodeContext, + watch, +} from "reactive-vscode"; +import * as vscode from "vscode"; +import { config } from "./config"; +import { activate as activateDoctor } from "./features/doctor"; +import { activate as activateNameCasing } from "./features/nameCasing"; +import { activate as activateSplitEditors } from "./features/splitEditors"; +import { enabledHybridMode, enabledTypeScriptPlugin, useHybridModeStatusItem, useHybridModeTips } from "./hybridMode"; +import { useInsidersStatusItem } from "./insiders"; let client: lsp.BaseLanguageClient; @@ -19,383 +26,125 @@ type CreateLanguageClient = ( langs: lsp.DocumentSelector, initOptions: VueInitializationOptions, port: number, - outputChannel: vscode.OutputChannel, + outputChannel: vscode.OutputChannel ) => lsp.BaseLanguageClient; -export const enabledHybridMode = ref(true); -export const enabledTypeScriptPlugin = computed(() => { - return enabledHybridMode.value || config.server.value.hybridMode === 'typeScriptPluginOnly'; -}); - -const extensions = useAllExtensions(); - -const incompatibleExtensions = computed(() => { - return extensions.value - .filter((ext) => isExtensionCompatibleWithHybridMode(ext) === false) - .map((ext) => ext.id); -}); - -const unknownExtensions = computed(() => { - return extensions.value - .filter((ext) => isExtensionCompatibleWithHybridMode(ext) === undefined && !!ext.packageJSON?.contributes?.typescriptServerPlugins) - .map((ext) => ext.id); -}); - -const vscodeTsdkVersion = computed(() => { - const nightly = extensions.value.find(({ id }) => id === '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); - } -}); - -const workspaceTsdkVersion = computed(() => { - const libPath = vscode.workspace.getConfiguration('typescript').get('tsdk')?.replace(/\\/g, '/'); - if (libPath) { - return getTsVersion(libPath); - } -}); - -watchEffect(() => { - switch (config.server.value.hybridMode) { - case 'typeScriptPluginOnly': { - enabledHybridMode.value = false; - break; - } - case 'auto': { - if (incompatibleExtensions.value.length || unknownExtensions.value.length) { - vscode.window.showInformationMessage( - `Hybrid Mode is disabled automatically because there is a potentially incompatible ${[...incompatibleExtensions.value, ...unknownExtensions.value].join(', ')} TypeScript plugin installed.`, - 'Open Settings', - 'Report a false positive' - ).then(value => { - if (value === 'Open Settings') { - 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')); - } - }); - enabledHybridMode.value = false; - } - else if ( - (vscodeTsdkVersion.value && !semver.gte(vscodeTsdkVersion.value, '5.3.0')) - || (workspaceTsdkVersion.value && !semver.gte(workspaceTsdkVersion.value, '5.3.0')) - ) { - let msg = `Hybrid Mode is disabled automatically because TSDK >= 5.3.0 is required (VSCode TSDK: ${vscodeTsdkVersion.value}`; - if (workspaceTsdkVersion.value) { - msg += `, Workspace TSDK: ${workspaceTsdkVersion.value}`; - } - msg += `).`; - vscode.window.showInformationMessage(msg, 'Open Settings').then(value => { - if (value === 'Open Settings') { - executeCommand('workbench.action.openSettings', 'vue.server.hybridMode'); - } - }); - enabledHybridMode.value = false; - } - else { - enabledHybridMode.value = true; - } - break; - } - default: { - if (config.server.value.hybridMode && incompatibleExtensions.value.length) { - vscode.window.showWarningMessage( - `You have explicitly enabled Hybrid Mode, but you have installed known incompatible extensions: ${incompatibleExtensions.value.join(', ')}. You may want to change vue.server.hybridMode to "auto" to avoid compatibility issues.`, - 'Open Settings', - 'Report a false positive' - ).then(value => { - if (value === 'Open Settings') { - 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')); - } - }); - } - enabledHybridMode.value = config.server.value.hybridMode; - } - } -}); - -export function activate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { +export function activate( + context: vscode.ExtensionContext, + createLc: CreateLanguageClient +) { const activeTextEditor = useActiveTextEditor(); const visibleTextEditors = useVisibleTextEditors(); - const outputChannel = useOutputChannel('Vue Language Server'); - executeCommand('setContext', 'vueHybridMode', enabledHybridMode.value); + useHybridModeTips(); - const { stop } = watch(activeTextEditor, async () => { - if (visibleTextEditors.value.every(editor => !config.server.value.includeLanguages.includes(editor.document.languageId))) { - return; + const { stop } = watch(activeTextEditor, () => { + if (visibleTextEditors.value.some((editor) => config.server.value.includeLanguages.includes(editor.document.languageId))) { + activateLc(context, createLc); + nextTick(() => { + stop(); + }); } + }, { + immediate: true + }); +} - executeCommand('setContext', 'vue.activated', true); - - const selectors = config.server.value.includeLanguages; - - client = createLc( - 'vue', - 'Vue', - selectors, - await getInitializationOptions(context, enabledHybridMode.value), - 6009, - outputChannel - ); - - watch([enabledHybridMode, enabledTypeScriptPlugin], (newValues, oldValues) => { - if (newValues[0] !== oldValues[0]) { - requestReloadVSCode( - newValues[0] - ? 'Please reload VSCode to enable Hybrid Mode.' - : 'Please reload VSCode to disable Hybrid Mode.' - ); - } - else if (newValues[1] !== oldValues[1]) { - requestReloadVSCode( - newValues[1] - ? 'Please reload VSCode to enable Vue TypeScript Plugin.' - : 'Please reload VSCode to disable Vue TypeScript Plugin.' - ); - } - }); - - watch(() => config.server.value.includeLanguages, () => { - if (enabledHybridMode.value) { - requestReloadVSCode('Please reload VSCode to apply the new language settings.'); - } - }); - - watch(config.server, () => { - if (!enabledHybridMode.value) { - executeCommand('vue.action.restartServer', false); - } - }); - - watch(Object.values(config).filter((conf) => conf !== config.server), () => { - executeCommand('vue.action.restartServer', false); - }); - - useCommand('vue.action.restartServer', async (restartTsServer: boolean = true) => { - if (restartTsServer) { - await executeCommand('typescript.restartTsServer'); - } - await client.stop(); - outputChannel.clear(); - client.clientOptions.initializationOptions = await getInitializationOptions(context, enabledHybridMode.value); - await client.start(); - nameCasing.activate(client, selectors); - }); - - doctor.register(context, client); - nameCasing.activate(client, selectors); - splitEditors.register(client); - - lsp.activateAutoInsertion(selectors, client); - lsp.activateDocumentDropEdit(selectors, client); - lsp.activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); - - if (!enabledHybridMode.value) { - lsp.activateTsConfigStatusItem(selectors, 'vue.tsconfig', client); - lsp.activateTsVersionStatusItem(selectors, 'vue.tsversion', context, text => 'TS ' + text); - lsp.activateFindFileReferences('vue.findAllFileReferences', client); +async function activateLc( + context: vscode.ExtensionContext, + createLc: CreateLanguageClient +) { + useVscodeContext("vue.activated", true); + const outputChannel = useOutputChannel("Vue Language Server"); + const selectors = config.server.value.includeLanguages; + + client = createLc( + "vue", + "Vue", + selectors, + await getInitializationOptions(context, enabledHybridMode.value), + 6009, + outputChannel + ); + + watch([enabledHybridMode, enabledTypeScriptPlugin], (newValues, oldValues) => { + if (newValues[0] !== oldValues[0]) { + requestReloadVscode( + newValues[0] + ? "Please reload VSCode to enable Hybrid Mode." + : "Please reload VSCode to disable Hybrid Mode." + ); + } else if (newValues[1] !== oldValues[1]) { + requestReloadVscode( + newValues[1] + ? "Please reload VSCode to enable Vue TypeScript Plugin." + : "Please reload VSCode to disable Vue TypeScript Plugin." + ); } + }); - const hybridModeStatus = vscode.languages.createLanguageStatusItem('vue-hybrid-mode', selectors); - hybridModeStatus.text = 'Hybrid Mode'; - hybridModeStatus.detail = (enabledHybridMode.value ? 'Enabled' : 'Disabled') + (config.server.value.hybridMode === 'auto' ? ' (Auto)' : ''); - hybridModeStatus.command = { - title: 'Open Setting', - command: 'workbench.action.openSettings', - arguments: ['vue.server.hybridMode'], - }; - if (!enabledHybridMode.value) { - hybridModeStatus.severity = vscode.LanguageStatusSeverity.Warning; + watch(() => config.server.value.includeLanguages, () => { + if (enabledHybridMode.value) { + requestReloadVscode( + "Please reload VSCode to apply the new language settings." + ); } + }); - const item = vscode.languages.createLanguageStatusItem('vue-insider', 'vue'); - item.text = 'Checking for Updates...'; - item.busy = true; - let succeed = false; - for (const url of [ - 'https://raw.githubusercontent.com/vuejs/language-tools/HEAD/insiders.json', - 'https://cdn.jsdelivr.net/gh/vuejs/language-tools/insiders.json', - ]) { - try { - const res = await fetch(url); - onJson(await res.json() as any); - succeed = true; - break; - } catch { } - } - item.busy = false; - if (!succeed) { - item.text = 'Failed to Fetch Versions'; - item.severity = vscode.LanguageStatusSeverity.Error; + watch(config.server, () => { + if (!enabledHybridMode.value) { + executeCommand("vue.action.restartServer", false); } + }); - function onJson(json: { - latest: string; - versions: { - version: string; - date: string; - downloads: { - GitHub: string; - AFDIAN: string; - }; - }[]; - }) { - item.detail = undefined; - item.command = { - title: 'Select Version', - command: 'vue-insiders.update', - }; - if (json.versions.some(version => version.version === context.extension.packageJSON.version)) { - item.text = 'πŸš€ Insiders Edition'; - item.severity = vscode.LanguageStatusSeverity.Information; - - if (context.extension.packageJSON.version !== json.latest) { - item.detail = 'New Version Available!'; - item.severity = vscode.LanguageStatusSeverity.Warning; - vscode.window - .showInformationMessage('New Insiders Version Available!', 'Download') - .then(download => { - if (download) { - executeCommand('vue-insiders.update'); - } - }); - } - } - else { - item.text = '✨ Get Insiders Edition'; - item.severity = vscode.LanguageStatusSeverity.Warning; - } - useCommand('vue-insiders.update', async () => { - const quickPickItems: { [version: string]: vscode.QuickPickItem; } = {}; - for (const { version, date } of json.versions) { - let description = date; - if (context.extension.packageJSON.version === version) { - description += ' (current)'; - } - quickPickItems[version] = { - label: version, - description, - }; - } - const version = await quickPick([quickPickItems, { - learnMore: { - label: 'Learn more about Insiders Edition', - }, - joinViaGitHub: { - label: 'Join via GitHub Sponsors', - }, - joinViaAFDIAN: { - label: 'Join via AFDIAN (ηˆ±ε‘η”΅)', - }, - }]); - if (version === 'learnMore') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition')); - } - else if (version === 'joinViaGitHub') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/sponsors/johnsoncodehk')); - } - else if (version === 'joinViaAFDIAN') { - vscode.env.openExternal(vscode.Uri.parse('https://afdian.net/a/johnsoncodehk')); - } - else { - const downloads = json.versions.find(v => v.version === version)?.downloads; - if (downloads) { - const quickPickItems: { [key: string]: vscode.QuickPickItem; } = { - GitHub: { - label: `${version} - GitHub Releases`, - description: 'Access via GitHub Sponsors', - detail: downloads.GitHub, - }, - AFDIAN: { - label: `${version} - Insiders η”΅εœˆ`, - description: 'Access via AFDIAN (ηˆ±ε‘η”΅)', - detail: downloads.AFDIAN, - }, - }; - const otherItems: { [key: string]: vscode.QuickPickItem; } = { - learnMore: { - label: 'Learn more about Insiders Edition', - }, - joinViaGitHub: { - label: 'Join via GitHub Sponsors', - }, - joinViaAFDIAN: { - label: 'Join via AFDIAN (ηˆ±ε‘η”΅)', - }, - }; - const option = await quickPick([quickPickItems, otherItems]); - if (option === 'learnMore') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition')); - } - else if (option === 'joinViaGitHub') { - vscode.env.openExternal(vscode.Uri.parse('https://github.com/sponsors/johnsoncodehk')); - } - else if (option === 'joinViaAFDIAN') { - vscode.env.openExternal(vscode.Uri.parse('https://afdian.net/a/johnsoncodehk')); - } - else if (option) { - vscode.env.openExternal(vscode.Uri.parse(downloads[option as keyof typeof downloads])); - } - } - } - }); - } + watch(Object.values(config).filter((conf) => conf !== config.server), () => { + executeCommand("vue.action.restartServer", false); + }); - async function requestReloadVSCode(msg: string) { - const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); - if (reload === undefined) { - return; // cancel - } - executeCommand('workbench.action.reloadWindow'); + useCommand("vue.action.restartServer", async (restartTsServer: boolean = true) => { + if (restartTsServer) { + await executeCommand("typescript.restartTsServer"); } + await client.stop(); + outputChannel.clear(); + client.clientOptions.initializationOptions = + await getInitializationOptions(context, enabledHybridMode.value); + await client.start(); + activateNameCasing(client, selectors); + }); + + activateDoctor(context, client); + activateNameCasing(client, selectors); + activateSplitEditors(client); + + lsp.activateAutoInsertion(selectors, client); + lsp.activateDocumentDropEdit(selectors, client); + lsp.activateWriteVirtualFiles("vue.action.writeVirtualFiles", client); + + if (!enabledHybridMode.value) { + lsp.activateTsConfigStatusItem(selectors, "vue.tsconfig", client); + lsp.activateTsVersionStatusItem( + selectors, + "vue.tsversion", + context, + (text) => "TS " + text + ); + lsp.activateFindFileReferences("vue.findAllFileReferences", client); + } - stop(); - }, { - immediate: true - }) -} - -function getTsVersion(libPath: string): string | undefined { - try { - 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'); + useHybridModeStatusItem(); + useInsidersStatusItem(context); - if (contents === undefined) { - return; - } - - let desc: any = null; - try { - desc = JSON.parse(contents); - } catch (err) { - return; - } - if (!desc || !desc.version) { - return; + async function requestReloadVscode(msg: string) { + const reload = await vscode.window.showInformationMessage( + msg, + "Reload Window" + ); + if (reload === undefined) { + return; // cancel } - - return desc.version; - } catch { } + executeCommand("workbench.action.reloadWindow"); + } } export function deactivate(): Thenable | undefined { @@ -410,35 +159,4 @@ async function getInitializationOptions( typescript: { tsdk: (await lsp.getTsdk(context))!.tsdk }, vue: { hybridMode }, }; -}; - -function isExtensionCompatibleWithHybridMode(extension: vscode.Extension) { - 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' - || extension.id === 'bierner.lit-html' - || extension.id === 'jenkey2011.string-highlight' - || extension.id === 'mxsdev.typescript-explorer' - || extension.id === 'miaonster.vscode-tsx-arrow-definition' - || extension.id === 'runem.lit-plugin' - || extension.id === 'kimuson.ts-type-expand' - || extension.id === 'p42ai.refactor' - || extension.id === 'styled-components.vscode-styled-components' - || extension.id === 'Divlo.vscode-styled-jsx-languageserver' - || extension.id === 'nrwl.angular-console' - || extension.id === 'ShenQingchuan.vue-vine-extension' - || extension.id === 'ms-dynamics-smb.al' - ) { - return true; - } - if (extension.id === 'denoland.vscode-deno') { - return !vscode.workspace.getConfiguration('deno').get('enable'); - } - if (extension.id === 'svelte.svelte-vscode') { - return semver.gte(extension.packageJSON.version, '108.4.0'); - } -} \ No newline at end of file +} diff --git a/extensions/vscode/src/compatibility.ts b/extensions/vscode/src/compatibility.ts new file mode 100644 index 0000000000..76e1c96b94 --- /dev/null +++ b/extensions/vscode/src/compatibility.ts @@ -0,0 +1,49 @@ +import * as vscode from 'vscode'; +import * as semver from 'semver'; +import { computed, useAllExtensions } from 'reactive-vscode'; + +const extensions = useAllExtensions(); + +export const incompatibleExtensions = computed(() => { + return extensions.value + .filter((ext) => isExtensionCompatibleWithHybridMode(ext) === false) + .map((ext) => ext.id); +}); + +export const unknownExtensions = computed(() => { + return extensions.value + .filter((ext) => isExtensionCompatibleWithHybridMode(ext) === undefined && !!ext.packageJSON?.contributes?.typescriptServerPlugins) + .map((ext) => ext.id); +}); + + +function isExtensionCompatibleWithHybridMode(extension: vscode.Extension) { + 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' + || extension.id === 'bierner.lit-html' + || extension.id === 'jenkey2011.string-highlight' + || extension.id === 'mxsdev.typescript-explorer' + || extension.id === 'miaonster.vscode-tsx-arrow-definition' + || extension.id === 'runem.lit-plugin' + || extension.id === 'kimuson.ts-type-expand' + || extension.id === 'p42ai.refactor' + || extension.id === 'styled-components.vscode-styled-components' + || extension.id === 'Divlo.vscode-styled-jsx-languageserver' + || extension.id === 'nrwl.angular-console' + || extension.id === 'ShenQingchuan.vue-vine-extension' + || extension.id === 'ms-dynamics-smb.al' + ) { + return true; + } + if (extension.id === 'denoland.vscode-deno') { + return !vscode.workspace.getConfiguration('deno').get('enable'); + } + if (extension.id === 'svelte.svelte-vscode') { + return semver.gte(extension.packageJSON.version, '108.4.0'); + } +} \ No newline at end of file diff --git a/extensions/vscode/src/features/doctor.ts b/extensions/vscode/src/features/doctor.ts index c90609b1d8..b933f8828f 100644 --- a/extensions/vscode/src/features/doctor.ts +++ b/extensions/vscode/src/features/doctor.ts @@ -1,10 +1,10 @@ import { BaseLanguageClient, ExecuteCommandParams, ExecuteCommandRequest, getTsdk } from '@volar/vscode'; import type { SFCParseResult } from '@vue/language-server'; import { commands } from '@vue/language-server/lib/types'; +import { executeCommand, useActiveTextEditor, useCommand, useEventEmitter, useStatusBarItem, watch } from 'reactive-vscode'; import * as semver from 'semver'; import * as vscode from 'vscode'; import { config } from '../config'; -import { executeCommand, useActiveTextEditor, useCommand, useEventEmitter, useStatusBarItem, watch } from 'reactive-vscode'; const scheme = 'vue-doctor'; const knownValidSyntaxHighlightExtensions = { @@ -13,7 +13,7 @@ const knownValidSyntaxHighlightExtensions = { sass: ['Syler.sass-indented'], }; -export async function register(context: vscode.ExtensionContext, client: BaseLanguageClient) { +export async function activate(context: vscode.ExtensionContext, client: BaseLanguageClient) { const item = useStatusBarItem({ alignment: vscode.StatusBarAlignment.Right, diff --git a/extensions/vscode/src/features/nameCasing.ts b/extensions/vscode/src/features/nameCasing.ts index a4534927ed..d7802ba905 100644 --- a/extensions/vscode/src/features/nameCasing.ts +++ b/extensions/vscode/src/features/nameCasing.ts @@ -1,9 +1,9 @@ import { BaseLanguageClient, ExecuteCommandParams, ExecuteCommandRequest, TextEdit } from '@volar/vscode'; import { quickPick } from '@volar/vscode/lib/common'; import { AttrNameCasing, TagNameCasing, commands } from '@vue/language-server/lib/types'; +import { ref, useActiveTextEditor, useCommand, watch } from 'reactive-vscode'; import * as vscode from 'vscode'; import { config } from '../config'; -import { ref, useActiveTextEditor, useCommand, watch } from 'reactive-vscode'; export const attrNameCasings = ref(new Map()); export const tagNameCasings = ref(new Map()); diff --git a/extensions/vscode/src/features/splitEditors.ts b/extensions/vscode/src/features/splitEditors.ts index 3836933059..5afcc2add2 100644 --- a/extensions/vscode/src/features/splitEditors.ts +++ b/extensions/vscode/src/features/splitEditors.ts @@ -1,13 +1,13 @@ import { ExecuteCommandParams, ExecuteCommandRequest, type BaseLanguageClient } from '@volar/vscode'; import type { SFCParseResult } from '@vue/language-server'; import { commands } from '@vue/language-server/lib/types'; +import { executeCommand, useActiveTextEditor, useCommand } from 'reactive-vscode'; import * as vscode from 'vscode'; import { config } from '../config'; -import { executeCommand, useActiveTextEditor, useCommand } from 'reactive-vscode'; type SFCBlock = SFCParseResult['descriptor']['customBlocks'][number]; -export function register(client: BaseLanguageClient) { +export function activate(client: BaseLanguageClient) { const activeTextEditor = useActiveTextEditor(); const getDocDescriptor = useDocDescriptor(client); diff --git a/extensions/vscode/src/hybridMode.ts b/extensions/vscode/src/hybridMode.ts new file mode 100644 index 0000000000..4eebfdf3fb --- /dev/null +++ b/extensions/vscode/src/hybridMode.ts @@ -0,0 +1,195 @@ +import * as path from 'node:path'; +import * as fs from 'node:fs'; +import * as semver from 'semver'; +import * as vscode from 'vscode'; +import { computed, executeCommand, ref, useAllExtensions, useVscodeContext, watchEffect } from "reactive-vscode"; +import { incompatibleExtensions, unknownExtensions } from './compatibility'; +import { config } from './config'; + +const extensions = useAllExtensions(); + +export const enabledHybridMode = ref(true); + +export const enabledTypeScriptPlugin = computed(() => { + return ( + enabledHybridMode.value || + config.server.value.hybridMode === "typeScriptPluginOnly" + ); +}); + +const vscodeTsdkVersion = computed(() => { + const nightly = extensions.value.find( + ({ id }) => id === "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); + } +}); + +const workspaceTsdkVersion = computed(() => { + const libPath = vscode.workspace + .getConfiguration("typescript") + .get("tsdk") + ?.replace(/\\/g, "/"); + if (libPath) { + return getTsVersion(libPath); + } +}); + +export function useHybridModeTips() { + useVscodeContext("vueHybridMode", enabledHybridMode); + + watchEffect(() => { + switch (config.server.value.hybridMode) { + case "typeScriptPluginOnly": { + enabledHybridMode.value = false; + break; + } + case "auto": { + if ( + incompatibleExtensions.value.length || + unknownExtensions.value.length + ) { + vscode.window + .showInformationMessage( + `Hybrid Mode is disabled automatically because there is a potentially incompatible ${[ + ...incompatibleExtensions.value, + ...unknownExtensions.value, + ].join(", ")} TypeScript plugin installed.`, + "Open Settings", + "Report a false positive" + ) + .then((value) => { + if (value === "Open Settings") { + 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" + ) + ); + } + }); + enabledHybridMode.value = false; + } + else if ( + (vscodeTsdkVersion.value && !semver.gte(vscodeTsdkVersion.value, "5.3.0")) || + (workspaceTsdkVersion.value && !semver.gte(workspaceTsdkVersion.value, "5.3.0")) + ) { + let msg = `Hybrid Mode is disabled automatically because TSDK >= 5.3.0 is required (VSCode TSDK: ${vscodeTsdkVersion.value}`; + if (workspaceTsdkVersion.value) { + msg += `, Workspace TSDK: ${workspaceTsdkVersion.value}`; + } + msg += `).`; + vscode.window + .showInformationMessage(msg, "Open Settings") + .then((value) => { + if (value === "Open Settings") { + executeCommand( + "workbench.action.openSettings", + "vue.server.hybridMode" + ); + } + }); + enabledHybridMode.value = false; + } else { + enabledHybridMode.value = true; + } + break; + } + default: { + if ( + config.server.value.hybridMode && + incompatibleExtensions.value.length + ) { + vscode.window + .showWarningMessage( + `You have explicitly enabled Hybrid Mode, but you have installed known incompatible extensions: ${incompatibleExtensions.value.join( + ", " + )}. You may want to change vue.server.hybridMode to "auto" to avoid compatibility issues.`, + "Open Settings", + "Report a false positive" + ) + .then((value) => { + if (value === "Open Settings") { + 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" + ) + ); + } + }); + } + enabledHybridMode.value = config.server.value.hybridMode; + } + } + }); +} + +export function useHybridModeStatusItem() { + const item = vscode.languages.createLanguageStatusItem( + "vue-hybrid-mode", + config.server.value.includeLanguages + ); + + item.text = "Hybrid Mode"; + item.detail = + (enabledHybridMode.value ? "Enabled" : "Disabled") + + (config.server.value.hybridMode === "auto" ? " (Auto)" : ""); + item.command = { + title: "Open Setting", + command: "workbench.action.openSettings", + arguments: ["vue.server.hybridMode"], + }; + + if (!enabledHybridMode.value) { + item.severity = vscode.LanguageStatusSeverity.Warning; + } +} + +function getTsVersion(libPath: string) { + try { + 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 as string; + } catch { } +} diff --git a/extensions/vscode/src/insiders.ts b/extensions/vscode/src/insiders.ts new file mode 100644 index 0000000000..ff29bf16db --- /dev/null +++ b/extensions/vscode/src/insiders.ts @@ -0,0 +1,158 @@ +import { quickPick } from "@volar/vscode/lib/common"; +import { executeCommand, useCommand } from 'reactive-vscode'; +import * as vscode from 'vscode'; + +export function useInsidersStatusItem(context: vscode.ExtensionContext) { + const item = vscode.languages.createLanguageStatusItem("vue-insider", "vue"); + item.text = "Checking for Updates..."; + item.busy = true; + let succeed = false; + + Promise.race([ + "https://raw.githubusercontent.com/vuejs/language-tools/HEAD/insiders.json", + "https://cdn.jsdelivr.net/gh/vuejs/language-tools/insiders.json", + ].map((url) => fetch(url))) + .then(async (res) => { + onJson(await res.json() as any); + item.busy = false; + if (!succeed) { + item.text = "Failed to Fetch Versions"; + item.severity = vscode.LanguageStatusSeverity.Error; + } + }) + .catch(); + + function onJson(json: { + latest: string; + versions: { + version: string; + date: string; + downloads: { + GitHub: string; + AFDIAN: string; + }; + }[]; + }) { + item.detail = undefined; + item.command = { + title: "Select Version", + command: "vue-insiders.update", + }; + if ( + json.versions.some( + (version) => version.version === context.extension.packageJSON.version + ) + ) { + item.text = "πŸš€ Insiders Edition"; + item.severity = vscode.LanguageStatusSeverity.Information; + + if (context.extension.packageJSON.version !== json.latest) { + item.detail = "New Version Available!"; + item.severity = vscode.LanguageStatusSeverity.Warning; + vscode.window + .showInformationMessage("New Insiders Version Available!", "Download") + .then((download) => { + if (download) { + executeCommand("vue-insiders.update"); + } + }); + } + } + else { + item.text = "✨ Get Insiders Edition"; + item.severity = vscode.LanguageStatusSeverity.Warning; + } + + useCommand("vue-insiders.update", async () => { + const quickPickItems: { [version: string]: vscode.QuickPickItem; } = {}; + for (const { version, date } of json.versions) { + let description = date; + if (context.extension.packageJSON.version === version) { + description += " (current)"; + } + quickPickItems[version] = { + label: version, + description, + }; + } + const version = await quickPick([ + quickPickItems, + { + learnMore: { + label: "Learn more about Insiders Edition", + }, + joinViaGitHub: { + label: "Join via GitHub Sponsors", + }, + joinViaAFDIAN: { + label: "Join via AFDIAN (ηˆ±ε‘η”΅)", + }, + }, + ]); + if (version === "learnMore") { + vscode.env.openExternal( + vscode.Uri.parse( + "https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition" + ) + ); + } else if (version === "joinViaGitHub") { + vscode.env.openExternal( + vscode.Uri.parse("https://github.com/sponsors/johnsoncodehk") + ); + } else if (version === "joinViaAFDIAN") { + vscode.env.openExternal( + vscode.Uri.parse("https://afdian.net/a/johnsoncodehk") + ); + } else { + const downloads = json.versions.find( + (v) => v.version === version + )?.downloads; + if (downloads) { + const quickPickItems: { [key: string]: vscode.QuickPickItem; } = { + GitHub: { + label: `${version} - GitHub Releases`, + description: "Access via GitHub Sponsors", + detail: downloads.GitHub, + }, + AFDIAN: { + label: `${version} - Insiders η”΅εœˆ`, + description: "Access via AFDIAN (ηˆ±ε‘η”΅)", + detail: downloads.AFDIAN, + }, + }; + const otherItems: { [key: string]: vscode.QuickPickItem; } = { + learnMore: { + label: "Learn more about Insiders Edition", + }, + joinViaGitHub: { + label: "Join via GitHub Sponsors", + }, + joinViaAFDIAN: { + label: "Join via AFDIAN (ηˆ±ε‘η”΅)", + }, + }; + const option = await quickPick([quickPickItems, otherItems]); + if (option === "learnMore") { + vscode.env.openExternal( + vscode.Uri.parse( + "https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition" + ) + ); + } else if (option === "joinViaGitHub") { + vscode.env.openExternal( + vscode.Uri.parse("https://github.com/sponsors/johnsoncodehk") + ); + } else if (option === "joinViaAFDIAN") { + vscode.env.openExternal( + vscode.Uri.parse("https://afdian.net/a/johnsoncodehk") + ); + } else if (option) { + vscode.env.openExternal( + vscode.Uri.parse(downloads[option as keyof typeof downloads]) + ); + } + } + } + }); + } +} \ No newline at end of file diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 7ee9e97462..3895aadf6b 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -3,8 +3,9 @@ import * as protocol from '@vue/language-server/protocol'; import * as fs from 'fs'; import * as vscode from 'vscode'; import * as lsp from '@volar/vscode/node'; -import { defineExtension, executeCommand, onActivate, onDeactivate } from 'reactive-vscode'; -import { activate as commonActivate, deactivate as commonDeactivate, enabledHybridMode, enabledTypeScriptPlugin } from './common'; +import { defineExtension, executeCommand, extensionContext, onDeactivate } from 'reactive-vscode'; +import { enabledHybridMode, enabledTypeScriptPlugin } from './hybridMode'; +import { activate as commonActivate, deactivate as commonDeactivate } from './common'; import { config } from './config'; import { middleware } from './middleware'; @@ -39,16 +40,11 @@ export const { activate, deactivate } = defineExtension(async () => { }); } - onActivate((context) => { - commonActivate(context, ( - id, - name, - documentSelector, - initOptions, - port, - outputChannel - ) => { - + // δΈ‹δΈͺε°η‰ˆζœ¬ε°±δΈιœ€θ¦θΏ™θ‘ŒδΊ†οΌŒζˆ‘ζ™šη‚Ήε‘εΈƒ + const context = extensionContext.value!; + commonActivate( + context, + (id, name, documentSelector, initOptions, port, outputChannel) => { class _LanguageClient extends lsp.LanguageClient { fillInitializeParams(params: lsp.InitializeParams) { // fix https://github.com/vuejs/language-tools/issues/1959 @@ -61,9 +57,13 @@ export const { activate, deactivate } = defineExtension(async () => { const runOptions: lsp.ForkOptions = {}; if (config.server.value.maxOldSpaceSize) { runOptions.execArgv ??= []; - runOptions.execArgv.push("--max-old-space-size=" + config.server.value.maxOldSpaceSize); + runOptions.execArgv.push( + '--max-old-space-size=' + config.server.value.maxOldSpaceSize + ); } - const debugOptions: lsp.ForkOptions = { execArgv: ['--nolazy', '--inspect=' + port] }; + const debugOptions: lsp.ForkOptions = { + execArgv: ['--nolazy', '--inspect=' + port] + }; const serverOptions: lsp.ServerOptions = { run: { module: serverModule.fsPath, @@ -74,7 +74,7 @@ export const { activate, deactivate } = defineExtension(async () => { module: serverModule.fsPath, transport: lsp.TransportKind.ipc, options: debugOptions - }, + } }; const clientOptions: lsp.LanguageClientOptions = { middleware, @@ -82,15 +82,15 @@ export const { activate, deactivate } = defineExtension(async () => { initializationOptions: initOptions, markdown: { isTrusted: true, - supportHtml: true, + supportHtml: true }, - outputChannel, + outputChannel }; const client = new _LanguageClient( id, name, serverOptions, - clientOptions, + clientOptions ); client.start(); @@ -99,8 +99,8 @@ export const { activate, deactivate } = defineExtension(async () => { updateProviders(client); return client; - }); - }); + } + ); onDeactivate(() => { commonDeactivate(); @@ -110,11 +110,11 @@ export const { activate, deactivate } = defineExtension(async () => { }); function updateProviders(client: lsp.LanguageClient) { - const initializeFeatures = (client as any).initializeFeatures; (client as any).initializeFeatures = (...args: any) => { - const capabilities = (client as any)._capabilities as lsp.ServerCapabilities; + const capabilities = (client as any) + ._capabilities as lsp.ServerCapabilities; if (!config.codeActions.value.enabled) { capabilities.codeActionProvider = undefined; @@ -122,7 +122,10 @@ function updateProviders(client: lsp.LanguageClient) { if (!config.codeLens.value.enabled) { capabilities.codeLensProvider = undefined; } - if (!config.updateImportsOnFileMove.value.enabled && capabilities.workspace?.fileOperations?.willRename) { + if ( + !config.updateImportsOnFileMove.value.enabled && + capabilities.workspace?.fileOperations?.willRename + ) { capabilities.workspace.fileOperations.willRename = undefined; } @@ -131,9 +134,13 @@ function updateProviders(client: lsp.LanguageClient) { } try { - const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')!; + const tsExtension = vscode.extensions.getExtension( + 'vscode.typescript-language-features' + )!; const readFileSync = fs.readFileSync; - const extensionJsPath = require.resolve('./dist/extension.js', { paths: [tsExtension.extensionPath] }); + const extensionJsPath = require.resolve('./dist/extension.js', { + paths: [tsExtension.extensionPath], + }); // @ts-expect-error fs.readFileSync = (...args) => { @@ -144,27 +151,40 @@ try { if (!enabledTypeScriptPlugin.value) { text = text.replace( 'for(const e of n.contributes.typescriptServerPlugins', - s => s + `.filter(p=>p.name!=='typescript-vue-plugin-bundle')` + (s) => s + `.filter(p=>p.name!=='typescript-vue-plugin-bundle')` ); - } - else if (enabledHybridMode.value) { + } else if (enabledHybridMode.value) { // patch readPlugins text = text.replace( 'languages:Array.isArray(e.languages)', [ 'languages:', - `e.name==='typescript-vue-plugin-bundle'?[${config.server.value.includeLanguages.map(lang => `"${lang}"`).join(',')}]`, - ':Array.isArray(e.languages)', + `e.name==='typescript-vue-plugin-bundle'?[${config.server.value.includeLanguages + .map((lang) => `'${lang}'`) + .join(',')}]`, + ':Array.isArray(e.languages)' ].join('') ); // VSCode < 1.87.0 - text = text.replace('t.$u=[t.$r,t.$s,t.$p,t.$q]', s => s + '.concat("vue")'); // patch jsTsLanguageModes - text = text.replace('.languages.match([t.$p,t.$q,t.$r,t.$s]', s => s + '.concat("vue")'); // patch isSupportedLanguageMode + text = text.replace( + 't.$u=[t.$r,t.$s,t.$p,t.$q]', + (s) => s + '.concat("vue")' + ); // patch jsTsLanguageModes + text = text.replace( + '.languages.match([t.$p,t.$q,t.$r,t.$s]', + (s) => s + '.concat("vue")' + ); // patch isSupportedLanguageMode // VSCode >= 1.87.0 - text = text.replace('t.jsTsLanguageModes=[t.javascript,t.javascriptreact,t.typescript,t.typescriptreact]', s => s + '.concat("vue")'); // patch jsTsLanguageModes - text = text.replace('.languages.match([t.typescript,t.typescriptreact,t.javascript,t.javascriptreact]', s => s + '.concat("vue")'); // patch isSupportedLanguageMode + text = text.replace( + 't.jsTsLanguageModes=[t.javascript,t.javascriptreact,t.typescript,t.typescriptreact]', + (s) => s + '.concat("vue")' + ); // patch jsTsLanguageModes + text = text.replace( + '.languages.match([t.typescript,t.typescriptreact,t.javascript,t.javascriptreact]', + (s) => s + '.concat("vue")' + ); // patch isSupportedLanguageMode } return text; From 851d9ca78cabe407a6e6a5425992c162bca383dc Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Sat, 26 Oct 2024 16:53:50 +0800 Subject: [PATCH 04/10] fix: fetch insiders data correctly --- extensions/vscode/src/common.ts | 67 ++++++++++++-------------- extensions/vscode/src/compatibility.ts | 3 +- extensions/vscode/src/insiders.ts | 33 ++++++++----- 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index c537979ad9..5f6f332c58 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -1,5 +1,5 @@ -import * as lsp from "@volar/vscode"; -import type { VueInitializationOptions } from "@vue/language-server"; +import * as lsp from '@volar/vscode'; +import type { VueInitializationOptions } from '@vue/language-server'; import { executeCommand, nextTick, @@ -9,14 +9,14 @@ import { useCommand, useVscodeContext, watch, -} from "reactive-vscode"; -import * as vscode from "vscode"; -import { config } from "./config"; -import { activate as activateDoctor } from "./features/doctor"; -import { activate as activateNameCasing } from "./features/nameCasing"; -import { activate as activateSplitEditors } from "./features/splitEditors"; -import { enabledHybridMode, enabledTypeScriptPlugin, useHybridModeStatusItem, useHybridModeTips } from "./hybridMode"; -import { useInsidersStatusItem } from "./insiders"; +} from 'reactive-vscode'; +import * as vscode from 'vscode'; +import { config } from './config'; +import { activate as activateDoctor } from './features/doctor'; +import { activate as activateNameCasing } from './features/nameCasing'; +import { activate as activateSplitEditors } from './features/splitEditors'; +import { enabledHybridMode, enabledTypeScriptPlugin, useHybridModeStatusItem, useHybridModeTips } from './hybridMode'; +import { useInsidersStatusItem } from './insiders'; let client: lsp.BaseLanguageClient; @@ -54,13 +54,13 @@ async function activateLc( context: vscode.ExtensionContext, createLc: CreateLanguageClient ) { - useVscodeContext("vue.activated", true); - const outputChannel = useOutputChannel("Vue Language Server"); + useVscodeContext('vue.activated', true); + const outputChannel = useOutputChannel('Vue Language Server'); const selectors = config.server.value.includeLanguages; client = createLc( - "vue", - "Vue", + 'vue', + 'Vue', selectors, await getInitializationOptions(context, enabledHybridMode.value), 6009, @@ -71,14 +71,14 @@ async function activateLc( if (newValues[0] !== oldValues[0]) { requestReloadVscode( newValues[0] - ? "Please reload VSCode to enable Hybrid Mode." - : "Please reload VSCode to disable Hybrid Mode." + ? 'Please reload VSCode to enable Hybrid Mode.' + : 'Please reload VSCode to disable Hybrid Mode.' ); } else if (newValues[1] !== oldValues[1]) { requestReloadVscode( newValues[1] - ? "Please reload VSCode to enable Vue TypeScript Plugin." - : "Please reload VSCode to disable Vue TypeScript Plugin." + ? 'Please reload VSCode to enable Vue TypeScript Plugin.' + : 'Please reload VSCode to disable Vue TypeScript Plugin.' ); } }); @@ -86,31 +86,29 @@ async function activateLc( watch(() => config.server.value.includeLanguages, () => { if (enabledHybridMode.value) { requestReloadVscode( - "Please reload VSCode to apply the new language settings." + 'Please reload VSCode to apply the new language settings.' ); } }); watch(config.server, () => { if (!enabledHybridMode.value) { - executeCommand("vue.action.restartServer", false); + executeCommand('vue.action.restartServer', false); } }); watch(Object.values(config).filter((conf) => conf !== config.server), () => { - executeCommand("vue.action.restartServer", false); + executeCommand('vue.action.restartServer', false); }); - useCommand("vue.action.restartServer", async (restartTsServer: boolean = true) => { + useCommand('vue.action.restartServer', async (restartTsServer: boolean = true) => { if (restartTsServer) { - await executeCommand("typescript.restartTsServer"); + await executeCommand('typescript.restartTsServer'); } await client.stop(); outputChannel.clear(); - client.clientOptions.initializationOptions = - await getInitializationOptions(context, enabledHybridMode.value); + client.clientOptions.initializationOptions = await getInitializationOptions(context, enabledHybridMode.value); await client.start(); - activateNameCasing(client, selectors); }); activateDoctor(context, client); @@ -119,17 +117,12 @@ async function activateLc( lsp.activateAutoInsertion(selectors, client); lsp.activateDocumentDropEdit(selectors, client); - lsp.activateWriteVirtualFiles("vue.action.writeVirtualFiles", client); + lsp.activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); if (!enabledHybridMode.value) { - lsp.activateTsConfigStatusItem(selectors, "vue.tsconfig", client); - lsp.activateTsVersionStatusItem( - selectors, - "vue.tsversion", - context, - (text) => "TS " + text - ); - lsp.activateFindFileReferences("vue.findAllFileReferences", client); + lsp.activateTsConfigStatusItem(selectors, 'vue.tsconfig', client); + lsp.activateTsVersionStatusItem(selectors, 'vue.tsversion', context, (text) => 'TS ' + text); + lsp.activateFindFileReferences('vue.findAllFileReferences', client); } useHybridModeStatusItem(); @@ -138,12 +131,12 @@ async function activateLc( async function requestReloadVscode(msg: string) { const reload = await vscode.window.showInformationMessage( msg, - "Reload Window" + 'Reload Window' ); if (reload === undefined) { return; // cancel } - executeCommand("workbench.action.reloadWindow"); + executeCommand('workbench.action.reloadWindow'); } } diff --git a/extensions/vscode/src/compatibility.ts b/extensions/vscode/src/compatibility.ts index 76e1c96b94..4b56428902 100644 --- a/extensions/vscode/src/compatibility.ts +++ b/extensions/vscode/src/compatibility.ts @@ -16,7 +16,6 @@ export const unknownExtensions = computed(() => { .map((ext) => ext.id); }); - function isExtensionCompatibleWithHybridMode(extension: vscode.Extension) { if ( extension.id === 'Vue.volar' @@ -46,4 +45,4 @@ function isExtensionCompatibleWithHybridMode(extension: vscode.Extension) { if (extension.id === 'svelte.svelte-vscode') { return semver.gte(extension.packageJSON.version, '108.4.0'); } -} \ No newline at end of file +} diff --git a/extensions/vscode/src/insiders.ts b/extensions/vscode/src/insiders.ts index ff29bf16db..9e98159699 100644 --- a/extensions/vscode/src/insiders.ts +++ b/extensions/vscode/src/insiders.ts @@ -8,19 +8,28 @@ export function useInsidersStatusItem(context: vscode.ExtensionContext) { item.busy = true; let succeed = false; - Promise.race([ - "https://raw.githubusercontent.com/vuejs/language-tools/HEAD/insiders.json", - "https://cdn.jsdelivr.net/gh/vuejs/language-tools/insiders.json", - ].map((url) => fetch(url))) - .then(async (res) => { - onJson(await res.json() as any); - item.busy = false; - if (!succeed) { - item.text = "Failed to Fetch Versions"; - item.severity = vscode.LanguageStatusSeverity.Error; + fetchJson(); + + async function fetchJson() { + for (const url of [ + "https://raw.githubusercontent.com/vuejs/language-tools/HEAD/insiders.json", + "https://cdn.jsdelivr.net/gh/vuejs/language-tools/insiders.json", + ]) { + try { + const res = await fetch(url); + onJson(await res.json() as any); + succeed = true; + break; } - }) - .catch(); + catch {}; + } + + item.busy = false; + if (!succeed) { + item.text = "Failed to Fetch Versions"; + item.severity = vscode.LanguageStatusSeverity.Error; + } + } function onJson(json: { latest: string; From 15fd06e55ca785d6efa72ad2e2d55fc4db407cbc Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Sat, 26 Oct 2024 17:06:47 +0800 Subject: [PATCH 05/10] refactor: rename --- extensions/vscode/src/{common.ts => languageClient.ts} | 8 ++++---- extensions/vscode/src/nodeClientMain.ts | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) rename extensions/vscode/src/{common.ts => languageClient.ts} (100%) diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/languageClient.ts similarity index 100% rename from extensions/vscode/src/common.ts rename to extensions/vscode/src/languageClient.ts index 5f6f332c58..8d3a4c1c78 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/languageClient.ts @@ -50,6 +50,10 @@ export function activate( }); } +export function deactivate(): Thenable | undefined { + return client?.stop(); +} + async function activateLc( context: vscode.ExtensionContext, createLc: CreateLanguageClient @@ -140,10 +144,6 @@ async function activateLc( } } -export function deactivate(): Thenable | undefined { - return client?.stop(); -} - async function getInitializationOptions( context: vscode.ExtensionContext, hybridMode: boolean diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 3895aadf6b..49cb419aa7 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as lsp from '@volar/vscode/node'; import { defineExtension, executeCommand, extensionContext, onDeactivate } from 'reactive-vscode'; import { enabledHybridMode, enabledTypeScriptPlugin } from './hybridMode'; -import { activate as commonActivate, deactivate as commonDeactivate } from './common'; +import { activate as activateLanguageClient, deactivate as deactivateLanguageClient } from './languageClient'; import { config } from './config'; import { middleware } from './middleware'; @@ -40,9 +40,8 @@ export const { activate, deactivate } = defineExtension(async () => { }); } - // δΈ‹δΈͺε°η‰ˆζœ¬ε°±δΈιœ€θ¦θΏ™θ‘ŒδΊ†οΌŒζˆ‘ζ™šη‚Ήε‘εΈƒ const context = extensionContext.value!; - commonActivate( + activateLanguageClient( context, (id, name, documentSelector, initOptions, port, outputChannel) => { class _LanguageClient extends lsp.LanguageClient { @@ -103,7 +102,7 @@ export const { activate, deactivate } = defineExtension(async () => { ); onDeactivate(() => { - commonDeactivate(); + deactivateLanguageClient(); }); return volarLabs.extensionExports; From a001b770376e34ec92c41ef2325becc72bc3beac Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 30 Oct 2024 17:56:28 +0800 Subject: [PATCH 06/10] feat: use vscode-ext-gen --- .gitignore | 1 + extensions/vscode/README.md | 48 +++++++++++++++++++++++++++++++++ extensions/vscode/package.json | 10 +++---- extensions/vscode/src/config.ts | 40 +++++---------------------- pnpm-lock.yaml | 39 +++++++++++++++++++-------- 5 files changed, 88 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 433b140dac..7b42626d66 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules .vscode-test-web extensions/*/meta.json extensions/*/stats.html +extensions/vscode/src/generated-meta.ts packages/*/*.d.ts packages/*/*.js diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md index c26404c94e..e4fe7e211a 100644 --- a/extensions/vscode/README.md +++ b/extensions/vscode/README.md @@ -254,3 +254,51 @@ Finally you need to make VS Code recognize your new extension and automatically - [angular](https://github.com/angular/angular) shows how TS server plugin working with language service. - Syntax highlight is rewritten base on [vue-syntax-highlight](https://github.com/vuejs/vue-syntax-highlight). - [vscode-fenced-code-block-grammar-injection-example](https://github.com/mjbvz/vscode-fenced-code-block-grammar-injection-example) shows how to inject vue syntax highlight to markdown. + +## Commands + + + +| Command | Title | +| ------------------------------ | ------------------------------------------------- | +| `vue.action.restartServer` | Vue: Restart Vue and TS servers | +| `vue.action.doctor` | Vue: Doctor | +| `vue.action.writeVirtualFiles` | Vue (Debug): Write Virtual Files | +| `vue.action.splitEditors` | Vue: Split