import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; import * as config from './config'; import * as configWatcher from './configWatcher'; /** * This class represents the context of a specific workspace folder. */ class WorkspaceFolderContext implements vscode.Disposable { dispose() { this.clients.forEach(async client => await client.stop()); this.clients.clear(); } clients: Map = new Map(); } /** * This class manages all of the MLIR extension state, * including the language client. */ export class MLIRContext implements vscode.Disposable { subscriptions: vscode.Disposable[] = []; workspaceFolders: Map = new Map(); outputChannel: vscode.OutputChannel; /** * Activate the MLIR context, and start the language clients. */ async activate(outputChannel: vscode.OutputChannel) { this.outputChannel = outputChannel; // This lambda is used to lazily start language clients for the given // document. It removes the need to pro-actively start language clients for // every folder within the workspace and every language type we provide. const startClientOnOpenDocument = async (document: vscode.TextDocument) => { await this.getOrActivateLanguageClient(document.uri, document.languageId); }; // Process any existing documents. for (const textDoc of vscode.workspace.textDocuments) { await startClientOnOpenDocument(textDoc); } // Watch any new documents to spawn servers when necessary. this.subscriptions.push( vscode.workspace.onDidOpenTextDocument(startClientOnOpenDocument)); this.subscriptions.push( vscode.workspace.onDidChangeWorkspaceFolders((event) => { for (const folder of event.removed) { const client = this.workspaceFolders.get(folder.uri.toString()); if (client) { client.dispose(); this.workspaceFolders.delete(folder.uri.toString()); } } })); } /** * Open or return a language server for the given uri and language. */ async getOrActivateLanguageClient(uri: vscode.Uri, languageId: string): Promise { let serverSettingName: string; if (languageId === 'mlir') { serverSettingName = 'server_path'; } else if (languageId === 'pdll') { serverSettingName = 'pdll_server_path'; } else if (languageId === 'tablegen') { serverSettingName = 'tablegen_server_path'; } else { return null; } // Check the scheme of the uri. let validSchemes = [ 'file', 'mlir.bytecode-mlir' ]; if (!validSchemes.includes(uri.scheme)) { return null; } // Resolve the workspace folder if this document is in one. We use the // workspace folder when determining if a server needs to be started. let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); let workspaceFolderStr = workspaceFolder ? workspaceFolder.uri.toString() : ""; // Get or create a client context for this folder. let folderContext = this.workspaceFolders.get(workspaceFolderStr); if (!folderContext) { folderContext = new WorkspaceFolderContext(); this.workspaceFolders.set(workspaceFolderStr, folderContext); } // Start the client for this language if necessary. let client = folderContext.clients.get(languageId); if (!client) { client = await this.activateWorkspaceFolder( workspaceFolder, serverSettingName, languageId, this.outputChannel); folderContext.clients.set(languageId, client); } return client; } /** * Prepare a compilation database option for a server. */ async prepareCompilationDatabaseServerOptions( languageName: string, workspaceFolder: vscode.WorkspaceFolder, configsToWatch: string[], pathsToWatch: string[], additionalServerArgs: string[]) { // Process the compilation databases attached for the workspace folder. let databases = config.get( `${languageName}_compilation_databases`, workspaceFolder, []); // If no databases were explicitly specified, default to a database in the // 'build' directory within the current workspace. if (databases.length === 0) { if (workspaceFolder) { databases.push(workspaceFolder.uri.fsPath + `/build/${languageName}_compile_commands.yml`); } // Otherwise, try to resolve each of the paths. } else { for await (let database of databases) { database = await this.resolvePath(database, '', workspaceFolder); } } configsToWatch.push(`${languageName}_compilation_databases`); pathsToWatch.push(...databases); // Setup the compilation databases as additional arguments to pass to the // server. databases.filter(database => database !== ''); additionalServerArgs.push(...databases.map( (database) => `--${languageName}-compilation-database=${database}`)); } /** * Prepare the server options for a PDLL server, e.g. populating any * accessible compilation databases. */ async preparePDLLServerOptions(workspaceFolder: vscode.WorkspaceFolder, configsToWatch: string[], pathsToWatch: string[], additionalServerArgs: string[]) { await this.prepareCompilationDatabaseServerOptions( 'pdll', workspaceFolder, configsToWatch, pathsToWatch, additionalServerArgs); } /** * Prepare the server options for a TableGen server, e.g. populating any * accessible compilation databases. */ async prepareTableGenServerOptions(workspaceFolder: vscode.WorkspaceFolder, configsToWatch: string[], pathsToWatch: string[], additionalServerArgs: string[]) { await this.prepareCompilationDatabaseServerOptions( 'tablegen', workspaceFolder, configsToWatch, pathsToWatch, additionalServerArgs); } /** * Activate the language client for the given language in the given workspace * folder. */ async activateWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder, serverSettingName: string, languageName: string, outputChannel: vscode.OutputChannel): Promise { let configsToWatch: string[] = []; let filepathsToWatch: string[] = []; let additionalServerArgs: string[] = []; // Initialize additional configurations for this server. if (languageName === 'pdll') { await this.preparePDLLServerOptions(workspaceFolder, configsToWatch, filepathsToWatch, additionalServerArgs); } else if (languageName == 'tablegen') { await this.prepareTableGenServerOptions(workspaceFolder, configsToWatch, filepathsToWatch, additionalServerArgs); } // Try to activate the language client. const [server, serverPath] = await this.startLanguageClient( workspaceFolder, outputChannel, serverSettingName, languageName, additionalServerArgs); configsToWatch.push(serverSettingName); filepathsToWatch.push(serverPath); // Watch for configuration changes on this folder. await configWatcher.activate(this, workspaceFolder, configsToWatch, filepathsToWatch); return server; } /** * Start a new language client for the given language. Returns an array * containing the opened server, or null if the server could not be started, * and the resolved server path. */ async startLanguageClient(workspaceFolder: vscode.WorkspaceFolder, outputChannel: vscode.OutputChannel, serverSettingName: string, languageName: string, additionalServerArgs: string[]): Promise<[ vscodelc.LanguageClient, string ]> { const clientTitle = languageName.toUpperCase() + ' Language Client'; // Get the path of the lsp-server that is used to provide language // functionality. var serverPath = await this.resolveServerPath(serverSettingName, workspaceFolder); // If the server path is empty, bail. We don't emit errors if the user // hasn't explicitly configured the server. if (serverPath === '') { return [ null, serverPath ]; } // Check that the file actually exists. if (!fs.existsSync(serverPath)) { vscode.window .showErrorMessage( `${clientTitle}: Unable to resolve path for '${ serverSettingName}', please ensure the path is correct`, "Open Setting") .then((value) => { if (value === "Open Setting") { vscode.commands.executeCommand( 'workbench.action.openWorkspaceSettings', {openToSide : false, query : `mlir.${serverSettingName}`}); } }); return [ null, serverPath ]; } // Configure the server options. const serverOptions: vscodelc.ServerOptions = { command : serverPath, args : additionalServerArgs }; // Configure file patterns relative to the workspace folder. let filePattern: vscode.GlobPattern = '**/*.' + languageName; let selectorPattern: string = null; if (workspaceFolder) { filePattern = new vscode.RelativePattern(workspaceFolder, filePattern); selectorPattern = `${workspaceFolder.uri.fsPath}/**/*`; } // Configure the middleware of the client. This is sort of abused to allow // for defining a "fallback" language server that operates on non-workspace // folders. Workspace folder language servers can properly filter out // documents not within the folder, but we can't effectively filter for // documents outside of the workspace. To support this, and avoid having two // servers targeting the same set of files, we use middleware to inject the // dynamic logic for checking if a document is in the workspace. let middleware = {}; if (!workspaceFolder) { middleware = { didOpen : (document, next) : Promise => { if (!vscode.workspace.getWorkspaceFolder(document.uri)) { return next(document); } return Promise.resolve(); } }; } // Configure the client options. const clientOptions: vscodelc.LanguageClientOptions = { documentSelector : [ {language : languageName, pattern : selectorPattern}, ], synchronize : { // Notify the server about file changes to language files contained in // the workspace. fileEvents : vscode.workspace.createFileSystemWatcher(filePattern) }, outputChannel : outputChannel, workspaceFolder : workspaceFolder, middleware : middleware, // Don't switch to output window when the server returns output. revealOutputChannelOn : vscodelc.RevealOutputChannelOn.Never, }; // Create the language client and start the client. let languageClient = new vscodelc.LanguageClient( languageName + '-lsp', clientTitle, serverOptions, clientOptions); languageClient.start(); return [ languageClient, serverPath ]; } /** * Given a server setting, return the default server path. */ static getDefaultServerFilename(serverSettingName: string): string { if (serverSettingName === 'pdll_server_path') { return 'mlir-pdll-lsp-server'; } if (serverSettingName === 'server_path') { return 'mlir-lsp-server'; } if (serverSettingName === 'tablegen_server_path') { return 'tblgen-lsp-server'; } return ''; } /** * Try to resolve the given path, or the default path, with an optional * workspace folder. If a path could not be resolved, just returns the * input filePath. */ async resolvePath(filePath: string, defaultPath: string, workspaceFolder: vscode.WorkspaceFolder): Promise { const configPath = filePath; // If the path is already fully resolved, there is nothing to do. if (path.isAbsolute(filePath)) { return filePath; } // If a path hasn't been set, try to use the default path. if (filePath === '') { if (defaultPath === '') { return filePath; } filePath = defaultPath; // Fallthrough to try resolving the default path. } // Try to resolve the path relative to the workspace. let filePattern: vscode.GlobPattern = '**/' + filePath; if (workspaceFolder) { filePattern = new vscode.RelativePattern(workspaceFolder, filePattern); } let foundUris = await vscode.workspace.findFiles(filePattern, null, 1); if (foundUris.length === 0) { // If we couldn't resolve it, just return the original path anyways. The // file might not exist yet. return configPath; } // Otherwise, return the resolved path. return foundUris[0].fsPath; } /** * Try to resolve the path for the given server setting, with an optional * workspace folder. */ async resolveServerPath(serverSettingName: string, workspaceFolder: vscode.WorkspaceFolder): Promise { const serverPath = config.get(serverSettingName, workspaceFolder); const defaultPath = MLIRContext.getDefaultServerFilename(serverSettingName); return this.resolvePath(serverPath, defaultPath, workspaceFolder); } /** * Return the language client for the given language and uri, or null if no * client is active. */ getLanguageClient(uri: vscode.Uri, languageName: string): vscodelc.LanguageClient { let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); let workspaceFolderStr = workspaceFolder ? workspaceFolder.uri.toString() : ""; let folderContext = this.workspaceFolders.get(workspaceFolderStr); if (!folderContext) { return null; } return folderContext.clients.get(languageName); } dispose() { this.subscriptions.forEach((d) => { d.dispose(); }); this.subscriptions = []; this.workspaceFolders.forEach((d) => { d.dispose(); }); this.workspaceFolders.clear(); } }