393 lines · typescript
1import * as fs from 'fs';2import * as path from 'path';3import * as vscode from 'vscode';4import * as vscodelc from 'vscode-languageclient/node';5 6import * as config from './config';7import * as configWatcher from './configWatcher';8 9/**10 * This class represents the context of a specific workspace folder.11 */12class WorkspaceFolderContext implements vscode.Disposable {13 dispose() {14 this.clients.forEach(async client => await client.stop());15 this.clients.clear();16 }17 18 clients: Map<string, vscodelc.LanguageClient> = new Map();19}20 21/**22 * This class manages all of the MLIR extension state,23 * including the language client.24 */25export class MLIRContext implements vscode.Disposable {26 subscriptions: vscode.Disposable[] = [];27 workspaceFolders: Map<string, WorkspaceFolderContext> = new Map();28 outputChannel: vscode.OutputChannel;29 30 /**31 * Activate the MLIR context, and start the language clients.32 */33 async activate(outputChannel: vscode.OutputChannel) {34 this.outputChannel = outputChannel;35 36 // This lambda is used to lazily start language clients for the given37 // document. It removes the need to pro-actively start language clients for38 // 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);41 };42 // Process any existing documents.43 for (const textDoc of vscode.workspace.textDocuments) {44 await startClientOnOpenDocument(textDoc);45 }46 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());54 if (client) {55 client.dispose();56 this.workspaceFolders.delete(folder.uri.toString());57 }58 }59 }));60 }61 62 /**63 * Open or return a language server for the given uri and language.64 */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';74 } else {75 return null;76 }77 78 // Check the scheme of the uri.79 let validSchemes = [ 'file', 'mlir.bytecode-mlir' ];80 if (!validSchemes.includes(uri.scheme)) {81 return null;82 }83 84 // Resolve the workspace folder if this document is in one. We use the85 // 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() : "";89 90 // Get or create a client context for this folder.91 let folderContext = this.workspaceFolders.get(workspaceFolderStr);92 if (!folderContext) {93 folderContext = new WorkspaceFolderContext();94 this.workspaceFolders.set(workspaceFolderStr, folderContext);95 }96 // Start the client for this language if necessary.97 let client = folderContext.clients.get(languageId);98 if (!client) {99 client = await this.activateWorkspaceFolder(100 workspaceFolder, serverSettingName, languageId, this.outputChannel);101 folderContext.clients.set(languageId, client);102 }103 return client;104 }105 106 /**107 * Prepare a compilation database option for a server.108 */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, []);116 117 // If no databases were explicitly specified, default to a database in the118 // '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`);123 }124 125 // Otherwise, try to resolve each of the paths.126 } else {127 for await (let database of databases) {128 database = await this.resolvePath(database, '', workspaceFolder);129 }130 }131 132 configsToWatch.push(`${languageName}_compilation_databases`);133 pathsToWatch.push(...databases);134 135 // Setup the compilation databases as additional arguments to pass to the136 // server.137 databases.filter(database => database !== '');138 additionalServerArgs.push(...databases.map(139 (database) => `--${languageName}-compilation-database=${database}`));140 }141 142 /**143 * Prepare the server options for a PDLL server, e.g. populating any144 * accessible compilation databases.145 */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);153 }154 155 /**156 * Prepare the server options for a TableGen server, e.g. populating any157 * accessible compilation databases.158 */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);166 }167 168 /**169 * Activate the language client for the given language in the given workspace170 * folder.171 */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, []);180 181 // Initialize additional configurations for this server.182 if (languageName === 'pdll') {183 await this.preparePDLLServerOptions(workspaceFolder, configsToWatch,184 filepathsToWatch,185 additionalServerArgs);186 } else if (languageName == 'tablegen') {187 await this.prepareTableGenServerOptions(workspaceFolder, configsToWatch,188 filepathsToWatch,189 additionalServerArgs);190 }191 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);198 199 // Watch for configuration changes on this folder.200 await configWatcher.activate(this, workspaceFolder, configsToWatch,201 filepathsToWatch);202 return server;203 }204 205 /**206 * Start a new language client for the given language. Returns an array207 * containing the opened server, or null if the server could not be started,208 * and the resolved server path.209 */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';216 217 // Get the path of the lsp-server that is used to provide language218 // functionality.219 var serverPath =220 await this.resolveServerPath(serverSettingName, workspaceFolder);221 222 // If the server path is empty, bail. We don't emit errors if the user223 // hasn't explicitly configured the server.224 if (serverPath === '') {225 return [ null, serverPath ];226 }227 228 // Check that the file actually exists.229 if (!fs.existsSync(serverPath)) {230 vscode.window231 .showErrorMessage(232 `${clientTitle}: Unable to resolve path for '${233 serverSettingName}', please ensure the path is correct`,234 "Open Setting")235 .then((value) => {236 if (value === "Open Setting") {237 vscode.commands.executeCommand(238 'workbench.action.openWorkspaceSettings',239 {openToSide : false, query : `mlir.${serverSettingName}`});240 }241 });242 return [ null, serverPath ];243 }244 245 // Configure the server options.246 const serverOptions: vscodelc.ServerOptions = {247 command : serverPath,248 args : additionalServerArgs249 };250 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}/**/*`;257 }258 259 // Configure the middleware of the client. This is sort of abused to allow260 // for defining a "fallback" language server that operates on non-workspace261 // folders. Workspace folder language servers can properly filter out262 // documents not within the folder, but we can't effectively filter for263 // documents outside of the workspace. To support this, and avoid having two264 // servers targeting the same set of files, we use middleware to inject the265 // dynamic logic for checking if a document is in the workspace.266 let middleware = {};267 if (!workspaceFolder) {268 middleware = {269 didOpen : (document, next) : Promise<void> => {270 if (!vscode.workspace.getWorkspaceFolder(document.uri)) {271 return next(document);272 }273 return Promise.resolve();274 }275 };276 }277 278 // Configure the client options.279 const clientOptions: vscodelc.LanguageClientOptions = {280 documentSelector : [281 {language : languageName, pattern : selectorPattern},282 ],283 synchronize : {284 // Notify the server about file changes to language files contained in285 // the workspace.286 fileEvents : vscode.workspace.createFileSystemWatcher(filePattern)287 },288 outputChannel : outputChannel,289 workspaceFolder : workspaceFolder,290 middleware : middleware,291 292 // Don't switch to output window when the server returns output.293 revealOutputChannelOn : vscodelc.RevealOutputChannelOn.Never,294 };295 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 ];301 }302 303 /**304 * Given a server setting, return the default server path.305 */306 static getDefaultServerFilename(serverSettingName: string): string {307 if (serverSettingName === 'pdll_server_path') {308 return 'mlir-pdll-lsp-server';309 }310 if (serverSettingName === 'server_path') {311 return 'mlir-lsp-server';312 }313 if (serverSettingName === 'tablegen_server_path') {314 return 'tblgen-lsp-server';315 }316 return '';317 }318 319 /**320 * Try to resolve the given path, or the default path, with an optional321 * workspace folder. If a path could not be resolved, just returns the322 * input filePath.323 */324 async resolvePath(filePath: string, defaultPath: string,325 workspaceFolder: vscode.WorkspaceFolder): Promise<string> {326 const configPath = filePath;327 328 // If the path is already fully resolved, there is nothing to do.329 if (path.isAbsolute(filePath)) {330 return filePath;331 }332 333 // If a path hasn't been set, try to use the default path.334 if (filePath === '') {335 if (defaultPath === '') {336 return filePath;337 }338 filePath = defaultPath;339 340 // Fallthrough to try resolving the default path.341 }342 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);347 }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. The351 // file might not exist yet.352 return configPath;353 }354 // Otherwise, return the resolved path.355 return foundUris[0].fsPath;356 }357 358 /**359 * Try to resolve the path for the given server setting, with an optional360 * workspace folder.361 */362 async resolveServerPath(serverSettingName: string,363 workspaceFolder: vscode.WorkspaceFolder):364 Promise<string> {365 const serverPath = config.get<string>(serverSettingName, workspaceFolder);366 const defaultPath = MLIRContext.getDefaultServerFilename(serverSettingName);367 return this.resolvePath(serverPath, defaultPath, workspaceFolder);368 }369 370 /**371 * Return the language client for the given language and uri, or null if no372 * client is active.373 */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) {381 return null;382 }383 return folderContext.clients.get(languageName);384 }385 386 dispose() {387 this.subscriptions.forEach((d) => { d.dispose(); });388 this.subscriptions = [];389 this.workspaceFolders.forEach((d) => { d.dispose(); });390 this.workspaceFolders.clear();391 }392}393