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 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); 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 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() : ""; 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 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`); 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 the 136 // 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 any 144 * 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 any 157 * 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 workspace 170 * 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 array 207 * 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 language 218 // 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 user 223 // 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.window 231 .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 : additionalServerArgs 249 }; 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 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. 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 in 285 // 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 optional 321 * workspace folder. If a path could not be resolved, just returns the 322 * 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. The 351 // 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 optional 360 * 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 no 372 * 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