From 1e6a7d8e190db3e284eb93f40910c8b222eaa6f0 Mon Sep 17 00:00:00 2001 From: machty Date: Tue, 14 Jan 2025 17:05:51 -0500 Subject: [PATCH 01/10] wip --- packages/vscode/.gitignore | 2 + packages/vscode/package.json | 51 +++- packages/vscode/src/config.ts | 7 + packages/vscode/src/extension.ts | 339 +++++++++++++++---------- packages/vscode/src/hybrid-mode.ts | 200 +++++++++++++++ packages/vscode/src/language-client.ts | 160 ++++++++++++ packages/vscode/tsconfig.json | 5 +- yarn.lock | 28 +- 8 files changed, 643 insertions(+), 149 deletions(-) create mode 100644 packages/vscode/src/config.ts create mode 100644 packages/vscode/src/hybrid-mode.ts create mode 100644 packages/vscode/src/language-client.ts diff --git a/packages/vscode/.gitignore b/packages/vscode/.gitignore index 43bb79d6d..0c0c91648 100644 --- a/packages/vscode/.gitignore +++ b/packages/vscode/.gitignore @@ -4,3 +4,5 @@ node_modules lib/ dist/ tsconfig.tsbuildinfo + +generated-meta.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 37761b60c..4f801bc61 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -35,7 +35,9 @@ "bundle": "node ./scripts/build.mjs", "bundle:watch": "node ./scripts/build.mjs -- --watch", "extension:package": "vsce package --no-dependencies", - "extension:publish": "vsce publish --no-dependencies" + "extension:publish": "vsce publish --no-dependencies", + "postinstall": "vscode-ext-gen --scope glint", + "prebuild": "yarn postinstall && yarn build" }, "engines": { "vscode": "^1.68.1" @@ -188,6 +190,7 @@ ], "configuration": [ { + "type": "object", "title": "Glint", "properties": { "glint.libraryPath": { @@ -205,18 +208,44 @@ "verbose" ] }, - "glint.server.typescriptMode": { - "type": "string", - "default": "languageServer", + "glint.server.hybridMode": { + "type": [ + "boolean", + "string" + ], + "default": "auto", "enum": [ - "languageServer", - "typescriptPlugin" + "auto", + "typeScriptPluginOnly", + true, + false ], "enumDescriptions": [ - "(Old) use Glint Language Server for typechecking", - "(New) Use TypeScript server plugin for typechecking" + "Automatically detect and enable TypeScript Plugin/Hybrid Mode in a safe environment.", + "Only enable Glint TypeScript Plugin but disable the Glint language server.", + "Enable TypeScript Plugin/Hybrid Mode.", + "Disable TypeScript Plugin/Hybrid Mode." ], - "description": "Glint 2 shifts typechecking to a TypeScript server plugin. This setting allows you to opt into the new behavior." + "description": "Hybrid mode means that Glint will use a TypeScript server plugin for typechecking and use the Glint language server for other features. This is the recommended mode." + }, + "glint.server.compatibleExtensions": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Set compatible extensions to skip automatic detection of Hybrid Mode." + }, + "glint.server.includeLanguages": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "glimmer-js", + "glimmer-ts", + "handlebars" + ] } } } @@ -241,7 +270,9 @@ "esbuild": "^0.15.16", "expect": "^29.5.0", "glob": "^10.2.4", - "mocha": "^10.2.0" + "mocha": "^10.2.0", + "reactive-vscode": "0.2.7-beta.1", + "vscode-ext-gen": "^0.5.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/vscode/src/config.ts b/packages/vscode/src/config.ts new file mode 100644 index 000000000..11ac8ef56 --- /dev/null +++ b/packages/vscode/src/config.ts @@ -0,0 +1,7 @@ +import { defineConfigObject } from 'reactive-vscode'; +import { NestedScopedConfigs, scopedConfigs } from './generated-meta'; + +export const config = defineConfigObject( + scopedConfigs.scope, + scopedConfigs.defaults +); diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 02d91ed29..ff23e006b 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -1,154 +1,221 @@ import { createRequire } from 'node:module'; import * as path from 'node:path'; -import { - ExtensionContext, - WorkspaceFolder, - FileSystemWatcher, - window, - extensions, - commands, - workspace, - WorkspaceConfiguration, -} from 'vscode'; +import * as lsp from '@volar/vscode/node'; +import * as vscode from 'vscode'; import * as languageServerProtocol from '@volar/language-server/protocol.js'; import { LabsInfo, createLabsInfo, getTsdk } from '@volar/vscode'; +import { config } from './config'; import { Disposable, LanguageClient, ServerOptions } from '@volar/vscode/node.js'; +import { + defineExtension, + executeCommand, + extensionContext, + onDeactivate, + useWorkspaceFolders, + watchEffect, +} from 'reactive-vscode'; +import { + activate as activateLanguageClient, + deactivate as deactivateLanguageClient, +} from './language-client'; -/////////////////////////////////////////////////////////////////////////////// -// Setup and extension lifecycle - -const outputChannel = window.createOutputChannel('Glint Language Server'); -const clients = new Map(); -const fileExtensions = ['.js', '.ts', '.gjs', '.gts', '.hbs']; -const filePattern = `**/*{${fileExtensions.join(',')}}`; - -export function activate(context: ExtensionContext): LabsInfo { - // We need to activate the default VSCode TypeScript extension so that our - // TS Plugin kicks in. We do this because the TS extension is (obviously) not - // configured to activate for, say, .gts files: - // https://github.com/microsoft/vscode/blob/878af07/extensions/typescript-language-features/package.json#L62..L75 - extensions.getExtension('vscode.typescript-language-features')?.activate(); - - // TODO: Volar: i think this happens as part of dynamic registerCapability, i.e. - // I think maybe we can remove this from `activate` and wait for it to happen - // when the server sends the registerCapability questions for all dynamicRegistration=true capabilities. - let fileWatcher = workspace.createFileSystemWatcher(filePattern); - - context.subscriptions.push(fileWatcher, createConfigWatcher()); - context.subscriptions.push( - commands.registerCommand('glint.restart-language-server', restartClients), - ); - - // TODO: how to each multiple workspace reloads with VolarLabs? +export const { activate, deactivate } = defineExtension(async () => { const volarLabs = createLabsInfo(languageServerProtocol); - workspace.workspaceFolders?.forEach((folder) => - addWorkspaceFolder(context, folder, fileWatcher, volarLabs), - ); - workspace.onDidChangeWorkspaceFolders(({ added, removed }) => { - added.forEach((folder) => addWorkspaceFolder(context, folder, fileWatcher)); - removed.forEach((folder) => removeWorkspaceFolder(folder)); - }); - - workspace.onDidChangeConfiguration((changeEvent) => { - if (changeEvent.affectsConfiguration('glint.libraryPath')) { - reloadAllWorkspaces(context, fileWatcher); - } - }); - - return volarLabs.extensionExports; -} - -export async function deactivate(): Promise { - await Promise.all([...clients.values()].map((client) => client.stop())); -} - -/////////////////////////////////////////////////////////////////////////////// -// Commands - -async function restartClients(): Promise { - outputChannel.appendLine(`Restarting Glint language server...`); - await Promise.all([...clients.values()].map((client) => client.restart())); -} - -/////////////////////////////////////////////////////////////////////////////// -// Workspace folder management - -async function reloadAllWorkspaces( - context: ExtensionContext, - fileWatcher: FileSystemWatcher, -): Promise { - let folders = workspace.workspaceFolders ?? []; - - await Promise.all( - folders.map(async (folder) => { - await removeWorkspaceFolder(folder); - await addWorkspaceFolder(context, folder, fileWatcher); - }), - ); -} + const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features'); + + if (tsExtension) { + // We need to activate the default VSCode TypeScript extension so that our + // TS Plugin kicks in. We do this because the TS extension is (obviously) not + // configured to activate for, say, .gts files: + // https://github.com/microsoft/vscode/blob/878af07/extensions/typescript-language-features/package.json#L62..L75 + + await tsExtension.activate(); + } else { + // TODO: we may decide to commit fully to TS Plugin mode, in which case it might be nice + // to have the message displayed below to guide the user. + // NOTE: Vue language tooling will continue to display this message even when willfully + // setting hybrid mode to false (i.e. using old LS approach). If we want to continue to support + // LS mode then we should leave this message commented out. + // vscode.window + // .showWarningMessage( + // 'Takeover mode is no longer needed since v2. Please enable the "TypeScript and JavaScript Language Features" extension.', + // 'Show Extension', + // ) + // .then((selected) => { + // if (selected) { + // executeCommand('workbench.extensions.search', '@builtin typescript-language-features'); + // } + // }); + } -async function addWorkspaceFolder( - context: ExtensionContext, - workspaceFolder: WorkspaceFolder, - watcher: FileSystemWatcher, - volarLabs?: ReturnType, -): Promise { - let folderPath = workspaceFolder.uri.fsPath; - if (clients.has(folderPath)) return; - - let serverPath = findLanguageServer(folderPath); - if (!serverPath) return; - - let serverOptions: ServerOptions = { module: serverPath }; - - const typescriptFormatOptions = getOptions(workspace.getConfiguration('typescript'), 'format'); - const typescriptUserPreferences = getOptions( - workspace.getConfiguration('typescript'), - 'preferences', - ); - const javascriptFormatOptions = getOptions(workspace.getConfiguration('javascript'), 'format'); - const javascriptUserPreferences = getOptions( - workspace.getConfiguration('javascript'), - 'preferences', - ); - - let client = new LanguageClient('glint', 'Glint', serverOptions, { - workspaceFolder, - outputChannel, - initializationOptions: { - javascript: { - format: javascriptFormatOptions, - preferences: javascriptUserPreferences, - }, - typescript: { - format: typescriptFormatOptions, - preferences: typescriptUserPreferences, - tsdk: (await getTsdk(context))!.tsdk, - }, - }, - documentSelector: [{ scheme: 'file', pattern: `${folderPath}/${filePattern}` }], - synchronize: { fileEvents: watcher }, + const context = extensionContext.value!; + + const workspaceFolders = useWorkspaceFolders(); + + watchEffect(() => { + workspaceFolders.value?.forEach((workspaceFolder) => { + activateLanguageClient( + context, + (id, name, documentSelector, initOptions, port, outputChannel) => { + class _LanguageClient extends lsp.LanguageClient { + override fillInitializeParams(params: lsp.InitializeParams) { + // fix https://github.com/vuejs/language-tools/issues/1959 + params.locale = vscode.env.language; + } + } + + // vscode.workspace.workspaceFolders + + // Here we load the server module; + // - Vue includes the server in the extension bundle + // - Glint does not; expects it to come from workspace folder + // let serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); + + let folderPath = workspaceFolder.uri.fsPath; + if (clients.has(folderPath)) return; + + let serverPath = findLanguageServer(folderPath); + if (!serverPath) return; + + 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: serverPath, + transport: lsp.TransportKind.ipc, + options: runOptions, + }, + debug: { + module: serverPath, + 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); + }, + ); + + onDeactivate(() => { + deactivateLanguageClient(); + }); + }); }); - if (volarLabs) { - volarLabs.addLanguageClient(client); - } - - clients.set(folderPath, client); + // workspaceFolders.value.forEach((workspaceFolder) => - await client.start(); + return volarLabs.extensionExports; +}); + +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; + + // NOTE: these are legacy config for Language Server and hence the VSCode options + // for Vanilla TS; TS Plugin won't use these options but will rather the same + // Vanilla TS options. + // + // if (!config.codeActions.enabled) { + // capabilities.codeActionProvider = undefined; + // } + // if (!config.codeLens.enabled) { + // capabilities.codeLensProvider = undefined; + // } + // if ( + // !config.updateImportsOnFileMove.enabled && + // capabilities.workspace?.fileOperations?.willRename + // ) { + // capabilities.workspace.fileOperations.willRename = undefined; + // } + + return initializeFeatures.call(client, ...args); + }; } -async function removeWorkspaceFolder(workspaceFolder: WorkspaceFolder): Promise { - let folderPath = workspaceFolder.uri.fsPath; - let client = clients.get(folderPath); - if (client) { - clients.delete(folderPath); - await client.stop(); - } -} +// async function addWorkspaceFolder( +// context: ExtensionContext, +// workspaceFolder: WorkspaceFolder, +// watcher: FileSystemWatcher, +// volarLabs?: ReturnType, +// ): Promise { +// let folderPath = workspaceFolder.uri.fsPath; +// if (clients.has(folderPath)) return; + +// let serverPath = findLanguageServer(folderPath); +// if (!serverPath) return; + +// let serverOptions: ServerOptions = { module: serverPath }; + +// const typescriptFormatOptions = getOptions(workspace.getConfiguration('typescript'), 'format'); +// const typescriptUserPreferences = getOptions( +// workspace.getConfiguration('typescript'), +// 'preferences', +// ); +// const javascriptFormatOptions = getOptions(workspace.getConfiguration('javascript'), 'format'); +// const javascriptUserPreferences = getOptions( +// workspace.getConfiguration('javascript'), +// 'preferences', +// ); + +// let client = new LanguageClient('glint', 'Glint', serverOptions, { +// workspaceFolder, +// outputChannel, +// initializationOptions: { +// javascript: { +// format: javascriptFormatOptions, +// preferences: javascriptUserPreferences, +// }, +// typescript: { +// format: typescriptFormatOptions, +// preferences: typescriptUserPreferences, +// tsdk: (await getTsdk(context))!.tsdk, +// }, +// }, +// documentSelector: [{ scheme: 'file', pattern: `${folderPath}/${filePattern}` }], +// synchronize: { fileEvents: watcher }, +// }); + +// if (volarLabs) { +// volarLabs.addLanguageClient(client); +// } + +// clients.set(folderPath, client); + +// await client.start(); +// } + +// async function removeWorkspaceFolder(workspaceFolder: WorkspaceFolder): Promise { +// let folderPath = workspaceFolder.uri.fsPath; +// let client = clients.get(folderPath); +// if (client) { +// clients.delete(folderPath); +// await client.stop(); +// } +// } /////////////////////////////////////////////////////////////////////////////// // Utilities diff --git a/packages/vscode/src/hybrid-mode.ts b/packages/vscode/src/hybrid-mode.ts new file mode 100644 index 000000000..21eb689f4 --- /dev/null +++ b/packages/vscode/src/hybrid-mode.ts @@ -0,0 +1,200 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { computed, executeCommand, useAllExtensions, useVscodeContext, watchEffect } from 'reactive-vscode'; +import * as semver from 'semver'; +import * as vscode from 'vscode'; +import { incompatibleExtensions, unknownExtensions } from './compatibility'; +import { config } from './config'; + +const extensions = useAllExtensions(); + +export const enabledHybridMode = computed(() => { + if (config.server.hybridMode === 'typeScriptPluginOnly') { + return false; + } + else if (config.server.hybridMode === 'auto') { + if ( + incompatibleExtensions.value.length || + unknownExtensions.value.length + ) { + return false; + } + else if ( + (vscodeTsdkVersion.value && !semver.gte(vscodeTsdkVersion.value, '5.3.0')) || + (workspaceTsdkVersion.value && !semver.gte(workspaceTsdkVersion.value, '5.3.0')) + ) { + return false; + } + return true; + } + return config.server.hybridMode; +}); + +export const enabledTypeScriptPlugin = computed(() => { + return ( + enabledHybridMode.value || + config.server.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(() => { + if (config.server.hybridMode === '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' + ) + ); + } + }); + } + 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' + ); + } + }); + } + } + else if (config.server.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' + ) + ); + } + }); + } + }); +} + +export function useHybridModeStatusItem() { + const item = vscode.languages.createLanguageStatusItem( + 'vue-hybrid-mode', + config.server.includeLanguages + ); + + item.text = 'Hybrid Mode'; + item.detail = + (enabledHybridMode.value ? 'Enabled' : 'Disabled') + + (config.server.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/packages/vscode/src/language-client.ts b/packages/vscode/src/language-client.ts new file mode 100644 index 000000000..4439b59df --- /dev/null +++ b/packages/vscode/src/language-client.ts @@ -0,0 +1,160 @@ +import * as lsp from '@volar/vscode'; +import { + executeCommand, + nextTick, + useActiveTextEditor, + useCommand, + useOutputChannel, + useVisibleTextEditors, + 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 './hybrid-mode'; +// import { useInsidersStatusItem } from './insiders'; + +let client: lsp.BaseLanguageClient; + +type GlintInitializationOptions = any; + +type CreateLanguageClient = ( + id: string, + name: string, + langs: lsp.DocumentSelector, + initOptions: GlintInitializationOptions, + port: number, + outputChannel: vscode.OutputChannel, +) => lsp.BaseLanguageClient; + +// What is this activating? +export function activate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { + const activeTextEditor = useActiveTextEditor(); + const visibleTextEditors = useVisibleTextEditors(); + + useHybridModeTips(); + + const { stop } = watch( + activeTextEditor, + () => { + if ( + visibleTextEditors.value.some((editor) => + config.server.includeLanguages.includes(editor.document.languageId), + ) + ) { + activateLc(context, createLc); + nextTick(() => { + stop(); + }); + } + }, + { + immediate: true, // causes above callback to be run immediately (i.e. not lazily) + }, + ); +} + +export function deactivate() { + return client?.stop(); +} + +async function activateLc(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { + useVscodeContext('vue.activated', true); + const outputChannel = useOutputChannel('Glint Language Server'); + const selectors = config.server.includeLanguages; + + client = createLc( + 'glint', + 'Glint', + selectors, + await getInitializationOptions(context, enabledHybridMode.value), + 6009, + outputChannel, + ); + + watch([enabledHybridMode, enabledTypeScriptPlugin], (newValues, oldValues) => { + if (newValues[0] !== oldValues[0]) { + requestReloadVscode( + `Please reload VSCode to ${newValues[0] ? 'enable' : 'disable'} Hybrid Mode.`, + ); + } else if (newValues[1] !== oldValues[1]) { + requestReloadVscode( + `Please reload VSCode to ${newValues[1] ? 'enable' : 'disable'} Vue TypeScript Plugin.`, + ); + } + }); + + watch( + () => config.server.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); + } + }, + { deep: true }, + ); + + 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(); + }); + + // activateDoctor(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); + } + + useHybridModeStatusItem(); + // useInsidersStatusItem(context); + + async function requestReloadVscode(msg: string) { + const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); + if (reload) { + executeCommand('workbench.action.reloadWindow'); + } + } +} + +async function getInitializationOptions( + context: vscode.ExtensionContext, + hybridMode: boolean, +): Promise { + return { + typescript: { tsdk: (await lsp.getTsdk(context))!.tsdk }, + vue: { hybridMode }, + }; +} diff --git a/packages/vscode/tsconfig.json b/packages/vscode/tsconfig.json index c258c5e90..815eb735e 100644 --- a/packages/vscode/tsconfig.json +++ b/packages/vscode/tsconfig.json @@ -1,8 +1,9 @@ { "extends": "../../tsconfig.compileroptions.json", "compilerOptions": { - "module": "Node16", - "outDir": "lib" + "outDir": "lib", + "module": "CommonJS", + "moduleResolution": "Node" }, "include": ["src", "__tests__"], "references": [{ "path": "../core" }] diff --git a/yarn.lock b/yarn.lock index 5470821b6..7ff3f66e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2684,6 +2684,11 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" integrity sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ== +"@reactive-vscode/reactivity@0.2.7-beta.1": + version "0.2.7-beta.1" + resolved "https://registry.yarnpkg.com/@reactive-vscode/reactivity/-/reactivity-0.2.7-beta.1.tgz#a387bf07a420a1b592e4e87353a3159cb6d7994f" + integrity sha512-ma7DOAFSXhB7h2HLiDrus4as5So1rS3u4zNHKoKCRRh4cBxxnQDFZUUQNafsssM15ggxtf8km5IXyW81ZCWnsg== + "@release-it-plugins/lerna-changelog@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@release-it-plugins/lerna-changelog/-/lerna-changelog-5.0.0.tgz#cbbbc47fd40ef212f2d7c0e24867d3fcea4cd232" @@ -12242,6 +12247,13 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +reactive-vscode@0.2.7-beta.1: + version "0.2.7-beta.1" + resolved "https://registry.yarnpkg.com/reactive-vscode/-/reactive-vscode-0.2.7-beta.1.tgz#6d92f14e9d28892391095c295864bb91db56603b" + integrity sha512-7D9sFMA4S6owUNdiuiuiJLOzAuy3y4ZgcNW3bj58n1hME0U8repz3hhYep6VCLbAf89F+TF/bB7u+WNGHndLGg== + dependencies: + "@reactive-vscode/reactivity" "0.2.7-beta.1" + read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -12874,6 +12886,11 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +scule@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/scule/-/scule-1.3.0.tgz#6efbd22fd0bb801bdcc585c89266a7d2daa8fbd3" + integrity sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g== + semver-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" @@ -14521,6 +14538,15 @@ volar-service-typescript@volar-2.4: vscode-nls "^5.2.0" vscode-uri "^3.0.8" +vscode-ext-gen@^0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/vscode-ext-gen/-/vscode-ext-gen-0.5.5.tgz#e4f5bb0b237c839c31170da41b6d117afa7dd53e" + integrity sha512-wTwcPvGF9xZ0fN7sPgdUPESH+Aw20Tk1vvgbYnKzWT4sFOqRP54qcpxjPUMdDoDGfiVIoXW87TNxn0yKXq3djw== + dependencies: + cac "^6.7.14" + scule "^1.3.0" + yargs "^17.7.2" + vscode-jsonrpc@8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" @@ -14955,7 +14981,7 @@ yargs@16.2.0, yargs@^16.0.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.1.0, yargs@^17.5.1: +yargs@^17.1.0, yargs@^17.5.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== From ab3e135c4f9655d4ad731e876c91e9ff09b11322 Mon Sep 17 00:00:00 2001 From: machty Date: Tue, 14 Jan 2025 23:07:14 -0500 Subject: [PATCH 02/10] progress --- packages/vscode/src/extension.ts | 178 +++++++++++++++---------- packages/vscode/src/language-client.ts | 56 ++++++-- 2 files changed, 148 insertions(+), 86 deletions(-) diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index ff23e006b..cf98c2429 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -12,13 +12,12 @@ import { executeCommand, extensionContext, onDeactivate, +import { workspace } from 'vscode'; useWorkspaceFolders, watchEffect, + watch, } from 'reactive-vscode'; -import { - activate as activateLanguageClient, - deactivate as deactivateLanguageClient, -} from './language-client'; +import { activateLanguageClientForWorkspace } from './language-client'; export const { activate, deactivate } = defineExtension(async () => { const volarLabs = createLabsInfo(languageServerProtocol); @@ -52,77 +51,102 @@ export const { activate, deactivate } = defineExtension(async () => { const context = extensionContext.value!; - const workspaceFolders = useWorkspaceFolders(); - - watchEffect(() => { - workspaceFolders.value?.forEach((workspaceFolder) => { - activateLanguageClient( - context, - (id, name, documentSelector, initOptions, port, outputChannel) => { - class _LanguageClient extends lsp.LanguageClient { - override fillInitializeParams(params: lsp.InitializeParams) { - // fix https://github.com/vuejs/language-tools/issues/1959 - params.locale = vscode.env.language; + const clients = new Map(); + + watch( + useWorkspaceFolders(), + (newWorkspaceFolders, oldWorkspaceFolders) => { + // TODO: diff the old and new, only activate new and only deactivate old + + const removedFolders = + oldWorkspaceFolders?.filter( + (oldFolder) => + !newWorkspaceFolders?.some( + (newFolder) => newFolder.uri.fsPath === oldFolder.uri.fsPath, + ), + ) ?? []; + const addedFolders = + newWorkspaceFolders?.filter( + (newFolder) => + !oldWorkspaceFolders?.some( + (oldFolder) => oldFolder.uri.fsPath === newFolder.uri.fsPath, + ), + ) ?? []; + + addedFolders.forEach((workspaceFolder) => { + const teardownClient = activateLanguageClientForWorkspace( + context, + (id, name, documentSelector, initOptions, port, outputChannel) => { + class _LanguageClient extends lsp.LanguageClient { + override fillInitializeParams(params: lsp.InitializeParams) { + // fix https://github.com/vuejs/language-tools/issues/1959 + params.locale = vscode.env.language; + } } - } - - // vscode.workspace.workspaceFolders - - // Here we load the server module; - // - Vue includes the server in the extension bundle - // - Glint does not; expects it to come from workspace folder - // let serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); - - let folderPath = workspaceFolder.uri.fsPath; - if (clients.has(folderPath)) return; - - let serverPath = findLanguageServer(folderPath); - if (!serverPath) return; - - 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: serverPath, - transport: lsp.TransportKind.ipc, - options: runOptions, - }, - debug: { - module: serverPath, - 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); - }, - ); - - onDeactivate(() => { - deactivateLanguageClient(); + + // vscode.workspace.workspaceFolders + + // Here we load the server module; + // - Vue includes the server in the extension bundle + // - Glint does not; expects it to come from workspace folder + // let serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); + + let folderPath = workspaceFolder.uri.fsPath; + if (clients.has(folderPath)) return null; + + let serverPath = findLanguageServer(folderPath); + if (!serverPath) return null; + + 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: serverPath, + transport: lsp.TransportKind.ipc, + options: runOptions, + }, + debug: { + module: serverPath, + 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(() => { + teardownClient(); + }); }); - }); - }); + }, + { + immediate: true, // causes above callback to be run immediately (i.e. not lazily) + }, + ); // workspaceFolders.value.forEach((workspaceFolder) => @@ -221,6 +245,14 @@ function updateProviders(client: lsp.LanguageClient) { // Utilities function findLanguageServer(workspaceDir: string): string | null { + // TODO: reinstate reloading on configuration change: + // workspace.onDidChangeConfiguration((changeEvent) => { + // if (changeEvent.affectsConfiguration('glint.libraryPath')) { + // reloadAllWorkspaces(context, fileWatcher); + // } + // }); + + let userLibraryPath = workspace.getConfiguration().get('glint.libraryPath', '.'); let resolutionDir = path.resolve(workspaceDir, userLibraryPath); let require = createRequire(path.join(resolutionDir, 'package.json')); diff --git a/packages/vscode/src/language-client.ts b/packages/vscode/src/language-client.ts index 4439b59df..3a2f15aec 100644 --- a/packages/vscode/src/language-client.ts +++ b/packages/vscode/src/language-client.ts @@ -20,6 +20,7 @@ import { useHybridModeStatusItem, useHybridModeTips, } from './hybrid-mode'; +import { NullLiteral } from 'typescript'; // import { useInsidersStatusItem } from './insiders'; let client: lsp.BaseLanguageClient; @@ -33,13 +34,21 @@ type CreateLanguageClient = ( initOptions: GlintInitializationOptions, port: number, outputChannel: vscode.OutputChannel, -) => lsp.BaseLanguageClient; +) => lsp.BaseLanguageClient | null; // What is this activating? -export function activate(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { +// +// this will get passed a workspace. Then we watch that workspace for LC activation. +export function activateLanguageClientForWorkspace( + context: vscode.ExtensionContext, + createLanguageClient: CreateLanguageClient, +) { + // for each const activeTextEditor = useActiveTextEditor(); const visibleTextEditors = useVisibleTextEditors(); + let clientPromise: Promise | null = null; + useHybridModeTips(); const { stop } = watch( @@ -50,28 +59,43 @@ export function activate(context: vscode.ExtensionContext, createLc: CreateLangu config.server.includeLanguages.includes(editor.document.languageId), ) ) { - activateLc(context, createLc); - nextTick(() => { - stop(); - }); + if (!clientPromise) { + clientPromise = activateLanguageClient(context, createLanguageClient); + + // disable this watcher so that we don't keep activation language client + nextTick(() => { + stop(); + }); + } } }, { immediate: true, // causes above callback to be run immediately (i.e. not lazily) }, ); -} -export function deactivate() { - return client?.stop(); + return () => { + // Stop the watcher. + stop(); + + // Tear down the client if it exists. + if (clientPromise) { + clientPromise.then((client) => client?.stop()); + clientPromise = null; + } + }; } -async function activateLc(context: vscode.ExtensionContext, createLc: CreateLanguageClient) { +async function activateLanguageClient( + context: vscode.ExtensionContext, + createLanguageClient: CreateLanguageClient, +): Promise { useVscodeContext('vue.activated', true); const outputChannel = useOutputChannel('Glint Language Server'); const selectors = config.server.includeLanguages; - client = createLc( + // This might return null if there is no... + const client = createLanguageClient( 'glint', 'Glint', selectors, @@ -80,6 +104,10 @@ async function activateLc(context: vscode.ExtensionContext, createLc: CreateLang outputChannel, ); + if (!client) { + return null; + } + watch([enabledHybridMode, enabledTypeScriptPlugin], (newValues, oldValues) => { if (newValues[0] !== oldValues[0]) { requestReloadVscode( @@ -105,13 +133,13 @@ async function activateLc(context: vscode.ExtensionContext, createLc: CreateLang config.server, () => { if (!enabledHybridMode.value) { - executeCommand('vue.action.restartServer', false); + executeCommand('glint.restart-language-server', false); } }, { deep: true }, ); - useCommand('vue.action.restartServer', async (restartTsServer: boolean = true) => { + useCommand('glint.restart-language-server', async (restartTsServer: boolean = true) => { if (restartTsServer) { await executeCommand('typescript.restartTsServer'); } @@ -147,6 +175,8 @@ async function activateLc(context: vscode.ExtensionContext, createLc: CreateLang executeCommand('workbench.action.reloadWindow'); } } + + return client; } async function getInitializationOptions( From 739818f9e9a58bb8444af2ec6d02eb382d645ccf Mon Sep 17 00:00:00 2001 From: machty Date: Wed, 15 Jan 2025 15:57:09 -0500 Subject: [PATCH 03/10] TS building --- packages/vscode/src/compatibility.ts | 57 ++++++++++++++++++++++++++ packages/vscode/src/extension.ts | 45 +++++++------------- packages/vscode/src/language-client.ts | 15 +++++-- 3 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 packages/vscode/src/compatibility.ts diff --git a/packages/vscode/src/compatibility.ts b/packages/vscode/src/compatibility.ts new file mode 100644 index 000000000..6f888bcbd --- /dev/null +++ b/packages/vscode/src/compatibility.ts @@ -0,0 +1,57 @@ +import { computed, useAllExtensions } from 'reactive-vscode'; +import * as semver from 'semver'; +import * as vscode from 'vscode'; +import { config } from './config'; + +// TODO: does Glint need this concept of "compatible" extensions? + +const defaultCompatibleExtensions = new Set([ + 'astro-build.astro-vscode', + 'bierner.lit-html', + 'Divlo.vscode-styled-jsx-languageserver', + 'GitHub.copilot-chat', + 'ije.esm-vscode', + 'jenkey2011.string-highlight', + 'johnsoncodehk.vscode-tsslint', + 'kimuson.ts-type-expand', + 'miaonster.vscode-tsx-arrow-definition', + 'ms-dynamics-smb.al', + 'mxsdev.typescript-explorer', + 'nrwl.angular-console', + 'p42ai.refactor', + 'runem.lit-plugin', + 'ShenQingchuan.vue-vine-extension', + 'styled-components.vscode-styled-components', + 'unifiedjs.vscode-mdx', + 'VisualStudioExptTeam.vscodeintellicode', + 'Vue.volar', +]); + +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 ( + defaultCompatibleExtensions.has(extension.id) || + config.server.compatibleExtensions.includes(extension.id) + ) { + 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'); + } +} diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index cf98c2429..4be8e8fdd 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -12,12 +12,12 @@ import { executeCommand, extensionContext, onDeactivate, -import { workspace } from 'vscode'; useWorkspaceFolders, watchEffect, watch, } from 'reactive-vscode'; -import { activateLanguageClientForWorkspace } from './language-client'; + +import { watchWorkspaceFolderForLanguageClientActivation } from './language-client'; export const { activate, deactivate } = defineExtension(async () => { const volarLabs = createLabsInfo(languageServerProtocol); @@ -74,7 +74,7 @@ export const { activate, deactivate } = defineExtension(async () => { ) ?? []; addedFolders.forEach((workspaceFolder) => { - const teardownClient = activateLanguageClientForWorkspace( + const teardownClient = watchWorkspaceFolderForLanguageClientActivation( context, (id, name, documentSelector, initOptions, port, outputChannel) => { class _LanguageClient extends lsp.LanguageClient { @@ -94,7 +94,7 @@ export const { activate, deactivate } = defineExtension(async () => { let folderPath = workspaceFolder.uri.fsPath; if (clients.has(folderPath)) return null; - let serverPath = findLanguageServer(folderPath); + let serverPath = findLanguageServer(folderPath, outputChannel); if (!serverPath) return null; const runOptions: lsp.ForkOptions = {}; @@ -244,16 +244,8 @@ function updateProviders(client: lsp.LanguageClient) { /////////////////////////////////////////////////////////////////////////////// // Utilities -function findLanguageServer(workspaceDir: string): string | null { - // TODO: reinstate reloading on configuration change: - // workspace.onDidChangeConfiguration((changeEvent) => { - // if (changeEvent.affectsConfiguration('glint.libraryPath')) { - // reloadAllWorkspaces(context, fileWatcher); - // } - // }); - - - let userLibraryPath = workspace.getConfiguration().get('glint.libraryPath', '.'); +function findLanguageServer(workspaceDir: string, outputChannel: vscode.OutputChannel): string | null { + let userLibraryPath = vscode.workspace.getConfiguration().get('glint.libraryPath', '.'); let resolutionDir = path.resolve(workspaceDir, userLibraryPath); let require = createRequire(path.join(resolutionDir, 'package.json')); try { @@ -273,23 +265,14 @@ function findLanguageServer(workspaceDir: string): string | null { } // Automatically restart running servers when config files in the workspace change -function createConfigWatcher(): Disposable { - let configWatcher = workspace.createFileSystemWatcher('**/{ts,js}config*.json'); - - configWatcher.onDidCreate(restartClients); - configWatcher.onDidChange(restartClients); - configWatcher.onDidDelete(restartClients); +// TODO: reinstate this or see whether Vue / reactive-vscode already does this - return configWatcher; -} +// function createConfigWatcher(): Disposable { +// let configWatcher = workspace.createFileSystemWatcher('**/{ts,js}config*.json'); -// Loads the TypeScript and JavaScript formating options from the workspace and subsets them to -// pass to the language server. -function getOptions(config: WorkspaceConfiguration, key: string): object { - const formatOptions = config.get(key); - if (formatOptions) { - return formatOptions; - } +// configWatcher.onDidCreate(restartClients); +// configWatcher.onDidChange(restartClients); +// configWatcher.onDidDelete(restartClients); - return {}; -} +// return configWatcher; +// } diff --git a/packages/vscode/src/language-client.ts b/packages/vscode/src/language-client.ts index 3a2f15aec..96940655e 100644 --- a/packages/vscode/src/language-client.ts +++ b/packages/vscode/src/language-client.ts @@ -36,10 +36,14 @@ type CreateLanguageClient = ( outputChannel: vscode.OutputChannel, ) => lsp.BaseLanguageClient | null; -// What is this activating? -// -// this will get passed a workspace. Then we watch that workspace for LC activation. -export function activateLanguageClientForWorkspace( +/** + * A workspace consists of 1+ open folders. This function will watch one of + * those folders to see if file has been opened with a known language ID + * (e.g. 'glimmer-ts', 'handlebars', 'vue', etc.). When that happens we + * invoke the `createLanguageClient` function to create a language server + * client. + */ +export function watchWorkspaceFolderForLanguageClientActivation( context: vscode.ExtensionContext, createLanguageClient: CreateLanguageClient, ) { @@ -129,6 +133,9 @@ async function activateLanguageClient( }, ); + // NOTE: this will fire when `glint.libraryPath` is changed, among others + // (leaving this note here so I don't re-implement the `affectsConfiguration` logic we used + // to have when changing this config value) watch( config.server, () => { From a82971c29fcffe6d95ad0ef08c12c6c0b03c08b3 Mon Sep 17 00:00:00 2001 From: machty Date: Wed, 15 Jan 2025 17:03:44 -0500 Subject: [PATCH 04/10] maybe something workingish --- packages/vscode/src/extension.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 4be8e8fdd..6370fc265 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -53,11 +53,19 @@ export const { activate, deactivate } = defineExtension(async () => { const clients = new Map(); + const reactiveWorkspaceFolders = useWorkspaceFolders(); + + let oldWorkspaceFolders: readonly vscode.WorkspaceFolder[] | undefined; + + // NOTE: I tried to use the `watch` callback API to provide the old and new values + // for workspace folders but for some reason they kept coming in as undefined + // so I'm tracking new and old values menually. watch( - useWorkspaceFolders(), - (newWorkspaceFolders, oldWorkspaceFolders) => { - // TODO: diff the old and new, only activate new and only deactivate old + reactiveWorkspaceFolders, + () => { + const newWorkspaceFolders = reactiveWorkspaceFolders.value; + // TODO: handle removed folders. const removedFolders = oldWorkspaceFolders?.filter( (oldFolder) => @@ -73,6 +81,8 @@ export const { activate, deactivate } = defineExtension(async () => { ), ) ?? []; + oldWorkspaceFolders = newWorkspaceFolders; + addedFolders.forEach((workspaceFolder) => { const teardownClient = watchWorkspaceFolderForLanguageClientActivation( context, @@ -244,7 +254,10 @@ function updateProviders(client: lsp.LanguageClient) { /////////////////////////////////////////////////////////////////////////////// // Utilities -function findLanguageServer(workspaceDir: string, outputChannel: vscode.OutputChannel): string | null { +function findLanguageServer( + workspaceDir: string, + outputChannel: vscode.OutputChannel, +): string | null { let userLibraryPath = vscode.workspace.getConfiguration().get('glint.libraryPath', '.'); let resolutionDir = path.resolve(workspaceDir, userLibraryPath); let require = createRequire(path.join(resolutionDir, 'package.json')); From ae48c99c9ceec367d9430f3623f951887293cb3d Mon Sep 17 00:00:00 2001 From: machty Date: Wed, 15 Jan 2025 19:50:00 -0500 Subject: [PATCH 05/10] progress --- .../__tests__/support/launch-from-cli.mts | 12 +- packages/vscode/package.json | 2 +- packages/vscode/src/language-client.ts | 130 ++++++++++-------- 3 files changed, 78 insertions(+), 66 deletions(-) diff --git a/packages/vscode/__tests__/support/launch-from-cli.mts b/packages/vscode/__tests__/support/launch-from-cli.mts index c4069e6b8..eaa255439 100644 --- a/packages/vscode/__tests__/support/launch-from-cli.mts +++ b/packages/vscode/__tests__/support/launch-from-cli.mts @@ -1,11 +1,9 @@ import * as path from 'node:path'; import * as os from 'node:os'; -import { fileURLToPath } from 'node:url'; import { runTests } from '@vscode/test-electron'; import * as fs from 'node:fs'; -const dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageRoot = path.resolve(dirname, '../../..'); +const packageRoot = path.resolve(process.cwd()); const emptyExtensionsDir = path.join(os.tmpdir(), `extensions-${Math.random()}`); const emptyUserDataDir = path.join(os.tmpdir(), `user-data-${Math.random()}`); @@ -28,13 +26,13 @@ let disableExtensionArgs: string[] = []; let testRunner: string; switch (testType) { case 'language-server': - testRunner = 'vscode-runner-language-server.js'; + testRunner = 'lib/__tests__/support/vscode-runner-language-server.js'; // Disable vanilla TS for full "takeover" mode. disableExtensionArgs = ['--disable-extension', 'vscode.typescript-language-features']; break; case 'ts-plugin': - testRunner = 'vscode-runner-ts-plugin.js'; + testRunner = 'lib/__tests__/support/vscode-runner-ts-plugin.js'; // Note: here, we WANT vanilla TS to be enabled since we're testing the TS Plugin. break; @@ -44,9 +42,9 @@ switch (testType) { } try { - await runTests({ + runTests({ extensionDevelopmentPath: packageRoot, - extensionTestsPath: path.resolve(dirname, testRunner), + extensionTestsPath: path.resolve(process.cwd(), testRunner), launchArgs: [ // Don't show the "hey do you trust this folder?" prompt '--disable-workspace-trust', diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 4f801bc61..0f9fc07b9 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -37,7 +37,7 @@ "extension:package": "vsce package --no-dependencies", "extension:publish": "vsce publish --no-dependencies", "postinstall": "vscode-ext-gen --scope glint", - "prebuild": "yarn postinstall && yarn build" + "prebuild": "vscode-ext-gen --scope glint" }, "engines": { "vscode": "^1.68.1" diff --git a/packages/vscode/src/language-client.ts b/packages/vscode/src/language-client.ts index 96940655e..40478254c 100644 --- a/packages/vscode/src/language-client.ts +++ b/packages/vscode/src/language-client.ts @@ -39,7 +39,7 @@ type CreateLanguageClient = ( /** * A workspace consists of 1+ open folders. This function will watch one of * those folders to see if file has been opened with a known language ID - * (e.g. 'glimmer-ts', 'handlebars', 'vue', etc.). When that happens we + * (e.g. 'glimmer-ts', 'handlebars', etc.). When that happens we * invoke the `createLanguageClient` function to create a language server * client. */ @@ -90,11 +90,16 @@ export function watchWorkspaceFolderForLanguageClientActivation( }; } +let hasInitialized = false; + async function activateLanguageClient( context: vscode.ExtensionContext, createLanguageClient: CreateLanguageClient, ): Promise { - useVscodeContext('vue.activated', true); + // This is not used now but can be used to conditionally reveal commands that should + // only be visible when glint has been activated. + useVscodeContext('glint.activated', true); + const outputChannel = useOutputChannel('Glint Language Server'); const selectors = config.server.includeLanguages; @@ -112,69 +117,78 @@ async function activateLanguageClient( return null; } - watch([enabledHybridMode, enabledTypeScriptPlugin], (newValues, oldValues) => { - if (newValues[0] !== oldValues[0]) { - requestReloadVscode( - `Please reload VSCode to ${newValues[0] ? 'enable' : 'disable'} Hybrid Mode.`, - ); - } else if (newValues[1] !== oldValues[1]) { - requestReloadVscode( - `Please reload VSCode to ${newValues[1] ? 'enable' : 'disable'} Vue TypeScript Plugin.`, - ); - } - }); - - watch( - () => config.server.includeLanguages, - () => { - if (enabledHybridMode.value) { - requestReloadVscode('Please reload VSCode to apply the new language settings.'); - } - }, - ); - - // NOTE: this will fire when `glint.libraryPath` is changed, among others - // (leaving this note here so I don't re-implement the `affectsConfiguration` logic we used - // to have when changing this config value) - watch( - config.server, - () => { - if (!enabledHybridMode.value) { - executeCommand('glint.restart-language-server', false); + if (!hasInitialized) { + watch([enabledHybridMode, enabledTypeScriptPlugin], (newValues, oldValues) => { + if (newValues[0] !== oldValues[0]) { + requestReloadVscode( + `Please reload VSCode to ${newValues[0] ? 'enable' : 'disable'} Hybrid Mode.`, + ); + } else if (newValues[1] !== oldValues[1]) { + requestReloadVscode( + `Please reload VSCode to ${newValues[1] ? 'enable' : 'disable'} Glint TypeScript Plugin.`, + ); } - }, - { deep: true }, - ); + }); - useCommand('glint.restart-language-server', async (restartTsServer: boolean = true) => { - if (restartTsServer) { - await executeCommand('typescript.restartTsServer'); - } - await client.stop(); - outputChannel.clear(); - client.clientOptions.initializationOptions = await getInitializationOptions( - context, - enabledHybridMode.value, + watch( + () => config.server.includeLanguages, + () => { + if (enabledHybridMode.value) { + requestReloadVscode('Please reload VSCode to apply the new language settings.'); + } + }, ); - await client.start(); - }); - // activateDoctor(client); - // activateNameCasing(client, selectors); - // activateSplitEditors(client); + // NOTE: this will fire when `glint.libraryPath` is changed, among others + // (leaving this note here so I don't re-implement the `affectsConfiguration` logic we used + // to have when changing this config value) + watch( + () => config.server, + () => { + if (!enabledHybridMode.value) { + executeCommand('glint.restart-language-server', false); + } + }, + { deep: true }, + ); - lsp.activateAutoInsertion(selectors, client); - lsp.activateDocumentDropEdit(selectors, client); - lsp.activateWriteVirtualFiles('vue.action.writeVirtualFiles', client); + useCommand('glint.restart-language-server', 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(); + }); + + // activateDoctor(client); + // activateNameCasing(client, selectors); + // activateSplitEditors(client); + + lsp.activateAutoInsertion(selectors, client); + lsp.activateDocumentDropEdit(selectors, client); + lsp.activateWriteVirtualFiles('glint.action.writeVirtualFiles', client); + + if (!enabledHybridMode.value) { + lsp.activateTsConfigStatusItem(selectors, 'glint.tsconfig', client); + lsp.activateTsVersionStatusItem( + selectors, + 'glint.tsversion', + context, + (text) => 'TS ' + text, + ); + lsp.activateFindFileReferences('glint.findAllFileReferences', client); + } - if (!enabledHybridMode.value) { - lsp.activateTsConfigStatusItem(selectors, 'vue.tsconfig', client); - lsp.activateTsVersionStatusItem(selectors, 'vue.tsversion', context, (text) => 'TS ' + text); - lsp.activateFindFileReferences('vue.findAllFileReferences', client); + useHybridModeStatusItem(); + // useInsidersStatusItem(context); } - useHybridModeStatusItem(); - // useInsidersStatusItem(context); + hasInitialized = true; async function requestReloadVscode(msg: string) { const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); @@ -192,6 +206,6 @@ async function getInitializationOptions( ): Promise { return { typescript: { tsdk: (await lsp.getTsdk(context))!.tsdk }, - vue: { hybridMode }, + glint: { hybridMode }, }; } From 3aa970cd52b50f0d3ce6e47e09f6a825c240c572 Mon Sep 17 00:00:00 2001 From: machty Date: Thu, 16 Jan 2025 17:14:00 -0500 Subject: [PATCH 06/10] constraint lsp client to workspace folder --- packages/vscode/src/extension.ts | 2 ++ packages/vscode/src/language-client.ts | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 6370fc265..1dac355fc 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -86,6 +86,7 @@ export const { activate, deactivate } = defineExtension(async () => { addedFolders.forEach((workspaceFolder) => { const teardownClient = watchWorkspaceFolderForLanguageClientActivation( context, + workspaceFolder, (id, name, documentSelector, initOptions, port, outputChannel) => { class _LanguageClient extends lsp.LanguageClient { override fillInitializeParams(params: lsp.InitializeParams) { @@ -127,6 +128,7 @@ export const { activate, deactivate } = defineExtension(async () => { options: debugOptions, }, }; + const clientOptions: lsp.LanguageClientOptions = { // middleware, documentSelector: documentSelector, diff --git a/packages/vscode/src/language-client.ts b/packages/vscode/src/language-client.ts index 40478254c..98714e2e7 100644 --- a/packages/vscode/src/language-client.ts +++ b/packages/vscode/src/language-client.ts @@ -45,6 +45,7 @@ type CreateLanguageClient = ( */ export function watchWorkspaceFolderForLanguageClientActivation( context: vscode.ExtensionContext, + workspaceFolder: vscode.WorkspaceFolder, createLanguageClient: CreateLanguageClient, ) { // for each @@ -64,7 +65,7 @@ export function watchWorkspaceFolderForLanguageClientActivation( ) ) { if (!clientPromise) { - clientPromise = activateLanguageClient(context, createLanguageClient); + clientPromise = activateLanguageClient(context, createLanguageClient, workspaceFolder); // disable this watcher so that we don't keep activation language client nextTick(() => { @@ -95,19 +96,24 @@ let hasInitialized = false; async function activateLanguageClient( context: vscode.ExtensionContext, createLanguageClient: CreateLanguageClient, + workspaceFolder: vscode.WorkspaceFolder, ): Promise { // This is not used now but can be used to conditionally reveal commands that should // only be visible when glint has been activated. useVscodeContext('glint.activated', true); const outputChannel = useOutputChannel('Glint Language Server'); - const selectors = config.server.includeLanguages; + const documentSelectors = config.server.includeLanguages.map((language) => ({ + language, + scheme: 'file', + pattern: `${workspaceFolder.uri.fsPath}/**/*`, + })); // This might return null if there is no... const client = createLanguageClient( 'glint', 'Glint', - selectors, + documentSelectors, await getInitializationOptions(context, enabledHybridMode.value), 6009, outputChannel, @@ -169,14 +175,14 @@ async function activateLanguageClient( // activateNameCasing(client, selectors); // activateSplitEditors(client); - lsp.activateAutoInsertion(selectors, client); - lsp.activateDocumentDropEdit(selectors, client); + lsp.activateAutoInsertion(documentSelectors, client); + lsp.activateDocumentDropEdit(documentSelectors, client); lsp.activateWriteVirtualFiles('glint.action.writeVirtualFiles', client); if (!enabledHybridMode.value) { - lsp.activateTsConfigStatusItem(selectors, 'glint.tsconfig', client); + lsp.activateTsConfigStatusItem(documentSelectors, 'glint.tsconfig', client); lsp.activateTsVersionStatusItem( - selectors, + documentSelectors, 'glint.tsversion', context, (text) => 'TS ' + text, From ba613f93b30260d1f0577c0009f9cbe461740d0b Mon Sep 17 00:00:00 2001 From: machty Date: Thu, 16 Jan 2025 17:26:16 -0500 Subject: [PATCH 07/10] lint and cleanup --- packages/vscode/src/compatibility.ts | 82 ++++--- packages/vscode/src/extension.ts | 96 +------- packages/vscode/src/hybrid-mode.ts | 309 ++++++++++++------------- packages/vscode/src/language-client.ts | 6 +- 4 files changed, 194 insertions(+), 299 deletions(-) diff --git a/packages/vscode/src/compatibility.ts b/packages/vscode/src/compatibility.ts index 6f888bcbd..40fa0a9d3 100644 --- a/packages/vscode/src/compatibility.ts +++ b/packages/vscode/src/compatibility.ts @@ -6,52 +6,58 @@ import { config } from './config'; // TODO: does Glint need this concept of "compatible" extensions? const defaultCompatibleExtensions = new Set([ - 'astro-build.astro-vscode', - 'bierner.lit-html', - 'Divlo.vscode-styled-jsx-languageserver', - 'GitHub.copilot-chat', - 'ije.esm-vscode', - 'jenkey2011.string-highlight', - 'johnsoncodehk.vscode-tsslint', - 'kimuson.ts-type-expand', - 'miaonster.vscode-tsx-arrow-definition', - 'ms-dynamics-smb.al', - 'mxsdev.typescript-explorer', - 'nrwl.angular-console', - 'p42ai.refactor', - 'runem.lit-plugin', - 'ShenQingchuan.vue-vine-extension', - 'styled-components.vscode-styled-components', - 'unifiedjs.vscode-mdx', - 'VisualStudioExptTeam.vscodeintellicode', - 'Vue.volar', + 'astro-build.astro-vscode', + 'bierner.lit-html', + 'Divlo.vscode-styled-jsx-languageserver', + 'GitHub.copilot-chat', + 'ije.esm-vscode', + 'jenkey2011.string-highlight', + 'johnsoncodehk.vscode-tsslint', + 'kimuson.ts-type-expand', + 'miaonster.vscode-tsx-arrow-definition', + 'ms-dynamics-smb.al', + 'mxsdev.typescript-explorer', + 'nrwl.angular-console', + 'p42ai.refactor', + 'runem.lit-plugin', + 'ShenQingchuan.vue-vine-extension', + 'styled-components.vscode-styled-components', + 'unifiedjs.vscode-mdx', + 'VisualStudioExptTeam.vscodeintellicode', + 'Vue.volar', ]); const extensions = useAllExtensions(); export const incompatibleExtensions = computed(() => { - return extensions.value - .filter(ext => isExtensionCompatibleWithHybridMode(ext) === false) - .map(ext => ext.id); + 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); + return extensions.value + .filter( + (ext) => + isExtensionCompatibleWithHybridMode(ext) === undefined && + !!ext.packageJSON?.contributes?.typescriptServerPlugins, + ) + .map((ext) => ext.id); }); -function isExtensionCompatibleWithHybridMode(extension: vscode.Extension) { - if ( - defaultCompatibleExtensions.has(extension.id) || - config.server.compatibleExtensions.includes(extension.id) - ) { - 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'); - } +function isExtensionCompatibleWithHybridMode( + extension: vscode.Extension, +): boolean | undefined { + if ( + defaultCompatibleExtensions.has(extension.id) || + config.server.compatibleExtensions.includes(extension.id) + ) { + 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'); + } } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 1dac355fc..69d750f1d 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -3,17 +3,13 @@ import * as path from 'node:path'; import * as lsp from '@volar/vscode/node'; import * as vscode from 'vscode'; import * as languageServerProtocol from '@volar/language-server/protocol.js'; -import { LabsInfo, createLabsInfo, getTsdk } from '@volar/vscode'; -import { config } from './config'; +import { createLabsInfo } from '@volar/vscode'; -import { Disposable, LanguageClient, ServerOptions } from '@volar/vscode/node.js'; import { defineExtension, - executeCommand, extensionContext, onDeactivate, useWorkspaceFolders, - watchEffect, watch, } from 'reactive-vscode'; @@ -89,19 +85,12 @@ export const { activate, deactivate } = defineExtension(async () => { workspaceFolder, (id, name, documentSelector, initOptions, port, outputChannel) => { class _LanguageClient extends lsp.LanguageClient { - override fillInitializeParams(params: lsp.InitializeParams) { + override fillInitializeParams(params: lsp.InitializeParams): lsp.InitializeParams { // fix https://github.com/vuejs/language-tools/issues/1959 params.locale = vscode.env.language; } } - // vscode.workspace.workspaceFolders - - // Here we load the server module; - // - Vue includes the server in the extension bundle - // - Glint does not; expects it to come from workspace folder - // let serverModule = vscode.Uri.joinPath(context.extensionUri, 'server.js'); - let folderPath = workspaceFolder.uri.fsPath; if (clients.has(folderPath)) return null; @@ -109,10 +98,12 @@ export const { activate, deactivate } = defineExtension(async () => { if (!serverPath) return null; 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], }; @@ -165,7 +156,7 @@ export const { activate, deactivate } = defineExtension(async () => { return volarLabs.extensionExports; }); -function updateProviders(client: lsp.LanguageClient) { +function updateProviders(client: lsp.LanguageClient): void { const initializeFeatures = (client as any).initializeFeatures; (client as any).initializeFeatures = (...args: any) => { @@ -192,70 +183,6 @@ function updateProviders(client: lsp.LanguageClient) { }; } -// async function addWorkspaceFolder( -// context: ExtensionContext, -// workspaceFolder: WorkspaceFolder, -// watcher: FileSystemWatcher, -// volarLabs?: ReturnType, -// ): Promise { -// let folderPath = workspaceFolder.uri.fsPath; -// if (clients.has(folderPath)) return; - -// let serverPath = findLanguageServer(folderPath); -// if (!serverPath) return; - -// let serverOptions: ServerOptions = { module: serverPath }; - -// const typescriptFormatOptions = getOptions(workspace.getConfiguration('typescript'), 'format'); -// const typescriptUserPreferences = getOptions( -// workspace.getConfiguration('typescript'), -// 'preferences', -// ); -// const javascriptFormatOptions = getOptions(workspace.getConfiguration('javascript'), 'format'); -// const javascriptUserPreferences = getOptions( -// workspace.getConfiguration('javascript'), -// 'preferences', -// ); - -// let client = new LanguageClient('glint', 'Glint', serverOptions, { -// workspaceFolder, -// outputChannel, -// initializationOptions: { -// javascript: { -// format: javascriptFormatOptions, -// preferences: javascriptUserPreferences, -// }, -// typescript: { -// format: typescriptFormatOptions, -// preferences: typescriptUserPreferences, -// tsdk: (await getTsdk(context))!.tsdk, -// }, -// }, -// documentSelector: [{ scheme: 'file', pattern: `${folderPath}/${filePattern}` }], -// synchronize: { fileEvents: watcher }, -// }); - -// if (volarLabs) { -// volarLabs.addLanguageClient(client); -// } - -// clients.set(folderPath, client); - -// await client.start(); -// } - -// async function removeWorkspaceFolder(workspaceFolder: WorkspaceFolder): Promise { -// let folderPath = workspaceFolder.uri.fsPath; -// let client = clients.get(folderPath); -// if (client) { -// clients.delete(folderPath); -// await client.stop(); -// } -// } - -/////////////////////////////////////////////////////////////////////////////// -// Utilities - function findLanguageServer( workspaceDir: string, outputChannel: vscode.OutputChannel, @@ -278,16 +205,3 @@ function findLanguageServer( return null; } } - -// Automatically restart running servers when config files in the workspace change -// TODO: reinstate this or see whether Vue / reactive-vscode already does this - -// function createConfigWatcher(): Disposable { -// let configWatcher = workspace.createFileSystemWatcher('**/{ts,js}config*.json'); - -// configWatcher.onDidCreate(restartClients); -// configWatcher.onDidChange(restartClients); -// configWatcher.onDidDelete(restartClients); - -// return configWatcher; -// } diff --git a/packages/vscode/src/hybrid-mode.ts b/packages/vscode/src/hybrid-mode.ts index 21eb689f4..cc7c0030c 100644 --- a/packages/vscode/src/hybrid-mode.ts +++ b/packages/vscode/src/hybrid-mode.ts @@ -1,6 +1,12 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { computed, executeCommand, useAllExtensions, useVscodeContext, watchEffect } from 'reactive-vscode'; +import { + computed, + executeCommand, + useAllExtensions, + useVscodeContext, + watchEffect, +} from 'reactive-vscode'; import * as semver from 'semver'; import * as vscode from 'vscode'; import { incompatibleExtensions, unknownExtensions } from './compatibility'; @@ -9,192 +15,161 @@ import { config } from './config'; const extensions = useAllExtensions(); export const enabledHybridMode = computed(() => { - if (config.server.hybridMode === 'typeScriptPluginOnly') { - return false; - } - else if (config.server.hybridMode === 'auto') { - if ( - incompatibleExtensions.value.length || - unknownExtensions.value.length - ) { - return false; - } - else if ( - (vscodeTsdkVersion.value && !semver.gte(vscodeTsdkVersion.value, '5.3.0')) || - (workspaceTsdkVersion.value && !semver.gte(workspaceTsdkVersion.value, '5.3.0')) - ) { - return false; - } - return true; - } - return config.server.hybridMode; + if (config.server.hybridMode === 'typeScriptPluginOnly') { + return false; + } else if (config.server.hybridMode === 'auto') { + if (incompatibleExtensions.value.length || unknownExtensions.value.length) { + return false; + } else if ( + (vscodeTsdkVersion.value && !semver.gte(vscodeTsdkVersion.value, '5.3.0')) || + (workspaceTsdkVersion.value && !semver.gte(workspaceTsdkVersion.value, '5.3.0')) + ) { + return false; + } + return true; + } + return config.server.hybridMode; }); export const enabledTypeScriptPlugin = computed(() => { - return ( - enabledHybridMode.value || - config.server.hybridMode === 'typeScriptPluginOnly' - ); + return enabledHybridMode.value || config.server.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); - } + 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); - } + 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); - } + const libPath = vscode.workspace + .getConfiguration('typescript') + .get('tsdk') + ?.replace(/\\/g, '/'); + if (libPath) { + return getTsVersion(libPath); + } }); -export function useHybridModeTips() { - useVscodeContext('vueHybridMode', enabledHybridMode); +export function useHybridModeTips(): void { + useVscodeContext('glintHybridMode', enabledHybridMode); - watchEffect(() => { - if (config.server.hybridMode === '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' - ) - ); - } - }); - } - 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' - ); - } - }); - } - } - else if (config.server.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' - ) - ); - } - }); - } - }); + watchEffect(() => { + if (config.server.hybridMode === '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', 'glint.server.hybridMode'); + } else if (value == 'Report a false positive') { + vscode.env.openExternal( + vscode.Uri.parse('https://github.com/vuejs/language-tools/pull/4206'), + ); + } + }); + } 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', 'glint.server.hybridMode'); + } + }); + } + } else if (config.server.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 glint.server.hybridMode to "auto" to avoid compatibility issues.`, + 'Open Settings', + 'Report a false positive', + ) + .then((value) => { + if (value === 'Open Settings') { + executeCommand('workbench.action.openSettings', 'glint.server.hybridMode'); + } else if (value == 'Report a false positive') { + vscode.env.openExternal( + vscode.Uri.parse('https://github.com/vuejs/language-tools/pull/4206'), + ); + } + }); + } + }); } -export function useHybridModeStatusItem() { - const item = vscode.languages.createLanguageStatusItem( - 'vue-hybrid-mode', - config.server.includeLanguages - ); +export function useHybridModeStatusItem(): void { + const item = vscode.languages.createLanguageStatusItem( + 'vue-hybrid-mode', + config.server.includeLanguages, + ); - item.text = 'Hybrid Mode'; - item.detail = - (enabledHybridMode.value ? 'Enabled' : 'Disabled') + - (config.server.hybridMode === 'auto' ? ' (Auto)' : ''); - item.command = { - title: 'Open Setting', - command: 'workbench.action.openSettings', - arguments: ['vue.server.hybridMode'], - }; + item.text = 'Hybrid Mode'; + item.detail = + (enabledHybridMode.value ? 'Enabled' : 'Disabled') + + (config.server.hybridMode === 'auto' ? ' (Auto)' : ''); + item.command = { + title: 'Open Setting', + command: 'workbench.action.openSettings', + arguments: ['glint.server.hybridMode'], + }; - if (!enabledHybridMode.value) { - item.severity = vscode.LanguageStatusSeverity.Warning; - } + 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'); +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; - } + if (contents === undefined) { + return; + } - let desc: any = null; - try { - desc = JSON.parse(contents); - } - catch (err) { - return; - } - if (!desc || !desc.version) { - return; - } + let desc: any = null; + try { + desc = JSON.parse(contents); + } catch (err) { + return; + } + if (!desc || !desc.version) { + return; + } - return desc.version as string; - } catch { } + return desc.version as string; + } catch { + // Ignore + } } diff --git a/packages/vscode/src/language-client.ts b/packages/vscode/src/language-client.ts index 98714e2e7..09dac39e7 100644 --- a/packages/vscode/src/language-client.ts +++ b/packages/vscode/src/language-client.ts @@ -47,7 +47,7 @@ export function watchWorkspaceFolderForLanguageClientActivation( context: vscode.ExtensionContext, workspaceFolder: vscode.WorkspaceFolder, createLanguageClient: CreateLanguageClient, -) { +): () => void { // for each const activeTextEditor = useActiveTextEditor(); const visibleTextEditors = useVisibleTextEditors(); @@ -158,7 +158,7 @@ async function activateLanguageClient( { deep: true }, ); - useCommand('glint.restart-language-server', async (restartTsServer: boolean = true) => { + useCommand('glint.restart-language-server', async (restartTsServer = true) => { if (restartTsServer) { await executeCommand('typescript.restartTsServer'); } @@ -196,7 +196,7 @@ async function activateLanguageClient( hasInitialized = true; - async function requestReloadVscode(msg: string) { + async function requestReloadVscode(msg: string): Promise { const reload = await vscode.window.showInformationMessage(msg, 'Reload Window'); if (reload) { executeCommand('workbench.action.reloadWindow'); From ae858e7fb20d08e77b5b04fc193f34f3563c2051 Mon Sep 17 00:00:00 2001 From: machty Date: Thu, 16 Jan 2025 17:31:49 -0500 Subject: [PATCH 08/10] fix ci --- .prettierignore | 2 ++ packages/vscode/src/config.ts | 4 ++-- packages/vscode/src/extension.ts | 2 +- packages/vscode/src/hybrid-mode.ts | 4 ++-- packages/vscode/tsconfig.json | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.prettierignore b/.prettierignore index e32674f09..7bd5a2d42 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,6 +20,8 @@ dist/ !/packages/environment*/-private/dsl/**/*.d.ts !/packages/environment*/-private/intrinsics/**/*.d.ts +!/packages/vscode/generated-meta.ts + # Markdown files: the formatting Prettier uses by default *does. not. match.* # the formatting applied by Gitbook. Having both present is a recipe for ongoing # CI problems. diff --git a/packages/vscode/src/config.ts b/packages/vscode/src/config.ts index 11ac8ef56..47555f27b 100644 --- a/packages/vscode/src/config.ts +++ b/packages/vscode/src/config.ts @@ -2,6 +2,6 @@ import { defineConfigObject } from 'reactive-vscode'; import { NestedScopedConfigs, scopedConfigs } from './generated-meta'; export const config = defineConfigObject( - scopedConfigs.scope, - scopedConfigs.defaults + scopedConfigs.scope, + scopedConfigs.defaults, ); diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 69d750f1d..81724821d 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -85,7 +85,7 @@ export const { activate, deactivate } = defineExtension(async () => { workspaceFolder, (id, name, documentSelector, initOptions, port, outputChannel) => { class _LanguageClient extends lsp.LanguageClient { - override fillInitializeParams(params: lsp.InitializeParams): lsp.InitializeParams { + override fillInitializeParams(params: lsp.InitializeParams): void { // fix https://github.com/vuejs/language-tools/issues/1959 params.locale = vscode.env.language; } diff --git a/packages/vscode/src/hybrid-mode.ts b/packages/vscode/src/hybrid-mode.ts index cc7c0030c..85426589d 100644 --- a/packages/vscode/src/hybrid-mode.ts +++ b/packages/vscode/src/hybrid-mode.ts @@ -170,6 +170,6 @@ function getTsVersion(libPath: string): string | undefined { return desc.version as string; } catch { - // Ignore - } + // Ignore + } } diff --git a/packages/vscode/tsconfig.json b/packages/vscode/tsconfig.json index 815eb735e..f8b0fcb78 100644 --- a/packages/vscode/tsconfig.json +++ b/packages/vscode/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "lib", "module": "CommonJS", - "moduleResolution": "Node" + "moduleResolution": "Node" }, "include": ["src", "__tests__"], "references": [{ "path": "../core" }] From e2f24e9640f92a244a4b33f1ca74e78b37fb43b6 Mon Sep 17 00:00:00 2001 From: machty Date: Thu, 16 Jan 2025 17:35:40 -0500 Subject: [PATCH 09/10] prettier ignore --- .prettierignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index 7bd5a2d42..27b2a63b1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,7 +20,7 @@ dist/ !/packages/environment*/-private/dsl/**/*.d.ts !/packages/environment*/-private/intrinsics/**/*.d.ts -!/packages/vscode/generated-meta.ts +!/packages/vscode/src/generated-meta.ts # Markdown files: the formatting Prettier uses by default *does. not. match.* # the formatting applied by Gitbook. Having both present is a recipe for ongoing From 2c4e9cf35f3d04fe7b18dd8fd2f5bbb51ac25cde Mon Sep 17 00:00:00 2001 From: machty Date: Thu, 16 Jan 2025 17:39:25 -0500 Subject: [PATCH 10/10] prettier ignore again --- .prettierignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index 27b2a63b1..aa03f7a9f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,7 +20,7 @@ dist/ !/packages/environment*/-private/dsl/**/*.d.ts !/packages/environment*/-private/intrinsics/**/*.d.ts -!/packages/vscode/src/generated-meta.ts +/packages/vscode/src/generated-meta.ts # Markdown files: the formatting Prettier uses by default *does. not. match.* # the formatting applied by Gitbook. Having both present is a recipe for ongoing