1 import * as fs from 'fs';
2 import * as path from 'path';
3 import * as vscode from 'vscode';
4 import * as vscodelc from 'vscode-languageclient/node';
6 import * as config from './config';
7 import * as configWatcher from './configWatcher';
10 * This class represents the context of a specific workspace folder.
12 class WorkspaceFolderContext implements vscode.Disposable {
14 this.clients.forEach(async client => await client.stop());
18 clients: Map<string, vscodelc.LanguageClient> = new Map();
22 * This class manages all of the MLIR extension state,
23 * including the language client.
25 export class MLIRContext implements vscode.Disposable {
26 subscriptions: vscode.Disposable[] = [];
27 workspaceFolders: Map<string, WorkspaceFolderContext> = new Map();
28 outputChannel: vscode.OutputChannel;
31 * Activate the MLIR context, and start the language clients.
33 async activate(outputChannel: vscode.OutputChannel) {
34 this.outputChannel = outputChannel;
36 // This lambda is used to lazily start language clients for the given
37 // document. It removes the need to pro-actively start language clients for
38 // every folder within the workspace and every language type we provide.
39 const startClientOnOpenDocument = async (document: vscode.TextDocument) => {
40 await this.getOrActivateLanguageClient(document.uri, document.languageId);
42 // Process any existing documents.
43 for (const textDoc of vscode.workspace.textDocuments) {
44 await startClientOnOpenDocument(textDoc);
47 // Watch any new documents to spawn servers when necessary.
48 this.subscriptions.push(
49 vscode.workspace.onDidOpenTextDocument(startClientOnOpenDocument));
50 this.subscriptions.push(
51 vscode.workspace.onDidChangeWorkspaceFolders((event) => {
52 for (const folder of event.removed) {
53 const client = this.workspaceFolders.get(folder.uri.toString());
56 this.workspaceFolders.delete(folder.uri.toString());
63 * Open or return a language server for the given uri and language.
65 async getOrActivateLanguageClient(uri: vscode.Uri, languageId: string):
66 Promise<vscodelc.LanguageClient> {
67 let serverSettingName: string;
68 if (languageId === 'mlir') {
69 serverSettingName = 'server_path';
70 } else if (languageId === 'pdll') {
71 serverSettingName = 'pdll_server_path';
72 } else if (languageId === 'tablegen') {
73 serverSettingName = 'tablegen_server_path';
78 // Check the scheme of the uri.
79 let validSchemes = [ 'file', 'mlir.bytecode-mlir' ];
80 if (!validSchemes.includes(uri.scheme)) {
84 // Resolve the workspace folder if this document is in one. We use the
85 // workspace folder when determining if a server needs to be started.
86 let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
87 let workspaceFolderStr =
88 workspaceFolder ? workspaceFolder.uri.toString() : "";
90 // Get or create a client context for this folder.
91 let folderContext = this.workspaceFolders.get(workspaceFolderStr);
93 folderContext = new WorkspaceFolderContext();
94 this.workspaceFolders.set(workspaceFolderStr, folderContext);
96 // Start the client for this language if necessary.
97 let client = folderContext.clients.get(languageId);
99 client = await this.activateWorkspaceFolder(
100 workspaceFolder, serverSettingName, languageId, this.outputChannel);
101 folderContext.clients.set(languageId, client);
107 * Prepare a compilation database option for a server.
109 async prepareCompilationDatabaseServerOptions(
110 languageName: string, workspaceFolder: vscode.WorkspaceFolder,
111 configsToWatch: string[], pathsToWatch: string[],
112 additionalServerArgs: string[]) {
113 // Process the compilation databases attached for the workspace folder.
114 let databases = config.get<string[]>(
115 `${languageName}_compilation_databases`, workspaceFolder, []);
117 // If no databases were explicitly specified, default to a database in the
118 // 'build' directory within the current workspace.
119 if (databases.length === 0) {
120 if (workspaceFolder) {
121 databases.push(workspaceFolder.uri.fsPath +
122 `/build/${languageName}_compile_commands.yml`);
125 // Otherwise, try to resolve each of the paths.
127 for await (let database of databases) {
128 database = await this.resolvePath(database, '', workspaceFolder);
132 configsToWatch.push(`${languageName}_compilation_databases`);
133 pathsToWatch.push(...databases);
135 // Setup the compilation databases as additional arguments to pass to the
137 databases.filter(database => database !== '');
138 additionalServerArgs.push(...databases.map(
139 (database) => `--${languageName}-compilation-database=${database}`));
143 * Prepare the server options for a PDLL server, e.g. populating any
144 * accessible compilation databases.
146 async preparePDLLServerOptions(workspaceFolder: vscode.WorkspaceFolder,
147 configsToWatch: string[],
148 pathsToWatch: string[],
149 additionalServerArgs: string[]) {
150 await this.prepareCompilationDatabaseServerOptions(
151 'pdll', workspaceFolder, configsToWatch, pathsToWatch,
152 additionalServerArgs);
156 * Prepare the server options for a TableGen server, e.g. populating any
157 * accessible compilation databases.
159 async prepareTableGenServerOptions(workspaceFolder: vscode.WorkspaceFolder,
160 configsToWatch: string[],
161 pathsToWatch: string[],
162 additionalServerArgs: string[]) {
163 await this.prepareCompilationDatabaseServerOptions(
164 'tablegen', workspaceFolder, configsToWatch, pathsToWatch,
165 additionalServerArgs);
169 * Activate the language client for the given language in the given workspace
172 async activateWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder,
173 serverSettingName: string, languageName: string,
174 outputChannel: vscode.OutputChannel):
175 Promise<vscodelc.LanguageClient> {
176 let configsToWatch: string[] = [];
177 let filepathsToWatch: string[] = [];
178 let additionalServerArgs: string[] = [];
179 additionalServerArgs = config.get<string[]>(languageName + "_additional_server_args", null, []);
181 // Initialize additional configurations for this server.
182 if (languageName === 'pdll') {
183 await this.preparePDLLServerOptions(workspaceFolder, configsToWatch,
185 additionalServerArgs);
186 } else if (languageName == 'tablegen') {
187 await this.prepareTableGenServerOptions(workspaceFolder, configsToWatch,
189 additionalServerArgs);
192 // Try to activate the language client.
193 const [server, serverPath] = await this.startLanguageClient(
194 workspaceFolder, outputChannel, serverSettingName, languageName,
195 additionalServerArgs);
196 configsToWatch.push(serverSettingName);
197 filepathsToWatch.push(serverPath);
199 // Watch for configuration changes on this folder.
200 await configWatcher.activate(this, workspaceFolder, configsToWatch,
206 * Start a new language client for the given language. Returns an array
207 * containing the opened server, or null if the server could not be started,
208 * and the resolved server path.
210 async startLanguageClient(workspaceFolder: vscode.WorkspaceFolder,
211 outputChannel: vscode.OutputChannel,
212 serverSettingName: string, languageName: string,
213 additionalServerArgs: string[]):
214 Promise<[ vscodelc.LanguageClient, string ]> {
215 const clientTitle = languageName.toUpperCase() + ' Language Client';
217 // Get the path of the lsp-server that is used to provide language
220 await this.resolveServerPath(serverSettingName, workspaceFolder);
222 // If the server path is empty, bail. We don't emit errors if the user
223 // hasn't explicitly configured the server.
224 if (serverPath === '') {
225 return [ null, serverPath ];
228 // Check that the file actually exists.
229 if (!fs.existsSync(serverPath)) {
232 `${clientTitle}: Unable to resolve path for '${
233 serverSettingName}', please ensure the path is correct`,
236 if (value === "Open Setting") {
237 vscode.commands.executeCommand(
238 'workbench.action.openWorkspaceSettings',
239 {openToSide : false, query : `mlir.${serverSettingName}`});
242 return [ null, serverPath ];
245 // Configure the server options.
246 const serverOptions: vscodelc.ServerOptions = {
247 command : serverPath,
248 args : additionalServerArgs
251 // Configure file patterns relative to the workspace folder.
252 let filePattern: vscode.GlobPattern = '**/*.' + languageName;
253 let selectorPattern: string = null;
254 if (workspaceFolder) {
255 filePattern = new vscode.RelativePattern(workspaceFolder, filePattern);
256 selectorPattern = `${workspaceFolder.uri.fsPath}/**/*`;
259 // Configure the middleware of the client. This is sort of abused to allow
260 // for defining a "fallback" language server that operates on non-workspace
261 // folders. Workspace folder language servers can properly filter out
262 // documents not within the folder, but we can't effectively filter for
263 // documents outside of the workspace. To support this, and avoid having two
264 // servers targeting the same set of files, we use middleware to inject the
265 // dynamic logic for checking if a document is in the workspace.
267 if (!workspaceFolder) {
269 didOpen : (document, next) : Promise<void> => {
270 if (!vscode.workspace.getWorkspaceFolder(document.uri)) {
271 return next(document);
273 return Promise.resolve();
278 // Configure the client options.
279 const clientOptions: vscodelc.LanguageClientOptions = {
281 {language : languageName, pattern : selectorPattern},
284 // Notify the server about file changes to language files contained in
286 fileEvents : vscode.workspace.createFileSystemWatcher(filePattern)
288 outputChannel : outputChannel,
289 workspaceFolder : workspaceFolder,
290 middleware : middleware,
292 // Don't switch to output window when the server returns output.
293 revealOutputChannelOn : vscodelc.RevealOutputChannelOn.Never,
296 // Create the language client and start the client.
297 let languageClient = new vscodelc.LanguageClient(
298 languageName + '-lsp', clientTitle, serverOptions, clientOptions);
299 languageClient.start();
300 return [ languageClient, serverPath ];
304 * Given a server setting, return the default server path.
306 static getDefaultServerFilename(serverSettingName: string): string {
307 if (serverSettingName === 'pdll_server_path') {
308 return 'mlir-pdll-lsp-server';
310 if (serverSettingName === 'server_path') {
311 return 'mlir-lsp-server';
313 if (serverSettingName === 'tablegen_server_path') {
314 return 'tblgen-lsp-server';
320 * Try to resolve the given path, or the default path, with an optional
321 * workspace folder. If a path could not be resolved, just returns the
324 async resolvePath(filePath: string, defaultPath: string,
325 workspaceFolder: vscode.WorkspaceFolder): Promise<string> {
326 const configPath = filePath;
328 // If the path is already fully resolved, there is nothing to do.
329 if (path.isAbsolute(filePath)) {
333 // If a path hasn't been set, try to use the default path.
334 if (filePath === '') {
335 if (defaultPath === '') {
338 filePath = defaultPath;
340 // Fallthrough to try resolving the default path.
343 // Try to resolve the path relative to the workspace.
344 let filePattern: vscode.GlobPattern = '**/' + filePath;
345 if (workspaceFolder) {
346 filePattern = new vscode.RelativePattern(workspaceFolder, filePattern);
348 let foundUris = await vscode.workspace.findFiles(filePattern, null, 1);
349 if (foundUris.length === 0) {
350 // If we couldn't resolve it, just return the original path anyways. The
351 // file might not exist yet.
354 // Otherwise, return the resolved path.
355 return foundUris[0].fsPath;
359 * Try to resolve the path for the given server setting, with an optional
362 async resolveServerPath(serverSettingName: string,
363 workspaceFolder: vscode.WorkspaceFolder):
365 const serverPath = config.get<string>(serverSettingName, workspaceFolder);
366 const defaultPath = MLIRContext.getDefaultServerFilename(serverSettingName);
367 return this.resolvePath(serverPath, defaultPath, workspaceFolder);
371 * Return the language client for the given language and uri, or null if no
374 getLanguageClient(uri: vscode.Uri,
375 languageName: string): vscodelc.LanguageClient {
376 let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
377 let workspaceFolderStr =
378 workspaceFolder ? workspaceFolder.uri.toString() : "";
379 let folderContext = this.workspaceFolders.get(workspaceFolderStr);
380 if (!folderContext) {
383 return folderContext.clients.get(languageName);
387 this.subscriptions.forEach((d) => { d.dispose(); });
388 this.subscriptions = [];
389 this.workspaceFolders.forEach((d) => { d.dispose(); });
390 this.workspaceFolders.clear();