diff --git a/mlir/cmake/modules/AddMLIR.cmake b/mlir/cmake/modules/AddMLIR.cmake --- a/mlir/cmake/modules/AddMLIR.cmake +++ b/mlir/cmake/modules/AddMLIR.cmake @@ -7,6 +7,10 @@ PARENT_SCOPE) endfunction() +# Clear out any pre-existing compile_commands file before processing. This +# allows for generating a clean compile_commands on each configure. +file(REMOVE ${CMAKE_BINARY_DIR}/pdll_compile_commands.yml) + # Declare a PDLL library in the current directory. function(add_mlir_pdll_library target inputFile ofn) set(LLVM_TARGET_DEFINITIONS ${inputFile}) @@ -15,6 +19,28 @@ set(TABLEGEN_OUTPUT ${TABLEGEN_OUTPUT} ${CMAKE_CURRENT_BINARY_DIR}/${ofn} PARENT_SCOPE) + # Get the current set of include paths for this pdll file. + cmake_parse_arguments(ARG "" "" "DEPENDS;EXTRA_INCLUDES" ${ARGN}) + get_directory_property(tblgen_includes INCLUDE_DIRECTORIES) + list(APPEND tblgen_includes ${ARG_EXTRA_INCLUDES}) + # Filter out any empty include items. + list(REMOVE_ITEM tblgen_includes "") + + # Build the absolute path for the current input file. + if (IS_ABSOLUTE ${LLVM_TARGET_DEFINITIONS}) + set(LLVM_TARGET_DEFINITIONS_ABSOLUTE ${inputFile}) + else() + set(LLVM_TARGET_DEFINITIONS_ABSOLUTE ${CMAKE_CURRENT_SOURCE_DIR}/${inputFile}) + endif() + + # Append the includes used for this file to the pdll_compilation_commands + # file. + file(APPEND ${CMAKE_BINARY_DIR}/pdll_compile_commands.yml + "--- !FileInfo:\n" + " filepath: \"${LLVM_TARGET_DEFINITIONS_ABSOLUTE}\"\n" + " includes: \"${CMAKE_CURRENT_SOURCE_DIR};${tblgen_includes}\"\n" + ) + add_public_tablegen_target(${target}) endfunction() diff --git a/mlir/lib/Tools/mlir-pdll-lsp-server/CMakeLists.txt b/mlir/lib/Tools/mlir-pdll-lsp-server/CMakeLists.txt --- a/mlir/lib/Tools/mlir-pdll-lsp-server/CMakeLists.txt +++ b/mlir/lib/Tools/mlir-pdll-lsp-server/CMakeLists.txt @@ -1,4 +1,5 @@ llvm_add_library(MLIRPdllLspServerLib + CompilationDatabase.cpp LSPServer.cpp PDLLServer.cpp MlirPdllLspServerMain.cpp diff --git a/mlir/lib/Tools/mlir-pdll-lsp-server/CompilationDatabase.h b/mlir/lib/Tools/mlir-pdll-lsp-server/CompilationDatabase.h new file mode 100644 --- /dev/null +++ b/mlir/lib/Tools/mlir-pdll-lsp-server/CompilationDatabase.h @@ -0,0 +1,58 @@ +//===- CompilationDatabase.h - PDLL Compilation Database --------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LIB_MLIR_TOOLS_MLIRPDLLSPSERVER_COMPILATIONDATABASE_H_ +#define LIB_MLIR_TOOLS_MLIRPDLLSPSERVER_COMPILATIONDATABASE_H_ + +#include "mlir/Support/LLVM.h" +#include "llvm/ADT/StringMap.h" +#include +#include + +namespace mlir { +namespace lsp { +/// This class contains a collection of compilation information for files +/// provided to the language server, such as the available include directories. +/// This database acts as an aggregate in-memory form of compilation databases +/// used by the current language client. The textual form of a compilation +/// database is a YAML file containing documents of the following form: +/// +/// --- !FileInfo: +/// filepath: - Absolute file path of the file. +/// includes: - Semi-colon delimited list of include directories. +/// +class CompilationDatabase { +public: + /// Compilation information for a specific file within the database. + struct FileInfo { + /// The absolute path to the file. + std::string filename; + /// The include directories available for the file. + std::vector includeDirs; + }; + + /// Construct a compilation database from the provided files containing YAML + /// descriptions of the database. + CompilationDatabase(ArrayRef databases); + + /// Get the compilation information for the provided file, or nullptr if the + /// database doesn't include information for `filename`. + const FileInfo *getFileInfo(StringRef filename) const; + +private: + /// Load the given database file into this database. + void loadDatabase(StringRef filename); + + /// A map of filename to file information for each known file within the + /// databases. + llvm::StringMap files; +}; +} // namespace lsp +} // namespace mlir + +#endif // LIB_MLIR_TOOLS_MLIRPDLLSPSERVER_COMPILATIONDATABASE_H_ diff --git a/mlir/lib/Tools/mlir-pdll-lsp-server/CompilationDatabase.cpp b/mlir/lib/Tools/mlir-pdll-lsp-server/CompilationDatabase.cpp new file mode 100644 --- /dev/null +++ b/mlir/lib/Tools/mlir-pdll-lsp-server/CompilationDatabase.cpp @@ -0,0 +1,81 @@ +//===- CompilationDatabase.cpp - PDLL Compilation Database ----------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "CompilationDatabase.h" +#include "../lsp-server-support/Logging.h" +#include "mlir/Support/FileUtilities.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/YAMLTraits.h" + +using namespace mlir; +using namespace mlir::lsp; + +//===----------------------------------------------------------------------===// +// CompilationDatabase +//===----------------------------------------------------------------------===// + +LLVM_YAML_IS_DOCUMENT_LIST_VECTOR(CompilationDatabase::FileInfo) + +namespace llvm { +namespace yaml { +template <> +struct MappingTraits { + static void mapping(IO &io, CompilationDatabase::FileInfo &info) { + io.mapRequired("filepath", info.filename); + + // Parse the includes from the yaml stream. These are in the form of a + // semi-colon delimited list. + std::string combinedIncludes; + io.mapRequired("includes", combinedIncludes); + for (StringRef include : llvm::split(combinedIncludes, ";")) { + if (!include.empty()) + info.includeDirs.push_back(include.str()); + } + } +}; +} // end namespace yaml +} // end namespace llvm + +CompilationDatabase::CompilationDatabase(ArrayRef databases) { + for (StringRef filename : databases) + loadDatabase(filename); +} + +const CompilationDatabase::FileInfo * +CompilationDatabase::getFileInfo(StringRef filename) const { + auto it = files.find(filename); + return it == files.end() ? nullptr : &it->second; +} + +void CompilationDatabase::loadDatabase(StringRef filename) { + if (filename.empty()) + return; + + // Set up the input file. + std::string errorMessage; + std::unique_ptr inputFile = + openInputFile(filename, &errorMessage); + if (!inputFile) { + Logger::error("Failed to open compilation database: {0}", errorMessage); + return; + } + llvm::yaml::Input yaml(inputFile->getBuffer()); + + // Parse the yaml description and add any new files to the database. + std::vector parsedFiles; + yaml >> parsedFiles; + for (auto &file : parsedFiles) { + auto it = files.try_emplace(file.filename, std::move(file)); + + // If we encounter a duplicate file, log a warning and ignore it. + if (!it.second) { + Logger::info("Duplicate .pdll file in compilation database: {0}", + file.filename); + } + } +} diff --git a/mlir/lib/Tools/mlir-pdll-lsp-server/MlirPdllLspServerMain.cpp b/mlir/lib/Tools/mlir-pdll-lsp-server/MlirPdllLspServerMain.cpp --- a/mlir/lib/Tools/mlir-pdll-lsp-server/MlirPdllLspServerMain.cpp +++ b/mlir/lib/Tools/mlir-pdll-lsp-server/MlirPdllLspServerMain.cpp @@ -54,6 +54,10 @@ llvm::cl::list extraIncludeDirs( "pdll-extra-dir", llvm::cl::desc("Extra directory of include files"), llvm::cl::value_desc("directory"), llvm::cl::Prefix); + llvm::cl::list compilationDatabases( + "pdll-compilation-database", + llvm::cl::desc("Compilation YAML databases containing additional " + "compilation information for .pdll files")); llvm::cl::ParseCommandLineOptions(argc, argv, "PDLL LSP Language Server"); @@ -71,7 +75,7 @@ JSONTransport transport(stdin, llvm::outs(), inputStyle, prettyPrint); // Configure the servers and start the main language server. - PDLLServer::Options options(extraIncludeDirs); + PDLLServer::Options options(compilationDatabases, extraIncludeDirs); PDLLServer server(options); return runPdllLSPServer(server, transport); } diff --git a/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.h b/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.h --- a/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.h +++ b/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.h @@ -10,12 +10,14 @@ #define LIB_MLIR_TOOLS_MLIRPDLLSPSERVER_SERVER_H_ #include "mlir/Support/LLVM.h" +#include "llvm/ADT/StringRef.h" #include #include namespace mlir { namespace lsp { struct Diagnostic; +class CompilationDatabase; struct CompletionList; struct DocumentSymbol; struct Hover; @@ -30,7 +32,13 @@ class PDLLServer { public: struct Options { - Options(const std::vector &extraDirs) : extraDirs(extraDirs){}; + Options(const std::vector &compilationDatabases, + const std::vector &extraDirs) + : compilationDatabases(compilationDatabases), extraDirs(extraDirs) {} + + /// The filenames for databases containing compilation commands for PDLL + /// files passed to the server. + const std::vector &compilationDatabases; /// Additional list of include directories to search. const std::vector &extraDirs; diff --git a/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.cpp b/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.cpp --- a/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.cpp +++ b/mlir/lib/Tools/mlir-pdll-lsp-server/PDLLServer.cpp @@ -10,6 +10,7 @@ #include "../lsp-server-support/Logging.h" #include "../lsp-server-support/Protocol.h" +#include "CompilationDatabase.h" #include "mlir/Tools/PDLL/AST/Context.h" #include "mlir/Tools/PDLL/AST/Nodes.h" #include "mlir/Tools/PDLL/AST/Types.h" @@ -325,7 +326,7 @@ return; } - // TODO: Properly provide include directories from the client. + // Build the set of include directories for this file. llvm::SmallString<32> uriDirectory(uri.file()); llvm::sys::path::remove_filename(uriDirectory); includeDirs.push_back(uriDirectory.str().str()); @@ -1225,11 +1226,16 @@ //===----------------------------------------------------------------------===// struct lsp::PDLLServer::Impl { - explicit Impl(const Options &options) : options(options) {} + explicit Impl(const Options &options) + : options(options), compilationDatabase(options.compilationDatabases) {} /// PDLL LSP options. const Options &options; + /// The compilation database containing additional information for files + /// passed to the server. + lsp::CompilationDatabase compilationDatabase; + /// The files held by the server, mapped by their URI file name. llvm::StringMap> files; }; @@ -1245,8 +1251,12 @@ void lsp::PDLLServer::addOrUpdateDocument( const URIForFile &uri, StringRef contents, int64_t version, std::vector &diagnostics) { + std::vector additionalIncludeDirs = impl->options.extraDirs; + if (auto *fileInfo = impl->compilationDatabase.getFileInfo(uri.file())) + llvm::append_range(additionalIncludeDirs, fileInfo->includeDirs); + impl->files[uri.file()] = std::make_unique( - uri, contents, version, impl->options.extraDirs, diagnostics); + uri, contents, version, additionalIncludeDirs, diagnostics); } Optional lsp::PDLLServer::removeDocument(const URIForFile &uri) { diff --git a/mlir/test/mlir-pdll-lsp-server/compilation_database.test b/mlir/test/mlir-pdll-lsp-server/compilation_database.test new file mode 100644 --- /dev/null +++ b/mlir/test/mlir-pdll-lsp-server/compilation_database.test @@ -0,0 +1,21 @@ +// RUN: echo -e '--- !FileInfo:\n filepath: "/foo.pdll"\n includes: "%S;%S/../../include"' > %t.yml +// RUN: mlir-pdll-lsp-server -pdll-compilation-database=%t.yml -lit-test < %s | FileCheck %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"pdll","capabilities":{},"trace":"off"}} +// ----- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{ + "uri":"test:///foo.pdll", + "languageId":"pdll", + "version":1, + "text":"#include \"include/included.td\"\n#include \"include/included.pdll\"" +}}} +// Check that we can properly process the includes without errors. +// CHECK: "method": "textDocument/publishDiagnostics", +// CHECK-NEXT: "params": { +// CHECK-NEXT: "diagnostics": [], +// CHECK-NEXT: "uri": "test:///foo.pdll", +// CHECK-NEXT: "version": 1 +// CHECK-NEXT: } +// ----- +{"jsonrpc":"2.0","id":7,"method":"shutdown"} +// ----- +{"jsonrpc":"2.0","method":"exit"} diff --git a/mlir/test/mlir-pdll-lsp-server/include/included.pdll b/mlir/test/mlir-pdll-lsp-server/include/included.pdll new file mode 100644 --- /dev/null +++ b/mlir/test/mlir-pdll-lsp-server/include/included.pdll @@ -0,0 +1,2 @@ +// This file is merely to test the processing of includes, it has +// no other purpose or contents. diff --git a/mlir/test/mlir-pdll-lsp-server/include/included.td b/mlir/test/mlir-pdll-lsp-server/include/included.td new file mode 100644 --- /dev/null +++ b/mlir/test/mlir-pdll-lsp-server/include/included.td @@ -0,0 +1,4 @@ +include "mlir/IR/OpBase.td" + +// This file is merely to test the processing of includes, it has +// no other purpose or contents. diff --git a/mlir/test/mlir-pdll-lsp-server/lit.local.cfg b/mlir/test/mlir-pdll-lsp-server/lit.local.cfg new file mode 100644 --- /dev/null +++ b/mlir/test/mlir-pdll-lsp-server/lit.local.cfg @@ -0,0 +1 @@ +config.excludes = ['include'] diff --git a/mlir/utils/vscode/package.json b/mlir/utils/vscode/package.json --- a/mlir/utils/vscode/package.json +++ b/mlir/utils/vscode/package.json @@ -124,6 +124,11 @@ "type": "string", "description": "The file path of the mlir-pdll-lsp-server executable." }, + "mlir.pdll_compilation_databases": { + "scope": "resource", + "type": "array", + "description": "A list of `pdll_compile_commands.yml` database files containing information about .pdll files processed by the server." + }, "mlir.onSettingsChanged": { "type": "string", "default": "prompt", diff --git a/mlir/utils/vscode/src/configWatcher.ts b/mlir/utils/vscode/src/configWatcher.ts --- a/mlir/utils/vscode/src/configWatcher.ts +++ b/mlir/utils/vscode/src/configWatcher.ts @@ -41,41 +41,45 @@ * Activate watchers that track configuration changes for the given workspace * folder, or null if the workspace is top-level. */ -export async function activate(mlirContext: MLIRContext, - workspaceFolder: vscode.WorkspaceFolder, - serverSetting: string, serverPath: string) { +export async function activate( + mlirContext: MLIRContext, workspaceFolder: vscode.WorkspaceFolder, + serverSettings: string[], serverPaths: string[]) { // When a configuration change happens, check to see if we should restart the // server. mlirContext.subscriptions.push(vscode.workspace.onDidChangeConfiguration(event => { - const expandedSetting = `mlir.${serverSetting}`; - if (event.affectsConfiguration(expandedSetting, workspaceFolder)) { - promptRestart( - 'onSettingsChanged', - `setting '${ - expandedSetting}' has changed. Do you want to reload the server?`); + for (const serverSetting of serverSettings) { + const expandedSetting = `mlir.${serverSetting}`; + if (event.affectsConfiguration(expandedSetting, workspaceFolder)) { + promptRestart( + 'onSettingsChanged', + `setting '${ + expandedSetting}' has changed. Do you want to reload the server?`); + } } })); - // If the server path actually exists, track it in case it changes. Check that - // the path actually exists. - if (serverPath === '') { - return; - } - + // Setup watchers for the provided server paths. const fileWatcherConfig = { disableGlobbing : true, followSymlinks : true, ignoreInitial : true, awaitWriteFinish : true, }; - const fileWatcher = chokidar.watch(serverPath, fileWatcherConfig); - fileWatcher.on('all', (event, _filename, _details) => { - if (event != 'unlink') { - promptRestart( - 'onSettingsChanged', - 'MLIR language server binary has changed. Do you want to reload the server?'); + for (const serverPath of serverPaths) { + if (serverPath === '') { + return; } - }); - mlirContext.subscriptions.push( - new vscode.Disposable(() => { fileWatcher.close(); })); + + // If the server path actually exists, track it in case it changes. + const fileWatcher = chokidar.watch(serverPath, fileWatcherConfig); + fileWatcher.on('all', (event, _filename, _details) => { + if (event != 'unlink') { + promptRestart( + 'onSettingsChanged', + 'MLIR language server file has changed. Do you want to reload the server?'); + } + }); + mlirContext.subscriptions.push( + new vscode.Disposable(() => { fileWatcher.close(); })); + } } diff --git a/mlir/utils/vscode/src/mlirContext.ts b/mlir/utils/vscode/src/mlirContext.ts --- a/mlir/utils/vscode/src/mlirContext.ts +++ b/mlir/utils/vscode/src/mlirContext.ts @@ -85,6 +85,43 @@ })); } + /** + * 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[]) { + // Process the compilation databases attached for the workspace folder. + let databases = + config.get('pdll_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/pdll_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('pdll_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) => `--pdll-compilation-database=${database}`)); + } + /** * Activate the language client for the given language in the given workspace * folder. @@ -93,12 +130,27 @@ 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); + } + + // Try to activate the language client. const [server, serverPath] = await this.startLanguageClient( - workspaceFolder, outputChannel, serverSettingName, languageName); + workspaceFolder, outputChannel, serverSettingName, languageName, + additionalServerArgs); + configsToWatch.push(serverSettingName); + filepathsToWatch.push(serverPath); // Watch for configuration changes on this folder. - await configWatcher.activate(this, workspaceFolder, serverSettingName, - serverPath); + await configWatcher.activate(this, workspaceFolder, configsToWatch, + filepathsToWatch); return server; } @@ -109,7 +161,8 @@ */ async startLanguageClient(workspaceFolder: vscode.WorkspaceFolder, outputChannel: vscode.OutputChannel, - serverSettingName: string, languageName: string): + serverSettingName: string, languageName: string, + additionalServerArgs: string[]): Promise<[ vscodelc.LanguageClient, string ]> { const clientTitle = languageName.toUpperCase() + ' Language Client'; @@ -146,12 +199,12 @@ run : { command : serverPath, transport : vscodelc.TransportKind.stdio, - args : [] + args : additionalServerArgs }, debug : { command : serverPath, transport : vscodelc.TransportKind.stdio, - args : [] + args : additionalServerArgs } }; @@ -217,45 +270,56 @@ } /** - * Try to resolve the path for the given server setting, with an optional - * workspace folder. + * 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 resolveServerPath(serverSettingName: string, - workspaceFolder: vscode.WorkspaceFolder): - Promise { - const configServerPath = - config.get(serverSettingName, workspaceFolder); - let serverPath = configServerPath; + 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(serverPath)) { - return serverPath; + if (path.isAbsolute(filePath)) { + return filePath; } // If a path hasn't been set, try to use the default path. - if (serverPath === '') { - serverPath = MLIRContext.getDefaultServerFilename(serverSettingName); - if (serverPath === '') { - return serverPath; + 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 = '**/' + serverPath; + 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 current configuration path - // anyways. The file might not exist yet. - return configServerPath; + // 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); + } + dispose() { this.subscriptions.forEach((d) => { d.dispose(); }); this.subscriptions = [];