-
Notifications
You must be signed in to change notification settings - Fork 50
/
extension.ts
196 lines (163 loc) · 6.83 KB
/
extension.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { createRequire } from 'node:module';
import * as path from 'node:path';
import {
ExtensionContext,
WorkspaceFolder,
FileSystemWatcher,
window,
extensions,
commands,
workspace,
WorkspaceConfiguration,
} from 'vscode';
import * as languageServerProtocol from '@volar/language-server/protocol.js';
import { LabsInfo, createLabsInfo, getTsdk } from '@volar/vscode';
import { Disposable, LanguageClient, ServerOptions } from '@volar/vscode/node.js';
///////////////////////////////////////////////////////////////////////////////
// Setup and extension lifecycle
const outputChannel = window.createOutputChannel('Glint Language Server');
const clients = new Map<string, LanguageClient>();
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?
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<void> {
await Promise.all([...clients.values()].map((client) => client.stop()));
}
///////////////////////////////////////////////////////////////////////////////
// Commands
async function restartClients(): Promise<void> {
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<void> {
let folders = workspace.workspaceFolders ?? [];
await Promise.all(
folders.map(async (folder) => {
await removeWorkspaceFolder(folder);
await addWorkspaceFolder(context, folder, fileWatcher);
}),
);
}
async function addWorkspaceFolder(
context: ExtensionContext,
workspaceFolder: WorkspaceFolder,
watcher: FileSystemWatcher,
volarLabs?: ReturnType<typeof createLabsInfo>,
): Promise<void> {
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<void> {
let folderPath = workspaceFolder.uri.fsPath;
let client = clients.get(folderPath);
if (client) {
clients.delete(folderPath);
await client.stop();
}
}
///////////////////////////////////////////////////////////////////////////////
// Utilities
function findLanguageServer(workspaceDir: string): string | null {
let userLibraryPath = workspace.getConfiguration().get('glint.libraryPath', '.');
let resolutionDir = path.resolve(workspaceDir, userLibraryPath);
let require = createRequire(path.join(resolutionDir, 'package.json'));
try {
return require.resolve('@glint/core/bin/glint-language-server');
} catch {
// Many workspaces with `tsconfig` files won't be Glint projects, so it's totally fine for us to
// just bail out if we don't see `@glint/core`. If someone IS expecting Glint to run for this
// project, though, we leave a message in our channel explaining why we didn't launch.
outputChannel.appendLine(
`Unable to resolve @glint/core from ${resolutionDir} — not launching Glint for this directory.\n` +
`If Glint is installed in a child directory, you may wish to set the 'glint.libraryPath' option ` +
`in your workspace settings for the Glint VS Code extension.`,
);
return 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);
return configWatcher;
}
// 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<object>(key);
if (formatOptions) {
return formatOptions;
}
return {};
}