Skip to content

Commit b87ea2e

Browse files
authored
(feat) improve Ts plugin enable config (#1401)
Enable ts plugin without updating the package.json, making it possible to enable/disable it without a restart #1358
1 parent f149c62 commit b87ea2e

File tree

9 files changed

+176
-68
lines changed

9 files changed

+176
-68
lines changed

packages/svelte-vscode/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
},
4242
"activationEvents": [
4343
"onLanguage:svelte",
44-
"onCommand:svelte.restartLanguageServer"
44+
"onCommand:svelte.restartLanguageServer",
45+
"onLanguage:javascript",
46+
"onLanguage:typescript"
4547
],
4648
"capabilities": {
4749
"untrustedWorkspaces": {
@@ -50,7 +52,7 @@
5052
}
5153
},
5254
"contributes": {
53-
"typescriptServerPlugins-disabled": [
55+
"typescriptServerPlugins": [
5456
{
5557
"name": "typescript-svelte-plugin",
5658
"enableForWorkspaceTypeScriptVersions": true

packages/svelte-vscode/src/extension.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,45 @@ namespace TagCloseRequest {
3737
}
3838

3939
export function activate(context: ExtensionContext) {
40+
// The extension is activated on TS/JS/Svelte files because else it might be too late to configure the TS plugin:
41+
// If we only activate on Svelte file and the user opens a TS file first, the configuration command is issued too late.
42+
// We wait until there's a Svelte file open and only then start the actual language client.
43+
const tsPlugin = new TsPlugin(context);
44+
let lsApi: { getLS(): LanguageClient } | undefined;
45+
46+
if (workspace.textDocuments.some((doc) => doc.languageId === 'svelte')) {
47+
lsApi = activateSvelteLanguageServer(context);
48+
tsPlugin.askToEnable();
49+
} else {
50+
const onTextDocumentListener = workspace.onDidOpenTextDocument((doc) => {
51+
if (doc.languageId === 'svelte') {
52+
lsApi = activateSvelteLanguageServer(context);
53+
tsPlugin.askToEnable();
54+
onTextDocumentListener.dispose();
55+
}
56+
});
57+
58+
context.subscriptions.push(onTextDocumentListener);
59+
}
60+
61+
// This API is considered private and only exposed for experimenting.
62+
// Interface may change at any time. Use at your own risk!
63+
return {
64+
/**
65+
* As a function, because restarting the server
66+
* will result in another instance.
67+
*/
68+
getLanguageServer() {
69+
if (!lsApi) {
70+
lsApi = activateSvelteLanguageServer(context);
71+
}
72+
73+
return lsApi.getLS();
74+
}
75+
};
76+
}
77+
78+
export function activateSvelteLanguageServer(context: ExtensionContext) {
4079
warnIfOldExtensionInstalled();
4180

4281
const runtimeConfig = workspace.getConfiguration('svelte.language-server');
@@ -190,8 +229,6 @@ export function activate(context: ExtensionContext) {
190229

191230
addExtracComponentCommand(getLS, context);
192231

193-
TsPlugin.create(context);
194-
195232
languages.setLanguageConfiguration('svelte', {
196233
indentationRules: {
197234
// Matches a valid opening tag that is:
@@ -261,14 +298,8 @@ export function activate(context: ExtensionContext) {
261298
]
262299
});
263300

264-
// This API is considered private and only exposed for experimenting.
265-
// Interface may change at any time. Use at your own risk!
266301
return {
267-
/**
268-
* As a function, because restarting the server
269-
* will result in another instance.
270-
*/
271-
getLanguageServer: getLS
302+
getLS
272303
};
273304
}
274305

packages/svelte-vscode/src/tsplugin.ts

Lines changed: 13 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import { readFileSync, writeFileSync } from 'fs';
2-
import { join } from 'path';
31
import { commands, ExtensionContext, extensions, window, workspace } from 'vscode';
42

53
export class TsPlugin {
64
private enabled: boolean;
75

8-
static create(context: ExtensionContext) {
9-
new TsPlugin(context);
10-
}
11-
12-
private constructor(context: ExtensionContext) {
6+
constructor(context: ExtensionContext) {
137
this.enabled = this.getEnabledState();
14-
this.askToEnable(this.enabled);
158
this.toggleTsPlugin(this.enabled);
169

1710
context.subscriptions.push(
@@ -29,56 +22,27 @@ export class TsPlugin {
2922
return workspace.getConfiguration('svelte').get<boolean>('enable-ts-plugin') ?? false;
3023
}
3124

32-
private toggleTsPlugin(enable: boolean) {
33-
const extension = extensions.getExtension('svelte.svelte-vscode');
25+
private async toggleTsPlugin(enable: boolean) {
26+
const extension = extensions.getExtension('vscode.typescript-language-features');
27+
3428
if (!extension) {
35-
// This shouldn't be possible
3629
return;
3730
}
3831

39-
const packageJson = join(extension.extensionPath, 'package.json');
40-
const enabled = '"typescriptServerPlugins"';
41-
const disabled = '"typescriptServerPlugins-disabled"';
42-
try {
43-
const packageText = readFileSync(packageJson, 'utf8');
44-
if (packageText.includes(disabled) && enable) {
45-
const newText = packageText.replace(disabled, enabled);
46-
writeFileSync(packageJson, newText, 'utf8');
47-
this.showReload(true);
48-
} else if (packageText.includes(enabled) && !enable) {
49-
const newText = packageText.replace(enabled, disabled);
50-
writeFileSync(packageJson, newText, 'utf8');
51-
this.showReload(false);
52-
} else if (!packageText.includes(enabled) && !packageText.includes(disabled)) {
53-
window.showWarningMessage('Unknown Svelte for VS Code package.json status.');
54-
}
55-
} catch (err) {
56-
window.showWarningMessage(
57-
'Svelte for VS Code package.json update failed, TypeScript plugin could not be toggled.'
58-
);
59-
}
60-
}
61-
62-
private async showReload(enabled: boolean) {
63-
// Restarting the TSServer via a command isn't enough, the whole VS Code window needs to reload
64-
let message = `TypeScript Svelte Plugin ${enabled ? 'enabled' : 'disabled'}.`;
65-
if (enabled) {
66-
message +=
67-
' Note that changes of Svelte files are only noticed by TS/JS files after they are saved to disk.';
68-
}
69-
message += ' Please reload VS Code to restart the TS Server.';
70-
71-
const reload = await window.showInformationMessage(message, 'Reload Window');
72-
if (reload) {
73-
commands.executeCommand('workbench.action.reloadWindow');
74-
}
32+
// This somewhat semi-public command configures our TypeScript plugin.
33+
// The plugin itself is always present, but enabled/disabled depending on this config.
34+
// It is done this way because it allows us to toggle the plugin without restarting VS Code
35+
// and without having to do hacks like updating the extension's package.json.
36+
commands.executeCommand('_typescript.configurePlugin', 'typescript-svelte-plugin', {
37+
enable
38+
});
7539
}
7640

77-
private async askToEnable(enabled: boolean) {
41+
async askToEnable() {
7842
const shouldAsk = workspace
7943
.getConfiguration('svelte')
8044
.get<boolean>('ask-to-enable-ts-plugin');
81-
if (enabled || !shouldAsk) {
45+
if (this.enabled || !shouldAsk) {
8246
return;
8347
}
8448

packages/typescript-plugin/internal.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The last step is to enhance the language service. For that, we patch the desired
1616

1717
Along the way, we need to patch some internal methods, which is brittly and hacky, but to our knowledge there currently is no other way.
1818

19+
To make it work with the VS Code extension we need to provide the plugin within `contributes.typescriptServerPlugins`. That way the plugin is always loaded. To enable/disable it, we use a semi-public command that tells TypeScript to configure the plugin. That configuration then tells this plugin whether or not it is enabled.
20+
1921
## Limitations
2022

2123
Currently, changes to Svelte files are only recognized after they are saved to disk. That could be changed by adding `"languages": ["svelte"]` to the plugin provide options. The huge disadvantage is that diagnostics, rename etc within Svelte files no longer stay in the control of the language-server, instead TS/JS starts interacting with Svelte files on a much deeper level, which would mean patching many more undocumented/private methods, and having less control of the situation overall.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { EventEmitter } from 'events';
2+
3+
const configurationEventName = 'configuration-changed';
4+
5+
export interface Configuration {
6+
enable: boolean;
7+
}
8+
9+
export class ConfigManager {
10+
private emitter = new EventEmitter();
11+
private config: Configuration = {
12+
enable: true
13+
};
14+
15+
onConfigurationChanged(listener: (config: Configuration) => void) {
16+
this.emitter.on(configurationEventName, listener);
17+
}
18+
19+
updateConfigFromPluginConfig(config: Configuration) {
20+
this.config = {
21+
...this.config,
22+
...config
23+
};
24+
this.emitter.emit(configurationEventName, config);
25+
}
26+
27+
getConfig() {
28+
return this.config;
29+
}
30+
}

packages/typescript-plugin/src/index.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@ import { Logger } from './logger';
44
import { patchModuleLoader } from './module-loader';
55
import { SvelteSnapshotManager } from './svelte-snapshots';
66
import type ts from 'typescript/lib/tsserverlibrary';
7+
import { ConfigManager, Configuration } from './config-manager';
78

89
function init(modules: { typescript: typeof ts }) {
10+
const configManager = new ConfigManager();
11+
912
function create(info: ts.server.PluginCreateInfo) {
1013
const logger = new Logger(info.project.projectService.logger);
1114
if (!isSvelteProject(info.project.getCompilerOptions())) {
1215
logger.log('Detected that this is not a Svelte project, abort patching TypeScript');
1316
return info.languageService;
1417
}
1518

16-
logger.log('Starting Svelte plugin');
19+
configManager.updateConfigFromPluginConfig(info.config);
20+
if (configManager.getConfig().enable) {
21+
logger.log('Starting Svelte plugin');
22+
} else {
23+
logger.log('Svelte plugin disabled');
24+
logger.log(info.config);
25+
}
26+
1727
// If someone knows a better/more performant way to get svelteOptions,
1828
// please tell us :)
1929
const svelteOptions = info.languageServiceHost.getParsedCommandLine?.(
@@ -25,17 +35,31 @@ function init(modules: { typescript: typeof ts }) {
2535
modules.typescript,
2636
info.project.projectService,
2737
svelteOptions,
28-
logger
38+
logger,
39+
configManager
2940
);
3041

3142
patchModuleLoader(
3243
logger,
3344
snapshotManager,
3445
modules.typescript,
3546
info.languageServiceHost,
36-
info.project
47+
info.project,
48+
configManager
49+
);
50+
51+
configManager.onConfigurationChanged(() => {
52+
// enabling/disabling the plugin means TS has to recompute stuff
53+
info.languageService.cleanupSemanticCache();
54+
info.project.markAsDirty();
55+
});
56+
57+
return decorateLanguageService(
58+
info.languageService,
59+
snapshotManager,
60+
logger,
61+
configManager
3762
);
38-
return decorateLanguageService(info.languageService, snapshotManager, logger);
3963
}
4064

4165
function getExternalFiles(project: ts.server.ConfiguredProject) {
@@ -66,7 +90,11 @@ function init(modules: { typescript: typeof ts }) {
6690
}
6791
}
6892

69-
return { create, getExternalFiles };
93+
function onConfigurationChanged(config: Configuration) {
94+
configManager.updateConfigFromPluginConfig(config);
95+
}
96+
97+
return { create, getExternalFiles, onConfigurationChanged };
7098
}
7199

72100
export = init;

packages/typescript-plugin/src/language-service/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type ts from 'typescript/lib/tsserverlibrary';
2+
import { ConfigManager } from '../config-manager';
23
import { Logger } from '../logger';
34
import { SvelteSnapshotManager } from '../svelte-snapshots';
45
import { isSvelteFilePath } from '../utils';
@@ -10,6 +11,19 @@ import { decorateGetImplementation } from './implementation';
1011
import { decorateRename } from './rename';
1112

1213
export function decorateLanguageService(
14+
ls: ts.LanguageService,
15+
snapshotManager: SvelteSnapshotManager,
16+
logger: Logger,
17+
configManager: ConfigManager
18+
) {
19+
// Decorate using a proxy so we can dynamically enable/disable method
20+
// patches depending on the enabled state of our config
21+
const proxy = new Proxy(ls, createProxyHandler(configManager));
22+
decorateLanguageServiceInner(proxy, snapshotManager, logger);
23+
return proxy;
24+
}
25+
26+
function decorateLanguageServiceInner(
1327
ls: ts.LanguageService,
1428
snapshotManager: SvelteSnapshotManager,
1529
logger: Logger
@@ -24,6 +38,27 @@ export function decorateLanguageService(
2438
return ls;
2539
}
2640

41+
function createProxyHandler(configManager: ConfigManager): ProxyHandler<ts.LanguageService> {
42+
const decorated: Partial<ts.LanguageService> = {};
43+
44+
return {
45+
get(target, p) {
46+
if (!configManager.getConfig().enable) {
47+
return target[p as keyof ts.LanguageService];
48+
}
49+
50+
return (
51+
decorated[p as keyof ts.LanguageService] ?? target[p as keyof ts.LanguageService]
52+
);
53+
},
54+
set(_, p, value) {
55+
decorated[p as keyof ts.LanguageService] = value;
56+
57+
return true;
58+
}
59+
};
60+
}
61+
2762
function patchLineColumnOffset(ls: ts.LanguageService, snapshotManager: SvelteSnapshotManager) {
2863
if (!ls.toLineColumnOffset) {
2964
return;

packages/typescript-plugin/src/module-loader.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type ts from 'typescript/lib/tsserverlibrary';
2+
import { ConfigManager } from './config-manager';
23
import { Logger } from './logger';
34
import { SvelteSnapshotManager } from './svelte-snapshots';
45
import { createSvelteSys } from './svelte-sys';
@@ -39,6 +40,10 @@ class ModuleResolutionCache {
3940
});
4041
}
4142

43+
clear() {
44+
this.cache.clear();
45+
}
46+
4247
private getKey(moduleName: string, containingFile: string) {
4348
return containingFile + ':::' + ensureRealSvelteFilePath(moduleName);
4449
}
@@ -58,7 +63,8 @@ export function patchModuleLoader(
5863
snapshotManager: SvelteSnapshotManager,
5964
typescript: typeof ts,
6065
lsHost: ts.LanguageServiceHost,
61-
project: ts.server.Project
66+
project: ts.server.Project,
67+
configManager: ConfigManager
6268
): void {
6369
const svelteSys = createSvelteSys(logger);
6470
const moduleCache = new ModuleResolutionCache();
@@ -73,6 +79,10 @@ export function patchModuleLoader(
7379
return origRemoveFile(info, fileExists, detachFromProject);
7480
};
7581

82+
configManager.onConfigurationChanged(() => {
83+
moduleCache.clear();
84+
});
85+
7686
function resolveModuleNames(
7787
moduleNames: string[],
7888
containingFile: string,
@@ -94,6 +104,10 @@ export function patchModuleLoader(
94104
compilerOptions
95105
) || Array.from<undefined>(Array(moduleNames.length));
96106

107+
if (!configManager.getConfig().enable) {
108+
return resolved;
109+
}
110+
97111
return resolved.map((moduleName, idx) => {
98112
const fileName = moduleNames[idx];
99113
if (moduleName || !ensureRealSvelteFilePath(fileName).endsWith('.svelte')) {

0 commit comments

Comments
 (0)