From 893edc763474c0aa1e438173790dfb11bd0d44a5 Mon Sep 17 00:00:00 2001 From: ykiko Date: Wed, 30 Jul 2025 23:41:03 +0800 Subject: [PATCH] Publish diagnostics (#164) --- .github/workflows/cmake.yml | 4 +- .github/workflows/xmake.yml | 2 + include/Compiler/Command.h | 6 +- include/Compiler/Compilation.h | 7 + include/Compiler/CompilationUnit.h | 2 +- include/Compiler/Diagnostic.h | 44 +- include/Compiler/Module.h | 1 - include/Feature/Diagnostic.h | 19 + include/Feature/DocumentSymbol.h | 1 - include/Protocol/Basic.h | 126 +++++ include/Protocol/Feature/CallHierarchy.h | 11 + include/Protocol/Feature/CodeAction.h | 11 + include/Protocol/Feature/CodeCompletion.h | 39 ++ include/Protocol/Feature/CodeLens.h | 11 + include/Protocol/Feature/Declaration.h | 11 + include/Protocol/Feature/Definition.h | 11 + include/Protocol/Feature/Diagnostic.h | 82 +++ include/Protocol/Feature/DocumentHighlight.h | 11 + include/Protocol/Feature/DocumentLink.h | 11 + include/Protocol/Feature/DocumentSymbol.h | 11 + include/Protocol/Feature/FoldingRange.h | 11 + include/Protocol/Feature/Formatting.h | 19 + include/Protocol/Feature/Hover.h | 11 + include/Protocol/Feature/Implementation.h | 11 + include/Protocol/Feature/InlayHint.h | 15 + include/Protocol/Feature/Reference.h | 11 + include/Protocol/Feature/Rename.h | 11 + include/Protocol/Feature/SemanticTokens.h | 36 ++ include/Protocol/Feature/SignatureHelp.h | 11 + include/Protocol/Feature/TypeDefinition.h | 11 + include/Protocol/Feature/TypeHierarchy.h | 11 + include/Protocol/Initialize.h | 186 +++++++ include/Protocol/Notebook.h | 9 + include/Protocol/Protocol.h | 3 + include/Protocol/TextDocument.h | 201 +++++++ include/Protocol/Workspace.h | 30 ++ include/Server/Convert.h | 267 ++++++++++ include/Server/LSPConverter.h | 65 --- include/Server/Protocol.h | 229 -------- include/Server/Scheduler.h | 74 --- include/Server/Server.h | 114 +++- include/Support/JSON.h | 7 +- include/Support/TypeTraits.h | 45 ++ include/Test/CTest.h | 2 +- src/Compiler/Command.cpp | 66 ++- src/Compiler/Compilation.cpp | 157 +++--- src/Compiler/Diagnostic.cpp | 265 ++++++---- src/Driver/clice.cc | 2 +- src/Feature/Diagnostic.cpp | 122 +++++ src/Server/Convert.cpp | 118 +++++ src/Server/Document.cpp | 215 ++++++++ src/Server/Feature.cpp | 109 ++-- src/Server/LSPConverter.cpp | 518 ------------------- src/Server/Lifecycle.cpp | 59 +++ src/Server/Scheduler.cpp | 212 -------- src/Server/Server.cpp | 107 +--- tests/integration/test_file_operation.py | 2 +- tests/unit/Compiler/Command.cpp | 6 +- tests/unit/Compiler/Diagnostic.cpp | 99 ++++ tests/unit/Compiler/Module.cpp | 2 +- tests/unit/Server/LSPConverter.cpp | 4 +- 61 files changed, 2379 insertions(+), 1495 deletions(-) create mode 100644 include/Feature/Diagnostic.h create mode 100644 include/Protocol/Basic.h create mode 100644 include/Protocol/Feature/CallHierarchy.h create mode 100644 include/Protocol/Feature/CodeAction.h create mode 100644 include/Protocol/Feature/CodeCompletion.h create mode 100644 include/Protocol/Feature/CodeLens.h create mode 100644 include/Protocol/Feature/Declaration.h create mode 100644 include/Protocol/Feature/Definition.h create mode 100644 include/Protocol/Feature/Diagnostic.h create mode 100644 include/Protocol/Feature/DocumentHighlight.h create mode 100644 include/Protocol/Feature/DocumentLink.h create mode 100644 include/Protocol/Feature/DocumentSymbol.h create mode 100644 include/Protocol/Feature/FoldingRange.h create mode 100644 include/Protocol/Feature/Formatting.h create mode 100644 include/Protocol/Feature/Hover.h create mode 100644 include/Protocol/Feature/Implementation.h create mode 100644 include/Protocol/Feature/InlayHint.h create mode 100644 include/Protocol/Feature/Reference.h create mode 100644 include/Protocol/Feature/Rename.h create mode 100644 include/Protocol/Feature/SemanticTokens.h create mode 100644 include/Protocol/Feature/SignatureHelp.h create mode 100644 include/Protocol/Feature/TypeDefinition.h create mode 100644 include/Protocol/Feature/TypeHierarchy.h create mode 100644 include/Protocol/Initialize.h create mode 100644 include/Protocol/Notebook.h create mode 100644 include/Protocol/Protocol.h create mode 100644 include/Protocol/TextDocument.h create mode 100644 include/Protocol/Workspace.h create mode 100644 include/Server/Convert.h delete mode 100644 include/Server/LSPConverter.h delete mode 100644 include/Server/Protocol.h delete mode 100644 include/Server/Scheduler.h create mode 100644 src/Feature/Diagnostic.cpp create mode 100644 src/Server/Convert.cpp create mode 100644 src/Server/Document.cpp delete mode 100644 src/Server/LSPConverter.cpp create mode 100644 src/Server/Lifecycle.cpp delete mode 100644 src/Server/Scheduler.cpp diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ddb7a2af..ca63ff51 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -110,11 +110,11 @@ jobs: if: matrix.os == 'windows-2025' run: | ./build/bin/unit_tests.exe --test-dir="./tests/data" --resource-dir="./.llvm/lib/clang/20" - pytest -s tests/integration --executable=./build/bin/clice.exe --resource-dir="./.llvm/lib/clang/20" + pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice.exe --resource-dir="./.llvm/lib/clang/20" - name: Run tests if: matrix.os == 'ubuntu-24.04' || matrix.os == 'macos-15' run: | ./build/bin/unit_tests --test-dir="./tests/data" --resource-dir="./.llvm/lib/clang/20" - pytest -s tests/integration --executable=./build/bin/clice --resource-dir="./.llvm/lib/clang/20" + pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice --resource-dir="./.llvm/lib/clang/20" diff --git a/.github/workflows/xmake.yml b/.github/workflows/xmake.yml index edb33efd..e85329c3 100644 --- a/.github/workflows/xmake.yml +++ b/.github/workflows/xmake.yml @@ -138,4 +138,6 @@ jobs: - name: Run tests run: | + # Workaround for MacOS + export PATH="/opt/homebrew/opt/llvm@20/bin:/opt/homebrew/opt/lld@20/bin:$PATH" xmake test --verbose diff --git a/include/Compiler/Command.h b/include/Compiler/Command.h index 535d8266..a897148f 100644 --- a/include/Compiler/Command.h +++ b/include/Compiler/Command.h @@ -79,8 +79,10 @@ public: auto load_commands(this Self& self, llvm::StringRef json_content) -> std::expected, std::string>; - auto get_command(this Self& self, llvm::StringRef file, bool resource_dir = false) - -> LookupInfo; + auto get_command(this Self& self, + llvm::StringRef file, + bool resource_dir = false, + bool query_driver = false) -> LookupInfo; private: /// The memory pool to hold all cstring and command list. diff --git a/include/Compiler/Compilation.h b/include/Compiler/Compilation.h index 708478a4..42aeb390 100644 --- a/include/Compiler/Compilation.h +++ b/include/Compiler/Compilation.h @@ -34,6 +34,13 @@ struct CompilationParams { /// The memory buffers for all remapped file. llvm::StringMap> buffers; + /// A flag to inform to stop compilation, this is very useful + /// to cancel old compilation task. + std::shared_ptr stop; + + /// Store all compilation errors in the process. + std::shared_ptr> diagnostics; + void add_remapped_file(llvm::StringRef path, llvm::StringRef content, std::uint32_t bound = -1) { diff --git a/include/Compiler/CompilationUnit.h b/include/Compiler/CompilationUnit.h index 3cf5d7b2..50b22204 100644 --- a/include/Compiler/CompilationUnit.h +++ b/include/Compiler/CompilationUnit.h @@ -1,7 +1,7 @@ #pragma once #include "Directive.h" -#include "Diagnostic.h" +#include "Compiler/Diagnostic.h" #include "AST/SymbolID.h" #include "AST/SourceCode.h" #include "AST/Resolver.h" diff --git a/include/Compiler/Diagnostic.h b/include/Compiler/Diagnostic.h index 41800884..2bea73dc 100644 --- a/include/Compiler/Diagnostic.h +++ b/include/Compiler/Diagnostic.h @@ -20,24 +20,52 @@ enum class DiagnosticLevel : std::uint8_t { Invalid, }; -struct Diagnostic { - /// The diagnostic id. - std::uint32_t id; +enum class DiagnosticSource : std::uint8_t { + Unknown, + Clang, + ClangTidy, + Clice, +}; + +struct DiagnosticID { + /// The diagnostic id value. + std::uint32_t value; /// The level of this diagnostic. DiagnosticLevel level; + /// The source of diagnostic. + DiagnosticSource source; + + llvm::StringRef name; + + /// Get the diagnostic code. + llvm::StringRef diagnostic_code() const; + + /// Get help diagnostic uri for the diagnostic. + std::optional diagnostic_document_uri() const; + + /// Whether this diagnostic represents an deprecated diagnostic. + bool is_deprecated() const; + + /// Whether this diagnostic represents an unused diagnostic. + bool is_unused() const; +}; + +struct Diagnostic { + /// The diagnostic id. + DiagnosticID id; + + /// The file location of this diagnostic. + clang::FileID fid; + /// The source range of this diagnostic(may be invalid, if this diagnostic /// is from command line. e.g. unknown command line argument). - clang::SourceRange range; + LocalSourceRange range; /// The error message of this diagnostic. std::string message; - /// TODO: Collect fix it of diagnostics. - - static llvm::StringRef diagnostic_code(std::uint32_t id); - static clang::DiagnosticConsumer* create(std::shared_ptr> diagnostics); }; diff --git a/include/Compiler/Module.h b/include/Compiler/Module.h index 518608ba..cb563714 100644 --- a/include/Compiler/Module.h +++ b/include/Compiler/Module.h @@ -3,7 +3,6 @@ #include #include #include - #include "Support/Struct.h" namespace clice { diff --git a/include/Feature/Diagnostic.h b/include/Feature/Diagnostic.h new file mode 100644 index 00000000..1589aae4 --- /dev/null +++ b/include/Feature/Diagnostic.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Compiler/Diagnostic.h" +#include "Server/Convert.h" +#include "Support/JSON.h" + +namespace clice { + +struct CompilationUnit; + +} + +namespace clice::feature { + +/// FIXME: This is not correct way, we don't want to couple +/// `Feature with Protocol`? Return an array of LSP diagnostic. +json::Value diagnostics(PositionEncodingKind kind, PathMapping mapping, CompilationUnit& unit); + +} // namespace clice::feature diff --git a/include/Feature/DocumentSymbol.h b/include/Feature/DocumentSymbol.h index 5453704b..1d200a35 100644 --- a/include/Feature/DocumentSymbol.h +++ b/include/Feature/DocumentSymbol.h @@ -1,6 +1,5 @@ #pragma once -#include "Server/Protocol.h" #include "AST/SourceCode.h" #include "AST/SymbolKind.h" #include "Index/Shared.h" diff --git a/include/Protocol/Basic.h b/include/Protocol/Basic.h new file mode 100644 index 00000000..db9427fc --- /dev/null +++ b/include/Protocol/Basic.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include + +namespace clice::proto { + +using integer = std::int32_t; +/// range in [0, 2^31- 1] +using uinteger = std::uint32_t; +using decimal = double; + +using string = std::string; + +template +using array = std::vector; + +template +using optional = std::optional; + +using PositionEncodingKind = string; + +struct WorkDoneProgressOptions { + bool workDoneProgress; +}; + +using URI = string; +using DocumentUri = string; + +enum class ErrorCodes : integer { + /// Defined by JSON-RPC. + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + /// JSON-RPC error code indicating a server error. + serverErrorStart = -32099, + serverErrorEnd = -32000, + ServerNotInitialized = -32002, + UnknownErrorCode = -32001, + + /// Defined by the protocol. + RequestFailed = -32803, + ServerCancelled = -32802, + ContentModified = -32801, + RequestCancelled = -32800 +}; + +struct Position { + /// Line position in a document (zero-based). + uinteger line; + + /// Character offset on a line in a document (zero-based). + /// The meaning of this offset is determined by the negotiated + /// `PositionEncodingKind`. + uinteger character; + + constexpr friend bool operator== (const Position&, const Position&) = default; +}; + +struct Range { + /// The range's start position. + Position start; + + /// The range's end position. + Position end; + + constexpr friend bool operator== (const Range&, const Range&) = default; +}; + +struct Location { + DocumentUri uri; + + Range range; +}; + +struct TextEdit { + /// The range of the text document to be manipulated. To insert + /// text into a document create a range where start === end. + Range range; + + // The string to be inserted. For delete operations use an + // empty string. + string newText; +}; + +struct TextDocumentItem { + /// The text document's URI. + DocumentUri uri; + + /// The text document's language identifier. + string languageId; + + /// The version number of this document (it will strictly increase after each + /// change, including undo/redo). + uinteger version; + + /// The content of the opened text document. + string text; +}; + +struct TextDocumentIdentifier { + /// The text document's URI. + DocumentUri uri; +}; + +struct VersionedTextDocumentIdentifier { + /// The text document's URI. + DocumentUri uri; + + /// The version of document. + integer version; +}; + +struct TextDocumentPositionParams { + /// The text document. + TextDocumentIdentifier textDocument; + + /// The position inside the text document. + Position position; +}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/CallHierarchy.h b/include/Protocol/Feature/CallHierarchy.h new file mode 100644 index 00000000..95a24d8e --- /dev/null +++ b/include/Protocol/Feature/CallHierarchy.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct CallHierarchyClientCapabilities {}; + +using CallHierarchyOptions = WorkDoneProgressOptions; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/CodeAction.h b/include/Protocol/Feature/CodeAction.h new file mode 100644 index 00000000..ddb70555 --- /dev/null +++ b/include/Protocol/Feature/CodeAction.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct CodeActionClientCapabilities {}; + +struct CodeActionOptions {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/CodeCompletion.h b/include/Protocol/Feature/CodeCompletion.h new file mode 100644 index 00000000..2c7be90f --- /dev/null +++ b/include/Protocol/Feature/CodeCompletion.h @@ -0,0 +1,39 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct CompletionClientCapabilities {}; + +struct CompletionOptions { + /// The additional characters, beyond the defaults provided by the client (typically + /// [a-zA-Z]), that should automatically trigger a completion request. For example + ///`.` in JavaScript represents the beginning of an object property or method and is + /// thus a good candidate for triggering a completion request. + // + /// Most tools trigger a completion request automatically without explicitly + /// requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they + /// do so when the user starts to type an identifier. For example if the user + /// types `c` in a JavaScript file code complete will automatically pop up + /// present `console` besides others as a completion item. Characters that + /// make up identifiers don't need to be listed here. + array triggerCharacters; + + /// The server provides support to resolve additional information for a completion item. + bool resolveProvider; + + struct CompletionItemCapabilities { + /// The server has support for completion item label + /// details (see also `CompletionItemLabelDetails`) when receiving + /// a completion item in a resolve call. + bool labelDetailsSupport; + }; + + /// The server supports the following `CompletionItem` specific capabilities. + CompletionItemCapabilities completionItem; +}; + +using CompletionParams = TextDocumentPositionParams; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/CodeLens.h b/include/Protocol/Feature/CodeLens.h new file mode 100644 index 00000000..12a1a0e9 --- /dev/null +++ b/include/Protocol/Feature/CodeLens.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct CodeLensClientCapabilities {}; + +struct CodeLensOptions {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Declaration.h b/include/Protocol/Feature/Declaration.h new file mode 100644 index 00000000..cddf8c26 --- /dev/null +++ b/include/Protocol/Feature/Declaration.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct DeclarationClientCapabilities {}; + +using DeclarationOptions = WorkDoneProgressOptions; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Definition.h b/include/Protocol/Feature/Definition.h new file mode 100644 index 00000000..4a96b814 --- /dev/null +++ b/include/Protocol/Feature/Definition.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct DefinitionClientCapabilities {}; + +using DefinitionOptions = WorkDoneProgressOptions; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Diagnostic.h b/include/Protocol/Feature/Diagnostic.h new file mode 100644 index 00000000..1501409d --- /dev/null +++ b/include/Protocol/Feature/Diagnostic.h @@ -0,0 +1,82 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct PublishDiagnosticsClientCapabilities {}; + +struct DiagnosticClientCapabilities {}; + +enum class DiagnosticSeverity : std::uint8_t { + /// Reports an error. + Error = 1, + + /// Reports a warning. + Warning = 2, + + /// Reports an information. + Information = 3, + + /// Reports a hint. + Hint = 4, +}; + +enum class DiagnosticTag : std::uint8_t { + /// Unused or unnecessary code. Clients are allowed to render diagnostics + /// with this tag faded out instead of having an error squiggle. + Unnecessary = 1, + + /// Deprecated or obsolete code. Clients are allowed to rendered + /// diagnostics with this tag strike through. + Deprecated = 2, +}; + +struct CodeDescription { + /// An URI to open with more information about the diagnostic error. + URI uri; +}; + +/// Represents a related message and source code location for a diagnostic. +/// This should be used to point to code locations that cause or are related to +/// a diagnostics, e.g when duplicating a symbol in a scope. +struct DiagnosticRelatedInformation { + /// The location of this related diagnostic information. + Location location; + + /// The message of this related diagnostic information. + string message; +}; + +struct Diagnostic { + /// The range at which the message applies. + Range range; + + /// The diagnostic's severity. To avoid interpretation mismatches when a + /// server is used with different clients it is highly recommended that + /// servers always provide a severity value. If omitted, it’s recommended + /// for the client to interpret it as an Error severity. + DiagnosticSeverity severity; + + /// The diagnostic's code, which might appear in the user interface. + string code; + + /// An optional property to describe the error code. + optional codeDescription; + + /// A human-readable string describing the source of this + /// diagnostic, e.g. 'typescript' or 'super lint'. + string source; + + /// The diagnostic's message. + string message; + + /// Additional metadata about the diagnostic. + array tags; + + /// An array of related diagnostic information, e.g. when symbol-names within + /// a scope collide all definitions can be marked via this property. + array relatedInformation; +}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/DocumentHighlight.h b/include/Protocol/Feature/DocumentHighlight.h new file mode 100644 index 00000000..dff17e97 --- /dev/null +++ b/include/Protocol/Feature/DocumentHighlight.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct DocumentHighlightClientCapabilities {}; + +using DocumentHighlightOptions = bool; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/DocumentLink.h b/include/Protocol/Feature/DocumentLink.h new file mode 100644 index 00000000..d781ea80 --- /dev/null +++ b/include/Protocol/Feature/DocumentLink.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct DocumentLinkClientCapabilities {}; + +struct DocumentLinkOptions {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/DocumentSymbol.h b/include/Protocol/Feature/DocumentSymbol.h new file mode 100644 index 00000000..af53921e --- /dev/null +++ b/include/Protocol/Feature/DocumentSymbol.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct DocumentSymbolClientCapabilities {}; + +struct DocumentSymbolOptions {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/FoldingRange.h b/include/Protocol/Feature/FoldingRange.h new file mode 100644 index 00000000..54792979 --- /dev/null +++ b/include/Protocol/Feature/FoldingRange.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct FoldingRangeClientCapabilities {}; + +using FoldingRangeOptions = bool; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Formatting.h b/include/Protocol/Feature/Formatting.h new file mode 100644 index 00000000..79ab3e12 --- /dev/null +++ b/include/Protocol/Feature/Formatting.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct DocumentFormattingClientCapabilities {}; + +using DocumentFormattingOptions = bool; + +struct DocumentRangeFormattingClientCapabilities {}; + +using DocumentRangeFormattingOptions = bool; + +struct DocumentOnTypeFormattingClientCapabilities {}; + +struct DocumentOnTypeFormattingOptions {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Hover.h b/include/Protocol/Feature/Hover.h new file mode 100644 index 00000000..b758cb01 --- /dev/null +++ b/include/Protocol/Feature/Hover.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct HoverClientCapabilities {}; + +using HoverOptions = bool; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Implementation.h b/include/Protocol/Feature/Implementation.h new file mode 100644 index 00000000..fd1308e2 --- /dev/null +++ b/include/Protocol/Feature/Implementation.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct ImplementationClientCapabilities {}; + +using ImplementationOptions = WorkDoneProgressOptions; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/InlayHint.h b/include/Protocol/Feature/InlayHint.h new file mode 100644 index 00000000..f1eae0d3 --- /dev/null +++ b/include/Protocol/Feature/InlayHint.h @@ -0,0 +1,15 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct InlayHintClientCapabilities {}; + +struct InlayHintOptions { + /// The server provides support to resolve additional + /// information for an inlay hint item. + bool resolveProvider; +}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Reference.h b/include/Protocol/Feature/Reference.h new file mode 100644 index 00000000..0f2895b8 --- /dev/null +++ b/include/Protocol/Feature/Reference.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct ReferenceClientCapabilities {}; + +using ReferenceOptions = WorkDoneProgressOptions; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/Rename.h b/include/Protocol/Feature/Rename.h new file mode 100644 index 00000000..d9b2e6a3 --- /dev/null +++ b/include/Protocol/Feature/Rename.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct RenameClientCapabilities {}; + +struct RenameOptions {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/SemanticTokens.h b/include/Protocol/Feature/SemanticTokens.h new file mode 100644 index 00000000..37b7aa11 --- /dev/null +++ b/include/Protocol/Feature/SemanticTokens.h @@ -0,0 +1,36 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct SemanticTokensClientCapabilities {}; + +struct SemanticTokensLegend { + /// The token types a server uses. + array tokenTypes; + + /// The token modifiers a server uses. + array tokenModifiers; +}; + +struct SemanticTokensOptions { + /// The legend used by the server. + SemanticTokensLegend legend; + + /// Server supports providing semantic tokens for a specific + /// range of a document. + bool range = false; + + /// Server supports providing semantic tokens for a full document. + bool full = true; +}; + +struct SemanticTokensParams { + /// The text document. + TextDocumentIdentifier textDocument; +}; + +struct SemanticTokens {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/SignatureHelp.h b/include/Protocol/Feature/SignatureHelp.h new file mode 100644 index 00000000..846a1a80 --- /dev/null +++ b/include/Protocol/Feature/SignatureHelp.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct SignatureHelpClientCapabilities {}; + +struct SignatureHelpOptions {}; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/TypeDefinition.h b/include/Protocol/Feature/TypeDefinition.h new file mode 100644 index 00000000..fc46707e --- /dev/null +++ b/include/Protocol/Feature/TypeDefinition.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct TypeDefinitionClientCapabilities {}; + +using TypeDefinitionOptions = WorkDoneProgressOptions; + +} // namespace clice::proto diff --git a/include/Protocol/Feature/TypeHierarchy.h b/include/Protocol/Feature/TypeHierarchy.h new file mode 100644 index 00000000..f2151d9a --- /dev/null +++ b/include/Protocol/Feature/TypeHierarchy.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../Basic.h" + +namespace clice::proto { + +struct TypeHierarchyClientCapabilities {}; + +using TypeHierarchyOptions = WorkDoneProgressOptions; + +} // namespace clice::proto diff --git a/include/Protocol/Initialize.h b/include/Protocol/Initialize.h new file mode 100644 index 00000000..486255a8 --- /dev/null +++ b/include/Protocol/Initialize.h @@ -0,0 +1,186 @@ +#pragma once + +#include "Basic.h" +#include "TextDocument.h" +#include "Notebook.h" +#include "Workspace.h" + +/// clice currently ignores all `dynamicRegistration` field in LSP specification. + +namespace clice::proto { + +struct LSPInfo { + /// The name of server or client. + std::string name; + + /// The version of server or client. + std::string verion; +}; + +struct WindowCapacities {}; + +struct RegularExpressionsClientCapabilities {}; + +struct MarkdownClientCapabilities {}; + +struct GeneralCapacities { + /// FIXME: staleRequestSupport + + /// Client capabilities specific to regular expressions. + optional regularExpressions; + + /// Client capabilities specific to the client's markdown parser. + optional markdown; + + /// The position encodings supported by the client. + optional> positionEncodings; +}; + +struct ClientCapabilities { + /// Workspace specific client capabilities. + WorkspaceClientCapabilities workspace; + + /// Text document specific client capabilities. + TextDocumentClientCapabilities textDocument; + + /// Capabilities specific to the notebook document support. + NotebookDocumentClientCapabilities notebookDocument; + + /// Window specific client capabilities. + WindowCapacities window; + + /// General client capabilities. + GeneralCapacities general; +}; + +struct InitializeParams { + /// Information about client. + LSPInfo clientInfo; + + /// The capabilities provided by the client (editor or tool). + ClientCapabilities capabilities; + + /// The workspace folders configured in the client when the server starts. + /// This property is only available if the client supports workspace folders. + /// It can be `null` if the client supports workspace folders but none are + /// configured. + array workspaceFolders; +}; + +struct ServerCapabilities { + /// The position encoding the server picked from the encodings offered + /// by the client via the client capability `general.positionEncodings`. + PositionEncodingKind positionEncoding; + + /// Defines how text documents are synced. + TextDocumentSyncOptions textDocumentSync; + + /// Defines how notebook documents are synced. + /// FIXME: NotebookDocumentSyncOptions notebookDocumentSync; + + /// The server provides completion support. + CompletionOptions completionProvider; + + /// The server provides hover support. + HoverOptions hoverProvider; + + /// The server provides signature help support. + SignatureHelpOptions signatureHelpProvider; + + /// The server provides go to declaration support. + DeclarationOptions declarationProvider; + + /// The server provides goto definition support. + DefinitionOptions definitionProvider; + + /// The server provides goto type definition support. + TypeDefinitionOptions typeDefinitionProvider; + + /// The server provides goto implementation support. + ImplementationOptions implementationProvider; + + /// The server provides find references support. + ReferenceOptions referencesProvider; + + /// The server provides document highlight support. + DocumentHighlightOptions documentHighlightProvider; + + /// The server provides document symbol support. + DocumentSymbolOptions documentSymbolProvider; + + /// The server provides code actions. The `CodeActionOptions` return type is + /// only valid if the client signals code action literal support via the + /// property `textDocument.codeAction.codeActionLiteralSupport`. + CodeActionOptions codeActionProvider; + + /// The server provides code lens. + CodeLensOptions codeLensProvider; + + /// The server provides document link support. + DocumentLinkOptions documentLinkProvider; + + /// The server provides color provider support. + /// FIXME: DocumentColorOptions colorProvider; + + /// The server provides document formatting. + DocumentFormattingOptions documentFormattingProvider; + + /// The server provides document range formatting. + DocumentRangeFormattingOptions documentRangeFormattingProvider; + + /// The server provides document formatting on typing. + DocumentOnTypeFormattingOptions documentOnTypeFormattingProvider; + + /// The server provides rename support. RenameOptions may only be specified if the client + /// states that it supports `prepareSupport` in its initial `initialize` request. + RenameOptions renameProvider; + + /// The server provides folding provider support. + FoldingRangeOptions foldingRangeProvider; + + /// The server provides execute command support. + /// FIXME: ExecuteCommandOptions executeCommandProvider; + + /// The server provides selection range support. + /// FIXME: SelectionRangeOptions selectionRangeProvider; + + /// The server provides linked editing range support. + /// FIXME: LinkedEditingRangeOptions linkedEditingRangeProvider; + + /// The server provides call hierarchy support. + CallHierarchyOptions callHierarchyProvider; + + /// The server provides semantic tokens support. + SemanticTokensOptions semanticTokensProvider; + + /// Whether server provides moniker support. + /// FIXME: MonikerOptions monikerProvider; + + /// The server provides type hierarchy support. + TypeHierarchyOptions typeHierarchyProvider; + + /// The server provides inline values. + /// FIXME: InlineValueOptions inlineValueProvider; + + /// The server provides inlay hints. + InlayHintOptions inlayHintProvider; + + /// The server has support for pull model diagnostics. + /// FIXME: DiagnosticOptions diagnosticProvider; + + /// The server provides workspace symbol support. + WorkspaceSymbolOptions workspaceSymbolProvider; + + /// Workspace specific server capabilities. + WorkspaceServerCapabilities workspace; +}; + +struct InitializeResult { + /// Information about the server. + LSPInfo serverInfo; + + /// The capabilities the language server provides. + ServerCapabilities capabilities; +}; + +} // namespace clice::proto diff --git a/include/Protocol/Notebook.h b/include/Protocol/Notebook.h new file mode 100644 index 00000000..331353b4 --- /dev/null +++ b/include/Protocol/Notebook.h @@ -0,0 +1,9 @@ +#pragma once + +#include "Basic.h" + +namespace clice::proto { + +struct NotebookDocumentClientCapabilities {}; + +} // namespace clice::proto diff --git a/include/Protocol/Protocol.h b/include/Protocol/Protocol.h new file mode 100644 index 00000000..6dd6f762 --- /dev/null +++ b/include/Protocol/Protocol.h @@ -0,0 +1,3 @@ +#pragma once + +#include "Initialize.h" diff --git a/include/Protocol/TextDocument.h b/include/Protocol/TextDocument.h new file mode 100644 index 00000000..ded90ede --- /dev/null +++ b/include/Protocol/TextDocument.h @@ -0,0 +1,201 @@ +#pragma once + +#include "Basic.h" +#include "Feature/CallHierarchy.h" +#include "Feature/CodeAction.h" +#include "Feature/CodeLens.h" +#include "Feature/Diagnostic.h" +#include "Feature/DocumentLink.h" +#include "Feature/Declaration.h" +#include "Feature/Definition.h" +#include "Feature/FoldingRange.h" +#include "Feature/Formatting.h" +#include "Feature/Hover.h" +#include "Feature/InlayHint.h" +#include "Feature/Reference.h" +#include "Feature/Rename.h" +#include "Feature/SemanticTokens.h" +#include "Feature/SignatureHelp.h" +#include "Feature/Implementation.h" +#include "Feature/CodeCompletion.h" +#include "Feature/TypeDefinition.h" +#include "Feature/TypeHierarchy.h" +#include "Feature/DocumentSymbol.h" +#include "Feature/DocumentHighlight.h" + +namespace clice::proto { + +struct TextDocumentSyncClientCapabilities {}; + +struct TextDocumentClientCapabilities { + optional synchronization; + + /// Capabilities specific to the `textDocument/completion` request. + optional completion; + + /// Capabilities specific to the `textDocument/hover` request. + optional hover; + + /// Capabilities specific to the `textDocument/signatureHelp` request. + optional signatureHelp; + + /// Capabilities specific to the `textDocument/declaration` request. + optional declaration; + + /// Capabilities specific to the `textDocument/definition` request. + optional definition; + + /// Capabilities specific to the `textDocument/typeDefinition` request. + optional typeDefinition; + + /// Capabilities specific to the `textDocument/implementation` request. + optional implementation; + + /// Capabilities specific to the `textDocument/references` request. + optional references; + + /// Capabilities specific to the `textDocument/documentHighlight` request. + optional documentHighlight; + + /// Capabilities specific to the `textDocument/documentSymbol` request. + optional documentSymbol; + + /// Capabilities specific to the `textDocument/codeAction` request. + optional codeAction; + + /// Capabilities specific to the `textDocument/codeLens` request. + optional codeLens; + + /// Capabilities specific to the `textDocument/documentLink` request. + optional documentLink; + + /// Capabilities specific to the `textDocument/documentColor` and the + /// `textDocument/colorPresentation` request. + /// FIXME: optional colorProvider; + + /// Capabilities specific to the `textDocument/formatting` request. + optional formatting; + + /// Capabilities specific to the `textDocument/rangeFormatting` request. + optional rangeFormatting; + + /// Capabilities specific to the `textDocument/onTypeFormatting` request. + optional onTypeFormatting; + + /// Capabilities specific to the `textDocument/rename` request. + optional rename; + + /// Capabilities specific to the `textDocument/publishDiagnostics` notification. + optional publishDiagnostics; + + /// Capabilities specific to the `textDocument/foldingRange` request. + optional foldingRange; + + /// Capabilities specific to the `textDocument/selectionRange` request. + /// FIXME: optional selectionRange; + + /// Capabilities specific to the `textDocument/linkedEditingRange` request. + /// FIXME: optional linkedEditingRange; + + /// Capabilities specific to the various call hierarchy requests. + optional callHierarchy; + + /// Capabilities specific to the various semantic token requests. + optional semanticTokens; + + /// Capabilities specific to the `textDocument/moniker` request. + /// FIXME: optional moniker; + + /// Capabilities specific to the various type hierarchy requests. + optional typeHierarchy; + + /// Capabilities specific to the `textDocument/inlineValue` request. + /// FIXME: optional inlineValue; + + /// Capabilities specific to the `textDocument/inlayHint` request. + optional inlayHint; + + /// Capabilities specific to the diagnostic pull model. + optional diagnostic; +}; + +enum class TextDocumentSyncKind : std::uint8_t { + /// Documents should not be synced at all. + None = 0, + + /// Documents are synced by always sending the full content of the document. + Full = 1, + + /// Documents are synced by sending the full content on open. After that + /// only incremental updates to the document are sent. + Incremental = 2, +}; + +struct TextDocumentSyncOptions { + /// Open and close notifications are sent to the server. If omitted open + /// close notifications should not be sent. + bool openClose = true; + + /// Change notifications are sent to the server. + TextDocumentSyncKind change = TextDocumentSyncKind::Incremental; + + /// If present will save notifications are sent to the server. If omitted + /// the notification should not be sent. + /// FIXME: bool willSave; + + /// If present will save wait until requests are sent to the server. If + /// omitted the request should not be sent. + /// FIXME: bool willSaveWaitUntil; + + /// If present save notifications are sent to the server. If omitted the + /// notification should not be sent. + bool save = true; +}; + +struct DidOpenTextDocumentParams { + /// The document that was opened. + TextDocumentItem textDocument; +}; + +struct TextDocumentContentChangeEvent { + /// The new text of the whole document. + string text; +}; + +struct DidChangeTextDocumentParams { + /// The document that did change. The version number points + /// to the version after all provided content changes have + /// been applied. + VersionedTextDocumentIdentifier textDocument; + + /// The actual content changes. The content changes describe single state + /// changes to the document. So if there are two content changes c1 (at + /// array index 0) and c2 (at array index 1) for a document in state S then + /// c1 moves the document from S to S' and c2 from S' to S''. So c1 is + /// computed on the state S and c2 is computed on the state S'. + // + /// To mirror the content of a document using change events use the following + /// approach: + /// - start with the same initial content + /// - apply the 'textDocument/didChange' notifications in the order you + /// receive them. + /// - apply the `TextDocumentContentChangeEvent`s in a single notification + /// in the order you receive them. + array contentChanges; +}; + +struct DidSaveTextDocumentParams { + /// The document that was saved. + TextDocumentIdentifier textDocument; + + /// Optional the content when saved. Depends on the includeText value + /// when the save notification was requested. + string text; +}; + +struct DidCloseTextDocumentParams { + /// The document that was closed. + TextDocumentIdentifier textDocument; +}; + +} // namespace clice::proto diff --git a/include/Protocol/Workspace.h b/include/Protocol/Workspace.h new file mode 100644 index 00000000..b078a327 --- /dev/null +++ b/include/Protocol/Workspace.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Basic.h" + +namespace clice::proto { + +struct WorkspaceFolder { + /// The associated URI for this workspace folder. + URI uri; + + /// The name of the workspace folder. Used to refer to this + /// workspace folder in the user interface. + string name; +}; + +struct WorkspaceClientCapabilities {}; + +struct WorkspaceSymbolOptions {}; + +struct WorkspaceFoldersServerCapabilities { + /// The server has support for workspace folders. + bool supported = true; +}; + +struct WorkspaceServerCapabilities { + /// The server supports workspace folder. + WorkspaceFoldersServerCapabilities workspaceFolders; +}; + +} // namespace clice::proto diff --git a/include/Server/Convert.h b/include/Server/Convert.h new file mode 100644 index 00000000..84934fa1 --- /dev/null +++ b/include/Server/Convert.h @@ -0,0 +1,267 @@ +#pragma once + +#include "Protocol/Protocol.h" +#include "Feature/SemanticToken.h" +#include "Feature/CodeCompletion.h" +#include "Compiler/Diagnostic.h" +#include "Support/FileSystem.h" +#include "Support/JSON.h" + +namespace clice { + +enum class PositionEncodingKind { + UTF8, + UTF16, + UTF32, +}; + +struct PathMapping { + std::string to_path(llvm::StringRef uri) { + /// FIXME: Path mapping. + return fs::toPath(uri); + } + + std::string to_uri(llvm::StringRef path) { + /// FIXME: Path mapping. + return fs::toURI(path); + } +}; + +/// @brief Iterates over Unicode codepoints in a UTF-8 encoded string and invokes a callback for +/// each codepoint. +/// +/// Processes the input UTF-8 string, calculating the length of each Unicode codepoint in both +/// UTF-8 (bytes) and UTF-16 (code units), and passes these lengths to the callback. +/// Iteration stops early if the callback returns `false`. +/// +/// ASCII characters are treated as 1-byte UTF-8 codepoints with a UTF-16 length of 1. +/// Non-ASCII characters are processed based on their leading byte to determine UTF-8 length: +/// - Valid lengths are 2 to 4 bytes. +/// - Astral codepoints (UTF-8 length of 4) have a UTF-16 length of 2 code units. +/// Invalid UTF-8 sequences are treated as single-byte ASCII characters. +/// +/// Returns `false` if the callback stops the iteration. +template +bool iterateCodepoints(llvm::StringRef content, const Callback& callback) { + // Iterate over the input string, processing each codepoint. + for(size_t index = 0; index < content.size();) { + unsigned char c = static_cast(content[index]); + + // Handle ASCII characters (1-byte UTF-8, 1-code-unit UTF-16). + if(!(c & 0x80)) [[likely]] { + if(!callback(1, 1)) { + return true; + } + + ++index; + continue; + } + + // Determine the length of the codepoint in UTF-8 by counting the leading 1s. + size_t length = llvm::countl_one(c); + + // Validate UTF-8 encoding: length must be between 2 and 4. + if(length < 2 || length > 4) [[unlikely]] { + assert(false && "Invalid UTF-8 sequence"); + + // Treat the byte as an ASCII character. + if(!callback(1, 1)) { + return true; + } + + ++index; + continue; + } + + // Advance the index by the length of the current UTF-8 codepoint. + index += length; + + // Calculate the UTF-16 length: astral codepoints (4-byte UTF-8) take 2 code units. + if(!callback(length, length == 4 ? 2 : 1)) { + return true; + } + } + + return false; +} + +/// Remeasure the length (character count) of the content with the specified encoding kind. +inline std::uint32_t remeasure(llvm::StringRef content, PositionEncodingKind kind) { + if(kind == PositionEncodingKind::UTF8) { + return content.size(); + } + + if(kind == PositionEncodingKind::UTF16) { + std::uint32_t length = 0; + iterateCodepoints(content, [&](std::uint32_t, std::uint32_t utf16Length) { + length += utf16Length; + return true; + }); + return length; + } + + if(kind == PositionEncodingKind::UTF32) { + std::uint32_t length = 0; + iterateCodepoints(content, [&](std::uint32_t, std::uint32_t) { + length += 1; + return true; + }); + return length; + } + + std::unreachable(); +} + +class PositionConverter { +public: + PositionConverter(llvm::StringRef content, PositionEncodingKind encoding) : + content(content), encoding(encoding) {} + + /// Convert a offset to a proto::Position with given encoding. + /// The input offset must be UTF-8 encoded and in order. + proto::Position toPosition(uint32_t offset) { + assert(offset <= content.size() && "Offset is out of range"); + assert(offset >= lastInput && "Offset must be in order"); + + /// Fast path: return the last output. + if(offset == lastInput) [[unlikely]] { + return lastOutput; + } + + /// The length of the current line. + std::uint32_t lineLength = 0; + + /// Move the line offset to the current line. + for(std::uint32_t i = lastLineOffset; i < offset; i++) { + lineLength += 1; + if(content[i] == '\n') { + line += 1; + lastLineOffset += lineLength; + lineLength = 0; + } + } + + /// Get the content of the current line. + auto lineContent = content.substr(lastLineOffset, lineLength); + auto position = proto::Position{ + .line = line, + .character = remeasure(lineContent, encoding), + }; + + /// Cache the result. + lastInput = offset; + lastOutput = position; + + return position; + } + + template + void to_positions(Range&& range, Proj&& proj) { + std::vector offsets; + for(auto&& item: range) { + auto [begin, end] = proj(item); + offsets.emplace_back(begin); + offsets.emplace_back(end); + } + + ranges::sort(offsets); + + for(auto&& offset: offsets) { + if(auto it = cache.find(offset); it == cache.end()) { + cache.try_emplace(offset, toPosition(offset)); + } + } + } + + proto::Position lookup(uint32_t offset) { + auto it = cache.find(offset); + assert(it != cache.end() && "Offset is not cached"); + return it->second; + } + + proto::Range lookup(LocalSourceRange range) { + auto it = cache.find(range.begin); + assert(it != cache.end() && "Offset is not cached"); + auto begin = it->second; + it = cache.find(range.end); + assert(it != cache.end() && "Offset is not cached"); + auto end = it->second; + return proto::Range{begin, end}; + } + +private: + std::uint32_t line = 0; + /// The offset of the last line end. + std::uint32_t lastLineOffset = 0; + + /// The input offset of last call. + std::uint32_t lastInput = 0; + proto::Position lastOutput = {0, 0}; + + llvm::DenseMap cache; + + llvm::StringRef content; + PositionEncodingKind encoding; +}; + +inline std::uint32_t to_offset(clice::PositionEncodingKind kind, + llvm::StringRef content, + proto::Position position) { + std::uint32_t offset = 0; + for(auto i = 0; i < position.line; i++) { + auto pos = content.find('\n'); + assert(pos != llvm::StringRef::npos && "Line value is out of range"); + + offset += pos + 1; + content = content.substr(pos + 1); + } + + /// Drop the content after the line. + content = content.take_until([](char c) { return c == '\n'; }); + assert(position.character <= content.size() && "Character value is out of range"); + + if(position.character == 0) { + return offset; + } + + if(kind == PositionEncodingKind::UTF8) { + offset += position.character; + return offset; + } + + if(kind == PositionEncodingKind::UTF16) { + iterateCodepoints(content, [&](std::uint32_t utf8Length, std::uint32_t utf16Length) { + assert(position.character >= utf16Length && "Character value is out of range"); + position.character -= utf16Length; + offset += utf8Length; + return position.character != 0; + }); + return offset; + } + + if(kind == PositionEncodingKind::UTF32) { + iterateCodepoints(content, [&](std::uint32_t utf8Length, std::uint32_t) { + assert(position.character >= 1 && "Character value is out of range"); + position.character -= 1; + offset += utf8Length; + return position.character != 0; + }); + return offset; + } + + std::unreachable(); +} + +} // namespace clice + +namespace clice::proto { + +json::Value to_json(clice::PositionEncodingKind kind, + llvm::StringRef content, + llvm::ArrayRef tokens); + +json::Value to_json(clice::PositionEncodingKind kind, + llvm::StringRef content, + llvm::ArrayRef items); + +} // namespace clice::proto diff --git a/include/Server/LSPConverter.h b/include/Server/LSPConverter.h deleted file mode 100644 index 0f6342fc..00000000 --- a/include/Server/LSPConverter.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include "Config.h" -#include "Protocol.h" -#include "Async/Async.h" -#include "Feature/Hover.h" -#include "Feature/InlayHint.h" -#include "Feature/FoldingRange.h" -#include "Feature/DocumentLink.h" -#include "Feature/DocumentSymbol.h" -#include "Feature/SemanticToken.h" -#include "Feature/CodeCompletion.h" -#include "Feature/SignatureHelp.h" - -namespace clice { - -enum class PositionEncodingKind : std::uint8_t { - UTF8 = 0, - UTF16, - UTF32, -}; - -/// Responsible for converting between LSP and internal types. -class LSPConverter { -public: - json::Value initialize(json::Value value); - - PositionEncodingKind encoding() { - return kind; - } - - llvm::StringRef workspace() { - return workspacePath; - } - -public: - /// Convert URI to file path with path mapping. - std::string convert(llvm::StringRef URI); - - /// Convert a position into an offset relative to the beginning of the file. - std::uint32_t convert(llvm::StringRef content, proto::Position position); - - proto::Position convert(llvm::StringRef content, std::uint32_t offset); - - json::Value convert(llvm::StringRef content, const feature::Hover& hover); - - json::Value convert(llvm::StringRef content, const feature::InlayHints& hints); - - json::Value convert(llvm::StringRef content, const feature::FoldingRanges& foldings); - - json::Value convert(llvm::StringRef content, const feature::DocumentLinks& links); - - json::Value convert(llvm::StringRef content, const feature::DocumentSymbols& symbols); - - json::Value convert(llvm::StringRef content, const feature::SemanticTokens& tokens); - - json::Value convert(llvm::StringRef content, const std::vector& items); - -private: - PositionEncodingKind kind; - std::string workspacePath; -}; - -} // namespace clice - diff --git a/include/Server/Protocol.h b/include/Server/Protocol.h deleted file mode 100644 index 9d11d6e4..00000000 --- a/include/Server/Protocol.h +++ /dev/null @@ -1,229 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "Support/Enum.h" -#include "llvm/ADT/StringRef.h" - -namespace clice::proto { - -using integer = std::int32_t; - -/// range in [0, 2^31- 1] -using uinteger = std::uint32_t; - -using string = std::string; - -using string_literal = llvm::StringLiteral; - -template -using array = std::vector; - -using DocumentUri = std::string; - -using URI = std::string; - -struct Position { - /// Line position in a document (zero-based). - uinteger line; - - /// Character offset on a line in a document (zero-based). - /// The meaning of this offset is determined by the negotiated - /// `PositionEncodingKind`. - uinteger character; - - constexpr friend bool operator== (const Position&, const Position&) = default; -}; - -struct Range { - /// The range's start position. - Position start; - - /// The range's end position. - Position end; - - constexpr friend bool operator== (const Range&, const Range&) = default; -}; - -struct Location { - DocumentUri uri; - - Range range; -}; - -struct TextEdit { - /// The range of the text document to be manipulated. To insert - /// text into a document create a range where start === end. - Range range; - - // The string to be inserted. For delete operations use an - // empty string. - string newText; -}; - -struct TextDocumentItem { - /// The text document's URI. - DocumentUri uri; - - /// The text document's language identifier. - string languageId; - - /// The version number of this document (it will strictly increase after each - /// change, including undo/redo). - uinteger version; - - /// The content of the opened text document. - string text; -}; - -struct TextDocumentIdentifier { - /// The text document's URI. - DocumentUri uri; -}; - -struct VersionedTextDocumentIdentifier { - /// The text document's URI. - DocumentUri uri; - - std::uint32_t version; -}; - -struct TextDocumentPositionParams { - /// The text document. - TextDocumentIdentifier textDocument; - - /// The position inside the text document. - Position position; -}; - -enum class TextDocumentSyncKind { - None = 0, - Full = 1, - Incremental = 2, -}; - -struct WorkspaceFolder { - /// The associated URI for this workspace folder. - URI uri; - - /// The name of the workspace folder. Used to refer to this workspace folder - /// in the user interface. - std::string name; -}; - -enum class ErrorCodes { - // Defined by JSON-RPC - ParseError = -32700, - InvalidRequest = -32600, - MethodNotFound = -32601, - InvalidParams = -32602, - InternalError = -32603, - - /** - * Error code indicating that a server received a notification or - * request before the server has received the `initialize` request. - */ - ServerNotInitialized = -32002, - UnknownErrorCode = -32001, - - /** - * A request failed but it was syntactically correct, e.g the - * method name was known and the parameters were valid. The error - * message should contain human readable information about why - * the request failed. - * - * @since 3.17.0 - */ - RequestFailed = -32803, - - /** - * The server cancelled the request. This error code should - * only be used for requests that explicitly support being - * server cancellable. - * - * @since 3.17.0 - */ - ServerCancelled = -32802, - - /** - * The server detected that the content of a document got - * modified outside normal conditions. A server should - * NOT send this error code if it detects a content change - * in it unprocessed messages. The result even computed - * on an older state might still be useful for the client. - * - * If a client decides that a result is not of any use anymore - * the client should cancel the request. - */ - ContentModified = -32801, - - /** - * The client has canceled a request and a server has detected - * the cancel. - */ - RequestCancelled = -32800, -}; - -struct TextDocumentParams { - /// The text document. - TextDocumentIdentifier textDocument; -}; - -enum class SymbolKind {}; - -struct ResolveProvider { - bool resolveProvider; -}; - -struct SemanticTokenOptions { - struct { - std::vector tokenTypes; - std::vector tokenModifiers; - } legend; - - bool full = true; -}; - -struct CompletionOptions { - std::vector triggerCharacters = {".", "<", ">", ":", "\"", "/", "*"}; - bool resolveProvider = false; -}; - -struct HeaderContext { - /// The path of context file. - std::string file; - - /// The version of context file's unit. - uint32_t version; - - /// The include location id for further resolving. - uint32_t include; -}; - -struct IncludeLocation { - /// The line of include drective. - uint32_t line = -1; - - /// The file path of include drective. - std::string file; -}; - -struct HeaderContextGroup { - /// The index path of this header Context. - std::string indexFile; - - /// The header contexts. - std::vector contexts; -}; - -struct HeaderContextSwitchParams { - /// The header file path which wants to switch context. - std::string header; - - /// The context - HeaderContext context; -}; - -} // namespace clice::proto diff --git a/include/Server/Scheduler.h b/include/Server/Scheduler.h deleted file mode 100644 index 5b485ba3..00000000 --- a/include/Server/Scheduler.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include "Indexer.h" -#include "Async/Async.h" -#include "Compiler/CompilationUnit.h" -#include "Compiler/Module.h" -#include "Compiler/Preamble.h" - -namespace clice { - -class LSPConverter; -class CompilationDatabase; - -struct OpenFile { - /// The file version, every edition will increase it. - std::uint32_t version = 0; - - /// The file content. - std::string content; - - /// We build PCH for every opened file. - std::optional PCH; - async::Task<> PCHBuild; - async::Event PCHBuiltEvent; - - /// For each opened file, we would like to build an AST for it. - std::shared_ptr AST; - async::Task<> ASTBuild; - async::Lock ASTBuiltLock; - - /// For header with context, it may have multiple ASTs, use - /// an chain to store them. - std::unique_ptr next; -}; - -class Scheduler { -public: - Scheduler(Indexer& indexer, LSPConverter& converter, CompilationDatabase& database) : - indexer(indexer), converter(converter), database(database) {} - - /// Add or update a document. - void addDocument(std::string path, std::string content); - - /// Close a document. - void closeDocument(std::string path); - - llvm::StringRef getDocumentContent(llvm::StringRef path); - - /// Get the specific AST of given file. - async::Task semanticToken(std::string path); - - async::Task completion(std::string path, std::uint32_t offset); - -private: - async::Task isPCHOutdated(llvm::StringRef file, llvm::StringRef preamble); - - async::Task<> buildPCH(std::string file, std::string preamble); - - async::Task<> buildAST(std::string file, std::string content); - -private: - Indexer& indexer; - LSPConverter& converter; - CompilationDatabase& database; - - /// The task that runs in the thread pool. The number of tasks is fixed, - /// and we won't attempt to expand the vector, so the references are - /// guaranteed to remain valid. - std::vector> running; - - llvm::StringMap openFiles; -}; - -} // namespace clice diff --git a/include/Server/Server.h b/include/Server/Server.h index 36ff6035..f93ac1bf 100644 --- a/include/Server/Server.h +++ b/include/Server/Server.h @@ -1,20 +1,71 @@ #pragma once #include "Config.h" +#include "Convert.h" #include "Indexer.h" -#include "Protocol.h" -#include "Scheduler.h" -#include "LSPConverter.h" #include "Async/Async.h" #include "Compiler/Command.h" +#include "Compiler/Preamble.h" +#include "Compiler/Diagnostic.h" +#include "Protocol/Protocol.h" namespace clice { +struct OpenFile { + /// The file version, every edition will increase it. + std::uint32_t version = 0; + + /// The file content. + std::string content; + + /// We build PCH for every opened file. + std::optional pch; + async::Task pch_build_task; + async::Event pch_built_event; + + /// For each opened file, we would like to build an AST for it. + std::shared_ptr ast; + async::Task<> ast_build_task; + async::Lock ast_built_lock; + + /// Collect all diagnostics in the compilation. + std::shared_ptr> diagnostics = + std::make_unique>(); + + /// For header with context, it may have multiple ASTs, use + /// an chain to store them. + std::unique_ptr next; +}; + class Server { public: Server(); - async::Task<> onReceive(json::Value value); + using Self = Server; + + using Callback = async::Task (*)(Server&, json::Value); + + template + void register_callback(llvm::StringRef name) { + using MF = decltype(method); + static_assert(std::is_member_function_pointer_v, ""); + using F = member_type_t; + using Ret = function_return_t; + using Params = std::tuple_element_t<0, function_args_t>; + + Callback callback = [](Server& server, json::Value value) -> async::Task { + if constexpr(std::is_same_v>) { + co_await (server.*method)(json::deserialize(value)); + co_return json::Value(nullptr); + } else { + co_return co_await (server.*method)(json::deserialize(value)); + } + }; + + callbacks.try_emplace(name, callback); + } + + async::Task<> on_receive(json::Value value); private: /// Send a request to the client. @@ -34,31 +85,46 @@ private: json::Value registerOptions); private: - async::Task onInitialize(json::Value value); - - async::Task<> onDidOpen(json::Value value); - - async::Task<> onDidChange(json::Value value); - - async::Task<> onDidSave(json::Value value); - - async::Task<> onDidClose(json::Value value); - - async::Task onSemanticToken(json::Value value); - - async::Task onCodeCompletion(json::Value value); + async::Task on_initialize(proto::InitializeParams params); private: + async::Task add_document(std::string path, std::string content); + + async::Task<> build_pch(std::string file, std::string preamble); + + async::Task<> build_ast(std::string file, std::string content); + + async::Task<> on_did_open(proto::DidOpenTextDocumentParams params); + + async::Task<> on_did_change(proto::DidChangeTextDocumentParams params); + + async::Task<> on_did_save(proto::DidSaveTextDocumentParams params); + + async::Task<> on_did_close(proto::DidCloseTextDocumentParams params); + +private: + async::Task on_semantic_token(proto::SemanticTokensParams params); + + async::Task on_completion(proto::CompletionParams params); + +private: + /// The current request id. std::uint32_t id = 0; - Indexer indexer; - Scheduler scheduler; - LSPConverter converter; + + /// All registered LSP callbacks. + llvm::StringMap callbacks; + + PositionEncodingKind kind; + + std::string workspace; + + /// The compilation database. CompilationDatabase database; - using OnRequest = async::Task (Server::*)(json::Value); - using OnNotification = async::Task<> (Server::*)(json::Value); - llvm::StringMap onRequests; - llvm::StringMap onNotifications; + /// All opening files, TODO: use a LRU cache. + llvm::StringMap opening_files; + + PathMapping mapping; }; } // namespace clice diff --git a/include/Support/JSON.h b/include/Support/JSON.h index f6356bdf..bbd75893 100644 --- a/include/Support/JSON.h +++ b/include/Support/JSON.h @@ -320,8 +320,13 @@ struct Serde { if constexpr(!std::is_empty_v) { assert(value.kind() == json::Value::Object && "Expect an object"); refl::foreach(t, [&](std::string_view name, auto&& member) { + using Field = std::remove_cvref_t; if(auto v = value.getAsObject()->get(llvm::StringRef(name))) { - member = json::deserialize>(*v); + if constexpr(is_optional_v) { + member.emplace(json::deserialize(*v)); + } else { + member = json::deserialize(*v); + } } }); } diff --git a/include/Support/TypeTraits.h b/include/Support/TypeTraits.h index bb30ab49..d6b8f3e0 100644 --- a/include/Support/TypeTraits.h +++ b/include/Support/TypeTraits.h @@ -109,4 +109,49 @@ concept integral = template concept floating_point = std::is_floating_point_v; +// traits for function types +template +struct function_traits { + static_assert(dependent_false, "unsupported function type"); +}; + +#define FUNCTION_TRAITS_SPECIALIZE(...) \ + template \ + struct function_traits { \ + using return_type = R; \ + using args_type = std::tuple; \ + }; + +FUNCTION_TRAITS_SPECIALIZE() +FUNCTION_TRAITS_SPECIALIZE(&) +FUNCTION_TRAITS_SPECIALIZE(const) +FUNCTION_TRAITS_SPECIALIZE(const&) +FUNCTION_TRAITS_SPECIALIZE(noexcept) +FUNCTION_TRAITS_SPECIALIZE(& noexcept) +FUNCTION_TRAITS_SPECIALIZE(const noexcept) +FUNCTION_TRAITS_SPECIALIZE(const& noexcept) + +#undef FUNCTION_TRAITS_SPECIALIZE + +template +using function_return_t = typename function_traits::return_type; + +template +using function_args_t = typename function_traits::args_type; + +template +struct member_traits; + +template +struct member_traits { + using member_type = M; + using class_type = C; +}; + +template +using member_type_t = typename member_traits::member_type; + +template +using class_type_t = typename member_traits::class_type; + } // namespace clice diff --git a/include/Test/CTest.h b/include/Test/CTest.h index dd62d40e..395af605 100644 --- a/include/Test/CTest.h +++ b/include/Test/CTest.h @@ -2,7 +2,7 @@ #include "Test.h" #include "Annotation.h" -#include "Server/Protocol.h" +#include "Protocol/Protocol.h" #include "Compiler/Command.h" #include "Compiler/Compilation.h" diff --git a/src/Compiler/Command.cpp b/src/Compiler/Command.cpp index 51792b8e..3b0fdf12 100644 --- a/src/Compiler/Command.cpp +++ b/src/Compiler/Command.cpp @@ -5,6 +5,7 @@ #include "llvm/Support/CommandLine.h" #include "llvm/Support/Program.h" #include "clang/Driver/Driver.h" +#include "Support/Logger.h" namespace clice { @@ -96,7 +97,19 @@ std::optional CompilationDatabase::get_option_id(llvm::StringRef auto CompilationDatabase::query_driver(this Self& self, llvm::StringRef driver) -> std::expected { - driver = self.save_string(driver); + llvm::SmallString<128> buffer; + + /// FIXME: Should we use a better way? + if(auto error = fs::real_path(driver, buffer)) { + auto result = llvm::sys::findProgramByName(driver); + if(!result) { + return std::unexpected(std::format("{}", result.getError())); + } else { + buffer = *result; + } + } + + driver = self.save_string(buffer); auto it = self.driver_infos.find(driver.data()); if(it != self.driver_infos.end()) { @@ -106,7 +119,7 @@ auto CompilationDatabase::query_driver(this Self& self, llvm::StringRef driver) auto driver_name = path::filename(driver); llvm::SmallString<128> output_path; - if(auto error = llvm::sys::fs::createTemporaryFile("system-includes", "clangd", output_path)) { + if(auto error = llvm::sys::fs::createTemporaryFile("system-includes", "clice", output_path)) { return std::unexpected(std::format("{}", error)); } @@ -173,10 +186,6 @@ auto CompilationDatabase::query_driver(this Self& self, llvm::StringRef driver) } if(in_includes_block) { - if(line.contains("lib/gcc")) { - continue; - } - system_includes.push_back(line); } } @@ -191,7 +200,20 @@ auto CompilationDatabase::query_driver(this Self& self, llvm::StringRef driver) llvm::SmallVector includes; for(auto include: system_includes) { - includes.emplace_back(self.save_string(include).data()); + llvm::SmallString<64> buffer; + + /// Make sure the path is absolute, otherwise it may be + /// "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13", which + /// interferes with our determination of the resource directory + auto err = fs::real_path(include, buffer); + include = buffer; + + /// Remove resource dir of the driver. + if(err || include.contains("lib/gcc")) { + continue; + } + + includes.emplace_back(self.save_string(buffer).data()); } DriverInfo info; @@ -393,8 +415,10 @@ auto CompilationDatabase::load_commands(this Self& self, llvm::StringRef json_co return infos; } -auto CompilationDatabase::get_command(this Self& self, llvm::StringRef file, bool resource_dir) - -> LookupInfo { +auto CompilationDatabase::get_command(this Self& self, + llvm::StringRef file, + bool resource_dir, + bool query_driver) -> LookupInfo { LookupInfo info; file = self.save_string(file); @@ -408,9 +432,29 @@ auto CompilationDatabase::get_command(this Self& self, llvm::StringRef file, boo info.arguments = {"clang++", "-std=c++20"}; } + auto append_argument = [&](llvm::StringRef argument) { + info.arguments.emplace_back(self.save_string(argument).data()); + }; + + if(query_driver) { + if(auto driver_info = self.query_driver(info.arguments[0])) { + append_argument("-nostdlibinc"); + + /// FIXME: Use target information here, this is useful for cross compilation. + + /// FIXME: Cache -I so that we can append directly, avoid duplicate lookup. + for(auto& system_header: driver_info->system_includes) { + append_argument("-I"); + append_argument(system_header); + } + } else { + /// FIXME: Error handle here. + log::warn("Fail query info for {}, because", info.arguments[0], driver_info.error()); + } + } + if(resource_dir) { - info.arguments.emplace_back( - self.save_string(std::format("-resource-dir={}", fs::resource_dir)).data()); + append_argument(std::format("-resource-dir={}", fs::resource_dir)); } info.arguments.emplace_back(file.data()); diff --git a/src/Compiler/Compilation.cpp b/src/Compiler/Compilation.cpp index b8eaf8cc..91275f42 100644 --- a/src/Compiler/Compilation.cpp +++ b/src/Compiler/Compilation.cpp @@ -5,42 +5,15 @@ #include "clang/Lex/PreprocessorOptions.h" #include "clang/Frontend/TextDiagnosticPrinter.h" -#define TRY_OR_RETURN(expr) \ - do { \ - auto&& macro_result = (expr); \ - if(!macro_result.has_value()) { \ - return std::unexpected(std::move(macro_result.error())); \ - } \ - } while(0) - -#define ASSIGN_OR_RETURN(var, expr) \ - do { \ - auto&& macro_result = (expr); \ - if(!macro_result.has_value()) { \ - return std::unexpected(std::move(macro_result.error())); \ - } \ - var = std::move(*macro_result); \ - } while(0) - namespace clice { namespace { -std::unexpected report_diagnostics(llvm::StringRef message, - std::vector& diagnostics) { - std::string error = message.str(); - for(auto& diagnostic: diagnostics) { - error += std::format("{}\n", diagnostic.message); - } - return std::unexpected(std::move(error)); -} - /// create a `clang::CompilerInvocation` for compilation, it set and reset /// all necessary arguments and flags for clice compilation. auto create_invocation(CompilationParams& params, - std::shared_ptr>& diagnostics, llvm::IntrusiveRefCntPtr& diagnostic_engine) - -> std::expected, std::string> { + -> std::unique_ptr { /// Create clang invocation. clang::CreateInvocationOptions options = { @@ -54,7 +27,7 @@ auto create_invocation(CompilationParams& params, auto invocation = clang::createInvocation(params.arguments, options); if(!invocation) { - return report_diagnostics("fail to create compiler invocation", *diagnostics); + return nullptr; } auto& pp_opts = invocation->getPreprocessorOpts(); @@ -87,19 +60,21 @@ auto create_invocation(CompilationParams& params, } template -std::expected clang_compile(CompilationParams& params, - const Adjuster& adjuster) { - auto diagnostics = std::make_shared>(); +CompilationResult run_clang(CompilationParams& params, const Adjuster& adjuster) { + auto diagnostics = params.diagnostics ? std::move(params.diagnostics) + : std::make_shared>(); auto diagnostic_engine = clang::CompilerInstance::createDiagnostics(*params.vfs, new clang::DiagnosticOptions(), Diagnostic::create(diagnostics)); - auto invocation = create_invocation(params, diagnostics, diagnostic_engine); - TRY_OR_RETURN(invocation); + auto invocation = create_invocation(params, diagnostic_engine); + if(!invocation) { + return std::unexpected("Fail to create compilation invocation!"); + } auto instance = std::make_unique(); - instance->setInvocation(std::move(*invocation)); + instance->setInvocation(std::move(invocation)); instance->setDiagnostics(diagnostic_engine.get()); if(auto remapping = clang::createVFSFromCompilerInvocation(instance->getInvocation(), @@ -109,17 +84,16 @@ std::expected clang_compile(CompilationParams& par } if(!instance->createTarget()) { - return std::unexpected("fail to create target"); + return std::unexpected("Fail to create target!"); } /// Adjust the compiler instance, for example, set preamble or modules. adjuster(*instance); - auto action = std::make_unique(); + std::unique_ptr action = std::make_unique(); if(!action->BeginSourceFile(*instance, instance->getFrontendOpts().Inputs[0])) { - /// TODO: collect error message from diagnostics. - return report_diagnostics("Failed to begin source file", *diagnostics); + return std::unexpected("Fail to begin source file"); } auto& pp = instance->getPreprocessor(); @@ -133,29 +107,31 @@ std::expected clang_compile(CompilationParams& par Directive::attach(pp, directives); /// Collect tokens. - std::optional tokCollector; + std::optional tok_collector; /// It is not necessary to collect tokens if we are running code completion. /// And in fact will cause assertion failure. if(!instance->hasCodeCompletionConsumer()) { - tokCollector.emplace(pp); + tok_collector.emplace(pp); } if(auto error = action->Execute()) { return std::unexpected(std::format("Failed to execute action, because {} ", error)); } - /// FIXME: PCH building is very very strict, any error in compilation will - /// result in fail, but for main file building, it is relatively relaxed. - /// We should have a better way to handle this. - if(instance->getDiagnostics().hasFatalErrorOccurred()) { + /// If the output file is not empty, it represents that we are + /// generating a PCH or PCM. If error occurs, the AST must be + /// invalid to some extent, serialization of such AST may result + /// in crash frequently. So forbidden it here and return as error. + if(!instance->getFrontendOpts().OutputFile.empty() && + instance->getDiagnostics().hasErrorOccurred()) { action->EndSourceFile(); - return report_diagnostics("Fetal error occured!!!", *diagnostics); + return std::unexpected("Fail to build PCH or PCM, error occurs in compilation."); } - std::optional tokBuf; - if(tokCollector) { - tokBuf = std::move(*tokCollector).consume(); + std::optional tok_buf; + if(tok_collector) { + tok_buf = std::move(*tok_collector).consume(); } /// FIXME: getDependencies currently return ArrayRef, which actually results in @@ -172,7 +148,7 @@ std::expected clang_compile(CompilationParams& par .action = std::move(action), .instance = std::move(instance), .m_resolver = std::move(resolver), - .buffer = std::move(tokBuf), + .buffer = std::move(tok_buf), .m_directives = std::move(directives), .pathCache = llvm::DenseMap(), .symbolHashCache = llvm::DenseMap(), @@ -184,16 +160,49 @@ std::expected clang_compile(CompilationParams& par } // namespace -std::expected preprocess(CompilationParams& params) { - return clang_compile(params, [](auto&) {}); +CompilationResult preprocess(CompilationParams& params) { + return run_clang(params, [](auto&) {}); } -std::expected compile(CompilationParams& params) { - return clang_compile(params, [](auto&) {}); +CompilationResult compile(CompilationParams& params) { + return run_clang(params, [](auto&) {}); } -std::expected complete(CompilationParams& params, - clang::CodeCompleteConsumer* consumer) { +CompilationResult compile(CompilationParams& params, PCHInfo& out) { + assert(!params.outPath.empty() && "PCH file path cannot be empty"); + + out.path = params.outPath.str(); + /// out.preamble = params.content.substr(0, *params.bound); + /// out.command = params.arguments.str(); + /// FIXME: out.deps = info->deps(); + + return run_clang(params, [&](clang::CompilerInstance& instance) { + /// Set options to generate PCH. + instance.getFrontendOpts().OutputFile = params.outPath.str(); + instance.getFrontendOpts().ProgramAction = clang::frontend::GeneratePCH; + instance.getPreprocessorOpts().GeneratePreamble = true; + instance.getLangOpts().CompilingPCH = true; + }); +} + +CompilationResult compile(CompilationParams& params, PCMInfo& out) { + for(auto& [name, path]: params.pcms) { + out.mods.emplace_back(name); + } + out.path = params.outPath.str(); + + return run_clang( + params, + [&](clang::CompilerInstance& instance) { + /// Set options to generate PCH. + instance.getFrontendOpts().OutputFile = params.outPath.str(); + instance.getFrontendOpts().ProgramAction = + clang::frontend::GenerateReducedModuleInterface; + out.srcPath = instance.getFrontendOpts().Inputs[0].getFile(); + }); +} + +CompilationResult complete(CompilationParams& params, clang::CodeCompleteConsumer* consumer) { auto& [file, offset] = params.completion; @@ -214,7 +223,7 @@ std::expected complete(CompilationParams& params, column += 1; } - return clang_compile(params, [&](clang::CompilerInstance& instance) { + return run_clang(params, [&](clang::CompilerInstance& instance) { /// Set options to run code completion. instance.getFrontendOpts().CodeCompletionAt.FileName = std::move(file); instance.getFrontendOpts().CodeCompletionAt.Line = line; @@ -223,38 +232,4 @@ std::expected complete(CompilationParams& params, }); } -std::expected compile(CompilationParams& params, PCHInfo& out) { - /// assert(params.bound.has_value() && "Preamble bounds is required to build PCH"); - - out.path = params.outPath.str(); - /// out.preamble = params.content.substr(0, *params.bound); - /// out.command = params.arguments.str(); - /// FIXME: out.deps = info->deps(); - - return clang_compile(params, [&](clang::CompilerInstance& instance) { - /// Set options to generate PCH. - instance.getFrontendOpts().OutputFile = params.outPath.str(); - instance.getFrontendOpts().ProgramAction = clang::frontend::GeneratePCH; - instance.getPreprocessorOpts().GeneratePreamble = true; - instance.getLangOpts().CompilingPCH = true; - }); -} - -std::expected compile(CompilationParams& params, PCMInfo& out) { - for(auto& [name, path]: params.pcms) { - out.mods.emplace_back(name); - } - out.path = params.outPath.str(); - - return clang_compile( - params, - [&](clang::CompilerInstance& instance) { - /// Set options to generate PCH. - instance.getFrontendOpts().OutputFile = params.outPath.str(); - instance.getFrontendOpts().ProgramAction = - clang::frontend::GenerateReducedModuleInterface; - out.srcPath = instance.getFrontendOpts().Inputs[0].getFile(); - }); -} - } // namespace clice diff --git a/src/Compiler/Diagnostic.cpp b/src/Compiler/Diagnostic.cpp index 59c46843..15bff09b 100644 --- a/src/Compiler/Diagnostic.cpp +++ b/src/Compiler/Diagnostic.cpp @@ -6,11 +6,13 @@ #include "clang/Basic/DiagnosticIDs.h" #include "clang/Basic/AllDiagnostics.h" #include "clang/Basic/SourceManager.h" +#include "clang/Lex/Preprocessor.h" +#include "Support/Format.h" namespace clice { -llvm::StringRef Diagnostic::diagnostic_code(std::uint32_t ID) { - switch(ID) { +llvm::StringRef DiagnosticID::diagnostic_code() const { + switch(value) { #define DIAG(ENUM, \ CLASS, \ DEFAULT_MAPPING, \ @@ -39,86 +41,157 @@ llvm::StringRef Diagnostic::diagnostic_code(std::uint32_t ID) { } } -// see llvm/clang/include/clang/AST/ASTDiagnostic.h -void dumpArg(clang::DiagnosticsEngine::ArgumentKind kind, std::uint64_t value) { - switch(kind) { - case clang::DiagnosticsEngine::ak_identifierinfo: { - clang::IdentifierInfo* info = reinterpret_cast(value); - llvm::outs() << info->getName(); - break; +std::optional DiagnosticID::diagnostic_document_uri() const { + switch(source) { + case DiagnosticSource::Unknown: + case DiagnosticSource::Clang: { + // There is a page listing many warning flags, but it provides too little + // information to be worth linking. + // https://clang.llvm.org/docs/DiagnosticsReference.html + return std::nullopt; } - case clang::DiagnosticsEngine::ak_qual: { - clang::Qualifiers qual = clang::Qualifiers::fromOpaqueValue(value); - llvm::outs() << qual.getAsString(); - break; + case DiagnosticSource::ClangTidy: { + // This won't correctly get the module for clang-analyzer checks, but as we + // don't link in the analyzer that shouldn't be an issue. + // This would also need updating if anyone decides to create a module with a + // '-' in the name. + auto [module, check] = name.split('-'); + if(module.empty() || check.empty()) { + return std::nullopt; + } + + return std::format("https://clang.llvm.org/extra/clang-tidy/checks/{}/{}.html", + module, + check); } - case clang::DiagnosticsEngine::ak_qualtype: { - clang::QualType type = - clang::QualType::getFromOpaquePtr(reinterpret_cast(value)); - llvm::outs() << type.getAsString(); - break; + case DiagnosticSource::Clice: { + /// TODO: Add diagnostic for clice. + return std::nullopt; + } + } +} + +bool DiagnosticID::is_deprecated() const { + namespace diag = clang::diag; + static llvm::DenseSet deprecated_diags{ + diag::warn_access_decl_deprecated, + diag::warn_atl_uuid_deprecated, + diag::warn_deprecated, + diag::warn_deprecated_altivec_src_compat, + diag::warn_deprecated_comma_subscript, + diag::warn_deprecated_copy, + diag::warn_deprecated_copy_with_dtor, + diag::warn_deprecated_copy_with_user_provided_copy, + diag::warn_deprecated_copy_with_user_provided_dtor, + diag::warn_deprecated_def, + diag::warn_deprecated_increment_decrement_volatile, + diag::warn_deprecated_message, + diag::warn_deprecated_redundant_constexpr_static_def, + diag::warn_deprecated_register, + diag::warn_deprecated_simple_assign_volatile, + diag::warn_deprecated_string_literal_conversion, + diag::warn_deprecated_this_capture, + diag::warn_deprecated_volatile_param, + diag::warn_deprecated_volatile_return, + diag::warn_deprecated_volatile_structured_binding, + diag::warn_opencl_attr_deprecated_ignored, + diag::warn_property_method_deprecated, + diag::warn_vector_mode_deprecated, + }; + + /// TODO: Add clang tidy + return source == DiagnosticSource::Clang && deprecated_diags.contains(value); +} + +bool DiagnosticID::is_unused() const { + namespace diag = clang::diag; + static llvm::DenseSet unused_diags = { + diag::warn_opencl_attr_deprecated_ignored, + diag::warn_pragma_attribute_unused, + diag::warn_unused_but_set_parameter, + diag::warn_unused_but_set_variable, + diag::warn_unused_comparison, + diag::warn_unused_const_variable, + diag::warn_unused_exception_param, + diag::warn_unused_function, + diag::warn_unused_label, + diag::warn_unused_lambda_capture, + diag::warn_unused_local_typedef, + diag::warn_unused_member_function, + diag::warn_unused_parameter, + diag::warn_unused_private_field, + diag::warn_unused_property_backing_ivar, + diag::warn_unused_template, + diag::warn_unused_variable, + }; + + /// TODO: Add clang tidy + return source == DiagnosticSource::Clang && unused_diags.contains(value); +} + +static DiagnosticLevel diagnostic_level(clang::DiagnosticsEngine::Level level) { + switch(level) { + case clang::DiagnosticsEngine::Ignored: return DiagnosticLevel::Ignored; + case clang::DiagnosticsEngine::Note: return DiagnosticLevel::Note; + case clang::DiagnosticsEngine::Remark: return DiagnosticLevel::Remark; + case clang::DiagnosticsEngine::Warning: return DiagnosticLevel::Warning; + case clang::DiagnosticsEngine::Error: return DiagnosticLevel::Error; + case clang::DiagnosticsEngine::Fatal: return DiagnosticLevel::Fatal; + default: return DiagnosticLevel::Invalid; + } +} + +/// Get the range for given diagnostic. +/// FIXME: I would like to use `CompilationUnit`. +auto diagnostic_range(const clang::Diagnostic& diagnostic, const clang::LangOptions& options) + -> std::optional> { + /// If location is invalid, it represents the diagnostic is + /// from the command line. + auto location = diagnostic.getLocation(); + if(location.isInvalid()) { + return std::nullopt; + } + + /// If the location is valid, the `SourceManager` is valid too. + auto& src_mgr = diagnostic.getDiags()->getSourceManager(); + + /// Make sure the location is file location. + location = src_mgr.getFileLoc(location); + assert(location.isFileID()); + + auto [fid, offset] = src_mgr.getDecomposedLoc(location); + + /// Select a proper range for the diagnostic. + for(auto range: diagnostic.getRanges()) { + range = clang::Lexer::makeFileCharRange(range, src_mgr, options); + + auto [begin, end] = range.getAsRange(); + auto [begin_fid, begin_offset] = src_mgr.getDecomposedLoc(begin); + if(begin_fid != fid || begin_offset <= offset) { + continue; } - case clang::DiagnosticsEngine::ak_qualtype_pair: { - clang::TemplateDiffTypes& TDT = *reinterpret_cast(value); - clang::QualType type1 = - clang::QualType::getFromOpaquePtr(reinterpret_cast(TDT.FromType)); - clang::QualType type2 = - clang::QualType::getFromOpaquePtr(reinterpret_cast(TDT.ToType)); - llvm::outs() << type1.getAsString() << " -> " << type2.getAsString(); - break; + auto [end_fid, end_offset] = src_mgr.getDecomposedLoc(end); + if(range.isTokenRange()) { + end_offset += getTokenLength(src_mgr, end); } - case clang::DiagnosticsEngine::ak_declarationname: { - clang::DeclarationName name = clang::DeclarationName::getFromOpaqueInteger(value); - llvm::outs() << name.getAsString(); - break; - } - - case clang::DiagnosticsEngine::ak_nameddecl: { - clang::NamedDecl* decl = reinterpret_cast(value); - llvm::outs() << decl->getNameAsString(); - break; - } - - case clang::DiagnosticsEngine::ak_nestednamespec: { - clang::NestedNameSpecifier* spec = reinterpret_cast(value); - spec->dump(); - break; - } - - case clang::DiagnosticsEngine::ak_declcontext: { - clang::DeclContext* context = reinterpret_cast(value); - llvm::outs() << context->getDeclKindName(); - break; - } - - case clang::DiagnosticsEngine::ak_attr: { - clang::Attr* attr = reinterpret_cast(value); - break; - // attr->dump(); - } - - default: { - std::abort(); + if(end_fid == fid && end_offset >= offset) { + return std::pair{ + fid, + LocalSourceRange{begin_offset, end_offset} + }; } } - llvm::outs() << "\n"; -} - -// Checks whether a location is within a half-open range. -// Note that clang also uses closed source ranges, which this can't handle! -bool locationInRange(clang::SourceLocation L, - clang::CharSourceRange R, - const clang::SourceManager& M) { - /// assert(R.isCharRange()); - if(!R.isValid() || M.getFileID(R.getBegin()) != M.getFileID(R.getEnd()) || - M.getFileID(R.getBegin()) != M.getFileID(L)) - return false; - return L != R.getEnd() && M.isPointWithin(L, R.getBegin(), R.getEnd()); + /// Use token range. + auto end_offset = offset + getTokenLength(src_mgr, location); + return std::pair{ + fid, + LocalSourceRange{offset, end_offset} + }; } class DiagnosticCollector : public clang::DiagnosticConsumer { @@ -126,54 +199,46 @@ public: DiagnosticCollector(std::shared_ptr> diagnostics) : diagnostics(diagnostics) {} - static DiagnosticLevel diagnostic_level(clang::DiagnosticsEngine::Level level) { - switch(level) { - case clang::DiagnosticsEngine::Ignored: return DiagnosticLevel::Ignored; - case clang::DiagnosticsEngine::Note: return DiagnosticLevel::Note; - case clang::DiagnosticsEngine::Remark: return DiagnosticLevel::Remark; - case clang::DiagnosticsEngine::Warning: return DiagnosticLevel::Warning; - case clang::DiagnosticsEngine::Error: return DiagnosticLevel::Error; - case clang::DiagnosticsEngine::Fatal: return DiagnosticLevel::Fatal; - default: return DiagnosticLevel::Invalid; - } + void BeginSourceFile(const clang::LangOptions& Opts, const clang::Preprocessor* PP) override { + options = &Opts; + src_mgr = &PP->getSourceManager(); } - void BeginSourceFile(const clang::LangOptions& Opts, const clang::Preprocessor* PP) override {} - void HandleDiagnostic(clang::DiagnosticsEngine::Level level, const clang::Diagnostic& raw_diagnostic) override { auto& diagnostic = diagnostics->emplace_back(); - diagnostic.id = raw_diagnostic.getID(); - diagnostic.level = diagnostic_level(level); + diagnostic.id.value = raw_diagnostic.getID(); + diagnostic.id.level = diagnostic_level(level); + + /// TODO: + // use DiagnosticEngine::SetArgToStringFn to set a custom function to convert arguments to + // strings. Support markdown diagnostic in LSP 3.18. allow complex type to display in + // markdown code block. + /// + /// auto& engine = src_mgr->getDiagnostics(); + /// engine.SetArgToStringFn(); llvm::SmallString<256> message; raw_diagnostic.FormatDiagnostic(message); diagnostic.message = message.str(); - auto location = raw_diagnostic.getLocation(); - if(location.isInvalid()) { - return; + if(auto pair = diagnostic_range(raw_diagnostic, *options)) { + auto [fid, range] = *pair; + diagnostic.fid = fid; + diagnostic.range = range; } - auto& SM = raw_diagnostic.getDiags()->getSourceManager(); - for(auto& range: raw_diagnostic.getRanges()) { - if(locationInRange(raw_diagnostic.getLocation(), range, SM)) { - diagnostic.range = range.getAsRange(); - break; - } - } - - // TODO: - // use DiagnosticEngine::SetArgToStringFn to set a custom function to convert arguments to - // strings. Support markdown diagnostic in LSP 3.18. allow complex type to display in - // markdown code block. + /// TODO: handle FixIts + /// raw_diagnostic.getFixItHints(); } void EndSourceFile() override {} private: std::shared_ptr> diagnostics; + const clang::LangOptions* options; + clang::SourceManager* src_mgr; }; clang::DiagnosticConsumer* diff --git a/src/Driver/clice.cc b/src/Driver/clice.cc index 4c4ab99d..00ad4bb8 100644 --- a/src/Driver/clice.cc +++ b/src/Driver/clice.cc @@ -106,7 +106,7 @@ int main(int argc, const char** argv) { /// The global server instance. static Server instance; auto loop = [&](json::Value value) -> async::Task<> { - co_await instance.onReceive(value); + co_await instance.on_receive(value); }; if(mode == "pipe") { diff --git a/src/Feature/Diagnostic.cpp b/src/Feature/Diagnostic.cpp new file mode 100644 index 00000000..68e6966d --- /dev/null +++ b/src/Feature/Diagnostic.cpp @@ -0,0 +1,122 @@ +#include "Feature/Diagnostic.h" +#include "Compiler/CompilationUnit.h" +#include "Server/Convert.h" +#include "Support/Logger.h" + +namespace clice::feature { + +json::Value diagnostics(PositionEncodingKind kind, PathMapping mapping, CompilationUnit& unit) { + json::Array result; + + std::optional diagnostic; + + auto flush = [&]() { + if(diagnostic) { + /// FIXME: We should use a better way? + result.emplace_back(json::serialize(*diagnostic)); + diagnostic.reset(); + } + }; + + for(auto& raw_diagnostic: unit.diagnostics()) { + auto level = raw_diagnostic.id.level; + auto fid = raw_diagnostic.fid; + + /// FIXME: Is it possible that a group of notes following the + /// ignored diagnostic? so that we should skil them also. + if(level == DiagnosticLevel::Ignored) { + continue; + } + + /// Append to last. + if(level == DiagnosticLevel::Note || level == DiagnosticLevel::Remark) { + /// FIXME: figure out why it may be invalid. + if(fid.isInvalid()) { + log::info("code: {}, message: {}", + raw_diagnostic.id.diagnostic_code(), + raw_diagnostic.message); + continue; + } + + if(!raw_diagnostic.range.valid()) { + continue; + } + + auto content = unit.file_content(fid); + PositionConverter converter(content, kind); + + proto::Location location; + location.range.start = converter.toPosition(raw_diagnostic.range.begin); + location.range.end = converter.toPosition(raw_diagnostic.range.end); + location.uri = mapping.to_uri(unit.file_path(fid)); + + diagnostic->relatedInformation.emplace_back(std::move(location), + raw_diagnostic.message); + continue; + } + + /// Flash the last diagnostic. + flush(); + diagnostic.emplace(); + + /// If the fid is invalid, we add a default range for it. + if(fid.isInvalid()) { + diagnostic->range = {0, 0, 0, 0}; + } else if(fid == unit.interested_file()) { + PositionConverter converter(unit.interested_content(), kind); + diagnostic->range.start = converter.toPosition(raw_diagnostic.range.begin); + diagnostic->range.end = converter.toPosition(raw_diagnostic.range.end); + } else { + PositionConverter converter(unit.interested_content(), kind); + + /// Get the top level include location. + auto include_location = unit.include_location(fid); + while(true) { + auto fid2 = unit.file_id(include_location); + if(fid2.isValid()) { + include_location = unit.include_location(fid2); + } else { + break; + } + } + + /// Use the location of include directive. + auto offset = unit.file_offset(include_location); + auto end_offset = offset + unit.token_spelling(include_location).size(); + + diagnostic->range.start = converter.toPosition(offset); + diagnostic->range.end = converter.toPosition(end_offset); + } + + if(level == DiagnosticLevel::Warning) { + diagnostic->severity = proto::DiagnosticSeverity::Warning; + } else if(level == DiagnosticLevel::Error || level == DiagnosticLevel::Fatal) { + diagnostic->severity = proto::DiagnosticSeverity::Error; + } + + diagnostic->code = raw_diagnostic.id.diagnostic_code(); + + if(auto uri = raw_diagnostic.id.diagnostic_document_uri()) { + /// It is already be uri, mapping is not needed. + diagnostic->codeDescription.emplace(std::move(*uri)); + } + + /// FIXME: According to raw_diagnostic.id.source to assign, + /// currently all diagnostics are from clang. + diagnostic->source = "clang"; + + diagnostic->message = raw_diagnostic.message; + + if(raw_diagnostic.id.is_deprecated()) { + diagnostic->tags.emplace_back(proto::DiagnosticTag::Deprecated); + } else if(raw_diagnostic.id.is_unused()) { + diagnostic->tags.emplace_back(proto::DiagnosticTag::Unnecessary); + } + } + + flush(); + + return result; +} + +} // namespace clice::feature diff --git a/src/Server/Convert.cpp b/src/Server/Convert.cpp new file mode 100644 index 00000000..adfda576 --- /dev/null +++ b/src/Server/Convert.cpp @@ -0,0 +1,118 @@ +#include "Server/Convert.h" +#include "Protocol/Protocol.h" +#include "Support/Ranges.h" +#include "Support/JSON.h" +#include "Support/Format.h" + +namespace clice::proto { + +json::Value to_json(clice::PositionEncodingKind kind, + llvm::StringRef content, + llvm::ArrayRef tokens) { + std::vector groups; + + auto add_token = [&](uint32_t line, + uint32_t character, + uint32_t length, + clice::SymbolKind kind, + SymbolModifiers modifiers) { + groups.emplace_back(line); + groups.emplace_back(character); + groups.emplace_back(length); + groups.emplace_back(kind.value()); + groups.emplace_back(0); + }; + + PositionConverter converter(content, kind); + std::uint32_t last_line = 0; + std::uint32_t last_char = 0; + + for(auto& token: tokens) { + auto [begin_offset, end_offset] = token.range; + auto [begin_line, begin_char] = converter.toPosition(begin_offset); + auto [end_line, end_char] = converter.toPosition(end_offset); + + if(begin_line == end_line) [[likely]] { + std::uint32_t line = begin_line - last_line; + std::uint32_t character = (line == 0 ? begin_char - last_char : begin_char); + std::uint32_t length = end_char - begin_char; + add_token(line, character, length, token.kind, token.modifiers); + } else { + /// If the token spans multiple lines, split it into multiple tokens. + auto sub_content = content.substr(begin_offset, end_offset - begin_offset); + + /// The first line is special. + bool isFirst = true; + /// The offset of the last line end. + std::uint32_t last_line_offset = 0; + /// The length of the current line. + std::uint32_t line_length = 0; + + for(auto c: sub_content) { + line_length += 1; + if(c == '\n') { + std::uint32_t line; + std::uint32_t character; + + if(isFirst) [[unlikely]] { + line = begin_line - last_line; + character = (line == 0 ? begin_char - last_char : begin_char); + isFirst = false; + } else { + line = 1; + character = 0; + } + + std::uint32_t length = + remeasure(sub_content.substr(last_line_offset, line_length), kind); + add_token(line, character, length, token.kind, token.modifiers); + + last_line_offset += line_length; + line_length = 0; + } + } + + /// Process the last line if it's not empty. + if(line_length > 0) { + std::uint32_t length = remeasure(sub_content.substr(last_line_offset), kind); + add_token(1, 0, length, token.kind, token.modifiers); + } + } + + last_line = end_line; + last_char = begin_char; + } + + auto object = json::Object{ + /// The actual tokens. + {"data", json::serialize(groups)} + }; + return json::Value(std::move(object)); +} + +json::Value to_json(clice::PositionEncodingKind kind, + llvm::StringRef content, + llvm::ArrayRef items) { + PositionConverter converter(content, kind); + converter.to_positions(items, [](auto& item) { return item.edit.range; }); + + json::Array result; + + for(auto& item: items) { + json::Object object{ + {"label", item.label}, + {"kind", static_cast(item.kind)}, + {"textEdit", + json::Object{ + {"newText", item.edit.text}, + {"range", json::serialize(converter.lookup(item.edit.range))}, + }}, + {"sortText", std::format("{}", item.score)}, + }; + result.emplace_back(std::move(object)); + } + + return json::Value(std::move(result)); +} + +} // namespace clice::proto diff --git a/src/Server/Document.cpp b/src/Server/Document.cpp new file mode 100644 index 00000000..5b1e7489 --- /dev/null +++ b/src/Server/Document.cpp @@ -0,0 +1,215 @@ +#include "Support/Logger.h" +#include "Server/Server.h" +#include "Compiler/Compilation.h" +#include "Feature/Diagnostic.h" + +namespace clice { + +async::Task Server::add_document(std::string path, std::string content) { + auto& openFile = opening_files[path]; + openFile.content = content; + + auto& task = openFile.ast_build_task; + + /// If there is already an AST build task, cancel it. + if(!task.empty()) { + task.cancel(); + task.dispose(); + } + + /// Create and schedule a new task. + task = build_ast(std::move(path), std::move(content)); + co_await task; + + co_return &opening_files[path]; +} + +async::Task<> Server::build_pch(std::string path, std::string content) { + auto bound = computePreambleBound(content); + + auto openFile = &opening_files[path]; + bool outdated = true; + if(openFile->pch) { + /// FIXME: + /// outdated = co_await isPCHOutdated(path, llvm::StringRef(content).substr(0, bound)); + } + + /// If not need update, return directly. + if(!outdated) { + co_return; + } + + /// The actual PCH build task. + constexpr static auto PCHBuildTask = + [](Server& server, + std::string path, + std::uint32_t bound, + std::string content, + std::shared_ptr> diagnostics) -> async::Task { + if(!fs::exists(config::cache.dir)) { + auto error = fs::create_directories(config::cache.dir); + if(error) { + log::warn("Fail to create directory for PCH building: {}", config::cache.dir); + co_return false; + } + } + + /// Everytime we build a new pch, the old diagnostics should be discarded. + diagnostics->clear(); + + CompilationParams params; + params.outPath = path::join(config::cache.dir, path::filename(path) + ".pch"); + params.arguments = server.database.get_command(path, true, true).arguments; + params.diagnostics = diagnostics; + params.add_remapped_file(path, content, bound); + + PCHInfo info; + + std::string command; + for(auto argument: params.arguments) { + command += " "; + command += argument; + } + + log::info("Start building PCH for {}, command: [{}]", path, command); + + /// PCH file is written until destructing, Add a single block + /// for it. + bool cond = co_await async::submit([&] { + auto result = compile(params, info); + if(!result) { + log::warn("Building PCH fails for {}, Because: {}", path, result.error()); + + for(auto& diagnostic: *diagnostics) { + log::warn("{}", diagnostic.message); + } + return false; + } + + /// TODO: index PCH. + + return true; + }); + + if(!cond) { + co_return false; + } + + auto& openFile = server.opening_files[path]; + /// Update the built PCH info. + openFile.pch = std::move(info); + /// Resume waiters on this event. + openFile.pch_built_event.set(); + openFile.pch_built_event.clear(); + + co_return true; + }; + + openFile = &opening_files[path]; + + /// If there is already an PCH build task, cancel it. + auto& task = openFile->pch_build_task; + if(!task.empty()) { + task.cancel(); + task.dispose(); + } + + /// Schedule the new building task. + task = PCHBuildTask(*this, path, bound, std::move(content), openFile->diagnostics); + + if(co_await task) { + log::info("Building PCH successfully for {}", path); + + /// Dispose the task so that it will destroyed when task complete. + task.dispose(); + } + + /// TODO: report diagnostics in the preamble. +} + +async::Task<> Server::build_ast(std::string path, std::string content) { + auto file = &opening_files[path]; + + /// Try get the lock, the waiter on the lock will be resumed when + /// guard is destroyed. + auto guard = co_await file->ast_built_lock.try_lock(); + + /// PCH is already updated. + co_await build_pch(path, content); + + auto pch = opening_files[path].pch; + if(!pch) { + log::fatal("Expected PCH built at this point"); + } + + file = &opening_files[path]; + CompilationParams params; + params.arguments = database.get_command(path, true, true).arguments; + params.add_remapped_file(path, content); + params.pch = {pch->path, pch->preamble.size()}; + file->diagnostics->clear(); + params.diagnostics = file->diagnostics; + + /// Check result + auto ast = co_await async::submit([&] { return compile(params); }); + if(!ast) { + /// FIXME: Fails needs cancel waiting tasks. + log::warn("Building AST fails for {}, Beacuse: {}", path, ast.error()); + co_return; + } + + /// FIXME: Index the source file. + /// co_await indexer.index(*ast); + + file = &opening_files[path]; + /// Update built AST info. + file->ast = std::make_shared(std::move(*ast)); + /// Dispose the task so that it will destroyed when task complete. + file->ast_build_task.dispose(); + + log::info("Building AST successfully for {}", path); +} + +async::Task<> Server::on_did_open(proto::DidOpenTextDocumentParams params) { + auto path = mapping.to_path(params.textDocument.uri); + auto file = co_await add_document(path, std::move(params.textDocument.text)); + if(file->diagnostics) { + auto guard = co_await file->ast_built_lock.try_lock(); + file = &opening_files[path]; + auto diagnostics = feature::diagnostics(kind, mapping, *file->ast); + co_await notify("textDocument/publishDiagnostics", + json::Object{ + {"uri", mapping.to_uri(path) }, + {"diagnostics", std::move(diagnostics)}, + }); + } + co_return; +} + +async::Task<> Server::on_did_change(proto::DidChangeTextDocumentParams params) { + auto path = mapping.to_path(params.textDocument.uri); + auto file = co_await add_document(path, std::move(params.contentChanges[0].text)); + if(file->diagnostics) { + auto guard = co_await file->ast_built_lock.try_lock(); + file = &opening_files[path]; + auto diagnostics = feature::diagnostics(kind, mapping, *file->ast); + co_await notify("textDocument/publishDiagnostics", + json::Object{ + {"uri", mapping.to_uri(path) }, + {"diagnostics", std::move(diagnostics)}, + }); + } + co_return; +} + +async::Task<> Server::on_did_save(proto::DidSaveTextDocumentParams params) { + auto path = mapping.to_path(params.textDocument.uri); + co_return; +} + +async::Task<> Server::on_did_close(proto::DidCloseTextDocumentParams params) { + auto path = mapping.to_path(params.textDocument.uri); + co_return; +} + +} // namespace clice diff --git a/src/Server/Feature.cpp b/src/Server/Feature.cpp index 076114dc..36a93ca8 100644 --- a/src/Server/Feature.cpp +++ b/src/Server/Feature.cpp @@ -1,67 +1,54 @@ #include "Server/Server.h" +#include "Server/Convert.h" +#include "Compiler/Compilation.h" namespace clice { -// async::Task<> Server::onDocumentHighlight(json::Value id, -// const proto::DocumentHighlightParams& params) { -// co_return; -// } -// -// async::Task<> Server::onDocumentLink(json::Value id, const proto::DocumentLinkParams& params) { -// co_return; -// } -// -// async::Task<> Server::onHover(json::Value id, const proto::HoverParams& params) { -// co_return; -// } -// -// async::Task<> Server::onCodeLens(json::Value id, const proto::CodeLensParams& params) { -// co_return; -// } -// -// async::Task<> Server::onFoldingRange(json::Value id, const proto::FoldingRangeParams& params) { -// co_return; -// } -// -// async::Task<> Server::onDocumentSymbol(json::Value id, const proto::DocumentSymbolParams& params) -// { -// co_return; -// } -// -// async::Task<> Server::onSemanticTokens(json::Value id, const proto::SemanticTokensParams& params) -// { -// auto path = SourceConverter::toPath(params.textDocument.uri); -// /// auto tokens = co_await indexer.semanticTokens(path); -// /// co_await response(std::move(id), json::serialize(tokens)); -// co_return; -// } -// -// async::Task<> Server::onInlayHint(json::Value id, const proto::InlayHintParams& params) { -// co_return; -// } -// -// async::Task<> Server::onCodeCompletion(json::Value id, const proto::CompletionParams& params) { -// // auto path = URI::resolve(params.textDocument.uri); -// // async::response(std::move(id), json::serialize(result)); -// co_return; -// } -// -// async::Task<> Server::onSignatureHelp(json::Value id, const proto::SignatureHelpParams& params) { -// co_return; -// } -// -// async::Task<> Server::onCodeAction(json::Value id, const proto::CodeActionParams& params) { -// co_return; -// } -// -// async::Task<> Server::onFormatting(json::Value id, const proto::DocumentFormattingParams& params) -// { -// co_return; -// } -// -// async::Task<> Server::onRangeFormatting(json::Value id, -// const proto::DocumentRangeFormattingParams& params) { -// co_return; -// } +async::Task Server::on_semantic_token(proto::SemanticTokensParams params) { + auto path = mapping.to_path(params.textDocument.uri); + + auto openFile = &opening_files[path]; + auto guard = co_await openFile->ast_built_lock.try_lock(); + + openFile = &opening_files[path]; + auto content = openFile->content; + auto ast = openFile->ast; + if(!ast) { + co_return ""; + } + + co_return co_await async::submit([kind = this->kind, &ast] { + auto tokens = feature::semanticTokens(*ast); + return proto::to_json(kind, ast->interested_content(), tokens); + }); +} + +async::Task Server::on_completion(proto::CompletionParams params) { + auto path = mapping.to_path(params.textDocument.uri); + auto opening_file = &opening_files[path]; + auto offset = to_offset(kind, opening_file->content, params.position); + + if(!opening_file->pch_build_task.empty()) { + co_await opening_file->pch_built_event; + } + + opening_file = &opening_files[path]; + auto& pch = opening_file->pch; + + { + /// Set compilation params ... . + CompilationParams params; + params.arguments = database.get_command(path, true).arguments; + params.add_remapped_file(path, opening_file->content); + params.pch = {pch->path, pch->preamble.size()}; + params.completion = {path, offset}; + + co_return co_await async::submit( + [kind = this->kind, content = opening_file->content, ¶ms] { + auto items = feature::code_complete(params, {}); + return proto::to_json(kind, content, items); + }); + } +} } // namespace clice diff --git a/src/Server/LSPConverter.cpp b/src/Server/LSPConverter.cpp deleted file mode 100644 index fc863548..00000000 --- a/src/Server/LSPConverter.cpp +++ /dev/null @@ -1,518 +0,0 @@ -#include "Server/LSPConverter.h" -#include "Support/FileSystem.h" - -namespace clice { - -namespace { - -/// @brief Iterates over Unicode codepoints in a UTF-8 encoded string and invokes a callback for -/// each codepoint. -/// -/// Processes the input UTF-8 string, calculating the length of each Unicode codepoint in both -/// UTF-8 (bytes) and UTF-16 (code units), and passes these lengths to the callback. -/// Iteration stops early if the callback returns `false`. -/// -/// ASCII characters are treated as 1-byte UTF-8 codepoints with a UTF-16 length of 1. -/// Non-ASCII characters are processed based on their leading byte to determine UTF-8 length: -/// - Valid lengths are 2 to 4 bytes. -/// - Astral codepoints (UTF-8 length of 4) have a UTF-16 length of 2 code units. -/// Invalid UTF-8 sequences are treated as single-byte ASCII characters. -/// -/// Returns `false` if the callback stops the iteration. -template -bool iterateCodepoints(llvm::StringRef content, const Callback& callback) { - // Iterate over the input string, processing each codepoint. - for(size_t index = 0; index < content.size();) { - unsigned char c = static_cast(content[index]); - - // Handle ASCII characters (1-byte UTF-8, 1-code-unit UTF-16). - if(!(c & 0x80)) [[likely]] { - if(!callback(1, 1)) { - return true; - } - - ++index; - continue; - } - - // Determine the length of the codepoint in UTF-8 by counting the leading 1s. - size_t length = llvm::countl_one(c); - - // Validate UTF-8 encoding: length must be between 2 and 4. - if(length < 2 || length > 4) [[unlikely]] { - assert(false && "Invalid UTF-8 sequence"); - - // Treat the byte as an ASCII character. - if(!callback(1, 1)) { - return true; - } - - ++index; - continue; - } - - // Advance the index by the length of the current UTF-8 codepoint. - index += length; - - // Calculate the UTF-16 length: astral codepoints (4-byte UTF-8) take 2 code units. - if(!callback(length, length == 4 ? 2 : 1)) { - return true; - } - } - - return false; -} - -/// Convert a proto::Position to a file offset in the content with the specified encoding kind. -std::uint32_t toOffset(llvm::StringRef content, - PositionEncodingKind kind, - proto::Position position) { - std::uint32_t offset = 0; - for(auto i = 0; i < position.line; i++) { - auto pos = content.find('\n'); - assert(pos != llvm::StringRef::npos && "Line value is out of range"); - - offset += pos + 1; - content = content.substr(pos + 1); - } - - /// Drop the content after the line. - content = content.take_until([](char c) { return c == '\n'; }); - assert(position.character <= content.size() && "Character value is out of range"); - - if(position.character == 0) { - return offset; - } - - if(kind == PositionEncodingKind::UTF8) { - offset += position.character; - return offset; - } - - if(kind == PositionEncodingKind::UTF16) { - iterateCodepoints(content, [&](std::uint32_t utf8Length, std::uint32_t utf16Length) { - assert(position.character >= utf16Length && "Character value is out of range"); - position.character -= utf16Length; - offset += utf8Length; - return position.character != 0; - }); - return offset; - } - - if(kind == PositionEncodingKind::UTF32) { - iterateCodepoints(content, [&](std::uint32_t utf8Length, std::uint32_t) { - assert(position.character >= 1 && "Character value is out of range"); - position.character -= 1; - offset += utf8Length; - return position.character != 0; - }); - return offset; - } - - std::unreachable(); -} - -/// Remeasure the length (character count) of the content with the specified encoding kind. -std::uint32_t remeasure(llvm::StringRef content, PositionEncodingKind kind) { - if(kind == PositionEncodingKind::UTF8) { - return content.size(); - } - - if(kind == PositionEncodingKind::UTF16) { - std::uint32_t length = 0; - iterateCodepoints(content, [&](std::uint32_t, std::uint32_t utf16Length) { - length += utf16Length; - return true; - }); - return length; - } - - if(kind == PositionEncodingKind::UTF32) { - std::uint32_t length = 0; - iterateCodepoints(content, [&](std::uint32_t, std::uint32_t) { - length += 1; - return true; - }); - return length; - } - - std::unreachable(); -} - -class PositionConverter { -public: - PositionConverter(llvm::StringRef content, PositionEncodingKind encoding) : - content(content), encoding(encoding) {} - - /// Convert a offset to a proto::Position with given encoding. - /// The input offset must be UTF-8 encoded and in order. - proto::Position toPosition(uint32_t offset) { - assert(offset <= content.size() && "Offset is out of range"); - assert(offset >= lastInput && "Offset must be in order"); - - /// Fast path: return the last output. - if(offset == lastInput) [[unlikely]] { - return lastOutput; - } - - /// The length of the current line. - std::uint32_t lineLength = 0; - - /// Move the line offset to the current line. - for(std::uint32_t i = lastLineOffset; i < offset; i++) { - lineLength += 1; - if(content[i] == '\n') { - line += 1; - lastLineOffset += lineLength; - lineLength = 0; - } - } - - /// Get the content of the current line. - auto lineContent = content.substr(lastLineOffset, lineLength); - auto position = proto::Position{ - .line = line, - .character = remeasure(lineContent, encoding), - }; - - /// Cache the result. - lastInput = offset; - lastOutput = position; - - return position; - } - - template - void toPositions(Range&& range, Proj&& proj) { - std::vector offsets; - for(auto&& item: range) { - auto [begin, end] = proj(item); - offsets.emplace_back(begin); - offsets.emplace_back(end); - } - - ranges::sort(offsets); - - for(auto&& offset: offsets) { - if(auto it = cache.find(offset); it == cache.end()) { - cache.try_emplace(offset, toPosition(offset)); - } - } - } - - proto::Position lookup(uint32_t offset) { - auto it = cache.find(offset); - assert(it != cache.end() && "Offset is not cached"); - return it->second; - } - - proto::Range lookup(LocalSourceRange range) { - auto it = cache.find(range.begin); - assert(it != cache.end() && "Offset is not cached"); - auto begin = it->second; - it = cache.find(range.end); - assert(it != cache.end() && "Offset is not cached"); - auto end = it->second; - return proto::Range{begin, end}; - } - -private: - std::uint32_t line = 0; - /// The offset of the last line end. - std::uint32_t lastLineOffset = 0; - - /// The input offset of last call. - std::uint32_t lastInput = 0; - proto::Position lastOutput = {0, 0}; - - llvm::DenseMap cache; - - llvm::StringRef content; - PositionEncodingKind encoding; -}; - -} // namespace - -std::uint32_t LSPConverter::convert(llvm::StringRef content, proto::Position position) { - return toOffset(content, encoding(), position); -} - -proto::Position LSPConverter::convert(llvm::StringRef content, std::uint32_t offset) { - PositionConverter converter(content, encoding()); - return converter.toPosition(offset); -} - -std::string LSPConverter::convert(llvm::StringRef URI) { - return fs::toPath(URI); -} - -json::Value LSPConverter::convert(llvm::StringRef content, const feature::Hover& hover) { - return json::Value(nullptr); -} - -json::Value LSPConverter::convert(llvm::StringRef content, const feature::InlayHints& hints) { - return json::Value(nullptr); -} - -json::Value LSPConverter::convert(llvm::StringRef content, const feature::FoldingRanges& foldings) { - PositionConverter converter(content, encoding()); - converter.toPositions(foldings, [](auto&& folding) { return folding.range; }); - - json::Array result; - for(auto&& folding: foldings) { - auto [beginOffset, endOffset] = folding.range; - auto [beginLine, beginChar] = converter.lookup(beginOffset); - auto [endLine, endChar] = converter.lookup(endOffset); - - auto object = json::Object{ - {"startLine", beginLine}, - {"startCharacter", beginChar}, - {"endLine", endLine }, - {"kind", "region" }, - }; - - result.push_back(std::move(object)); - } - return result; -} - -json::Value LSPConverter::convert(llvm::StringRef content, const feature::DocumentLinks& links) { - PositionConverter converter(content, encoding()); - - json::Array result; - for(auto& link: links) { - proto::Range range{ - converter.toPosition(link.range.begin), - converter.toPosition(link.range.end), - }; - - auto object = json::Object{ - /// The range of document link. - {"range", json::serialize(range)}, - /// Target file URI. - {"target", fs::toURI(link.file) }, - }; - - result.emplace_back(std::move(object)); - } - - return result; -} - -json::Value LSPConverter::convert(llvm::StringRef content, - const feature::DocumentSymbols& symbols) { - PositionConverter converter(content, encoding()); - - struct DocumentSymbol { - std::string name; - std::string detail; - SymbolKind kind; - proto::Range range; - proto::Range selectionRange; - std::vector children; - }; - - json::Array result; - - /// TODO: Implementation. - - return result; -} - -json::Value LSPConverter::convert(llvm::StringRef content, const feature::SemanticTokens& tokens) { - std::vector groups; - - auto addGroup = [&](uint32_t line, - uint32_t character, - uint32_t length, - SymbolKind kind, - SymbolModifiers modifiers) { - groups.emplace_back(line); - groups.emplace_back(character); - groups.emplace_back(length); - groups.emplace_back(kind.value()); - groups.emplace_back(0); - }; - - PositionConverter converter(content, encoding()); - std::uint32_t lastLine = 0; - std::uint32_t lastChar = 0; - - for(auto& token: tokens) { - auto [beginOffset, endOffset] = token.range; - auto [beginLine, beginChar] = converter.toPosition(beginOffset); - auto [endLine, endChar] = converter.toPosition(endOffset); - - if(beginLine == endLine) [[likely]] { - std::uint32_t line = beginLine - lastLine; - std::uint32_t character = (line == 0 ? beginChar - lastChar : beginChar); - std::uint32_t length = endChar - beginChar; - addGroup(line, character, length, token.kind, token.modifiers); - } else { - /// If the token spans multiple lines, split it into multiple tokens. - auto subContent = content.substr(beginOffset, endOffset - beginOffset); - - /// The first line is special. - bool isFirst = true; - /// The offset of the last line end. - std::uint32_t lastLineOffset = 0; - /// The length of the current line. - std::uint32_t lineLength = 0; - - for(auto c: subContent) { - lineLength += 1; - if(c == '\n') { - std::uint32_t line; - std::uint32_t character; - - if(isFirst) [[unlikely]] { - line = beginLine - lastLine; - character = (line == 0 ? beginChar - lastChar : beginChar); - isFirst = false; - } else { - line = 1; - character = 0; - } - - std::uint32_t length = - remeasure(subContent.substr(lastLineOffset, lineLength), encoding()); - addGroup(line, character, length, token.kind, token.modifiers); - - lastLineOffset += lineLength; - lineLength = 0; - } - } - - /// Process the last line if it's not empty. - if(lineLength > 0) { - std::uint32_t length = remeasure(subContent.substr(lastLineOffset), encoding()); - addGroup(1, 0, length, token.kind, token.modifiers); - } - } - - lastLine = endLine; - lastChar = beginChar; - } - - return json::Object{ - /// The actual tokens. - {"data", json::serialize(groups)}, - }; -} - -json::Value LSPConverter::convert(llvm::StringRef content, - const std::vector& items) { - PositionConverter converter(content, encoding()); - converter.toPositions(items, [](auto& item) { return item.edit.range; }); - - json::Array result; - for(auto& item: items) { - json::Object object{ - {"label", item.label}, - {"kind", static_cast(item.kind)}, - {"textEdit", - json::Object{ - {"newText", item.edit.text}, - {"range", json::serialize(converter.lookup(item.edit.range))}, - }}, - {"sortText", std::format("{}", item.score)}, - }; - result.emplace_back(std::move(object)); - } - return result; -} - -namespace proto { - -struct InitializeParams { - struct ClientInfo { - std::string name; - std::string version; - } clientInfo; - - struct ClientCapabilities { - struct General { - std::vector positionEncodings; - } general; - } capabilities; - - std::vector workspaceFolders; -}; - -struct InitializeResult { - struct ServerInfo { - std::string name; - std::string version; - } serverInfo; - - struct ServerCapabilities { - std::string positionEncoding; - TextDocumentSyncKind textDocumentSync = TextDocumentSyncKind::Full; - - bool declarationProvider = true; - bool definitionProvider = true; - bool typeDefinitionProvider = true; - bool implementationProvider = true; - bool callHierarchyProvider = true; - bool typeHierarchyProvider = true; - - bool hoverProvider = true; - ResolveProvider inlayHintProvider = {true}; - bool foldingRangeProvider = true; - ResolveProvider documentLinkProvider = {false}; - bool documentSymbolProvider = true; - SemanticTokenOptions semanticTokensProvider; - - /// TODO: - CompletionOptions completionProvider; - /// signatureHelpProvider - /// codeLensProvider - /// codeActionProvider - /// documentFormattingProvider - /// documentRangeFormattingProvider - /// renameProvider - /// diagnosticProvider - } capabilities; -}; - -} // namespace proto - -json::Value LSPConverter::initialize(json::Value value) { - auto params = json::deserialize(value); - - auto& encodings = params.capabilities.general.positionEncodings; - /// Select the first one encoding if any. - if(encodings.empty()) { - kind = PositionEncodingKind::UTF16; - } else if(encodings[0] == "utf-8") { - kind = PositionEncodingKind::UTF8; - } else if(encodings[0] == "utf-16") { - kind = PositionEncodingKind::UTF16; - } else if(encodings[0] == "utf-32") { - kind = PositionEncodingKind::UTF32; - } - - if(params.workspaceFolders.empty()) { - log::fatal("The server must provide at least one workspace folder"); - } - - workspacePath = fs::toPath(params.workspaceFolders[0].uri); - - proto::InitializeResult result{ - .serverInfo = {"clice", "0.0.1"}, - .capabilities = { - .positionEncoding = encodings.empty() ? "utf-16" : encodings[0], - } - }; - - auto& semanticTokensProvider = result.capabilities.semanticTokensProvider; - for(auto name: SymbolKind::all()) { - std::string type{name}; - type[0] = std::tolower(type[0]); - semanticTokensProvider.legend.tokenTypes.emplace_back(std::move(type)); - } - - return json::serialize(result); -} - -} // namespace clice diff --git a/src/Server/Lifecycle.cpp b/src/Server/Lifecycle.cpp new file mode 100644 index 00000000..3e211356 --- /dev/null +++ b/src/Server/Lifecycle.cpp @@ -0,0 +1,59 @@ +#include "Server/Server.h" + +namespace clice { + +async::Task Server::on_initialize(proto::InitializeParams params) { + log::info("Initialize from client: {}, version: {}", + params.clientInfo.name, + params.clientInfo.verion); + + if(params.workspaceFolders.empty()) { + log::fatal("The client should provide one workspace folder at least!"); + } + + /// FIXME: adjust position encoding. + kind = PositionEncodingKind::UTF16; + workspace = mapping.to_path(params.workspaceFolders[0].uri); + + /// Initialize configuration. + config::init(workspace); + + /// Load compile commands.json + for(auto& dir: config::server.compile_commands_dirs) { + auto content = fs::read(dir + "/compile_commands.json"); + if(content) { + auto updated = database.load_commands(*content); + } + } + + proto::InitializeResult result; + auto& [info, capabilities] = result; + info.name = "clice"; + info.verion = "0.0.1"; + + capabilities.positionEncoding = "utf-16"; + + /// TextDocument synchronization. + capabilities.textDocumentSync.openClose = true; + /// FIXME: In the end, we should use `Incremental`. + capabilities.textDocumentSync.change = proto::TextDocumentSyncKind::Full; + capabilities.textDocumentSync.save = true; + + /// Completion + capabilities.completionProvider.triggerCharacters = {".", "<", ">", ":", "\"", "/", "*"}; + capabilities.completionProvider.resolveProvider = false; + capabilities.completionProvider.completionItem.labelDetailsSupport = true; + + /// Semantic tokens. + capabilities.semanticTokensProvider.range = false; + capabilities.semanticTokensProvider.full = true; + for(auto name: SymbolKind::all()) { + std::string type{name}; + type[0] = std::tolower(type[0]); + capabilities.semanticTokensProvider.legend.tokenTypes.emplace_back(std::move(type)); + } + + co_return json::serialize(result); +} + +} // namespace clice diff --git a/src/Server/Scheduler.cpp b/src/Server/Scheduler.cpp deleted file mode 100644 index 96fee34a..00000000 --- a/src/Server/Scheduler.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include "Server/Config.h" -#include "Server/Scheduler.h" -#include "Server/LSPConverter.h" -#include "Support/Logger.h" -#include "Support/FileSystem.h" -#include "Compiler/Command.h" -#include "Compiler/Compilation.h" - -namespace clice { - -void Scheduler::addDocument(std::string path, std::string content) { - auto& openFile = openFiles[path]; - openFile.content = content; - - auto& task = openFile.ASTBuild; - - /// If there is already an AST build task, cancel it. - if(!task.empty()) { - task.cancel(); - task.dispose(); - } - - /// Create and schedule a new task. - task = buildAST(std::move(path), std::move(content)); - task.schedule(); -} - -llvm::StringRef Scheduler::getDocumentContent(llvm::StringRef path) { - return openFiles[path].content; -} - -async::Task Scheduler::semanticToken(std::string path) { - auto openFile = &openFiles[path]; - auto guard = co_await openFile->ASTBuiltLock.try_lock(); - - openFile = &openFiles[path]; - auto content = openFile->content; - auto AST = openFile->AST; - if(!AST) { - co_return json::Value(nullptr); - } - - auto tokens = co_await async::submit([&] { return feature::semanticTokens(*AST); }); - - co_return converter.convert(content, tokens); -} - -async::Task Scheduler::completion(std::string path, std::uint32_t offset) { - /// Wait for PCH building. - auto openFile = &openFiles[path]; - if(!openFile->PCHBuild.empty()) { - co_await openFile->PCHBuiltEvent; - } - - openFile = &openFiles[path]; - auto& PCH = openFile->PCH; - - /// Set compilation params ... . - CompilationParams params; - params.arguments = database.get_command(path, true).arguments; - params.add_remapped_file(path, openFile->content); - params.pch = {PCH->path, PCH->preamble.size()}; - params.completion = {path, offset}; - - auto result = co_await async::submit([&] { return feature::code_complete(params, {}); }); - - openFile = &openFiles[path]; - co_return converter.convert(openFile->content, result); -} - -async::Task Scheduler::isPCHOutdated(llvm::StringRef path, llvm::StringRef preamble) { - auto openFile = &openFiles[path]; - - /// If there is not PCH, directly build it. - if(!openFile->PCH) { - co_return true; - } - - /// Check command and preamble matchs. - auto command = database.get_command(path, true).arguments; - /// FIXME: check command. openFile->PCH->command != command - if(openFile->PCH->preamble != preamble) { - co_return true; - } - - /// TODO: Check mtime. - - co_return false; -} - -async::Task<> Scheduler::buildPCH(std::string path, std::string content) { - auto bound = computePreambleBound(content); - - auto openFile = &openFiles[path]; - bool outdated = true; - if(openFile->PCH) { - outdated = co_await isPCHOutdated(path, llvm::StringRef(content).substr(0, bound)); - } - - /// If not need update, return directly. - if(!outdated) { - co_return; - } - - /// The actual PCH build task. - constexpr static auto PCHBuildTask = [](Scheduler& scheduler, - std::string path, - std::uint32_t bound, - std::string content) -> async::Task<> { - CompilationParams params; - params.arguments = scheduler.database.get_command(path, true).arguments; - if(!fs::exists(config::cache.dir)) { - auto error = fs::create_directories(config::cache.dir); - if(error) { - log::warn("Fail to create directory for PCH building: {}", config::cache.dir); - co_return; - } - } - - params.outPath = path::join(config::cache.dir, path::filename(path) + ".pch"); - params.add_remapped_file(path, content, bound); - - PCHInfo info; - - /// PCH file is written until destructing, Add a single block - /// for it. - bool cond = co_await async::submit([&] { - auto result = compile(params, info); - if(!result) { - log::warn("Building PCH fails for {}, Because: {}", path, result.error()); - return false; - } - - /// TODO: index PCH. - - return true; - }); - - if(!cond) { - co_return; - } - - auto& openFile = scheduler.openFiles[path]; - /// Update the built PCH info. - openFile.PCH = std::move(info); - /// Dispose the task so that it will destroyed when task complete. - openFile.PCHBuild.dispose(); - /// Resume waiters on this event. - openFile.PCHBuiltEvent.set(); - openFile.PCHBuiltEvent.clear(); - - log::info("Building PCH successfully for {}", path); - }; - - openFile = &openFiles[path]; - - /// If there is already an PCH build task, cancel it. - auto& task = openFile->PCHBuild; - if(!task.empty()) { - task.cancel(); - task.dispose(); - } - - /// Schedule the new building task. - task = PCHBuildTask(*this, std::move(path), bound, std::move(content)); - task.schedule(); - - /// Waiting for PCH building. - co_await openFile->PCHBuiltEvent; -} - -async::Task<> Scheduler::buildAST(std::string path, std::string content) { - auto file = &openFiles[path]; - - /// Try get the lock, the waiter on the lock will be resumed when - /// guard is destroyed. - auto guard = co_await file->ASTBuiltLock.try_lock(); - - /// PCH is already updated. - co_await buildPCH(path, content); - - auto PCH = openFiles[path].PCH; - if(!PCH) { - log::fatal("Expected PCH built at this point"); - } - - CompilationParams params; - params.arguments = database.get_command(path, true).arguments; - params.add_remapped_file(path, content); - params.pch = {PCH->path, PCH->preamble.size()}; - - /// Check result - auto AST = co_await async::submit([&] { return compile(params); }); - if(!AST) { - /// FIXME: Fails needs cancel waiting tasks. - log::warn("Building AST fails for {}, Beacuse: {}", path, AST.error()); - co_return; - } - - /// Index the source file. - co_await indexer.index(*AST); - - file = &openFiles[path]; - /// Update built AST info. - file->AST = std::make_shared(std::move(*AST)); - /// Dispose the task so that it will destroyed when task complete. - file->ASTBuild.dispose(); - - log::info("Building AST successfully for {}", path); -} - -} // namespace clice diff --git a/src/Server/Server.cpp b/src/Server/Server.cpp index 4b3bac90..b7afb644 100644 --- a/src/Server/Server.cpp +++ b/src/Server/Server.cpp @@ -55,17 +55,19 @@ async::Task<> Server::registerCapacity(llvm::StringRef id, }); } -Server::Server() : indexer(database), scheduler(indexer, converter, database) { - onRequests.try_emplace("initialize", &Server::onInitialize); - onRequests.try_emplace("textDocument/semanticTokens/full", &Server::onSemanticToken); - onRequests.try_emplace("textDocument/completion", &Server::onCodeCompletion); - onNotifications.try_emplace("textDocument/didOpen", &Server::onDidOpen); - onNotifications.try_emplace("textDocument/didChange", &Server::onDidChange); - onNotifications.try_emplace("textDocument/didSave", &Server::onDidSave); - onNotifications.try_emplace("textDocument/didClose", &Server::onDidClose); +Server::Server() { + register_callback<&Server::on_initialize>("initialize"); + + register_callback<&Server::on_did_open>("textDocument/didOpen"); + register_callback<&Server::on_did_change>("textDocument/didChange"); + register_callback<&Server::on_did_save>("textDocument/didSave"); + register_callback<&Server::on_did_close>("textDocument/didClose"); + + register_callback<&Server::on_completion>("textDocument/completion"); + register_callback<&Server::on_semantic_token>("textDocument/semanticTokens/full"); } -async::Task<> Server::onReceive(json::Value value) { +async::Task<> Server::on_receive(json::Value value) { auto object = value.getAsObject(); if(!object) [[unlikely]] { log::fatal("Invalid LSP message, not an object: {}", value); @@ -95,93 +97,24 @@ async::Task<> Server::onReceive(json::Value value) { /// Handle request and notification separately. /// TODO: Record the time of handling request and notification. + auto it = callbacks.find(method); + if(it == callbacks.end()) { + log::info("Ignore unhandled method: {}", method); + co_return; + } + if(id) { log::info("Handling request: {}", method); - if(auto iter = onRequests.find(method); iter != onRequests.end()) { - auto result = co_await (this->*(iter->second))(std::move(params)); - co_await response(std::move(*id), std::move(result)); - } + auto result = co_await it->second(*this, std::move(params)); + co_await response(std::move(*id), std::move(result)); log::info("Handled request: {}", method); } else { log::info("Handling notification: {}", method); - if(auto iter = onNotifications.find(method); iter != onNotifications.end()) { - co_await (this->*(iter->second))(std::move(params)); - } + auto result = co_await it->second(*this, std::move(params)); log::info("Handled notification: {}", method); } co_return; } -async::Task Server::onInitialize(json::Value value) { - auto result = converter.initialize(std::move(value)); - config::init(converter.workspace()); - - for(auto& dir: config::server.compile_commands_dirs) { - auto content = fs::read(dir + "/compile_commands.json"); - if(content) { - auto updated = database.load_commands(*content); - } - } - - co_return result; -} - -async::Task<> Server::onDidOpen(json::Value value) { - struct DidOpenTextDocumentParams { - proto::TextDocumentItem textDocument; - }; - - auto params = json::deserialize(value); - auto path = converter.convert(params.textDocument.uri); - scheduler.addDocument(std::move(path), std::move(params.textDocument.text)); - co_return; -} - -async::Task<> Server::onDidChange(json::Value value) { - struct DidChangeTextDocumentParams { - proto::VersionedTextDocumentIdentifier textDocument; - - struct TextDocumentContentChangeEvent { - std::string text; - }; - - std::vector contentChanges; - }; - - auto params = json::deserialize(value); - auto path = converter.convert(params.textDocument.uri); - scheduler.addDocument(std::move(path), std::move(params.contentChanges[0].text)); - - co_return; -} - -async::Task<> Server::onDidSave(json::Value value) { - co_return; -} - -async::Task<> Server::onDidClose(json::Value value) { - co_return; -} - -async::Task Server::onSemanticToken(json::Value value) { - struct SemanticTokensParams { - proto::TextDocumentIdentifier textDocument; - }; - - auto params = json::deserialize(value); - auto path = converter.convert(params.textDocument.uri); - co_return co_await scheduler.semanticToken(std::move(path)); -} - -async::Task Server::onCodeCompletion(json::Value value) { - using CompletionParams = proto::TextDocumentPositionParams; - auto params = json::deserialize(value); - - auto path = converter.convert(params.textDocument.uri); - auto content = scheduler.getDocumentContent(path); - auto offset = converter.convert(content, params.position); - co_return co_await scheduler.completion(std::move(path), offset); -} - } // namespace clice diff --git a/tests/integration/test_file_operation.py b/tests/integration/test_file_operation.py index e64b4ef3..dddfb385 100644 --- a/tests/integration/test_file_operation.py +++ b/tests/integration/test_file_operation.py @@ -13,5 +13,5 @@ async def test_did_open(executable, test_data_dir, resource_dir): await client.initialize(test_data_dir / "hello_world") await client.did_open("main.cpp") - await asyncio.sleep(3) + await asyncio.sleep(5) await client.exit() diff --git a/tests/unit/Compiler/Command.cpp b/tests/unit/Compiler/Command.cpp index fcdcd810..75151984 100644 --- a/tests/unit/Compiler/Command.cpp +++ b/tests/unit/Compiler/Command.cpp @@ -161,7 +161,7 @@ TEST(Command, QueryDriver) { using namespace std::literals; CompilationDatabase database; - auto info = database.query_driver("/usr/bin/g++"); + auto info = database.query_driver("g++"); ASSERT_TRUE(info); EXPECT_EQ(info->target, "x86_64-linux-gnu"); @@ -180,6 +180,10 @@ TEST(Command, QueryDriver) { EXPECT_EQ(info->system_includes[3], "/usr/local/include"sv); EXPECT_EQ(info->system_includes[4], "/usr/include/x86_64-linux-gnu"sv); EXPECT_EQ(info->system_includes[5], "/usr/include"sv); + + info = database.query_driver("clang++"); + ASSERT_TRUE(info); + #endif } diff --git a/tests/unit/Compiler/Diagnostic.cpp b/tests/unit/Compiler/Diagnostic.cpp index 9a506ea2..07b3f163 100644 --- a/tests/unit/Compiler/Diagnostic.cpp +++ b/tests/unit/Compiler/Diagnostic.cpp @@ -4,6 +4,76 @@ namespace clice::testing { +// see llvm/clang/include/clang/AST/ASTDiagnostic.h +void dump_arg(clang::DiagnosticsEngine::ArgumentKind kind, std::uint64_t value) { + switch(kind) { + case clang::DiagnosticsEngine::ak_identifierinfo: { + clang::IdentifierInfo* info = reinterpret_cast(value); + llvm::outs() << info->getName(); + break; + } + + case clang::DiagnosticsEngine::ak_qual: { + clang::Qualifiers qual = clang::Qualifiers::fromOpaqueValue(value); + llvm::outs() << qual.getAsString(); + break; + } + + case clang::DiagnosticsEngine::ak_qualtype: { + clang::QualType type = + clang::QualType::getFromOpaquePtr(reinterpret_cast(value)); + llvm::outs() << type.getAsString(); + break; + } + + case clang::DiagnosticsEngine::ak_qualtype_pair: { + clang::TemplateDiffTypes& TDT = *reinterpret_cast(value); + clang::QualType type1 = + clang::QualType::getFromOpaquePtr(reinterpret_cast(TDT.FromType)); + clang::QualType type2 = + clang::QualType::getFromOpaquePtr(reinterpret_cast(TDT.ToType)); + llvm::outs() << type1.getAsString() << " -> " << type2.getAsString(); + break; + } + + case clang::DiagnosticsEngine::ak_declarationname: { + clang::DeclarationName name = clang::DeclarationName::getFromOpaqueInteger(value); + llvm::outs() << name.getAsString(); + break; + } + + case clang::DiagnosticsEngine::ak_nameddecl: { + clang::NamedDecl* decl = reinterpret_cast(value); + llvm::outs() << decl->getNameAsString(); + break; + } + + case clang::DiagnosticsEngine::ak_nestednamespec: { + clang::NestedNameSpecifier* spec = reinterpret_cast(value); + spec->dump(); + break; + } + + case clang::DiagnosticsEngine::ak_declcontext: { + clang::DeclContext* context = reinterpret_cast(value); + llvm::outs() << context->getDeclKindName(); + break; + } + + case clang::DiagnosticsEngine::ak_attr: { + clang::Attr* attr = reinterpret_cast(value); + break; + // attr->dump(); + } + + default: { + std::abort(); + } + } + + llvm::outs() << "\n"; +} + namespace { using namespace clice; @@ -30,6 +100,35 @@ TEST(Diagnostic, Error) { } } +TEST(Diagnostic, PCHError) { + /// Any error in compilation will result in failure on generating PCH or PCM. + CompilationParams params; + params.arguments = {"clang++", "main.cpp"}; + params.outPath = "fake.pch"; + params.add_remapped_file("main.cpp", R"( +void foo() {} +void foo() {} +)"); + + PCHInfo info; + auto unit = compile(params, info); + ASSERT_FALSE(unit); +} + +TEST(Diagnostic, ASTError) { + /// Event fatal error may generate incomplete AST, but it is fine. + CompilationParams params; + params.arguments = {"clang++", "main.cpp"}; + params.add_remapped_file("main.cpp", R"( +void foo() {} +void foo() {} +)"); + + PCHInfo info; + auto unit = compile(params); + ASSERT_TRUE(unit); +} + } // namespace } // namespace clice::testing diff --git a/tests/unit/Compiler/Module.cpp b/tests/unit/Compiler/Module.cpp index 28aa69f6..6369fab8 100644 --- a/tests/unit/Compiler/Module.cpp +++ b/tests/unit/Compiler/Module.cpp @@ -44,7 +44,7 @@ ModuleInfo scan(llvm::StringRef content) { params.add_remapped_file("./test.h", "export module A"); auto info = scanModule(params); if(!info) { - clice::println("Fail to scan module: {}", info.error()); + /// clice::println("Fail to scan module: {}", info.error()); std::abort(); } return std::move(*info); diff --git a/tests/unit/Server/LSPConverter.cpp b/tests/unit/Server/LSPConverter.cpp index 0fd0b86c..f0a57fff 100644 --- a/tests/unit/Server/LSPConverter.cpp +++ b/tests/unit/Server/LSPConverter.cpp @@ -1,5 +1,5 @@ -#include -#include +#include "Test/Test.h" +#include "Server/Convert.h" namespace clice::testing {