From 09e65224feba65beb2f2ebeeb8e62b169c082220 Mon Sep 17 00:00:00 2001 From: csh Date: Wed, 24 Dec 2025 10:42:13 +0800 Subject: [PATCH] :sparkles: feat(lsp_server): implement missing providers and json coverage Implement workspace configuration/folders and apply WorkspaceEdit.changes. Strengthen provider JSON coverage (all methods require params, no errors). Verified: Release test_provider --- lsp-server/src/CMakeLists.txt | 1 + lsp-server/src/bridge/spdlog.cppm | 6 + lsp-server/src/bridge/win32_stdio.cppm | 23 + lsp-server/src/core/server.cppm | 144 +- .../src/language/symbol/internal/builder.cppm | 102 +- lsp-server/src/manager/bootstrap.cppm | 2 - lsp-server/src/manager/manager_hub.cppm | 90 + lsp-server/src/manager/symbol.cppm | 183 + .../protocol/initialize/configuration.cppm | 2 +- lsp-server/src/protocol/protocol.cppm | 64 +- .../protocol/text_document/code_actions.cppm | 2 +- .../protocol/text_document/navigation.cppm | 4 +- .../call_hierarchy/incoming_calls.cppm | 216 +- .../call_hierarchy/outgoing_calls.cppm | 215 +- .../provider/client/register_capability.cppm | 23 +- .../client/unregister_capability.cppm | 24 +- .../src/provider/code_action/resolve.cppm | 29 +- .../src/provider/code_lens/resolve.cppm | 87 +- .../src/provider/document_link/resolve.cppm | 222 +- .../src/provider/initialize/initialize.cppm | 120 +- .../src/provider/inlay_hint/resolve.cppm | 65 +- lsp-server/src/provider/manifest.cppm | 8 +- lsp-server/src/provider/telemetry/event.cppm | 27 +- .../provider/text_document/code_action.cppm | 420 ++- .../src/provider/text_document/code_lens.cppm | 124 +- .../text_document/color_presentation.cppm | 63 +- .../provider/text_document/completion.cppm | 2 - .../provider/text_document/definition.cppm | 4 +- .../provider/text_document/diagnostic.cppm | 95 +- .../text_document/document_color.cppm | 167 +- .../text_document/document_highlight.cppm | 174 +- .../provider/text_document/document_link.cppm | 408 ++- .../text_document/document_symbol.cppm | 277 +- .../provider/text_document/folding_range.cppm | 156 +- .../provider/text_document/formatting.cppm | 179 +- .../src/provider/text_document/hover.cppm | 286 +- .../text_document/implementation.cppm | 187 +- .../provider/text_document/inlay_hint.cppm | 797 ++++- .../provider/text_document/inline_value.cppm | 31 +- .../text_document/linked_editing_range.cppm | 170 +- .../src/provider/text_document/moniker.cppm | 76 +- .../text_document/on_type_formatting.cppm | 130 +- .../text_document/prepare_call_hierarchy.cppm | 194 +- .../text_document/prepare_rename.cppm | 81 +- .../text_document/prepare_type_hierarchy.cppm | 197 +- .../text_document/publish_diagnostics.cppm | 38 +- .../text_document/range_formatting.cppm | 114 +- .../provider/text_document/references.cppm | 256 +- .../src/provider/text_document/rename.cppm | 182 +- .../text_document/selection_range.cppm | 149 +- .../text_document/semantic_tokens.cppm | 441 ++- .../text_document/signature_help.cppm | 426 ++- .../text_document/type_definition.cppm | 350 +- .../src/provider/type_hierarchy/subtypes.cppm | 215 +- .../provider/type_hierarchy/supertypes.cppm | 211 +- .../src/provider/window/log_message.cppm | 20 +- .../src/provider/window/show_document.cppm | 38 +- .../src/provider/window/show_message.cppm | 20 +- .../provider/window/show_message_request.cppm | 46 +- .../window/work_done_progress_create.cppm | 35 +- .../src/provider/workspace/apply_edit.cppm | 403 ++- .../provider/workspace/code_lens_refresh.cppm | 21 +- .../src/provider/workspace/configuration.cppm | 136 +- .../src/provider/workspace/diagnostic.cppm | 130 +- .../workspace/diagnostic_refresh.cppm | 21 +- .../workspace/did_change_configuration.cppm | 34 +- .../workspace/did_change_watched_files.cppm | 104 +- .../did_change_workspace_folders.cppm | 230 +- .../provider/workspace/did_create_files.cppm | 57 +- .../provider/workspace/did_delete_files.cppm | 57 +- .../provider/workspace/did_rename_files.cppm | 62 +- .../provider/workspace/execute_command.cppm | 119 +- .../workspace/inlay_hint_refresh.cppm | 21 +- .../workspace/inline_value_refresh.cppm | 21 +- .../workspace/semantic_tokens_refresh.cppm | 21 +- lsp-server/src/provider/workspace/symbol.cppm | 110 +- .../provider/workspace/will_create_files.cppm | 31 +- .../provider/workspace/will_delete_files.cppm | 31 +- .../provider/workspace/will_rename_files.cppm | 31 +- .../provider/workspace/workspace_folders.cppm | 34 +- .../provider/workspace_symbol/resolve.cppm | 128 +- lsp-server/src/utils/args_parser.cppm | 19 +- lsp-server/test/test_provider/CMakeLists.txt | 4 + .../code_action_missing_semicolon.tsl | 3 + .../test_provider/fixtures/color_literals.tsl | 8 + .../fixtures/inlay_hint_case.tsl | 2 + .../fixtures/type_hierarchy_unit.tsf | 43 + .../json_provider_coverage_test.cppm | 1433 ++++++++ .../test_provider/provider_misc_test.cppm | 3119 ++++++++++++++++- .../test_provider/provider_surface_test.cppm | 105 +- .../test/test_provider/server_json_test.cppm | 51 +- lsp-server/test/test_provider/test_main.cppm | 3 + vscode/src/extension.ts | 49 +- 93 files changed, 14203 insertions(+), 856 deletions(-) create mode 100644 lsp-server/src/bridge/win32_stdio.cppm create mode 100644 lsp-server/test/test_provider/fixtures/code_action_missing_semicolon.tsl create mode 100644 lsp-server/test/test_provider/fixtures/color_literals.tsl create mode 100644 lsp-server/test/test_provider/fixtures/inlay_hint_case.tsl create mode 100644 lsp-server/test/test_provider/fixtures/type_hierarchy_unit.tsf create mode 100644 lsp-server/test/test_provider/json_provider_coverage_test.cppm diff --git a/lsp-server/src/CMakeLists.txt b/lsp-server/src/CMakeLists.txt index 9e326b9..3c2fd51 100644 --- a/lsp-server/src/CMakeLists.txt +++ b/lsp-server/src/CMakeLists.txt @@ -88,6 +88,7 @@ target_sources( bridge/spdlog.cppm bridge/taskflow.cppm bridge/tree_sitter.cppm + bridge/win32_stdio.cppm cli/launcher.cppm language/ast/ast.cppm language/ast/types.cppm diff --git a/lsp-server/src/bridge/spdlog.cppm b/lsp-server/src/bridge/spdlog.cppm index c22ea0c..68c3f26 100644 --- a/lsp-server/src/bridge/spdlog.cppm +++ b/lsp-server/src/bridge/spdlog.cppm @@ -3,6 +3,7 @@ module; // Global module fragment: pull in third-party headers #include #include +#include #include export module spdlog; @@ -40,6 +41,11 @@ export namespace spdlog using ::spdlog::flush_on; using ::spdlog::flush_every; + // Factories + using ::spdlog::basic_logger_mt; + using ::spdlog::stdout_logger_mt; + using ::spdlog::stderr_logger_mt; + // Types using logger = ::spdlog::logger; using ::spdlog::sink_ptr; diff --git a/lsp-server/src/bridge/win32_stdio.cppm b/lsp-server/src/bridge/win32_stdio.cppm new file mode 100644 index 0000000..441cc26 --- /dev/null +++ b/lsp-server/src/bridge/win32_stdio.cppm @@ -0,0 +1,23 @@ +module; + +#ifdef _WIN32 +#include +#include +#include +#endif + +export module lsp.bridge.win32_stdio; + +import std; + +export namespace lsp::bridge::win32_stdio +{ + inline void SetStdioBinaryMode() + { +#ifdef _WIN32 + _setmode(_fileno(stdout), _O_BINARY); + _setmode(_fileno(stdin), _O_BINARY); +#endif + } +} + diff --git a/lsp-server/src/core/server.cppm b/lsp-server/src/core/server.cppm index 0b67179..6f937bd 100644 --- a/lsp-server/src/core/server.cppm +++ b/lsp-server/src/core/server.cppm @@ -1,21 +1,19 @@ module; -#ifdef _WIN32 -#include -#include -#include -#endif - export module lsp.core.server; import spdlog; +import tree_sitter; import std; +import lsp.bridge.win32_stdio; import lsp.core.dispacther; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; import lsp.manager.manager_hub; import lsp.manager.bootstrap; +import lsp.manager.events; import lsp.scheduler.async_executor; import lsp.provider.base.interface; import lsp.provider.manifest; @@ -39,8 +37,8 @@ export namespace lsp::core // 处理LSP请求 - 返回序列化的响应或空字符串(对于通知) void HandleMessage(const std::string& raw_message); - // 发送LSP响应 - void SendResponse(const std::string& response); + // 发送LSP消息(响应/通知) + void SendMessage(const std::string& message); // 处理不同类型的消息 void HandleRequest(const protocol::RequestMessage& request); @@ -62,6 +60,13 @@ export namespace lsp::core private: void InitializeManagerHub(); void RegisterProviders(); + void RegisterDiagnosticsPublisher(); + + void PublishDiagnostics(const protocol::DocumentUri& uri, + std::optional version, + TSTree* tree, + const protocol::string& content); + void ClearDiagnostics(const protocol::DocumentUri& uri); // 错误处理 void SendError(const protocol::RequestMessage& request, protocol::ErrorCodes code, const std::string& message); @@ -89,6 +94,7 @@ namespace lsp::core InitializeManagerHub(); RegisterProviders(); + RegisterDiagnosticsPublisher(); spdlog::debug("LSP server initialized with {} providers.", dispatcher_.GetAllSupportedMethods().size()); } @@ -104,11 +110,7 @@ namespace lsp::core spdlog::info("LSP server starting main loop..."); spdlog::info("Waiting for LSP messages on stdin..."); -// 设置二进制模式 -#ifdef _WIN32 - _setmode(_fileno(stdout), _O_BINARY); - _setmode(_fileno(stdin), _O_BINARY); -#endif + bridge::win32_stdio::SetStdioBinaryMode(); while (!is_shutting_down_) { @@ -243,14 +245,14 @@ namespace lsp::core spdlog::warn("Unrecognized message: {}", raw_message); } - void LspServer::SendResponse(const std::string& response) + void LspServer::SendMessage(const std::string& message) { - if (response.empty()) + if (message.empty()) return; std::lock_guard lock(output_mutex_); - std::cout << "Content-Length: " << response.size() << "\r\n\r\n" - << response << std::flush; + std::cout << "Content-Length: " << message.size() << "\r\n\r\n" + << message << std::flush; } void LspServer::HandleRequest(const protocol::RequestMessage& request) @@ -260,7 +262,7 @@ namespace lsp::core if (request.method == "shutdown") { auto response = dispatcher_.Dispatch(request); - SendResponse(response); + SendMessage(response); is_shutting_down_ = true; return; } @@ -273,7 +275,7 @@ namespace lsp::core } auto response = dispatcher_.Dispatch(request); - SendResponse(response); + SendMessage(response); } void LspServer::HandleNotification(const protocol::NotificationMessage& notification) @@ -410,6 +412,108 @@ namespace lsp::core spdlog::info("Registered {} LSP providers", dispatcher_.GetAllSupportedMethods().size()); } + void LspServer::RegisterDiagnosticsPublisher() + { + auto& event_bus = manager_hub_.event_bus(); + + event_bus.Subscribe( + [this](const manager::events::DocumentParsed& event) { + PublishDiagnostics(event.item.uri, event.item.version, event.tree, event.item.text); + }); + + event_bus.Subscribe( + [this](const manager::events::DocumentReparsed& event) { + PublishDiagnostics(event.item.uri, event.item.version, event.tree, event.item.text); + }); + + event_bus.Subscribe( + [this](const manager::events::DocumentClosed& event) { + ClearDiagnostics(event.textDocument.uri); + }); + } + + void LspServer::PublishDiagnostics(const protocol::DocumentUri& uri, + std::optional version, + TSTree* tree, + const protocol::string& content) + { + if (!tree) + { + spdlog::warn("Skip diagnostics publish: null syntax tree for {}", uri); + return; + } + + language::ast::Deserializer deserializer; + auto errors = deserializer.DiagnoseSyntax(ts_tree_root_node(tree), content); + + std::vector diagnostics; + diagnostics.reserve(errors.size()); + + for (const auto& error : errors) + { + protocol::Diagnostic diagnostic; + diagnostic.range.start.line = error.location.start_line; + diagnostic.range.start.character = error.location.start_column; + diagnostic.range.end.line = error.location.end_line; + diagnostic.range.end.character = error.location.end_column; + + switch (error.severity) + { + case language::ast::ErrorSeverity::Warning: + diagnostic.severity = protocol::DiagnosticSeverity::Warning; + break; + case language::ast::ErrorSeverity::Fatal: + case language::ast::ErrorSeverity::Error: + default: + diagnostic.severity = protocol::DiagnosticSeverity::Error; + break; + } + + diagnostic.source = "tsl"; + diagnostic.message = error.message; + diagnostics.push_back(std::move(diagnostic)); + } + + protocol::PublishDiagnosticsParams params; + params.uri = uri; + params.version = version; + params.diagnostics = std::move(diagnostics); + + protocol::NotificationMessage notification; + notification.method = "textDocument/publishDiagnostics"; + notification.params = transform::ToLSPAny(params); + + auto json = transform::Serialize(notification); + if (!json) + { + spdlog::warn("Failed to serialize diagnostics notification for {}", uri); + return; + } + + SendMessage(*json); + } + + void LspServer::ClearDiagnostics(const protocol::DocumentUri& uri) + { + protocol::PublishDiagnosticsParams params; + params.uri = uri; + params.version = std::nullopt; + params.diagnostics = {}; + + protocol::NotificationMessage notification; + notification.method = "textDocument/publishDiagnostics"; + notification.params = transform::ToLSPAny(params); + + auto json = transform::Serialize(notification); + if (!json) + { + spdlog::warn("Failed to serialize diagnostics clear notification for {}", uri); + return; + } + + SendMessage(*json); + } + void LspServer::SendError(const protocol::RequestMessage& request, protocol::ErrorCodes code, const std::string& message) { protocol::ResponseMessage response; @@ -421,7 +525,7 @@ namespace lsp::core auto json = transform::Serialize(response); if (json) { - SendResponse(*json); + SendMessage(*json); } } diff --git a/lsp-server/src/language/symbol/internal/builder.cppm b/lsp-server/src/language/symbol/internal/builder.cppm index 9dc753c..3c0b429 100644 --- a/lsp-server/src/language/symbol/internal/builder.cppm +++ b/lsp-server/src/language/symbol/internal/builder.cppm @@ -13,6 +13,36 @@ namespace lsp::language::symbol { namespace { + bool SignatureMatches(const Function& function_symbol, + const std::vector& parameters, + const std::optional& return_type) + { + if (function_symbol.parameters.size() != parameters.size()) + { + return false; + } + + for (std::size_t i = 0; i < parameters.size(); ++i) + { + const auto& existing = function_symbol.parameters[i]; + const auto& incoming = parameters[i]; + + if (existing.type && incoming.type && + !utils::IEquals(*existing.type, *incoming.type)) + { + return false; + } + } + + if (function_symbol.return_type && return_type && + !utils::IEquals(*function_symbol.return_type, *return_type)) + { + return false; + } + + return true; + } + std::optional UnquoteStringLiteral(std::string value) { if (value.size() < 2) @@ -580,7 +610,61 @@ namespace lsp::language::symbol void Builder::VisitFunctionDefinition(ast::FunctionDefinition& node) { - auto func_id = CreateFunctionSymbol(node.name, node.location, node.parameters, node.return_type); + SymbolId func_id = kInvalidSymbolId; + const auto signature_params = BuildParameters(node.parameters); + const auto return_type_name = ExtractTypeName(node.return_type); + + if (node.body) + { + std::optional matched; + if (current_scope_id_ != kInvalidScopeId && !node.name.empty()) + { + auto candidates = table_.scopes().FindSymbols(current_scope_id_, node.name); + for (auto candidate_id : candidates) + { + auto* symbol = const_cast(table_.definition(candidate_id)); + if (!symbol || !symbol->Is()) + { + continue; + } + + auto* function_symbol = symbol->As(); + if (function_symbol->implementation_range) + { + continue; + } + + if (!SignatureMatches(*function_symbol, signature_params, return_type_name)) + { + continue; + } + + matched = candidate_id; + break; + } + } + + if (matched) + { + func_id = *matched; + } + else + { + func_id = CreateFunctionSymbol(node.name, node.location, node.parameters, node.return_type); + } + + if (auto* symbol = const_cast(table_.definition(func_id))) + { + if (auto* function_symbol = symbol->As()) + { + function_symbol->implementation_range = node.location; + } + } + } + else + { + func_id = CreateFunctionSymbol(node.name, node.location, node.parameters, node.return_type); + } if (node.body) { @@ -617,6 +701,14 @@ namespace lsp::language::symbol if (node.body) { + if (auto* symbol = const_cast(table_.definition(method_id))) + { + if (auto* method_symbol = symbol->As()) + { + method_symbol->implementation_range = node.location; + } + } + [[maybe_unused]] auto method_scope = EnterScopeWithSymbol(ScopeKind::kFunction, method_id, node.body->span); auto prev_function = current_function_id_; @@ -703,6 +795,14 @@ namespace lsp::language::symbol if (node.body) { + if (auto* symbol = const_cast(table_.definition(*method_id))) + { + if (auto* method_symbol = symbol->As()) + { + method_symbol->implementation_range = node.location; + } + } + [[maybe_unused]] auto method_scope = EnterScopeWithSymbol(ScopeKind::kFunction, *method_id, node.body->span); auto prev_function = current_function_id_; diff --git a/lsp-server/src/manager/bootstrap.cppm b/lsp-server/src/manager/bootstrap.cppm index e2f9098..bc7b980 100644 --- a/lsp-server/src/manager/bootstrap.cppm +++ b/lsp-server/src/manager/bootstrap.cppm @@ -1,7 +1,5 @@ module; -#include - export module lsp.manager.bootstrap; import spdlog; diff --git a/lsp-server/src/manager/manager_hub.cppm b/lsp-server/src/manager/manager_hub.cppm index e8809a9..1e07747 100644 --- a/lsp-server/src/manager/manager_hub.cppm +++ b/lsp-server/src/manager/manager_hub.cppm @@ -22,16 +22,33 @@ export namespace lsp::manager void Initialize(); void Shutdown(); + EventBus& event_bus() { return event_bus_; } + const EventBus& event_bus() const { return event_bus_; } + Document& documents() { return documents_; } Parser& parser() { return parser_; } Symbol& symbols() { return symbols_; } + void SetConfiguration(protocol::LSPAny settings); + protocol::LSPAny GetConfiguration() const; + + void SetWorkspaceFolders(std::vector folders); + void AddWorkspaceFolders(const std::vector& folders); + void RemoveWorkspaceFolders(const std::vector& folders); + std::vector GetWorkspaceFolders() const; + private: + void ClearWorkspaceState(); + EventBus event_bus_; Document documents_; Parser parser_; Symbol symbols_; + + mutable std::shared_mutex workspace_mutex_; + protocol::LSPAny configuration_settings_{ std::nullptr_t{} }; + std::vector workspace_folders_; }; } @@ -50,11 +67,84 @@ namespace lsp::manager { documents_.Clear(); parser_.Clear(); + ClearWorkspaceState(); } void ManagerHub::Shutdown() { documents_.Clear(); parser_.Clear(); + ClearWorkspaceState(); + } + + void ManagerHub::SetConfiguration(protocol::LSPAny settings) + { + std::unique_lock lock(workspace_mutex_); + configuration_settings_ = std::move(settings); + } + + protocol::LSPAny ManagerHub::GetConfiguration() const + { + std::shared_lock lock(workspace_mutex_); + return configuration_settings_; + } + + void ManagerHub::SetWorkspaceFolders(std::vector folders) + { + std::unique_lock lock(workspace_mutex_); + workspace_folders_ = std::move(folders); + } + + void ManagerHub::AddWorkspaceFolders(const std::vector& folders) + { + if (folders.empty()) + { + return; + } + + std::unique_lock lock(workspace_mutex_); + for (const auto& folder : folders) + { + auto existing = std::find_if(workspace_folders_.begin(), workspace_folders_.end(), [&folder](const protocol::WorkspaceFolder& item) { + return item.uri == folder.uri; + }); + + if (existing == workspace_folders_.end()) + { + workspace_folders_.push_back(folder); + continue; + } + + existing->name = folder.name; + } + } + + void ManagerHub::RemoveWorkspaceFolders(const std::vector& folders) + { + if (folders.empty()) + { + return; + } + + std::unique_lock lock(workspace_mutex_); + for (const auto& folder : folders) + { + std::erase_if(workspace_folders_, [&folder](const protocol::WorkspaceFolder& item) { + return item.uri == folder.uri; + }); + } + } + + std::vector ManagerHub::GetWorkspaceFolders() const + { + std::shared_lock lock(workspace_mutex_); + return workspace_folders_; + } + + void ManagerHub::ClearWorkspaceState() + { + std::unique_lock lock(workspace_mutex_); + configuration_settings_ = protocol::LSPAny(std::nullptr_t{}); + workspace_folders_.clear(); } } diff --git a/lsp-server/src/manager/symbol.cppm b/lsp-server/src/manager/symbol.cppm index 75d7720..7b988f1 100644 --- a/lsp-server/src/manager/symbol.cppm +++ b/lsp-server/src/manager/symbol.cppm @@ -40,6 +40,10 @@ export namespace lsp::manager void LoadSystemLibrary(const std::string& lib_path); void LoadWorkspace(const protocol::DocumentUri& workspace_uri); + void IndexWorkspaceFiles(const std::vector& uris); + void RemoveWorkspaceFiles(const std::vector& uris); + void RenameWorkspaceFiles(const std::vector>& files); + const language::symbol::SymbolTable* GetSymbolTable(const protocol::DocumentUri& uri) const; const language::semantic::SemanticModel* GetSemanticModel(const protocol::DocumentUri& uri) const; @@ -445,6 +449,185 @@ namespace lsp::manager duration); } + void Symbol::IndexWorkspaceFiles(const std::vector& uris) + { + std::unordered_map updates; + std::vector removals; + + updates.reserve(uris.size()); + removals.reserve(uris.size()); + + for (const auto& uri : uris) + { + auto file_path = std::filesystem::path(UriToPath(uri)); + auto kind = GetTslFileKind(file_path); + if (kind == TslFileKind::kOther) + { + continue; + } + + auto normalized_uri = PathToUri(file_path); + + if (!std::filesystem::exists(file_path)) + { + removals.push_back(std::move(normalized_uri)); + continue; + } + + auto table = BuildSymbolTableFromFile(file_path); + if (!table) + { + removals.push_back(std::move(normalized_uri)); + continue; + } + + auto stem = file_path.stem().string(); + if (kind == TslFileKind::kLibraryTsf && !HasMatchingTopLevelSymbol(*table, stem)) + { + spdlog::warn("Skipping workspace file {}: top-level symbol does not match file name", file_path.string()); + removals.push_back(std::move(normalized_uri)); + continue; + } + + StoredSymbolEntry stored; + stored.symbol_table = std::move(table); + stored.semantic_model = std::make_unique(*stored.symbol_table); + + updates[normalized_uri] = std::move(stored); + } + + if (updates.empty() && removals.empty()) + { + return; + } + + { + std::unique_lock lock(mutex_); + for (auto& [uri, entry] : updates) + { + workspace_symbols_[uri] = std::move(entry); + } + for (auto& uri : removals) + { + workspace_symbols_.erase(uri); + } + RebuildIndex(); + } + } + + void Symbol::RemoveWorkspaceFiles(const std::vector& uris) + { + if (uris.empty()) + { + return; + } + + std::vector removals; + removals.reserve(uris.size()); + + for (const auto& uri : uris) + { + auto file_path = std::filesystem::path(UriToPath(uri)); + auto kind = GetTslFileKind(file_path); + if (kind == TslFileKind::kOther) + { + continue; + } + removals.push_back(PathToUri(file_path)); + } + + if (removals.empty()) + { + return; + } + + { + std::unique_lock lock(mutex_); + for (auto& uri : removals) + { + workspace_symbols_.erase(uri); + } + RebuildIndex(); + } + } + + void Symbol::RenameWorkspaceFiles(const std::vector>& files) + { + if (files.empty()) + { + return; + } + + std::unordered_map updates; + std::vector removals; + + updates.reserve(files.size()); + removals.reserve(files.size()); + + for (const auto& [old_uri, new_uri] : files) + { + auto old_path = std::filesystem::path(UriToPath(old_uri)); + if (GetTslFileKind(old_path) != TslFileKind::kOther) + { + removals.push_back(PathToUri(old_path)); + } + + auto new_path = std::filesystem::path(UriToPath(new_uri)); + auto kind = GetTslFileKind(new_path); + if (kind == TslFileKind::kOther) + { + continue; + } + + auto normalized_uri = PathToUri(new_path); + + if (!std::filesystem::exists(new_path)) + { + removals.push_back(std::move(normalized_uri)); + continue; + } + + auto table = BuildSymbolTableFromFile(new_path); + if (!table) + { + removals.push_back(std::move(normalized_uri)); + continue; + } + + auto stem = new_path.stem().string(); + if (kind == TslFileKind::kLibraryTsf && !HasMatchingTopLevelSymbol(*table, stem)) + { + spdlog::warn("Skipping workspace file {}: top-level symbol does not match file name", new_path.string()); + removals.push_back(std::move(normalized_uri)); + continue; + } + + StoredSymbolEntry stored; + stored.symbol_table = std::move(table); + stored.semantic_model = std::make_unique(*stored.symbol_table); + + updates[normalized_uri] = std::move(stored); + } + + if (updates.empty() && removals.empty()) + { + return; + } + + { + std::unique_lock lock(mutex_); + for (auto& uri : removals) + { + workspace_symbols_.erase(uri); + } + for (auto& [uri, entry] : updates) + { + workspace_symbols_[uri] = std::move(entry); + } + RebuildIndex(); + } + } + const language::symbol::SymbolTable* Symbol::GetSymbolTable( const protocol::DocumentUri& uri) const { diff --git a/lsp-server/src/protocol/initialize/configuration.cppm b/lsp-server/src/protocol/initialize/configuration.cppm index 3778293..1be6c5d 100644 --- a/lsp-server/src/protocol/initialize/configuration.cppm +++ b/lsp-server/src/protocol/initialize/configuration.cppm @@ -37,7 +37,7 @@ export namespace lsp::protocol struct ExecuteCommandOptions : WorkDoneProgressOptions { - std::vector commands; + std::vector commands; }; struct ExecuteCommandRegistrationOptions : ExecuteCommandOptions diff --git a/lsp-server/src/protocol/protocol.cppm b/lsp-server/src/protocol/protocol.cppm index 8ea2761..10bd576 100644 --- a/lsp-server/src/protocol/protocol.cppm +++ b/lsp-server/src/protocol/protocol.cppm @@ -106,7 +106,7 @@ namespace glz struct meta { using T = lsp::protocol::CodeActionParams; - static constexpr auto value = glz::object(&T::textDocument, &T::position, &T::workDoneToken, &T::partialResultToken, &T::textDocument, &T::range, &T::context); + static constexpr auto value = glz::object(&T::workDoneToken, &T::partialResultToken, &T::textDocument, &T::range, &T::context); }; template<> @@ -176,7 +176,7 @@ namespace glz struct meta { using T = lsp::protocol::ExecuteCommandParams; - static constexpr auto value = glz::object(&T::workDoneToken, &T::arguments); + static constexpr auto value = glz::object(&T::workDoneToken, &T::command, &T::arguments); }; template<> @@ -431,6 +431,27 @@ namespace glz static constexpr auto value = glz::object(&T::documentSelector, &T::workDoneProgress, &T::legend, &T::range, &T::full, &T::id); }; + template<> + struct meta + { + using T = lsp::protocol::SemanticTokensParams; + static constexpr auto value = glz::object(&T::workDoneToken, &T::partialResultToken, &T::textDocument); + }; + + template<> + struct meta + { + using T = lsp::protocol::SemanticTokensRangeParams; + static constexpr auto value = glz::object(&T::workDoneToken, &T::partialResultToken, &T::textDocument, &T::range); + }; + + template<> + struct meta + { + using T = lsp::protocol::SemanticTokensDeltaParams; + static constexpr auto value = glz::object(&T::workDoneToken, &T::partialResultToken, &T::textDocument, &T::previousResultId); + }; + template<> struct meta { @@ -487,6 +508,41 @@ namespace glz static constexpr auto value = glz::object(&T::workDoneProgress, &T::rangesSupport); }; + template<> + struct meta + { + using T = lsp::protocol::DocumentFormattingParams; + static constexpr auto value = glz::object(&T::textDocument, &T::workDoneToken, &T::options); + }; + + template<> + struct meta + { + using T = lsp::protocol::DocumentRangeFormattingParams; + static constexpr auto value = glz::object(&T::textDocument, &T::workDoneToken, &T::range, &T::options); + }; + + template<> + struct meta + { + using T = lsp::protocol::DocumentOnTypeFormattingParams; + static constexpr auto value = glz::object(&T::textDocument, &T::position, &T::ch, &T::options); + }; + + template<> + struct meta + { + using T = lsp::protocol::InlineValueParams; + static constexpr auto value = glz::object(&T::textDocument, &T::workDoneToken, &T::range, &T::context); + }; + + template<> + struct meta + { + using T = lsp::protocol::MonikerParams; + static constexpr auto value = glz::object(&T::textDocument, &T::position, &T::workDoneToken, &T::partialResultToken); + }; + template<> struct meta { @@ -540,14 +596,14 @@ namespace glz struct meta { using T = lsp::protocol::TypeDefinitionParams; - static constexpr auto value = glz::object(&T::textDocument, &T::position, &T::workDoneToken); + static constexpr auto value = glz::object(&T::textDocument, &T::position, &T::workDoneToken, &T::partialResultToken); }; template<> struct meta { using T = lsp::protocol::ImplementationParams; - static constexpr auto value = glz::object(&T::workDoneToken, &T::partialResultToken); + static constexpr auto value = glz::object(&T::textDocument, &T::position, &T::workDoneToken, &T::partialResultToken); }; template<> diff --git a/lsp-server/src/protocol/text_document/code_actions.cppm b/lsp-server/src/protocol/text_document/code_actions.cppm index e960be4..8561c25 100644 --- a/lsp-server/src/protocol/text_document/code_actions.cppm +++ b/lsp-server/src/protocol/text_document/code_actions.cppm @@ -94,7 +94,7 @@ export namespace lsp::protocol std::optional triggerKind; }; - struct CodeActionParams : TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams + struct CodeActionParams : WorkDoneProgressParams, PartialResultParams { TextDocumentIdentifier textDocument; Range range; diff --git a/lsp-server/src/protocol/text_document/navigation.cppm b/lsp-server/src/protocol/text_document/navigation.cppm index c93341c..2c96252 100644 --- a/lsp-server/src/protocol/text_document/navigation.cppm +++ b/lsp-server/src/protocol/text_document/navigation.cppm @@ -65,7 +65,7 @@ export namespace lsp::protocol { }; - struct TypeDefinitionParams : TextDocumentPositionParams, WorkDoneProgressParams + struct TypeDefinitionParams : TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams { }; @@ -84,7 +84,7 @@ export namespace lsp::protocol { }; - struct ImplementationParams : WorkDoneProgressParams, PartialResultParams + struct ImplementationParams : TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams { }; diff --git a/lsp-server/src/provider/call_hierarchy/incoming_calls.cppm b/lsp-server/src/provider/call_hierarchy/incoming_calls.cppm index 5042ab2..82abdeb 100644 --- a/lsp-server/src/provider/call_hierarchy/incoming_calls.cppm +++ b/lsp-server/src/provider/call_hierarchy/incoming_calls.cppm @@ -6,9 +6,15 @@ import spdlog; import std; +import lsp.protocol; import lsp.protocol.types; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::call_hierarchy { @@ -25,18 +31,214 @@ export namespace lsp::provider::call_hierarchy namespace lsp::provider::call_hierarchy { - + namespace + { + namespace codec = lsp::codec; - + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + std::optional ParseSymbolId(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto it = obj.find("symbolId"); + if (it == obj.end()) + { + return std::nullopt; + } + + if (it->second.Is()) + { + const auto& text = it->second.Get(); + try + { + return static_cast(std::stoull(text)); + } + catch (const std::exception&) + { + return std::nullopt; + } + } + + if (it->second.Is()) + { + return static_cast(it->second.Get()); + } + + if (it->second.Is()) + { + auto value = it->second.Get(); + if (value < 0) + { + return std::nullopt; + } + return static_cast(value); + } + + return std::nullopt; + } + + std::optional ResolveSymbolIdFromItem(const protocol::CallHierarchyItem& item, + ExecutionContext& context) + { + if (item.data) + { + if (auto id = ParseSymbolId(*item.data)) + { + return id; + } + } + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(item.uri); + const auto* table = hub.symbols().GetSymbolTable(item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(item.uri); + if (!content.has_value() || !table || !semantic) + { + return std::nullopt; + } + + language::ast::Location loc{}; + loc.start_line = item.selectionRange.start.line; + loc.start_column = item.selectionRange.start.character; + loc.end_line = item.selectionRange.start.line; + loc.end_column = item.selectionRange.start.character; + + auto offset = utils::text_coordinates::ToOffset(item.selectionRange.start, *content); + loc.start_offset = static_cast(offset); + loc.end_offset = static_cast(offset); + + if (auto symbol_id = table->FindSymbolAt(loc)) + { + return symbol_id; + } + + auto resolved = semantic->name_resolver().ResolveNameAtLocation(item.name, loc); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + + auto matches = table->FindSymbolsByName(item.name); + if (!matches.empty()) + { + return matches.front(); + } + + return std::nullopt; + } + + protocol::LSPObject BuildCallHierarchyItem(const protocol::DocumentUri& uri, + const language::symbol::Symbol& symbol) + { + protocol::LSPObject item; + item["name"] = protocol::string(symbol.name()); + item["kind"] = static_cast(symbol.kind()); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(uri); + item["range"] = ToRangeObject(ToRange(symbol.range())); + item["selectionRange"] = ToRangeObject(ToRange(symbol.selection_range())); + + protocol::LSPObject data; + data["uri"] = protocol::string(uri); + data["symbolId"] = protocol::string(std::to_string(symbol.id())); + item["data"] = protocol::LSPAny(std::move(data)); + + return item; + } + } std::string IncomingCalls::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("IncomingCallsProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::CallHierarchyIncomingCallsParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::LSPArray result; + + auto symbol_id = ResolveSymbolIdFromItem(params.item, context); + if (symbol_id) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.item.uri); + + if (table && semantic) + { + std::unordered_map> grouped; + for (const auto& call : semantic->calls().callers(*symbol_id)) + { + grouped[call.caller].push_back(ToRange(call.call_site)); + } + + for (const auto& [caller_id, ranges] : grouped) + { + const auto* caller_symbol = table->definition(caller_id); + if (!caller_symbol) + { + continue; + } + + protocol::LSPObject entry; + entry["from"] = BuildCallHierarchyItem(params.item.uri, *caller_symbol); + + protocol::LSPArray from_ranges; + from_ranges.reserve(ranges.size()); + for (const auto& range : ranges) + { + from_ranges.emplace_back(ToRangeObject(range)); + } + entry["fromRanges"] = std::move(from_ranges); + + result.emplace_back(std::move(entry)); + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/call_hierarchy/outgoing_calls.cppm b/lsp-server/src/provider/call_hierarchy/outgoing_calls.cppm index 5c04a3b..09aa4bd 100644 --- a/lsp-server/src/provider/call_hierarchy/outgoing_calls.cppm +++ b/lsp-server/src/provider/call_hierarchy/outgoing_calls.cppm @@ -8,7 +8,12 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::call_hierarchy { @@ -25,18 +30,214 @@ export namespace lsp::provider::call_hierarchy namespace lsp::provider::call_hierarchy { - + namespace + { + namespace codec = lsp::codec; - + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + std::optional ParseSymbolId(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto it = obj.find("symbolId"); + if (it == obj.end()) + { + return std::nullopt; + } + + if (it->second.Is()) + { + const auto& text = it->second.Get(); + try + { + return static_cast(std::stoull(text)); + } + catch (const std::exception&) + { + return std::nullopt; + } + } + + if (it->second.Is()) + { + return static_cast(it->second.Get()); + } + + if (it->second.Is()) + { + auto value = it->second.Get(); + if (value < 0) + { + return std::nullopt; + } + return static_cast(value); + } + + return std::nullopt; + } + + std::optional ResolveSymbolIdFromItem(const protocol::CallHierarchyItem& item, + ExecutionContext& context) + { + if (item.data) + { + if (auto id = ParseSymbolId(*item.data)) + { + return id; + } + } + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(item.uri); + const auto* table = hub.symbols().GetSymbolTable(item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(item.uri); + if (!content.has_value() || !table || !semantic) + { + return std::nullopt; + } + + language::ast::Location loc{}; + loc.start_line = item.selectionRange.start.line; + loc.start_column = item.selectionRange.start.character; + loc.end_line = item.selectionRange.start.line; + loc.end_column = item.selectionRange.start.character; + + auto offset = utils::text_coordinates::ToOffset(item.selectionRange.start, *content); + loc.start_offset = static_cast(offset); + loc.end_offset = static_cast(offset); + + if (auto symbol_id = table->FindSymbolAt(loc)) + { + return symbol_id; + } + + auto resolved = semantic->name_resolver().ResolveNameAtLocation(item.name, loc); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + + auto matches = table->FindSymbolsByName(item.name); + if (!matches.empty()) + { + return matches.front(); + } + + return std::nullopt; + } + + protocol::LSPObject BuildCallHierarchyItem(const protocol::DocumentUri& uri, + const language::symbol::Symbol& symbol) + { + protocol::LSPObject item; + item["name"] = protocol::string(symbol.name()); + item["kind"] = static_cast(symbol.kind()); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(uri); + item["range"] = ToRangeObject(ToRange(symbol.range())); + item["selectionRange"] = ToRangeObject(ToRange(symbol.selection_range())); + + protocol::LSPObject data; + data["uri"] = protocol::string(uri); + data["symbolId"] = protocol::string(std::to_string(symbol.id())); + item["data"] = protocol::LSPAny(std::move(data)); + + return item; + } + } std::string OutgoingCalls::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("OutgoingCallsProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::CallHierarchyOutgoingCallsParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::LSPArray result; + + auto symbol_id = ResolveSymbolIdFromItem(params.item, context); + if (symbol_id) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.item.uri); + + if (table && semantic) + { + std::unordered_map> grouped; + for (const auto& call : semantic->calls().callees(*symbol_id)) + { + grouped[call.callee].push_back(ToRange(call.call_site)); + } + + for (const auto& [callee_id, ranges] : grouped) + { + const auto* callee_symbol = table->definition(callee_id); + if (!callee_symbol) + { + continue; + } + + protocol::LSPObject entry; + entry["to"] = BuildCallHierarchyItem(params.item.uri, *callee_symbol); + + protocol::LSPArray from_ranges; + from_ranges.reserve(ranges.size()); + for (const auto& range : ranges) + { + from_ranges.emplace_back(ToRangeObject(range)); + } + entry["fromRanges"] = std::move(from_ranges); + + result.emplace_back(std::move(entry)); + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/client/register_capability.cppm b/lsp-server/src/provider/client/register_capability.cppm index c591421..0fc548d 100644 --- a/lsp-server/src/provider/client/register_capability.cppm +++ b/lsp-server/src/provider/client/register_capability.cppm @@ -10,7 +10,7 @@ import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; -namespace transform = lsp::codec; +namespace codec = lsp::codec; export namespace lsp::provider::client { @@ -27,21 +27,30 @@ export namespace lsp::provider::client namespace lsp::provider::client { - - - - std::string RegisterCapability::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("RegisterCapabilityProvider: Providing response for method {}", request.method); // 这个方法通常不会被调用,因为这是服务器发起的请求 // 但为了完整性还是实现它 + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + const auto& obj = request.params->Get(); + auto regs_it = obj.find("registrations"); + if (regs_it != obj.end() && regs_it->second.Is()) + { + spdlog::debug("{}: Received {} registration(s)", GetProviderName(), regs_it->second.Get().size()); + } + protocol::ResponseMessage response; response.id = request.id; - response.result = protocol::LSPAny{}; // explicit null result + response.result = protocol::LSPAny(std::nullptr_t{}); - std::optional json = transform::Serialize(response); + std::optional json = codec::Serialize(response); if (!json.has_value()) return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Internal error"); return json.value(); diff --git a/lsp-server/src/provider/client/unregister_capability.cppm b/lsp-server/src/provider/client/unregister_capability.cppm index 8407b47..7a75ca8 100644 --- a/lsp-server/src/provider/client/unregister_capability.cppm +++ b/lsp-server/src/provider/client/unregister_capability.cppm @@ -10,7 +10,7 @@ import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; -namespace transform = lsp::codec; +namespace codec = lsp::codec; export namespace lsp::provider::client { @@ -27,18 +27,28 @@ export namespace lsp::provider::client namespace lsp::provider::client { - - - - std::string UnregisterCapability::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("UnregisterCapabilityProvider: Providing response for method {}", request.method); + + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + const auto& obj = request.params->Get(); + auto regs_it = obj.find("unregistrations"); + if (regs_it != obj.end() && regs_it->second.Is()) + { + spdlog::debug("{}: Received {} unregistration(s)", GetProviderName(), regs_it->second.Get().size()); + } + protocol::ResponseMessage response; response.id = request.id; - response.result = protocol::LSPAny{}; + response.result = protocol::LSPAny(std::nullptr_t{}); - std::optional json = transform::Serialize(response); + std::optional json = codec::Serialize(response); if (!json.has_value()) return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Internal error"); return json.value(); diff --git a/lsp-server/src/provider/code_action/resolve.cppm b/lsp-server/src/provider/code_action/resolve.cppm index c6a24b9..d42bd73 100644 --- a/lsp-server/src/provider/code_action/resolve.cppm +++ b/lsp-server/src/provider/code_action/resolve.cppm @@ -25,18 +25,31 @@ export namespace lsp::provider::code_action namespace lsp::provider::code_action { - - - + namespace + { + namespace codec = lsp::codec; + } std::string Resolve::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("CodeActionResolveProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = request.params.value(); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/code_lens/resolve.cppm b/lsp-server/src/provider/code_lens/resolve.cppm index a9602c2..570af21 100644 --- a/lsp-server/src/provider/code_lens/resolve.cppm +++ b/lsp-server/src/provider/code_lens/resolve.cppm @@ -25,18 +25,91 @@ export namespace lsp::provider::code_lens namespace lsp::provider::code_lens { - + namespace + { + namespace codec = lsp::codec; - + std::optional GetUInteger(const protocol::LSPAny& any) + { + if (any.Is()) + { + return any.Get(); + } + if (any.Is()) + { + return static_cast(any.Get()); + } + return std::nullopt; + } + + std::string BuildReferenceTitle(protocol::uinteger count) + { + if (count == 1) + { + return "1 reference"; + } + return std::to_string(count) + " references"; + } + } std::string Resolve::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("CodeLensResolveProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::CodeLens lens = + codec::FromLSPAny.template operator()(request.params.value()); + + if (lens.command.has_value()) + { + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(lens); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); + } + + if (lens.data && lens.data->Is()) + { + const auto& data = lens.data->Get(); + auto kind_it = data.find("kind"); + if (kind_it != data.end() && kind_it->second.Is() && + kind_it->second.Get() == "references") + { + auto count_it = data.find("count"); + if (count_it != data.end()) + { + if (auto count = GetUInteger(count_it->second)) + { + protocol::Command command; + command.title = BuildReferenceTitle(*count); + command.command = "tsl.showReferences"; + command.arguments = std::vector{ *lens.data }; + lens.command = std::move(command); + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(lens); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/document_link/resolve.cppm b/lsp-server/src/provider/document_link/resolve.cppm index e03dc68..df0bfe6 100644 --- a/lsp-server/src/provider/document_link/resolve.cppm +++ b/lsp-server/src/provider/document_link/resolve.cppm @@ -8,7 +8,12 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.manager.symbol; import lsp.provider.base.interface; +import lsp.utils.string; + +namespace codec = lsp::codec; export namespace lsp::provider::document_link { @@ -25,18 +30,219 @@ export namespace lsp::provider::document_link namespace lsp::provider::document_link { - + namespace + { + namespace utils = lsp::utils; - + std::string PathToUri(const std::filesystem::path& path) + { + auto absolute = std::filesystem::absolute(path).generic_string(); +#ifdef _WIN32 + std::replace(absolute.begin(), absolute.end(), '\\', '/'); +#endif + if (!absolute.starts_with("/")) + absolute = "/" + absolute; + return "file://" + absolute; + } - std::string Resolve::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::string UriToPath(const std::string& uri) + { + std::string path = uri; + if (path.starts_with("file://")) + path = path.substr(7); + +#ifdef _WIN32 + if (!path.empty() && path[0] == '/') + path = path.substr(1); + std::replace(path.begin(), path.end(), '/', '\\'); +#endif + + std::string decoded; + decoded.reserve(path.size()); + for (std::size_t i = 0; i < path.size(); ++i) + { + if (path[i] == '%' && i + 2 < path.size()) + { + std::string hex = path.substr(i + 1, 2); + char ch = static_cast(std::stoi(hex, nullptr, 16)); + decoded.push_back(ch); + i += 2; + } + else if (path[i] == '+') + { + decoded.push_back(' '); + } + else + { + decoded.push_back(path[i]); + } + } + return decoded; + } + + std::optional ResolvePathTarget(std::string_view raw, + const protocol::DocumentUri& base_uri) + { + if (raw.empty()) + { + return std::nullopt; + } + + if (raw.starts_with("file://")) + { + return std::string(raw); + } + + std::filesystem::path path(raw); + if (path.is_relative()) + { + auto base_path = UriToPath(base_uri); + std::filesystem::path base_dir = std::filesystem::path(base_path).parent_path(); + path = base_dir / path; + } + + auto try_candidate = [](const std::filesystem::path& candidate) -> std::optional { + if (std::filesystem::exists(candidate)) + { + return PathToUri(candidate); + } + return std::nullopt; + }; + + if (path.has_extension()) + { + if (auto uri = try_candidate(path)) + { + return uri; + } + } + else + { + if (auto uri = try_candidate(path.string() + ".tsl")) + { + return uri; + } + if (auto uri = try_candidate(path.string() + ".tsf")) + { + return uri; + } + } + + return std::nullopt; + } + + std::optional ResolveUnitTarget(const manager::Symbol& symbols, + const std::string& unit_name, + const std::optional& base_dir) + { + auto indexed = symbols.QueryIndexedSymbols(protocol::SymbolKind::Module); + for (const auto& item : indexed) + { + if (utils::IEquals(item.name, unit_name)) + { + return item.uri; + } + } + + if (base_dir) + { + auto candidate = *base_dir / (unit_name + ".tsf"); + if (std::filesystem::exists(candidate)) + { + return PathToUri(candidate); + } + } + + return std::nullopt; + } + + std::optional ResolveTargetFromData(const protocol::LSPObject& data, + ExecutionContext& context) + { + auto kind_it = data.find("kind"); + if (kind_it == data.end() || !kind_it->second.Is()) + { + return std::nullopt; + } + + auto base_it = data.find("baseUri"); + protocol::DocumentUri base_uri; + if (base_it != data.end() && base_it->second.Is()) + { + base_uri = base_it->second.Get(); + } + + std::optional base_dir; + if (!base_uri.empty()) + { + try + { + base_dir = std::filesystem::path(UriToPath(base_uri)).parent_path(); + } + catch (const std::exception&) + { + base_dir = std::nullopt; + } + } + + const auto& kind = kind_it->second.Get(); + if (kind == "unit") + { + auto name_it = data.find("name"); + if (name_it == data.end() || !name_it->second.Is()) + { + return std::nullopt; + } + return ResolveUnitTarget(context.GetManagerHub().symbols(), name_it->second.Get(), base_dir); + } + + if (kind == "path") + { + auto path_it = data.find("path"); + if (path_it == data.end() || !path_it->second.Is()) + { + return std::nullopt; + } + return ResolvePathTarget(path_it->second.Get(), base_uri); + } + + return std::nullopt; + } + } + + std::string Resolve::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("DocumentLinkResolveProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + auto link = request.params->Get(); + auto target_it = link.find("target"); + if (target_it == link.end()) + { + auto data_it = link.find("data"); + if (data_it != link.end() && data_it->second.Is()) + { + if (auto target = ResolveTargetFromData(data_it->second.Get(), context)) + { + link["target"] = protocol::string(*target); + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(link)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/initialize/initialize.cppm b/lsp-server/src/provider/initialize/initialize.cppm index 660188a..e214a19 100644 --- a/lsp-server/src/provider/initialize/initialize.cppm +++ b/lsp-server/src/provider/initialize/initialize.cppm @@ -1,7 +1,5 @@ module; -#include - export module lsp.provider.initialize.initialize; import spdlog; @@ -29,7 +27,11 @@ export namespace lsp::provider protocol::InitializeResult BuildInitializeResult(); protocol::TextDocumentSyncOptions BuildTextDocumentSyncOptions(); protocol::CompletionOptions BuildCompletionOptions(); + protocol::SignatureHelpOptions BuildSignatureHelpOptions(); protocol::SemanticTokensOptions BuildSemanticTokenOptions(); + protocol::CodeActionOptions BuildCodeActionOptions(); + protocol::DocumentOnTypeFormattingOptions BuildOnTypeFormattingOptions(); + protocol::ExecuteCommandOptions BuildExecuteCommandOptions(); void ProcessWorkspaceFolder(const std::vector& workspace_folders, ExecutionContext& context); }; @@ -51,7 +53,10 @@ namespace lsp::provider manager_hub.Initialize(); if (params.workspaceFolders) + { + manager_hub.SetWorkspaceFolders(params.workspaceFolders.value()); ProcessWorkspaceFolder(params.workspaceFolders.value(), context); + } protocol::ResponseMessage response; response.id = request.id; @@ -73,7 +78,51 @@ namespace lsp::provider result.serverInfo.version = __DATE__; result.capabilities.textDocumentSync = BuildTextDocumentSyncOptions(); result.capabilities.completionProvider = BuildCompletionOptions(); - // result.capabilities.semanticTokensProvider = BuildSemanticTokenOptions(); + result.capabilities.definitionProvider = true; + result.capabilities.typeDefinitionProvider = true; + result.capabilities.implementationProvider = true; + result.capabilities.hoverProvider = true; + result.capabilities.signatureHelpProvider = BuildSignatureHelpOptions(); + result.capabilities.codeActionProvider = BuildCodeActionOptions(); + result.capabilities.codeLensProvider = protocol::CodeLensOptions{ .resolveProvider = true }; + result.capabilities.documentLinkProvider = protocol::DocumentLinkOptions{ .resolveProvider = true }; + result.capabilities.colorProvider = true; + result.capabilities.referencesProvider = true; + result.capabilities.documentHighlightProvider = true; + result.capabilities.renameProvider = protocol::RenameOptions{ .prepareProvider = true }; + result.capabilities.documentSymbolProvider = true; + result.capabilities.workspaceSymbolProvider = protocol::WorkspaceSymbolOptions{ .resolveProvider = true }; + result.capabilities.semanticTokensProvider = BuildSemanticTokenOptions(); + result.capabilities.callHierarchyProvider = true; + result.capabilities.typeHierarchyProvider = true; + result.capabilities.inlayHintProvider = protocol::InlayHintOptions{ .resolveProvider = true }; + result.capabilities.foldingRangeProvider = true; + result.capabilities.selectionRangeProvider = true; + result.capabilities.linkedEditingRangeProvider = true; + result.capabilities.monikerProvider = true; + result.capabilities.inlineValueProvider = true; + result.capabilities.documentFormattingProvider = true; + result.capabilities.documentRangeFormattingProvider = true; + result.capabilities.documentOnTypeFormattingProvider = BuildOnTypeFormattingOptions(); + result.capabilities.executeCommandProvider = BuildExecuteCommandOptions(); + + protocol::DiagnosticOptions diagnostic; + diagnostic.identifier = "syntax"; + diagnostic.interFileDependencies = false; + diagnostic.workspaceDiagnostics = true; + result.capabilities.diagnosticProvider = std::move(diagnostic); + + protocol::ServerCapabilities::Workspace workspace; + protocol::ServerCapabilities::Workspace::FileOperations file_operations; + file_operations.didCreate = true; + file_operations.willCreate = true; + file_operations.didDelete = true; + file_operations.willDelete = true; + file_operations.didRename = true; + file_operations.willRename = true; + workspace.fileOperations = std::move(file_operations); + result.capabilities.workspace = std::move(workspace); + return result; } @@ -94,42 +143,65 @@ namespace lsp::provider return options; } + protocol::SignatureHelpOptions Initialize::BuildSignatureHelpOptions() + { + protocol::SignatureHelpOptions options; + options.triggerCharacters = { std::vector{ "(", "," } }; + options.retriggerCharacters = { std::vector{ "(", "," } }; + return options; + } + + protocol::CodeActionOptions Initialize::BuildCodeActionOptions() + { + protocol::CodeActionOptions options; + options.codeActionKinds = std::vector{ + protocol::CodeActionKindLiterals::QuickFix, + protocol::CodeActionKindLiterals::SourceFixAll, + }; + options.resolveProvider = true; + return options; + } + + protocol::DocumentOnTypeFormattingOptions Initialize::BuildOnTypeFormattingOptions() + { + protocol::DocumentOnTypeFormattingOptions options; + options.firstTriggerCharacter = ";"; + options.moreTriggerCharacter = std::vector{ "\n" }; + return options; + } + + protocol::ExecuteCommandOptions Initialize::BuildExecuteCommandOptions() + { + protocol::ExecuteCommandOptions options; + options.commands = std::vector{ + "tsl.noop", + "tsl.loadWorkspace", + "tsl.indexFiles", + }; + return options; + } + protocol::SemanticTokensOptions Initialize::BuildSemanticTokenOptions() { protocol::SemanticTokensOptions options; options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Namespace); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Type); options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Class); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Enum); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Interface); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Struct); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::TypeParameter); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Parameter); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Variable); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Property); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::EnumMember); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Event); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Function); options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Method); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Macro); + options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Function); + options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Property); + options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Variable); + options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Parameter); options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Keyword); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Modifier); options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Comment); options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::String); options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Number); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Regexp); options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Operator); - options.legend.tokenTypes.emplace_back(protocol::SemanticTokenTypesLiterals::Decorator); + options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Declaration); options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Definition); - options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Readonly); options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Static); - options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Deprecated); - options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Abstract); - options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Async); + options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Readonly); options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Modification); - options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::Documentation); - options.legend.tokenModifiers.emplace_back(protocol::SemanticTokenModifiersLiterals::DefaultLibrary); options.range = true; options.full = protocol::SemanticTokensOptions::Full{ .delta = true }; return options; diff --git a/lsp-server/src/provider/inlay_hint/resolve.cppm b/lsp-server/src/provider/inlay_hint/resolve.cppm index ac11798..01e665d 100644 --- a/lsp-server/src/provider/inlay_hint/resolve.cppm +++ b/lsp-server/src/provider/inlay_hint/resolve.cppm @@ -25,18 +25,69 @@ export namespace lsp::provider::inlay_hint namespace lsp::provider::inlay_hint { - + namespace + { + namespace codec = lsp::codec; - + std::optional GetStringField(const protocol::LSPObject& obj, std::string_view key) + { + auto it = obj.find(std::string(key)); + if (it == obj.end() || !it->second.Is()) + { + return std::nullopt; + } + return it->second.Get(); + } + } // namespace std::string Resolve::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("InlayHintResolveProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Invalid params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Invalid params"); + } + + protocol::LSPObject resolved = request.params->Get(); + + auto data_it = resolved.find("data"); + if (data_it != resolved.end() && data_it->second.Is()) + { + const auto& data = data_it->second.Get(); + + if (resolved.find("label") == resolved.end()) + { + if (auto label = GetStringField(data, "label")) + { + resolved["label"] = protocol::string(*label); + } + } + + if (resolved.find("tooltip") == resolved.end()) + { + if (auto tooltip = GetStringField(data, "tooltip")) + { + resolved["tooltip"] = protocol::string(*tooltip); + } + else if (auto detail = GetStringField(data, "detail")) + { + resolved["tooltip"] = protocol::string(*detail); + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(resolved)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/manifest.cppm b/lsp-server/src/provider/manifest.cppm index af837cb..e7480b7 100644 --- a/lsp-server/src/provider/manifest.cppm +++ b/lsp-server/src/provider/manifest.cppm @@ -65,22 +65,22 @@ import lsp.provider.workspace.apply_edit; import lsp.provider.workspace.code_lens_refresh; import lsp.provider.workspace.configuration; import lsp.provider.workspace.diagnostic; -import lsp.provider.workspace.diagnostic_refresh; import lsp.provider.workspace.did_change_configuration; import lsp.provider.workspace.did_change_watched_files; import lsp.provider.workspace.did_change_workspace_folders; import lsp.provider.workspace.did_create_files; import lsp.provider.workspace.did_delete_files; import lsp.provider.workspace.did_rename_files; +import lsp.provider.workspace.diagnostic_refresh; import lsp.provider.workspace.execute_command; import lsp.provider.workspace.inlay_hint_refresh; import lsp.provider.workspace.inline_value_refresh; import lsp.provider.workspace.semantic_tokens_refresh; import lsp.provider.workspace.symbol; +import lsp.provider.workspace.workspace_folders; import lsp.provider.workspace.will_create_files; import lsp.provider.workspace.will_delete_files; import lsp.provider.workspace.will_rename_files; -import lsp.provider.workspace.workspace_folders; import lsp.provider.workspace_symbol.resolve; export namespace lsp::provider @@ -148,22 +148,22 @@ export namespace lsp::provider workspace::CodeLensRefresh, workspace::Configuration, workspace::Diagnostic, - workspace::DiagnosticRefresh, workspace::DidChangeConfiguration, workspace::DidChangeWatchedFiles, workspace::DidChangeWorkspaceFolders, workspace::DidCreateFiles, workspace::DidDeleteFiles, workspace::DidRenameFiles, + workspace::DiagnosticRefresh, workspace::ExecuteCommand, workspace::InlayHintRefresh, workspace::InlineValueRefresh, workspace::SemanticTokensRefresh, workspace::Symbol, + workspace::WorkspaceFolders, workspace::WillCreateFiles, workspace::WillDeleteFiles, workspace::WillRenameFiles, - workspace::WorkspaceFolders, workspace_symbol::Resolve >; diff --git a/lsp-server/src/provider/telemetry/event.cppm b/lsp-server/src/provider/telemetry/event.cppm index 0065459..fc1af87 100644 --- a/lsp-server/src/provider/telemetry/event.cppm +++ b/lsp-server/src/provider/telemetry/event.cppm @@ -25,17 +25,28 @@ export namespace lsp::provider::telemetry namespace lsp::provider::telemetry { - - - - void Event::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TelemetryEventProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + const auto& any = notification.params.value(); + if (any.Is()) + { + spdlog::debug("{}: Received object payload with {} field(s)", GetProviderName(), any.Get().size()); + return; + } + if (any.Is()) + { + spdlog::debug("{}: Received array payload with {} item(s)", GetProviderName(), any.Get().size()); + return; + } + + spdlog::debug("{}: Received primitive telemetry payload", GetProviderName()); } } diff --git a/lsp-server/src/provider/text_document/code_action.cppm b/lsp-server/src/provider/text_document/code_action.cppm index ee5c19a..a721fe0 100644 --- a/lsp-server/src/provider/text_document/code_action.cppm +++ b/lsp-server/src/provider/text_document/code_action.cppm @@ -9,6 +9,8 @@ import std; import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; +import lsp.manager.manager_hub; +import lsp.utils.text_coordinates; export namespace lsp::provider::text_document { @@ -25,18 +27,422 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; - + std::optional ParsePosition(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto line_it = obj.find("line"); + auto character_it = obj.find("character"); + + if (line_it == obj.end() || character_it == obj.end()) + { + return std::nullopt; + } + + if (!line_it->second.Is() || !character_it->second.Is()) + { + return std::nullopt; + } + + protocol::Position position; + position.line = static_cast(line_it->second.Get()); + position.character = static_cast(character_it->second.Get()); + return position; + } + + std::optional ParseRange(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto start_it = obj.find("start"); + auto end_it = obj.find("end"); + + if (start_it == obj.end() || end_it == obj.end()) + { + return std::nullopt; + } + + auto start = ParsePosition(start_it->second); + auto end = ParsePosition(end_it->second); + if (!start || !end) + { + return std::nullopt; + } + + protocol::Range range; + range.start = *start; + range.end = *end; + return range; + } + + struct ParsedDiagnostic + { + protocol::Range range; + std::string message; + }; + + std::optional ParseDiagnostic(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto range_it = obj.find("range"); + auto message_it = obj.find("message"); + + if (range_it == obj.end() || message_it == obj.end()) + { + return std::nullopt; + } + + auto range = ParseRange(range_it->second); + if (!range) + { + return std::nullopt; + } + + if (!message_it->second.Is()) + { + return std::nullopt; + } + + return ParsedDiagnostic{ + .range = *range, + .message = message_it->second.Get(), + }; + } + + bool HasOnlyFilter(const protocol::LSPObject& context) + { + auto it = context.find("only"); + return it != context.end(); + } + + bool OnlyAllows(const protocol::LSPObject& context, std::string_view kind_prefix) + { + auto it = context.find("only"); + if (it == context.end()) + { + return true; + } + + if (!it->second.Is()) + { + return true; + } + + const auto& arr = it->second.Get(); + for (const auto& entry : arr) + { + if (!entry.Is()) + { + continue; + } + + const std::string& kind = entry.Get(); + if (kind == protocol::CodeActionKindLiterals::Empty) + { + continue; + } + + if (kind.rfind(kind_prefix, 0) == 0) + { + return true; + } + } + + return false; + } + + std::optional ExtractMissingToken(std::string_view message) + { + static constexpr std::string_view kPrefix = "Syntax error: missing "; + if (!message.starts_with(kPrefix)) + { + return std::nullopt; + } + + auto token = std::string(message.substr(kPrefix.size())); + while (!token.empty() && std::isspace(static_cast(token.front()))) + { + token.erase(token.begin()); + } + while (!token.empty() && std::isspace(static_cast(token.back()))) + { + token.pop_back(); + } + + if (token.empty()) + { + return std::nullopt; + } + + if (token.size() >= 2 && ((token.front() == '\'' && token.back() == '\'') || (token.front() == '"' && token.back() == '"'))) + { + token = token.substr(1, token.size() - 2); + } + + if (token.empty() || token == "MISSING" || token == "ERROR") + { + return std::nullopt; + } + + return token; + } + + std::optional MapMissingTokenToInsert(std::string_view token) + { + if (token == "identifier") + { + return std::string("TODO"); + } + + static const std::unordered_set kAllowed = { + ";", + ",", + ".", + ")", + "(", + "]", + "[", + ":", + "=", + "end", + "begin", + "then", + "do", + "of", + }; + + if (!kAllowed.contains(token)) + { + return std::nullopt; + } + + return std::string(token); + } + + bool WouldDuplicateInsert(const std::optional& content, + const protocol::Position& position, + std::string_view insertion) + { + if (!content.has_value() || insertion.empty()) + { + return false; + } + + auto offset = utils::text_coordinates::ToOffset(position, *content); + if (offset >= content->size()) + { + return false; + } + + if (insertion.size() == 1) + { + return (*content)[offset] == insertion.front(); + } + + return content->compare(offset, insertion.size(), insertion) == 0; + } + + protocol::TextEdit MakeInsertEdit(const protocol::Position& position, const std::string& text) + { + protocol::TextEdit edit; + edit.range.start = position; + edit.range.end = position; + edit.newText = text; + return edit; + } + + protocol::WorkspaceEdit MakeWorkspaceEdit(const protocol::DocumentUri& uri, + std::vector edits) + { + protocol::WorkspaceEdit workspace_edit; + workspace_edit.changes[uri] = std::move(edits); + + auto& per_doc = workspace_edit.changes[uri]; + std::sort(per_doc.begin(), per_doc.end(), [](const protocol::TextEdit& a, const protocol::TextEdit& b) { + if (a.range.start.line != b.range.start.line) + { + return a.range.start.line > b.range.start.line; + } + return a.range.start.character > b.range.start.character; + }); + + return workspace_edit; + } + + protocol::CodeAction MakeQuickFix(const std::string& title, + const protocol::DocumentUri& uri, + std::vector edits, + bool preferred) + { + protocol::CodeAction action; + action.title = title; + action.kind = protocol::CodeActionKindLiterals::QuickFix; + action.isPreferred = preferred; + action.edit = MakeWorkspaceEdit(uri, std::move(edits)); + return action; + } + + protocol::CodeAction MakeFixAllSemicolons(const protocol::DocumentUri& uri, + std::vector edits) + { + protocol::CodeAction action; + action.title = "Fix all missing semicolons"; + action.kind = protocol::CodeActionKindLiterals::SourceFixAll; + action.isPreferred = true; + action.edit = MakeWorkspaceEdit(uri, std::move(edits)); + return action; + } + + } // namespace std::string CodeAction::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentCodeActionProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value() || !request.params->Is()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Invalid params"); + } + + const auto& params = request.params->Get(); + + auto text_document_it = params.find("textDocument"); + if (text_document_it == params.end() || !text_document_it->second.Is()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing textDocument"); + } + + const auto& text_document = text_document_it->second.Get(); + auto uri_it = text_document.find("uri"); + if (uri_it == text_document.end() || !uri_it->second.Is()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing textDocument.uri"); + } + + const protocol::DocumentUri uri = uri_it->second.Get(); + + auto context_it = params.find("context"); + if (context_it == params.end() || !context_it->second.Is()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing context"); + } + + const auto& context_obj = context_it->second.Get(); + auto diagnostics_it = context_obj.find("diagnostics"); + if (diagnostics_it == context_obj.end() || !diagnostics_it->second.Is()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing context.diagnostics"); + } + + if (HasOnlyFilter(context_obj)) + { + const bool allow_quickfix = OnlyAllows(context_obj, protocol::CodeActionKindLiterals::QuickFix); + const bool allow_source_fixall = OnlyAllows(context_obj, protocol::CodeActionKindLiterals::SourceFixAll); + + if (!allow_quickfix && !allow_source_fixall) + { + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(std::vector{}); + auto json = codec::Serialize(response); + return json.value_or(BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response")); + } + } + + auto& hub = context.GetManagerHub(); + const auto content = hub.documents().GetContent(uri); + + std::vector actions; + std::vector semicolon_edits; + std::unordered_set semicolon_positions; + + const auto& diagnostics = diagnostics_it->second.Get(); + actions.reserve(diagnostics.size()); + + for (const auto& diag_any : diagnostics) + { + auto diag = ParseDiagnostic(diag_any); + if (!diag) + { + continue; + } + + if (auto missing_token = ExtractMissingToken(diag->message)) + { + auto insertion = MapMissingTokenToInsert(*missing_token); + if (!insertion) + { + continue; + } + + if (WouldDuplicateInsert(content, diag->range.start, *insertion)) + { + continue; + } + + auto edit = MakeInsertEdit(diag->range.start, *insertion); + const bool preferred = *missing_token == ";"; + + std::string title = "Insert '" + *insertion + "'"; + actions.push_back(MakeQuickFix(title, uri, std::vector{ edit }, preferred)); + + if (*missing_token == ";") + { + std::uint64_t key = (static_cast(diag->range.start.line) << 32) | + static_cast(diag->range.start.character); + if (semicolon_positions.insert(key).second) + { + semicolon_edits.push_back(std::move(edit)); + } + } + + continue; + } + + if (diag->message.rfind("Syntax error: unexpected token", 0) == 0) + { + if (diag->range.start.line == diag->range.end.line && diag->range.start.character != diag->range.end.character) + { + protocol::TextEdit edit; + edit.range = diag->range; + edit.newText = ""; + actions.push_back(MakeQuickFix("Remove unexpected token", uri, std::vector{ std::move(edit) }, false)); + } + } + } + + if (!semicolon_edits.empty() && OnlyAllows(context_obj, protocol::CodeActionKindLiterals::SourceFixAll)) + { + actions.push_back(MakeFixAllSemicolons(uri, std::move(semicolon_edits))); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(actions); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/code_lens.cppm b/lsp-server/src/provider/text_document/code_lens.cppm index 71fb9d0..0ddcd49 100644 --- a/lsp-server/src/provider/text_document/code_lens.cppm +++ b/lsp-server/src/provider/text_document/code_lens.cppm @@ -8,7 +8,11 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; export namespace lsp::provider::text_document { @@ -25,19 +29,125 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; - + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + std::optional GetAnchorLocation(const language::symbol::Symbol& symbol) + { + using namespace language::symbol; + + if (symbol.Is()) + { + const auto* fn = symbol.As(); + if (fn->implementation_range) + { + return *fn->implementation_range; + } + return fn->declaration_range; + } + + if (symbol.Is()) + { + const auto* method = symbol.As(); + if (method->implementation_range) + { + return *method->implementation_range; + } + return method->declaration_range; + } + + if (symbol.Is()) + { + const auto* cls = symbol.As(); + return cls->selection_range; + } + + return std::nullopt; + } + } std::string CodeLens::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentCodeLensProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::CodeLensParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + std::vector lenses; + + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.textDocument.uri); + if (table && semantic) + { + for (const auto& wrapper : table->all_definitions()) + { + const auto& symbol = wrapper.get(); + if (symbol.name().empty()) + { + continue; + } + + if (symbol.kind() != protocol::SymbolKind::Function && + symbol.kind() != protocol::SymbolKind::Method && + symbol.kind() != protocol::SymbolKind::Class) + { + continue; + } + + auto location = GetAnchorLocation(symbol); + if (!location) + { + continue; + } + + const auto& refs = semantic->references().references(symbol.id()); + protocol::CodeLens lens; + lens.range = ToRange(*location); + + protocol::LSPObject data; + data["kind"] = protocol::string("references"); + data["uri"] = protocol::string(params.textDocument.uri); + data["position"] = protocol::LSPObject{ + { "line", static_cast(location->start_line) }, + { "character", static_cast(location->start_column) }, + }; + data["name"] = protocol::string(symbol.name()); + data["symbolId"] = protocol::string(std::to_string(symbol.id())); + data["count"] = static_cast(refs.size()); + lens.data = protocol::LSPAny(std::move(data)); + + lenses.emplace_back(std::move(lens)); + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(lenses); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/color_presentation.cppm b/lsp-server/src/provider/text_document/color_presentation.cppm index c59e21f..245e1ea 100644 --- a/lsp-server/src/provider/text_document/color_presentation.cppm +++ b/lsp-server/src/provider/text_document/color_presentation.cppm @@ -1,7 +1,7 @@ module; - export module lsp.provider.text_document.color_presentation; + import spdlog; import std; @@ -10,6 +10,8 @@ import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; +namespace transform = lsp::codec; + export namespace lsp::provider::text_document { class ColorPresentation : public AutoRegisterProvider @@ -25,19 +27,64 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + int ToByte(protocol::decimal value) + { + if (std::isnan(value) || std::isinf(value)) + { + return 0; + } + value = std::clamp(value, protocol::decimal(0.0), protocol::decimal(1.0)); + return static_cast(std::lround(value * 255.0)); + } - + std::string ToHexColor(const protocol::Color& color) + { + const int r = ToByte(color.red); + const int g = ToByte(color.green); + const int b = ToByte(color.blue); + const int a = ToByte(color.alpha); + + if (a == 255) + { + return std::format("#{:02x}{:02x}{:02x}", r, g, b); + } + return std::format("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a); + } + } std::string ColorPresentation::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentColorPresentationProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::ColorPresentationParams params = + transform::FromLSPAny.template operator()(request.params.value()); + + protocol::ColorPresentation presentation; + presentation.label = ToHexColor(params.color); + + protocol::TextEdit edit; + edit.range = params.range; + edit.newText = presentation.label; + presentation.textEdit = std::move(edit); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = transform::ToLSPAny(std::vector{ std::move(presentation) }); + + std::optional json = transform::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/completion.cppm b/lsp-server/src/provider/text_document/completion.cppm index 62ff03e..a783c85 100644 --- a/lsp-server/src/provider/text_document/completion.cppm +++ b/lsp-server/src/provider/text_document/completion.cppm @@ -1,7 +1,5 @@ module; -#include - export module lsp.provider.text_document.completion; import spdlog; diff --git a/lsp-server/src/provider/text_document/definition.cppm b/lsp-server/src/provider/text_document/definition.cppm index fdddb5f..c0671aa 100644 --- a/lsp-server/src/provider/text_document/definition.cppm +++ b/lsp-server/src/provider/text_document/definition.cppm @@ -1,7 +1,5 @@ module; -#include - export module lsp.provider.text_document.definition; import tree_sitter; import spdlog; @@ -209,7 +207,7 @@ namespace lsp::provider::text_document while (!ts_node_is_null(node)) { const char* node_type = ts_node_type(node); - if (std::strcmp(node_type, kIdentifier) == 0) + if (std::string_view(node_type) == kIdentifier) { uint32_t start = ts_node_start_byte(node); uint32_t end = ts_node_end_byte(node); diff --git a/lsp-server/src/provider/text_document/diagnostic.cppm b/lsp-server/src/provider/text_document/diagnostic.cppm index 55e8991..333879e 100644 --- a/lsp-server/src/provider/text_document/diagnostic.cppm +++ b/lsp-server/src/provider/text_document/diagnostic.cppm @@ -1,15 +1,20 @@ module; - export module lsp.provider.text_document.diagnostic; + +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +namespace transform = lsp::codec; + export namespace lsp::provider::text_document { class Diagnostic : public AutoRegisterProvider @@ -25,19 +30,89 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - - - - std::string Diagnostic::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentDiagnosticProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::DiagnosticParams params = + transform::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto content_opt = hub.documents().GetContent(params.textDocument.uri); + auto version_opt = hub.documents().GetVersion(params.textDocument.uri); + + protocol::RelatedFullDocumentDiagnosticReport report; + if (version_opt.has_value()) + { + report.resultId = std::to_string(version_opt.value()); + } + + auto* tree = hub.parser().GetTree(params.textDocument.uri); + if (tree && content_opt.has_value()) + { + language::ast::Deserializer deserializer; + auto errors = deserializer.DiagnoseSyntax(ts_tree_root_node(tree), content_opt.value()); + + report.items.reserve(errors.size()); + for (const auto& error : errors) + { + protocol::Diagnostic diagnostic; + diagnostic.range.start.line = error.location.start_line; + diagnostic.range.start.character = error.location.start_column; + diagnostic.range.end.line = error.location.end_line; + diagnostic.range.end.character = error.location.end_column; + + switch (error.severity) + { + case language::ast::ErrorSeverity::Warning: + diagnostic.severity = protocol::DiagnosticSeverity::Warning; + break; + case language::ast::ErrorSeverity::Fatal: + case language::ast::ErrorSeverity::Error: + default: + diagnostic.severity = protocol::DiagnosticSeverity::Error; + break; + } + + diagnostic.source = "tsl"; + diagnostic.message = error.message; + report.items.push_back(std::move(diagnostic)); + } + } + else if (!content_opt.has_value()) + { + spdlog::debug("{}: Document content not found for {}", GetProviderName(), params.textDocument.uri); + } + else + { + spdlog::debug("{}: Syntax tree not found for {}", GetProviderName(), params.textDocument.uri); + } + + protocol::ResponseMessage response; + response.id = request.id; + + if (params.previousResultId && report.resultId && params.previousResultId.value() == report.resultId.value()) + { + protocol::RelatedUnchangedDocumentDiagnosticReport unchanged; + unchanged.resultId = report.resultId; + response.result = transform::ToLSPAny(unchanged); + } + else + { + response.result = transform::ToLSPAny(report); + } + + std::optional json = transform::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/document_color.cppm b/lsp-server/src/provider/text_document/document_color.cppm index b6f6922..108a483 100644 --- a/lsp-server/src/provider/text_document/document_color.cppm +++ b/lsp-server/src/provider/text_document/document_color.cppm @@ -1,15 +1,18 @@ module; - export module lsp.provider.text_document.document_color; + import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +namespace transform = lsp::codec; + export namespace lsp::provider::text_document { class DocumentColor : public AutoRegisterProvider @@ -25,19 +28,165 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + constexpr int HexNibble(char ch) + { + if (ch >= '0' && ch <= '9') + { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') + { + return 10 + (ch - 'a'); + } + if (ch >= 'A' && ch <= 'F') + { + return 10 + (ch - 'A'); + } + return -1; + } - + protocol::Color ParseHexColor(std::string_view literal) + { + // literal: #RRGGBB or #RRGGBBAA + auto byte_at = [&](std::size_t offset) -> std::uint8_t { + int hi = HexNibble(literal[offset]); + int lo = HexNibble(literal[offset + 1]); + if (hi < 0 || lo < 0) + { + return 0; + } + return static_cast((hi << 4) | lo); + }; - std::string DocumentColor::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::uint8_t r = byte_at(1); + std::uint8_t g = byte_at(3); + std::uint8_t b = byte_at(5); + std::uint8_t a = 255; + if (literal.size() == 9) + { + a = byte_at(7); + } + + protocol::Color color; + color.red = static_cast(r) / 255.0; + color.green = static_cast(g) / 255.0; + color.blue = static_cast(b) / 255.0; + color.alpha = static_cast(a) / 255.0; + return color; + } + + std::vector ScanHexColors(const std::string& content) + { + std::vector results; + protocol::uinteger line = 0; + protocol::uinteger character = 0; + + std::size_t i = 0; + while (i < content.size()) + { + const char ch = content[i]; + if (ch == '\n') + { + line++; + character = 0; + i++; + continue; + } + + if (ch != '#') + { + character++; + i++; + continue; + } + + auto starts_with_hex = [&](std::size_t count) -> bool { + if (i + 1 + count > content.size()) + { + return false; + } + for (std::size_t j = 0; j < count; ++j) + { + if (HexNibble(content[i + 1 + j]) < 0) + { + return false; + } + } + return true; + }; + + std::optional digits; + if (starts_with_hex(6)) + { + digits = 6; + if (starts_with_hex(8)) + { + digits = 8; + } + } + + if (!digits) + { + character++; + i++; + continue; + } + + protocol::ColorInformation info; + info.range.start.line = line; + info.range.start.character = character; + info.range.end.line = line; + info.range.end.character = character + 1 + static_cast(*digits); + + const std::size_t literal_len = 1 + *digits; + info.color = ParseHexColor(std::string_view(content).substr(i, literal_len)); + results.push_back(std::move(info)); + + i += literal_len; + character += static_cast(literal_len); + } + + return results; + } + } + + std::string DocumentColor::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentDocumentColorProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::DocumentColorParams params = + transform::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto content_opt = hub.documents().GetContent(params.textDocument.uri); + if (!content_opt) + { + spdlog::debug("{}: Document content not found for {}", GetProviderName(), params.textDocument.uri); + protocol::ResponseMessage response; + response.id = request.id; + response.result = transform::ToLSPAny(std::vector{}); + return transform::Serialize(response).value_or(BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response")); + } + + auto colors = ScanHexColors(*content_opt); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = transform::ToLSPAny(colors); + + std::optional json = transform::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/document_highlight.cppm b/lsp-server/src/provider/text_document/document_highlight.cppm index 7922174..ff942ed 100644 --- a/lsp-server/src/provider/text_document/document_highlight.cppm +++ b/lsp-server/src/provider/text_document/document_highlight.cppm @@ -1,14 +1,21 @@ module; - export module lsp.provider.text_document.document_highlight; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; + +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -20,24 +27,169 @@ export namespace lsp::provider::text_document DocumentHighlight() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; + + private: + static constexpr const char* kIdentifierNodeType = "identifier"; + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context); + std::optional ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context); + static protocol::Range ToRange(const language::ast::Location& loc); }; } namespace lsp::provider::text_document { - - - - - std::string DocumentHighlight::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::string DocumentHighlight::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentDocumentHighlightProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::DocumentHighlightParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + std::vector highlights; + + auto ident = GetIdentifierAtPosition(params.textDocument.uri, params.position, context); + if (ident) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.textDocument.uri); + + if (table && semantic) + { + if (auto symbol_id = ResolveSymbolId(params.textDocument.uri, ident->text, ident->location, context)) + { + const auto& refs = semantic->references().references(*symbol_id); + highlights.reserve(refs.size() + 1); + + for (const auto& ref : refs) + { + protocol::DocumentHighlight highlight; + highlight.range = ToRange(ref.location); + highlight.kind = ref.is_write ? protocol::DocumentHighlightKind::Write : protocol::DocumentHighlightKind::Read; + highlights.push_back(std::move(highlight)); + } + + if (const auto* symbol = table->definition(*symbol_id)) + { + protocol::DocumentHighlight highlight; + highlight.range = ToRange(symbol->selection_range()); + highlight.kind = protocol::DocumentHighlightKind::Text; + highlights.push_back(std::move(highlight)); + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(highlights); + + auto json = codec::Serialize(response); + if (!json.has_value()) + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + return json.value(); + } + + std::optional DocumentHighlight::GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + return std::nullopt; + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + const char* node_type = ts_node_type(node); + if (std::string_view(node_type) == kIdentifierNodeType) + { + uint32_t start = ts_node_start_byte(node); + uint32_t end = ts_node_end_byte(node); + if (start >= content->size() || end > content->size() || start >= end) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = content->substr(start, end - start); + info.location.start_line = position.line; + info.location.end_line = position.line; + info.location.start_column = position.character; + info.location.end_column = position.character; + info.location.start_offset = static_cast(byte_offset); + info.location.end_offset = static_cast(byte_offset); + return info; + } + node = ts_node_parent(node); + } + + return std::nullopt; + } + + std::optional DocumentHighlight::ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (table && semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(identifier, location); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(identifier); + if (!matches.empty()) + { + return matches.front(); + } + } + + return std::nullopt; + } + + protocol::Range DocumentHighlight::ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; } } diff --git a/lsp-server/src/provider/text_document/document_link.cppm b/lsp-server/src/provider/text_document/document_link.cppm index 2bd2aa0..5d6b90c 100644 --- a/lsp-server/src/provider/text_document/document_link.cppm +++ b/lsp-server/src/provider/text_document/document_link.cppm @@ -2,13 +2,19 @@ module; export module lsp.provider.text_document.document_link; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.manager.symbol; import lsp.provider.base.interface; +import lsp.utils.string; + +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -25,19 +31,405 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace utils = lsp::utils; - + struct RangeKey + { + protocol::uinteger start_line = 0; + protocol::uinteger start_character = 0; + protocol::uinteger end_line = 0; + protocol::uinteger end_character = 0; - std::string DocumentLink::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + bool operator==(const RangeKey& other) const + { + return start_line == other.start_line && + start_character == other.start_character && + end_line == other.end_line && + end_character == other.end_character; + } + }; + + struct RangeKeyHash + { + std::size_t operator()(const RangeKey& key) const + { + std::size_t seed = 0; + auto hash_combine = [&seed](auto value) { + seed ^= std::hash{}(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + }; + hash_combine(key.start_line); + hash_combine(key.start_character); + hash_combine(key.end_line); + hash_combine(key.end_character); + return seed; + } + }; + + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + RangeKey ToRangeKey(const protocol::Range& range) + { + return RangeKey{ + range.start.line, + range.start.character, + range.end.line, + range.end.character, + }; + } + + protocol::Range ToRange(TSNode node) + { + TSPoint start = ts_node_start_point(node); + TSPoint end = ts_node_end_point(node); + + protocol::Range range; + range.start.line = start.row; + range.start.character = start.column; + range.end.line = end.row; + range.end.character = end.column; + return range; + } + + std::string StripQuotes(std::string_view text) + { + if (text.size() >= 2) + { + char front = text.front(); + char back = text.back(); + if ((front == '"' && back == '"') || (front == '\'' && back == '\'')) + { + return std::string(text.substr(1, text.size() - 2)); + } + } + return std::string(text); + } + + bool LooksLikePath(std::string_view text) + { + if (text.starts_with("file://")) + { + return true; + } + + if (text.find('/') != std::string_view::npos || + text.find('\\') != std::string_view::npos) + { + return true; + } + + if (text.ends_with(".tsl") || text.ends_with(".tsf")) + { + return true; + } + + return false; + } + + std::string PathToUri(const std::filesystem::path& path) + { + auto absolute = std::filesystem::absolute(path).generic_string(); +#ifdef _WIN32 + std::replace(absolute.begin(), absolute.end(), '\\', '/'); +#endif + if (!absolute.starts_with("/")) + absolute = "/" + absolute; + return "file://" + absolute; + } + + std::string UriToPath(const std::string& uri) + { + std::string path = uri; + if (path.starts_with("file://")) + path = path.substr(7); + +#ifdef _WIN32 + if (!path.empty() && path[0] == '/') + path = path.substr(1); + std::replace(path.begin(), path.end(), '/', '\\'); +#endif + + std::string decoded; + decoded.reserve(path.size()); + for (size_t i = 0; i < path.size(); ++i) + { + if (path[i] == '%' && i + 2 < path.size()) + { + std::string hex = path.substr(i + 1, 2); + char ch = static_cast(std::stoi(hex, nullptr, 16)); + decoded.push_back(ch); + i += 2; + } + else if (path[i] == '+') + { + decoded.push_back(' '); + } + else + { + decoded.push_back(path[i]); + } + } + return decoded; + } + + std::optional ResolvePathTarget(std::string_view raw, + const protocol::DocumentUri& base_uri) + { + if (raw.empty()) + { + return std::nullopt; + } + + if (raw.starts_with("file://")) + { + return std::string(raw); + } + + std::filesystem::path path(raw); + if (path.is_relative()) + { + auto base_path = UriToPath(base_uri); + std::filesystem::path base_dir = std::filesystem::path(base_path).parent_path(); + path = base_dir / path; + } + + auto try_candidate = [](const std::filesystem::path& candidate) -> std::optional { + if (std::filesystem::exists(candidate)) + { + return PathToUri(candidate); + } + return std::nullopt; + }; + + if (path.has_extension()) + { + if (auto uri = try_candidate(path)) + { + return uri; + } + } + else + { + if (auto uri = try_candidate(path.string() + ".tsl")) + { + return uri; + } + if (auto uri = try_candidate(path.string() + ".tsf")) + { + return uri; + } + } + + return std::nullopt; + } + + std::optional ResolveUnitTarget(const manager::Symbol& symbols, + const std::string& unit_name, + const std::optional& base_dir) + { + auto indexed = symbols.QueryIndexedSymbols(protocol::SymbolKind::Module); + for (const auto& item : indexed) + { + if (utils::IEquals(item.name, unit_name)) + { + return item.uri; + } + } + + if (base_dir) + { + auto candidate = *base_dir / (unit_name + ".tsf"); + if (std::filesystem::exists(candidate)) + { + return PathToUri(candidate); + } + } + + return std::nullopt; + } + + void CollectUsesLinks(TSNode node, + const protocol::string& content, + const manager::Symbol& symbols, + const std::optional& base_dir, + const protocol::DocumentUri& base_uri, + protocol::LSPArray& links, + std::unordered_set& seen) + { + if (ts_node_is_null(node)) + { + return; + } + + if (std::string_view(ts_node_type(node)) == "uses_statement") + { + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; ++i) + { + const char* field = ts_node_field_name_for_child(node, i); + if (!field || std::string_view(field) != "unit") + { + continue; + } + + TSNode unit_node = ts_node_child(node, i); + uint32_t start = ts_node_start_byte(unit_node); + uint32_t end = ts_node_end_byte(unit_node); + if (start >= content.size() || end > content.size() || start >= end) + { + continue; + } + + std::string unit_name = std::string(content.substr(start, end - start)); + auto range = ToRange(unit_node); + auto key = ToRangeKey(range); + if (!seen.insert(key).second) + { + continue; + } + + protocol::LSPObject link; + link["range"] = ToRangeObject(range); + + if (auto target = ResolveUnitTarget(symbols, unit_name, base_dir)) + { + link["target"] = protocol::string(*target); + } + else + { + protocol::LSPObject data; + data["kind"] = protocol::string("unit"); + data["name"] = protocol::string(unit_name); + data["baseUri"] = protocol::string(base_uri); + link["data"] = protocol::LSPAny(std::move(data)); + } + + links.emplace_back(std::move(link)); + } + } + + uint32_t child_count = ts_node_child_count(node); + for (uint32_t i = 0; i < child_count; ++i) + { + CollectUsesLinks(ts_node_child(node, i), content, symbols, base_dir, base_uri, links, seen); + } + } + + void CollectPathLinks(TSNode node, + const protocol::string& content, + const protocol::DocumentUri& base_uri, + protocol::LSPArray& links, + std::unordered_set& seen) + { + if (ts_node_is_null(node)) + { + return; + } + + if (std::string_view(ts_node_type(node)) == "string") + { + uint32_t start = ts_node_start_byte(node); + uint32_t end = ts_node_end_byte(node); + if (start < content.size() && end <= content.size() && start < end) + { + std::string raw_text = std::string(content.substr(start, end - start)); + std::string path_text = StripQuotes(raw_text); + if (LooksLikePath(path_text)) + { + auto range = ToRange(node); + auto key = ToRangeKey(range); + if (seen.insert(key).second) + { + protocol::LSPObject link; + link["range"] = ToRangeObject(range); + + if (auto target = ResolvePathTarget(path_text, base_uri)) + { + link["target"] = protocol::string(*target); + } + else + { + protocol::LSPObject data; + data["kind"] = protocol::string("path"); + data["path"] = protocol::string(path_text); + data["baseUri"] = protocol::string(base_uri); + link["data"] = protocol::LSPAny(std::move(data)); + } + + links.emplace_back(std::move(link)); + } + } + } + } + + uint32_t child_count = ts_node_child_count(node); + for (uint32_t i = 0; i < child_count; ++i) + { + CollectPathLinks(ts_node_child(node, i), content, base_uri, links, seen); + } + } + } + + std::string DocumentLink::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentDocumentLinkProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + auto params = codec::FromLSPAny.template operator()(request.params.value()); + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(params.textDocument.uri); + auto tree = hub.parser().GetTree(params.textDocument.uri); + + protocol::LSPArray links; + std::unordered_set seen; + + if (content && tree) + { + std::optional base_dir; + try + { + auto base_path = UriToPath(params.textDocument.uri); + base_dir = std::filesystem::path(base_path).parent_path(); + } + catch (const std::exception&) + { + base_dir = std::nullopt; + } + + auto root = ts_tree_root_node(tree); + CollectUsesLinks(root, *content, hub.symbols(), base_dir, params.textDocument.uri, links, seen); + CollectPathLinks(root, *content, params.textDocument.uri, links, seen); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(links)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/document_symbol.cppm b/lsp-server/src/provider/text_document/document_symbol.cppm index a56fe2c..1c9b189 100644 --- a/lsp-server/src/provider/text_document/document_symbol.cppm +++ b/lsp-server/src/provider/text_document/document_symbol.cppm @@ -1,6 +1,5 @@ module; - export module lsp.provider.text_document.document_symbol; import spdlog; @@ -8,8 +7,13 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.symbol; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +namespace codec = lsp::codec; + export namespace lsp::provider::text_document { class DocumentSymbol : public AutoRegisterProvider @@ -20,6 +24,17 @@ export namespace lsp::provider::text_document DocumentSymbol() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; + + private: + static protocol::Range ToRange(const language::ast::Location& loc); + static std::string BuildDetail(const language::symbol::Symbol& symbol); + static std::string BuildParameterList(const std::vector& parameters); + static std::optional FindScopeOwnedBy(const language::symbol::SymbolTable& table, + language::symbol::SymbolId owner_id); + static std::vector CollectScopeSymbols(const language::symbol::ScopeInfo& scope_info); + static std::vector BuildScopeSymbols(const language::symbol::SymbolTable& table, + const language::symbol::ScopeInfo& scope_info, + bool include_children); }; } @@ -29,15 +44,263 @@ namespace lsp::provider::text_document - std::string DocumentSymbol::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::string DocumentSymbol::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentDocumentSymbolProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Invalid params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Invalid params"); + } - return "{}"; // Placeholder response + const auto& params = request.params->Get(); + auto text_document_it = params.find("textDocument"); + if (text_document_it == params.end() || !text_document_it->second.Is()) + { + spdlog::warn("{}: Missing textDocument in params", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing textDocument"); + } + + const auto& text_document = text_document_it->second.Get(); + auto uri_it = text_document.find("uri"); + if (uri_it == text_document.end() || !uri_it->second.Is()) + { + spdlog::warn("{}: Missing uri in textDocument", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing textDocument.uri"); + } + + const auto& uri = uri_it->second.Get(); + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + + std::vector symbols; + if (table) + { + auto global_scope_id = table->scopes().global_scope(); + const auto& all_scopes = table->scopes().all_scopes(); + auto it = all_scopes.find(global_scope_id); + if (it != all_scopes.end()) + { + symbols = BuildScopeSymbols(*table, it->second, true); + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(symbols); + + auto json = codec::Serialize(response); + if (!json.has_value()) + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + return json.value(); + } + + protocol::Range DocumentSymbol::ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + std::string DocumentSymbol::BuildDetail(const language::symbol::Symbol& symbol) + { + using namespace language::symbol; + + if (symbol.Is()) + { + const auto* fn = symbol.As(); + std::string out = "(" + BuildParameterList(fn->parameters) + ")"; + if (fn->return_type && !fn->return_type->empty()) + { + out += ": " + *fn->return_type; + } + return out; + } + + if (symbol.Is()) + { + const auto* method = symbol.As(); + std::string out; + if (method->is_static) + { + out += "class "; + } + out += "(" + BuildParameterList(method->parameters) + ")"; + if (method->return_type && !method->return_type->empty()) + { + out += ": " + *method->return_type; + } + return out; + } + + if (symbol.Is()) + { + const auto* prop = symbol.As(); + if (prop->type && !prop->type->empty()) + { + return *prop->type; + } + return "property"; + } + + if (symbol.Is()) + { + const auto* field = symbol.As(); + if (field->type && !field->type->empty()) + { + return *field->type; + } + return "field"; + } + + if (symbol.Is()) + { + const auto* var = symbol.As(); + if (var->type && !var->type->empty()) + { + return *var->type; + } + return "var"; + } + + if (symbol.Is()) + { + const auto* constant = symbol.As(); + if (!constant->value.empty()) + { + return constant->value; + } + return "const"; + } + + return {}; + } + + std::string DocumentSymbol::BuildParameterList(const std::vector& parameters) + { + if (parameters.empty()) + { + return ""; + } + + std::string out; + for (std::size_t i = 0; i < parameters.size(); ++i) + { + if (i > 0) + { + out += "; "; + } + const auto& param = parameters[i]; + out += param.name; + if (param.type && !param.type->empty()) + { + out += ": " + *param.type; + } + if (param.default_value && !param.default_value->empty()) + { + out += " = " + *param.default_value; + } + } + + return out; + } + + std::optional DocumentSymbol::FindScopeOwnedBy(const language::symbol::SymbolTable& table, + language::symbol::SymbolId owner_id) + { + const auto& all_scopes = table.scopes().all_scopes(); + for (const auto& [scope_id, scope_info] : all_scopes) + { + if (scope_info.owner && *scope_info.owner == owner_id) + { + return scope_id; + } + } + return std::nullopt; + } + + std::vector DocumentSymbol::CollectScopeSymbols(const language::symbol::ScopeInfo& scope_info) + { + std::vector ids; + for (const auto& [_, symbols] : scope_info.symbols) + { + ids.insert(ids.end(), symbols.begin(), symbols.end()); + } + + std::sort(ids.begin(), ids.end()); + ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); + return ids; + } + + std::vector DocumentSymbol::BuildScopeSymbols(const language::symbol::SymbolTable& table, + const language::symbol::ScopeInfo& scope_info, + bool include_children) + { + std::vector result; + auto ids = CollectScopeSymbols(scope_info); + result.reserve(ids.size()); + + struct WithOffset + { + std::uint32_t start_offset; + protocol::DocumentSymbol symbol; + }; + std::vector sortable; + sortable.reserve(ids.size()); + + for (auto id : ids) + { + const auto* symbol = table.definition(id); + if (!symbol) + { + continue; + } + + protocol::DocumentSymbol doc_symbol; + doc_symbol.name = symbol->name(); + auto detail = BuildDetail(*symbol); + if (!detail.empty()) + { + doc_symbol.detail = std::move(detail); + } + doc_symbol.kind = symbol->kind(); + doc_symbol.range = ToRange(symbol->range()); + doc_symbol.selectionRange = ToRange(symbol->selection_range()); + + if (include_children && + (symbol->kind() == protocol::SymbolKind::Module || symbol->kind() == protocol::SymbolKind::Class)) + { + if (auto child_scope_id = FindScopeOwnedBy(table, id)) + { + const auto* child_scope = table.scopes().scope(*child_scope_id); + if (child_scope) + { + auto children = BuildScopeSymbols(table, *child_scope, true); + if (!children.empty()) + { + doc_symbol.children = std::move(children); + } + } + } + } + + sortable.push_back({ symbol->selection_range().start_offset, std::move(doc_symbol) }); + } + + std::sort(sortable.begin(), sortable.end(), [](const WithOffset& a, const WithOffset& b) { + return a.start_offset < b.start_offset; + }); + + result.reserve(sortable.size()); + for (auto& item : sortable) + { + result.push_back(std::move(item.symbol)); + } + + return result; } } diff --git a/lsp-server/src/provider/text_document/folding_range.cppm b/lsp-server/src/provider/text_document/folding_range.cppm index 6d812d7..d76167f 100644 --- a/lsp-server/src/provider/text_document/folding_range.cppm +++ b/lsp-server/src/provider/text_document/folding_range.cppm @@ -2,14 +2,19 @@ module; export module lsp.provider.text_document.folding_range; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +namespace codec = lsp::codec; + export namespace lsp::provider::text_document { class FoldingRange : public AutoRegisterProvider @@ -25,19 +30,154 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + struct FoldingEntry + { + protocol::uinteger start_line = 0; + protocol::uinteger end_line = 0; + std::optional kind; + }; - + std::optional ResolveKind(TSNode node) + { + if (language::ast::ts_utils::IsComment(node)) + { + return std::string(protocol::FoldingRangeKindLiterals::Comment); + } - std::string FoldingRange::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::string_view type = ts_node_type(node); + if (type == "uses_statement") + { + return std::string(protocol::FoldingRangeKindLiterals::Imports); + } + + return std::nullopt; + } + + std::optional ToFoldingEntry(TSNode node) + { + if (!ts_node_is_named(node)) + { + return std::nullopt; + } + + std::string_view type = ts_node_type(node); + if (type == "program") + { + return std::nullopt; + } + + TSPoint start = ts_node_start_point(node); + TSPoint end = ts_node_end_point(node); + + if (end.row <= start.row) + { + return std::nullopt; + } + + protocol::uinteger start_line = start.row; + protocol::uinteger end_line = end.row; + if (end.column == 0 && end_line > start_line) + { + end_line -= 1; + } + + if (end_line <= start_line) + { + return std::nullopt; + } + + FoldingEntry entry; + entry.start_line = start_line; + entry.end_line = end_line; + entry.kind = ResolveKind(node); + return entry; + } + + void CollectRanges(TSNode node, std::vector& ranges) + { + if (ts_node_is_null(node)) + { + return; + } + + if (auto entry = ToFoldingEntry(node)) + { + ranges.push_back(*entry); + } + + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; ++i) + { + CollectRanges(ts_node_child(node, i), ranges); + } + } + + bool IsSameRange(const FoldingEntry& lhs, const FoldingEntry& rhs) + { + return lhs.start_line == rhs.start_line && + lhs.end_line == rhs.end_line && + lhs.kind == rhs.kind; + } + } + + std::string FoldingRange::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentFoldingRangeProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + auto params = codec::FromLSPAny.template operator()(request.params.value()); + auto& hub = context.GetManagerHub(); + auto tree = hub.parser().GetTree(params.textDocument.uri); + + std::vector entries; + if (tree) + { + CollectRanges(ts_tree_root_node(tree), entries); + } + + std::sort(entries.begin(), entries.end(), [](const FoldingEntry& lhs, const FoldingEntry& rhs) { + if (lhs.start_line != rhs.start_line) + { + return lhs.start_line < rhs.start_line; + } + if (lhs.end_line != rhs.end_line) + { + return lhs.end_line < rhs.end_line; + } + return lhs.kind < rhs.kind; + }); + entries.erase(std::unique(entries.begin(), entries.end(), IsSameRange), entries.end()); + + protocol::LSPArray result; + result.reserve(entries.size()); + for (const auto& entry : entries) + { + protocol::LSPObject obj; + obj["startLine"] = static_cast(entry.start_line); + obj["endLine"] = static_cast(entry.end_line); + if (entry.kind) + { + obj["kind"] = protocol::string(*entry.kind); + } + result.emplace_back(std::move(obj)); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/formatting.cppm b/lsp-server/src/provider/text_document/formatting.cppm index 35f625d..2aac93a 100644 --- a/lsp-server/src/provider/text_document/formatting.cppm +++ b/lsp-server/src/provider/text_document/formatting.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::text_document @@ -25,19 +26,183 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; - + protocol::Range FullDocumentRange(const protocol::string& content) + { + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + + protocol::uinteger line_count = 0; + protocol::uinteger last_line_len = 0; + for (char ch : content) + { + if (ch == '\n') + { + line_count++; + last_line_len = 0; + } + else + { + last_line_len++; + } + } + + range.end.line = line_count; + range.end.character = last_line_len; + return range; + } + + std::string_view DetectNewlineStyle(std::string_view content) + { + if (content.find("\r\n") != std::string_view::npos) + { + return "\r\n"; + } + return "\n"; + } + + std::string TrimTrailingWhitespace(std::string_view content) + { + std::string out; + out.reserve(content.size()); + + std::size_t cursor = 0; + while (cursor < content.size()) + { + auto newline_pos = content.find('\n', cursor); + bool has_newline = newline_pos != std::string_view::npos; + std::size_t line_end = has_newline ? newline_pos : content.size(); + + std::string_view newline_suffix; + if (has_newline && line_end > cursor && content[line_end - 1] == '\r') + { + newline_suffix = "\r\n"; + line_end -= 1; + } + else if (has_newline) + { + newline_suffix = "\n"; + } + else + { + newline_suffix = ""; + } + + std::size_t trim_end = line_end; + while (trim_end > cursor) + { + char ch = content[trim_end - 1]; + if (ch != ' ' && ch != '\t') + { + break; + } + trim_end--; + } + + out.append(content.substr(cursor, trim_end - cursor)); + out.append(newline_suffix); + + if (!has_newline) + { + break; + } + + cursor = newline_pos + 1; + } + + return out; + } + + void TrimFinalNewlines(std::string& text) + { + while (!text.empty()) + { + if (text.size() >= 2 && text.ends_with("\r\n")) + { + text.resize(text.size() - 2); + continue; + } + + if (text.back() == '\n') + { + text.pop_back(); + continue; + } + + break; + } + } + + std::string FormatDocumentText(std::string_view content, + const protocol::FormattingOptions& options) + { + const bool trim_trailing = options.trimTrailingWhitespace.value_or(true); + const bool trim_final_newlines = options.trimFinalNewlines.value_or(false); + const bool insert_final_newline = options.insertFinalNewline.value_or(false); + const auto newline = DetectNewlineStyle(content); + + std::string formatted = trim_trailing ? TrimTrailingWhitespace(content) : std::string(content); + + if (trim_final_newlines) + { + TrimFinalNewlines(formatted); + } + + if (insert_final_newline) + { + if (!formatted.ends_with(newline)) + { + formatted.append(newline); + } + } + + return formatted; + } + } std::string Formatting::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentFormattingProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::DocumentFormattingParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(params.textDocument.uri); + + protocol::LSPArray edits; + if (content) + { + std::string formatted = FormatDocumentText(*content, params.options); + if (formatted != *content) + { + protocol::LSPObject edit; + edit["range"] = codec::ToLSPAny(FullDocumentRange(*content)); + edit["newText"] = protocol::string(std::move(formatted)); + edits.emplace_back(std::move(edit)); + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(edits)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/hover.cppm b/lsp-server/src/provider/text_document/hover.cppm index aa45de4..9796595 100644 --- a/lsp-server/src/provider/text_document/hover.cppm +++ b/lsp-server/src/provider/text_document/hover.cppm @@ -1,14 +1,21 @@ module; - export module lsp.provider.text_document.hover; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; + +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -20,24 +27,283 @@ export namespace lsp::provider::text_document Hover() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; + + private: + static constexpr const char* kIdentifierNodeType = "identifier"; + + struct IdentifierInfo + { + std::string text; + protocol::Range range; + language::ast::Location location; + }; + + std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context); + + std::optional ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context); + + static std::string BuildHoverText(const language::symbol::Symbol& symbol); + static std::string BuildSignature(const language::symbol::Symbol& symbol); + static std::string BuildParameterList(const std::vector& parameters); }; } namespace lsp::provider::text_document { - - - - std::string Hover::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentHoverProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::HoverParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + std::optional hover; + + auto ident = GetIdentifierAtPosition(params.textDocument.uri, params.position, context); + if (ident) + { + if (auto symbol_id = ResolveSymbolId(params.textDocument.uri, ident->text, ident->location, context)) + { + auto& hub = context.GetManagerHub(); + if (const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri)) + { + if (const auto* symbol = table->definition(*symbol_id)) + { + protocol::Hover result; + result.contents.kind = protocol::MarkupKindLiterals::Markdown; + result.contents.value = BuildHoverText(*symbol); + result.range = ident->range; + hover = std::move(result); + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(hover); + + auto json = codec::Serialize(response); + if (!json.has_value()) + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + return json.value(); + } + + std::optional Hover::GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + const char* node_type = ts_node_type(node); + if (std::string_view(node_type) == kIdentifierNodeType) + { + uint32_t start = ts_node_start_byte(node); + uint32_t end = ts_node_end_byte(node); + if (start >= content->size() || end > content->size() || start >= end) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = content->substr(start, end - start); + + TSPoint start_point = ts_node_start_point(node); + TSPoint end_point = ts_node_end_point(node); + info.range.start.line = start_point.row; + info.range.start.character = start_point.column; + info.range.end.line = end_point.row; + info.range.end.character = end_point.column; + + info.location.start_line = position.line; + info.location.end_line = position.line; + info.location.start_column = position.character; + info.location.end_column = position.character; + info.location.start_offset = static_cast(byte_offset); + info.location.end_offset = static_cast(byte_offset); + return info; + } + node = ts_node_parent(node); + } + + return std::nullopt; + } + + std::optional Hover::ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (table && semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(identifier, location); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(identifier); + if (!matches.empty()) + { + return matches.front(); + } + } + + return std::nullopt; + } + + std::string Hover::BuildHoverText(const language::symbol::Symbol& symbol) + { + std::string text = "```tsl\n"; + text += BuildSignature(symbol); + text += "\n```"; + return text; + } + + std::string Hover::BuildSignature(const language::symbol::Symbol& symbol) + { + using namespace language::symbol; + + if (symbol.Is()) + { + const auto* fn = symbol.As(); + std::string out = "function " + fn->name + "(" + BuildParameterList(fn->parameters) + ")"; + if (fn->return_type && !fn->return_type->empty()) + { + out += ": " + *fn->return_type; + } + return out; + } + + if (symbol.Is()) + { + const auto* method = symbol.As(); + std::string out; + if (method->is_static) + { + out += "class "; + } + out += "function " + method->name + "(" + BuildParameterList(method->parameters) + ")"; + if (method->return_type && !method->return_type->empty()) + { + out += ": " + *method->return_type; + } + return out; + } + + if (symbol.Is()) + { + const auto* cls = symbol.As(); + return "type " + cls->name + " = class"; + } + + if (symbol.Is()) + { + const auto* unit = symbol.As(); + return "unit " + unit->name; + } + + if (symbol.Is()) + { + const auto* prop = symbol.As(); + std::string out = "property " + prop->name; + if (prop->type && !prop->type->empty()) + { + out += ": " + *prop->type; + } + return out; + } + + if (symbol.Is()) + { + const auto* field = symbol.As(); + std::string out = field->name; + if (field->type && !field->type->empty()) + { + out += ": " + *field->type; + } + return out; + } + + if (symbol.Is()) + { + const auto* var = symbol.As(); + std::string out = "var " + var->name; + if (var->type && !var->type->empty()) + { + out += ": " + *var->type; + } + return out; + } + + if (symbol.Is()) + { + const auto* constant = symbol.As(); + return "const " + constant->name + " = " + constant->value; + } + + return symbol.name(); + } + + std::string Hover::BuildParameterList(const std::vector& parameters) + { + if (parameters.empty()) + { + return ""; + } + + std::string out; + for (std::size_t i = 0; i < parameters.size(); ++i) + { + if (i > 0) + { + out += "; "; + } + + const auto& param = parameters[i]; + out += param.name; + if (param.type && !param.type->empty()) + { + out += ": " + *param.type; + } + if (param.default_value && !param.default_value->empty()) + { + out += " = " + *param.default_value; + } + } + return out; } } diff --git a/lsp-server/src/provider/text_document/implementation.cppm b/lsp-server/src/provider/text_document/implementation.cppm index 8b417da..1d33f2d 100644 --- a/lsp-server/src/provider/text_document/implementation.cppm +++ b/lsp-server/src/provider/text_document/implementation.cppm @@ -1,14 +1,21 @@ module; - export module lsp.provider.text_document.implementation; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; + +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -20,24 +27,184 @@ export namespace lsp::provider::text_document Implementation() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; + + private: + static constexpr const char* kIdentifierNodeType = "identifier"; + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + static std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context); + + static std::optional ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context); + + static protocol::Range ToRange(const language::ast::Location& loc); }; } namespace lsp::provider::text_document { - - - - std::string Implementation::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentImplementationProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + const auto position_params = + codec::FromLSPAny.template operator()(request.params.value()); + + std::optional location; + + auto ident = GetIdentifierAtPosition(position_params.textDocument.uri, position_params.position, context); + if (ident) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(position_params.textDocument.uri); + const auto* semantic = hub.symbols().GetSemanticModel(position_params.textDocument.uri); + + if (table && semantic) + { + if (auto symbol_id = ResolveSymbolId(position_params.textDocument.uri, ident->text, ident->location, context)) + { + if (const auto* symbol = table->definition(*symbol_id)) + { + if (symbol->Is()) + { + const auto* fn = symbol->As(); + if (fn->implementation_range) + { + protocol::Location result; + result.uri = position_params.textDocument.uri; + result.range = ToRange(*fn->implementation_range); + location = std::move(result); + } + } + else if (symbol->Is()) + { + const auto* method = symbol->As(); + if (method->implementation_range) + { + protocol::Location result; + result.uri = position_params.textDocument.uri; + result.range = ToRange(*method->implementation_range); + location = std::move(result); + } + } + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(location); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); + } + + std::optional Implementation::GetIdentifierAtPosition( + const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + if (std::string_view(ts_node_type(node)) == kIdentifierNodeType) + { + uint32_t start = ts_node_start_byte(node); + uint32_t end = ts_node_end_byte(node); + if (start >= content->size() || end > content->size() || start >= end) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = content->substr(start, end - start); + info.location.start_line = position.line; + info.location.end_line = position.line; + info.location.start_column = position.character; + info.location.end_column = position.character; + info.location.start_offset = static_cast(byte_offset); + info.location.end_offset = static_cast(byte_offset); + return info; + } + + node = ts_node_parent(node); + } + + return std::nullopt; + } + + std::optional Implementation::ResolveSymbolId( + const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (table && semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(identifier, location); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(identifier); + if (!matches.empty()) + { + return matches.front(); + } + } + + return std::nullopt; + } + + protocol::Range Implementation::ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; } } diff --git a/lsp-server/src/provider/text_document/inlay_hint.cppm b/lsp-server/src/provider/text_document/inlay_hint.cppm index 7477426..d07ce4e 100644 --- a/lsp-server/src/provider/text_document/inlay_hint.cppm +++ b/lsp-server/src/provider/text_document/inlay_hint.cppm @@ -2,6 +2,7 @@ module; export module lsp.provider.text_document.inlay_hint; +import tree_sitter; import spdlog; import std; @@ -9,6 +10,11 @@ import std; import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; +import lsp.utils.string; export namespace lsp::provider::text_document { @@ -25,19 +31,796 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; - + static constexpr std::string_view kCallExpressionNode = "call_expression"; + static constexpr std::string_view kVarDeclarationNode = "var_declaration"; + static constexpr std::string_view kAttributeExpressionNode = "attribute_expression"; + static constexpr std::string_view kIdentifierNode = "identifier"; + static constexpr std::string_view kArgumentNode = "argument"; + static constexpr std::string_view kNamedArgumentNode = "named_argument"; + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + struct HintContext + { + const protocol::DocumentUri& uri; + const std::string& content; + const protocol::Range& range; + const language::semantic::SemanticModel* semantic = nullptr; + const language::symbol::SymbolTable* table = nullptr; + }; + + std::optional GetUInteger(const protocol::LSPAny& any) + { + if (any.Is()) + { + return any.Get(); + } + if (any.Is()) + { + return static_cast(any.Get()); + } + return std::nullopt; + } + + std::optional ParsePosition(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto line_it = obj.find("line"); + auto character_it = obj.find("character"); + if (line_it == obj.end() || character_it == obj.end()) + { + return std::nullopt; + } + + auto line = GetUInteger(line_it->second); + auto character = GetUInteger(character_it->second); + if (!line || !character) + { + return std::nullopt; + } + + protocol::Position position; + position.line = *line; + position.character = *character; + return position; + } + + std::optional ParseRange(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto start_it = obj.find("start"); + auto end_it = obj.find("end"); + if (start_it == obj.end() || end_it == obj.end()) + { + return std::nullopt; + } + + auto start = ParsePosition(start_it->second); + auto end = ParsePosition(end_it->second); + if (!start || !end) + { + return std::nullopt; + } + + protocol::Range range; + range.start = *start; + range.end = *end; + return range; + } + + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + bool IsPositionBefore(const protocol::Position& lhs, const protocol::Position& rhs) + { + if (lhs.line < rhs.line) + { + return true; + } + if (lhs.line > rhs.line) + { + return false; + } + return lhs.character < rhs.character; + } + + bool IsPositionAfter(const protocol::Position& lhs, const protocol::Position& rhs) + { + if (lhs.line > rhs.line) + { + return true; + } + if (lhs.line < rhs.line) + { + return false; + } + return lhs.character > rhs.character; + } + + bool IsPositionInRange(const protocol::Position& position, const protocol::Range& range) + { + if (IsPositionBefore(position, range.start)) + { + return false; + } + if (IsPositionAfter(position, range.end)) + { + return false; + } + return true; + } + + bool NodeIntersectsRange(TSNode node, const protocol::Range& range) + { + if (ts_node_is_null(node)) + { + return false; + } + + TSPoint start = ts_node_start_point(node); + TSPoint end = ts_node_end_point(node); + + protocol::Position start_pos{ start.row, start.column }; + protocol::Position end_pos{ end.row, end.column }; + + if (IsPositionAfter(start_pos, range.end)) + { + return false; + } + if (IsPositionBefore(end_pos, range.start)) + { + return false; + } + return true; + } + + std::optional GetIdentifierFromNode(TSNode node, std::string_view content) + { + if (ts_node_is_null(node)) + { + return std::nullopt; + } + + if (std::string_view(ts_node_type(node)) != kIdentifierNode) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = language::ast::ts_utils::Text(node, content); + info.location = language::ast::ts_utils::NodeLocation(node); + if (info.text.empty()) + { + return std::nullopt; + } + return info; + } + + std::optional FindIdentifierInNode(TSNode node, std::string_view content) + { + if (ts_node_is_null(node)) + { + return std::nullopt; + } + + if (auto ident = GetIdentifierFromNode(node, content)) + { + return ident; + } + + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; ++i) + { + auto child = ts_node_child(node, i); + if (!ts_node_is_named(child)) + { + continue; + } + if (auto ident = FindIdentifierInNode(child, content)) + { + return ident; + } + } + return std::nullopt; + } + + std::optional ResolveCallTarget(TSNode callee_node, const HintContext& ctx) + { + if (ts_node_is_null(callee_node)) + { + return std::nullopt; + } + + if (!ctx.table) + { + return std::nullopt; + } + + auto is_callable = [table = ctx.table](language::symbol::SymbolId symbol_id) -> bool { + const auto* symbol = table->definition(symbol_id); + if (!symbol) + { + return false; + } + return symbol->Is() || symbol->Is(); + }; + + auto find_callable_by_name = [table = ctx.table](std::string_view name) -> std::optional { + auto matches = table->FindSymbolsByName(std::string(name)); + for (auto id : matches) + { + if (const auto* symbol = table->definition(id)) + { + if (symbol->Is() || symbol->Is()) + { + return id; + } + } + } + if (!name.empty()) + { + std::string name_value(name); + for (const auto& wrapper : table->all_definitions()) + { + const auto& symbol = wrapper.get(); + if (!utils::IEquals(symbol.name(), name_value)) + { + continue; + } + if (symbol.Is() || symbol.Is()) + { + return symbol.id(); + } + } + } + return std::nullopt; + }; + + if (std::string_view(ts_node_type(callee_node)) == kIdentifierNode) + { + auto ident = GetIdentifierFromNode(callee_node, ctx.content); + if (!ident) + { + return std::nullopt; + } + + if (ctx.semantic) + { + auto result = ctx.semantic->name_resolver().ResolveNameAtLocation(ident->text, ident->location); + if (result.IsResolved()) + { + if (is_callable(result.symbol_id)) + { + return result.symbol_id; + } + } + } + + if (auto callable = find_callable_by_name(ident->text)) + { + return callable; + } + return std::nullopt; + } + + if (std::string_view(ts_node_type(callee_node)) == kAttributeExpressionNode) + { + TSNode object_node = ts_node_child_by_field_name(callee_node, "object", 6); + TSNode attribute_node = ts_node_child_by_field_name(callee_node, "attribute", 9); + + auto object_ident = GetIdentifierFromNode(object_node, ctx.content); + auto member_ident = FindIdentifierInNode(attribute_node, ctx.content); + if (!object_ident || !member_ident) + { + return std::nullopt; + } + + if (ctx.semantic) + { + auto resolved = ctx.semantic->name_resolver().ResolveNameAtLocation(object_ident->text, object_ident->location); + if (resolved.IsResolved()) + { + auto member = ctx.semantic->name_resolver().ResolveMemberAccess(resolved.symbol_id, member_ident->text); + if (member.IsResolved() && is_callable(member.symbol_id)) + { + return member.symbol_id; + } + } + } + + if (auto callable = find_callable_by_name(member_ident->text)) + { + return callable; + } + return std::nullopt; + } + + if (auto ident = FindIdentifierInNode(callee_node, ctx.content)) + { + if (ctx.semantic) + { + auto result = ctx.semantic->name_resolver().ResolveNameAtLocation(ident->text, ident->location); + if (result.IsResolved() && is_callable(result.symbol_id)) + { + return result.symbol_id; + } + } + + if (auto callable = find_callable_by_name(ident->text)) + { + return callable; + } + } + + return std::nullopt; + } + + const std::vector* GetParametersForSymbol(const language::symbol::Symbol& symbol) + { + using namespace language::symbol; + + if (symbol.Is()) + { + return &symbol.As()->parameters; + } + if (symbol.Is()) + { + return &symbol.As()->parameters; + } + return nullptr; + } + + std::string BuildParameterDetail(const language::symbol::Parameter& parameter) + { + std::string label = parameter.name; + if (parameter.type && !parameter.type->empty()) + { + label += ": " + *parameter.type; + } + if (parameter.default_value && !parameter.default_value->empty()) + { + label += " = " + *parameter.default_value; + } + return label; + } + + std::string FormatType(const language::semantic::Type& type, const language::symbol::SymbolTable& table) + { + using namespace language::semantic; + + return std::visit( + [&table](const auto& type_data) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + return type_data.ToString(); + } + else if constexpr (std::is_same_v) + { + if (const auto* symbol = table.definition(type_data.class_id())) + { + if (!symbol->name().empty()) + { + return symbol->name(); + } + } + return "class#" + std::to_string(type_data.class_id()); + } + else if constexpr (std::is_same_v) + { + return "array<" + FormatType(type_data.element_type(), table) + ">"; + } + else if constexpr (std::is_same_v) + { + std::string out = "function("; + const auto& params = type_data.param_types(); + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i > 0) + { + out += ", "; + } + out += FormatType(*params[i], table); + } + out += ") -> " + FormatType(type_data.return_type(), table); + return out; + } + else if constexpr (std::is_same_v) + { + return FormatType(type_data.inner_type(), table) + "?"; + } + else if constexpr (std::is_same_v) + { + return "void"; + } + else if constexpr (std::is_same_v) + { + return "unknown"; + } + else if constexpr (std::is_same_v) + { + return "error"; + } + return "unknown"; + }, + type.data()); + } + + std::optional ResolveTypeLabel(language::symbol::SymbolId symbol_id, const HintContext& ctx) + { + if (!ctx.table) + { + return std::nullopt; + } + + if (ctx.semantic) + { + auto type = ctx.semantic->GetSymbolType(symbol_id); + if (type && type->kind() != language::semantic::TypeKind::kUnknown && + type->kind() != language::semantic::TypeKind::kError) + { + auto label = FormatType(*type, *ctx.table); + if (!label.empty() && label != "unknown" && label != "error") + { + return label; + } + } + } + + const auto* symbol = ctx.table->definition(symbol_id); + if (!symbol) + { + return std::nullopt; + } + + if (symbol->Is()) + { + const auto* var = symbol->As(); + if (var->type && !var->type->empty()) + { + return *var->type; + } + } + + if (symbol->Is()) + { + const auto* field = symbol->As(); + if (field->type && !field->type->empty()) + { + return *field->type; + } + } + + return std::nullopt; + } + + protocol::Position ToPosition(TSPoint point) + { + protocol::Position position; + position.line = point.row; + position.character = point.column; + return position; + } + + std::string MakeHintKey(const protocol::Position& position, std::string_view label, protocol::integer kind) + { + return std::to_string(position.line) + ":" + + std::to_string(position.character) + ":" + + std::to_string(kind) + ":" + + std::string(label); + } + + void AddHint(protocol::LSPArray& hints, + std::unordered_set& seen, + const protocol::Position& position, + std::string_view label, + protocol::InlayHintKind kind, + std::optional detail = std::nullopt) + { + if (label.empty()) + { + return; + } + + auto kind_value = static_cast(kind); + auto key = MakeHintKey(position, label, kind_value); + if (!seen.insert(key).second) + { + return; + } + + protocol::LSPObject hint; + hint["position"] = ToPositionObject(position); + hint["label"] = protocol::string(label); + hint["kind"] = kind_value; + + if (detail && !detail->empty()) + { + protocol::LSPObject data; + data["detail"] = protocol::string(*detail); + hint["data"] = protocol::LSPAny(std::move(data)); + } + + hints.emplace_back(std::move(hint)); + } + + void CollectCallHints(TSNode node, + const HintContext& ctx, + protocol::LSPArray& hints, + std::unordered_set& seen) + { + if (!ctx.table) + { + return; + } + + TSNode callee_node = ts_node_child_by_field_name(node, "callee", 6); + auto symbol_id = ResolveCallTarget(callee_node, ctx); + if (!symbol_id) + { + return; + } + + const auto* symbol = ctx.table->definition(*symbol_id); + if (!symbol) + { + return; + } + + const auto* parameters = GetParametersForSymbol(*symbol); + if (!parameters || parameters->empty()) + { + return; + } + + std::vector arg_nodes; + uint32_t child_count = ts_node_child_count(node); + for (uint32_t i = 0; i < child_count; ++i) + { + const char* field = ts_node_field_name_for_child(node, i); + if (!field || std::string_view(field) != "argument") + { + continue; + } + + TSNode child = ts_node_child(node, i); + if (std::string_view(ts_node_type(child)) == kArgumentNode) + { + arg_nodes.push_back(child); + } + } + + std::size_t param_index = 0; + for (const auto& arg_node : arg_nodes) + { + if (param_index >= parameters->size()) + { + break; + } + + TSNode inner = ts_node_child(arg_node, 0); + if (!ts_node_is_null(inner) && std::string_view(ts_node_type(inner)) == kNamedArgumentNode) + { + ++param_index; + continue; + } + + auto& parameter = (*parameters)[param_index]; + protocol::Position position = ToPosition(ts_node_start_point(arg_node)); + + if (IsPositionInRange(position, ctx.range)) + { + std::string label = parameter.name + ":"; + auto detail = BuildParameterDetail(parameter); + AddHint(hints, + seen, + position, + label, + protocol::InlayHintKind::Parameter, + detail.empty() ? std::nullopt : std::optional(detail)); + } + + ++param_index; + } + } + + void CollectVarHints(TSNode node, + const HintContext& ctx, + protocol::LSPArray& hints, + std::unordered_set& seen) + { + if (!ctx.table) + { + return; + } + + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) + { + return; + } + + TSNode init_node = ts_node_child_by_field_name(node, "initializer", 11); + if (ts_node_is_null(init_node)) + { + init_node = ts_node_child_by_field_name(node, "value", 5); + } + if (ts_node_is_null(init_node)) + { + return; + } + + std::vector names; + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; ++i) + { + const char* field = ts_node_field_name_for_child(node, i); + if (!field || std::string_view(field) != "name") + { + continue; + } + + names.push_back(ts_node_child(node, i)); + } + if (names.empty()) + { + return; + } + + TSNode name_node = names.back(); + auto location = language::ast::ts_utils::NodeLocation(name_node); + auto symbol_id = ctx.table->FindSymbolAt(location); + if (!symbol_id) + { + return; + } + + auto type_label = ResolveTypeLabel(*symbol_id, ctx); + if (!type_label || type_label->empty()) + { + return; + } + + protocol::Position position = ToPosition(ts_node_end_point(name_node)); + if (!IsPositionInRange(position, ctx.range)) + { + return; + } + + std::string label = ": " + *type_label; + AddHint(hints, + seen, + position, + label, + protocol::InlayHintKind::Type, + *type_label); + } + + void CollectHints(TSNode node, + const HintContext& ctx, + protocol::LSPArray& hints, + std::unordered_set& seen) + { + if (ts_node_is_null(node)) + { + return; + } + + if (!NodeIntersectsRange(node, ctx.range)) + { + return; + } + + std::string_view type = ts_node_type(node); + if (type == kCallExpressionNode) + { + CollectCallHints(node, ctx, hints, seen); + } + else if (type == kVarDeclarationNode) + { + CollectVarHints(node, ctx, hints, seen); + } + + uint32_t child_count = ts_node_child_count(node); + for (uint32_t i = 0; i < child_count; ++i) + { + CollectHints(ts_node_child(node, i), ctx, hints, seen); + } + } + } // namespace std::string InlayHint::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentInlayHintProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Invalid params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Invalid params"); + } - return "{}"; // Placeholder response + const auto& params = request.params->Get(); + auto text_document_it = params.find("textDocument"); + if (text_document_it == params.end() || !text_document_it->second.Is()) + { + spdlog::warn("{}: Missing textDocument in params", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing textDocument"); + } + + const auto& text_document = text_document_it->second.Get(); + auto uri_it = text_document.find("uri"); + if (uri_it == text_document.end() || !uri_it->second.Is()) + { + spdlog::warn("{}: Missing uri in textDocument", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing textDocument.uri"); + } + + auto range_it = params.find("range"); + if (range_it == params.end()) + { + spdlog::warn("{}: Missing range in params", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing range"); + } + + auto range = ParseRange(range_it->second); + if (!range) + { + spdlog::warn("{}: Invalid range in params", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Invalid range"); + } + + const auto& uri = uri_it->second.Get(); + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + protocol::LSPArray result; + if (content && tree) + { + HintContext hint_context{ + .uri = uri, + .content = *content, + .range = *range, + .semantic = hub.symbols().GetSemanticModel(uri), + .table = hub.symbols().GetSymbolTable(uri), + }; + + std::unordered_set seen; + CollectHints(ts_tree_root_node(tree), hint_context, result, seen); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/inline_value.cppm b/lsp-server/src/provider/text_document/inline_value.cppm index 3f14bbf..e8d0cf9 100644 --- a/lsp-server/src/provider/text_document/inline_value.cppm +++ b/lsp-server/src/provider/text_document/inline_value.cppm @@ -25,19 +25,34 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - - - + namespace + { + namespace codec = lsp::codec; + } std::string InlineValue::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentInlineValueProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + [[maybe_unused]] auto params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(protocol::LSPArray{}); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/linked_editing_range.cppm b/lsp-server/src/provider/text_document/linked_editing_range.cppm index 538f170..3dd1a8a 100644 --- a/lsp-server/src/provider/text_document/linked_editing_range.cppm +++ b/lsp-server/src/provider/text_document/linked_editing_range.cppm @@ -1,7 +1,8 @@ module; - export module lsp.provider.text_document.linked_editing_range; + +import tree_sitter; import spdlog; import std; @@ -9,6 +10,13 @@ import std; import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; +import lsp.utils.text_coordinates; + +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -25,19 +33,163 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + static constexpr const char* kIdentifierNodeType = "identifier"; - + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; - std::string LinkedEditingRange::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + if (std::string_view(ts_node_type(node)) == kIdentifierNodeType) + { + const uint32_t start = ts_node_start_byte(node); + const uint32_t end = ts_node_end_byte(node); + if (start >= content->size() || end > content->size() || start >= end) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = content->substr(start, end - start); + info.location.start_line = position.line; + info.location.end_line = position.line; + info.location.start_column = position.character; + info.location.end_column = position.character; + info.location.start_offset = static_cast(byte_offset); + info.location.end_offset = static_cast(byte_offset); + return info; + } + node = ts_node_parent(node); + } + + return std::nullopt; + } + + std::optional ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (table && semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(identifier, location); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(identifier); + if (!matches.empty()) + { + return matches.front(); + } + } + + return std::nullopt; + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + } + + std::string LinkedEditingRange::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentLinkedEditingRangeProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::LinkedEditingRangeParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::LinkedEditingRanges linked; + + auto ident = GetIdentifierAtPosition(params.textDocument.uri, params.position, context); + if (ident) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.textDocument.uri); + + if (table && semantic) + { + if (auto symbol_id = ResolveSymbolId(params.textDocument.uri, ident->text, ident->location, context)) + { + const auto& refs = semantic->references().references(*symbol_id); + + std::unordered_set seen; + linked.ranges.reserve(refs.size() + 1); + + for (const auto& ref : refs) + { + std::uint64_t key = (static_cast(ref.location.start_offset) << 32) | + static_cast(ref.location.end_offset); + if (!seen.insert(key).second) + { + continue; + } + + linked.ranges.push_back(ToRange(ref.location)); + } + + if (const auto* symbol = table->definition(*symbol_id)) + { + linked.ranges.push_back(ToRange(symbol->selection_range())); + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(linked); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/moniker.cppm b/lsp-server/src/provider/text_document/moniker.cppm index 92b78f1..e58d233 100644 --- a/lsp-server/src/provider/text_document/moniker.cppm +++ b/lsp-server/src/provider/text_document/moniker.cppm @@ -8,7 +8,11 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.symbol; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::text_document { @@ -25,19 +29,77 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; + namespace text = lsp::utils::text_coordinates; - + std::string BuildIdentifier(const protocol::DocumentUri& uri, + const language::symbol::Symbol& symbol) + { + const auto loc = symbol.selection_range(); + return uri + ":" + + std::to_string(loc.start_line) + ":" + + std::to_string(loc.start_column) + ":" + + symbol.name(); + } + } std::string Moniker::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentMonikerProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::MonikerParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(params.textDocument.uri); + const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri); + + protocol::LSPArray result; + + if (content && table) + { + language::ast::Location loc{}; + loc.start_line = params.position.line; + loc.start_column = params.position.character; + loc.end_line = params.position.line; + loc.end_column = params.position.character; + + auto offset = static_cast(text::ToOffset(params.position, *content)); + loc.start_offset = offset; + loc.end_offset = offset; + + if (auto symbol_id = table->FindSymbolAt(loc)) + { + if (const auto* symbol = table->definition(*symbol_id)) + { + protocol::LSPObject moniker; + moniker["scheme"] = protocol::string("tsl"); + moniker["identifier"] = protocol::string(BuildIdentifier(params.textDocument.uri, *symbol)); + moniker["unique"] = protocol::string(protocol::UniquenessLevelLiterals::Project); + moniker["kind"] = protocol::string(protocol::MonikerKindLiterals::Local); + result.emplace_back(std::move(moniker)); + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/on_type_formatting.cppm b/lsp-server/src/provider/text_document/on_type_formatting.cppm index 4c084bd..dc69085 100644 --- a/lsp-server/src/provider/text_document/on_type_formatting.cppm +++ b/lsp-server/src/provider/text_document/on_type_formatting.cppm @@ -8,7 +8,9 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::text_document { @@ -25,19 +27,133 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; + namespace text = lsp::utils::text_coordinates; - + protocol::uinteger Utf8CharCount(std::string_view text) + { + protocol::uinteger count = 0; + std::size_t i = 0; + while (i < text.size()) + { + unsigned char ch = static_cast(text[i]); + std::size_t advance = 1; + if ((ch & 0x80) == 0) + { + advance = 1; + } + else if ((ch & 0xE0) == 0xC0) + { + advance = 2; + } + else if ((ch & 0xF0) == 0xE0) + { + advance = 3; + } + else if ((ch & 0xF8) == 0xF0) + { + advance = 4; + } + i += std::min(advance, text.size() - i); + count++; + } + return count; + } + + std::optional TrimLineTrailingWhitespace(const protocol::Position& position, + const protocol::string& content) + { + protocol::Position line_start_pos = position; + line_start_pos.character = 0; + + protocol::Position next_line_pos = line_start_pos; + next_line_pos.line += 1; + next_line_pos.character = 0; + + auto line_start_offset = static_cast(text::ToOffset(line_start_pos, content)); + auto line_end_offset = static_cast(text::ToOffset(next_line_pos, content)); + line_start_offset = std::min(line_start_offset, content.size()); + line_end_offset = std::min(line_end_offset, content.size()); + if (line_start_offset > line_end_offset) + { + std::swap(line_start_offset, line_end_offset); + } + + std::string_view slice(content); + slice = slice.substr(line_start_offset, line_end_offset - line_start_offset); + while (!slice.empty() && (slice.ends_with("\n") || slice.ends_with("\r"))) + { + slice.remove_suffix(1); + } + + if (slice.empty()) + { + return std::nullopt; + } + + std::size_t trim_end = slice.size(); + while (trim_end > 0) + { + char ch = slice[trim_end - 1]; + if (ch != ' ' && ch != '\t') + { + break; + } + trim_end--; + } + + if (trim_end == slice.size()) + { + return std::nullopt; + } + + protocol::TextEdit edit; + edit.range.start.line = position.line; + edit.range.start.character = Utf8CharCount(slice.substr(0, trim_end)); + edit.range.end.line = position.line; + edit.range.end.character = Utf8CharCount(slice); + edit.newText = ""; + return edit; + } + } std::string OnTypeFormatting::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentOnTypeFormattingProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::DocumentOnTypeFormattingParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(params.textDocument.uri); + + protocol::LSPArray edits; + if (content) + { + if (auto edit = TrimLineTrailingWhitespace(params.position, *content)) + { + edits.emplace_back(codec::ToLSPAny(*edit)); + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(edits)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/prepare_call_hierarchy.cppm b/lsp-server/src/provider/text_document/prepare_call_hierarchy.cppm index a00fef8..ab85f18 100644 --- a/lsp-server/src/provider/text_document/prepare_call_hierarchy.cppm +++ b/lsp-server/src/provider/text_document/prepare_call_hierarchy.cppm @@ -1,14 +1,19 @@ module; - export module lsp.provider.text_document.prepare_call_hierarchy; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::text_document { @@ -25,19 +30,192 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; - + static constexpr const char* kIdentifierNodeType = "identifier"; + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + const char* node_type = ts_node_type(node); + if (std::string_view(node_type) == kIdentifierNodeType) + { + IdentifierInfo info; + info.text = language::ast::ts_utils::Text(node, *content); + info.location = language::ast::ts_utils::NodeLocation(node); + if (info.text.empty()) + { + return std::nullopt; + } + return info; + } + node = ts_node_parent(node); + } + + return std::nullopt; + } + + std::optional ResolveCallableSymbolId(const protocol::DocumentUri& uri, + const IdentifierInfo& ident, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(ident.text, ident.location); + if (resolved.IsResolved()) + { + if (table) + { + if (const auto* symbol = table->definition(resolved.symbol_id)) + { + if (symbol->Is() || symbol->Is()) + { + return resolved.symbol_id; + } + } + } + else + { + return resolved.symbol_id; + } + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(ident.text); + for (auto id : matches) + { + if (const auto* symbol = table->definition(id)) + { + if (symbol->Is() || symbol->Is()) + { + return id; + } + } + } + } + + return std::nullopt; + } + + protocol::LSPObject BuildCallHierarchyItem(const protocol::DocumentUri& uri, + const language::symbol::Symbol& symbol) + { + protocol::LSPObject item; + item["name"] = protocol::string(symbol.name()); + item["kind"] = static_cast(symbol.kind()); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(uri); + item["range"] = ToRangeObject(ToRange(symbol.range())); + item["selectionRange"] = ToRangeObject(ToRange(symbol.selection_range())); + + protocol::LSPObject data; + data["uri"] = protocol::string(uri); + data["symbolId"] = protocol::string(std::to_string(symbol.id())); + item["data"] = protocol::LSPAny(std::move(data)); + + return item; + } + } std::string PrepareCallHierarchy::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentPrepareCallHierarchyProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::CallHierarchyParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::LSPArray result; + + auto ident = GetIdentifierAtPosition(params.textDocument.uri, params.position, context); + if (ident) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri); + if (table) + { + if (auto symbol_id = ResolveCallableSymbolId(params.textDocument.uri, *ident, context)) + { + if (const auto* symbol = table->definition(*symbol_id)) + { + result.emplace_back(BuildCallHierarchyItem(params.textDocument.uri, *symbol)); + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/prepare_rename.cppm b/lsp-server/src/provider/text_document/prepare_rename.cppm index ff7d413..fe4d87c 100644 --- a/lsp-server/src/provider/text_document/prepare_rename.cppm +++ b/lsp-server/src/provider/text_document/prepare_rename.cppm @@ -1,14 +1,18 @@ module; - export module lsp.provider.text_document.prepare_rename; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; + +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -20,24 +24,79 @@ export namespace lsp::provider::text_document PrepareRename() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; + + private: + static constexpr const char* kIdentifierNodeType = "identifier"; + + static std::optional GetIdentifierRangeAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context); }; } namespace lsp::provider::text_document { - - - - - std::string PrepareRename::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::string PrepareRename::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentPrepareRenameProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::PrepareRenameParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + std::optional range = GetIdentifierRangeAtPosition(params.textDocument.uri, params.position, context); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(range); + + auto json = codec::Serialize(response); + if (!json.has_value()) + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + return json.value(); + } + + std::optional PrepareRename::GetIdentifierRangeAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + const char* node_type = ts_node_type(node); + if (std::string_view(node_type) == kIdentifierNodeType) + { + TSPoint start = ts_node_start_point(node); + TSPoint end = ts_node_end_point(node); + protocol::Range range; + range.start.line = start.row; + range.start.character = start.column; + range.end.line = end.row; + range.end.character = end.column; + return range; + } + node = ts_node_parent(node); + } + + return std::nullopt; } } diff --git a/lsp-server/src/provider/text_document/prepare_type_hierarchy.cppm b/lsp-server/src/provider/text_document/prepare_type_hierarchy.cppm index 844651d..f4937d8 100644 --- a/lsp-server/src/provider/text_document/prepare_type_hierarchy.cppm +++ b/lsp-server/src/provider/text_document/prepare_type_hierarchy.cppm @@ -1,14 +1,19 @@ module; - export module lsp.provider.text_document.prepare_type_hierarchy; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::text_document { @@ -25,19 +30,195 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; - + static constexpr const char* kIdentifierNodeType = "identifier"; + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + const char* node_type = ts_node_type(node); + if (std::string_view(node_type) == kIdentifierNodeType) + { + IdentifierInfo info; + info.text = language::ast::ts_utils::Text(node, *content); + info.location = language::ast::ts_utils::NodeLocation(node); + if (info.text.empty()) + { + return std::nullopt; + } + return info; + } + node = ts_node_parent(node); + } + + return std::nullopt; + } + + std::optional ResolveClassSymbolId(const protocol::DocumentUri& uri, + const IdentifierInfo& ident, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(ident.text, ident.location); + if (resolved.IsResolved()) + { + if (table) + { + if (const auto* symbol = table->definition(resolved.symbol_id)) + { + if (symbol->Is()) + { + return resolved.symbol_id; + } + } + } + else + { + return resolved.symbol_id; + } + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(ident.text); + for (auto id : matches) + { + if (const auto* symbol = table->definition(id)) + { + if (symbol->Is()) + { + return id; + } + } + } + } + + return std::nullopt; + } + + protocol::LSPObject BuildTypeHierarchyItem(const protocol::DocumentUri& uri, + const language::symbol::Symbol& symbol) + { + protocol::LSPObject item; + item["name"] = protocol::string(symbol.name()); + item["kind"] = static_cast(symbol.kind()); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(uri); + item["range"] = ToRangeObject(ToRange(symbol.range())); + item["selectionRange"] = ToRangeObject(ToRange(symbol.selection_range())); + + protocol::LSPObject data; + data["uri"] = protocol::string(uri); + data["symbolId"] = protocol::string(std::to_string(symbol.id())); + item["data"] = protocol::LSPAny(std::move(data)); + + return item; + } + } std::string PrepareTypeHierarchy::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentPrepareTypeHierarchyProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::TypeHierarchyPrepareParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::LSPArray result; + + auto ident = GetIdentifierAtPosition(params.textDocument.uri, params.position, context); + if (ident) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri); + if (table) + { + if (auto symbol_id = ResolveClassSymbolId(params.textDocument.uri, *ident, context)) + { + if (const auto* symbol = table->definition(*symbol_id)) + { + if (symbol->Is()) + { + result.emplace_back(BuildTypeHierarchyItem(params.textDocument.uri, *symbol)); + } + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/publish_diagnostics.cppm b/lsp-server/src/provider/text_document/publish_diagnostics.cppm index f3d3daf..6150f5e 100644 --- a/lsp-server/src/provider/text_document/publish_diagnostics.cppm +++ b/lsp-server/src/provider/text_document/publish_diagnostics.cppm @@ -25,17 +25,39 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - - - - void PublishDiagnostics::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentPublishDiagnosticsProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value() || !notification.params->Is()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + const auto& obj = notification.params->Get(); + auto uri_it = obj.find("uri"); + auto diags_it = obj.find("diagnostics"); + + std::string uri; + if (uri_it != obj.end() && uri_it->second.Is()) + { + uri = uri_it->second.Get(); + } + + std::size_t diag_count = 0; + if (diags_it != obj.end() && diags_it->second.Is()) + { + diag_count = diags_it->second.Get().size(); + } + + if (!uri.empty()) + { + spdlog::info("{}: Received {} diagnostic(s) for {}", GetProviderName(), diag_count, uri); + } + else + { + spdlog::info("{}: Received {} diagnostic(s)", GetProviderName(), diag_count); + } } } diff --git a/lsp-server/src/provider/text_document/range_formatting.cppm b/lsp-server/src/provider/text_document/range_formatting.cppm index aaf4ea8..7aee68e 100644 --- a/lsp-server/src/provider/text_document/range_formatting.cppm +++ b/lsp-server/src/provider/text_document/range_formatting.cppm @@ -8,7 +8,9 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::text_document { @@ -25,19 +27,117 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; + namespace text = lsp::utils::text_coordinates; - + std::string TrimTrailingWhitespace(std::string_view content) + { + std::string out; + out.reserve(content.size()); + + std::size_t cursor = 0; + while (cursor < content.size()) + { + auto newline_pos = content.find('\n', cursor); + bool has_newline = newline_pos != std::string_view::npos; + std::size_t line_end = has_newline ? newline_pos : content.size(); + + std::string_view newline_suffix; + if (has_newline && line_end > cursor && content[line_end - 1] == '\r') + { + newline_suffix = "\r\n"; + line_end -= 1; + } + else if (has_newline) + { + newline_suffix = "\n"; + } + else + { + newline_suffix = ""; + } + + std::size_t trim_end = line_end; + while (trim_end > cursor) + { + char ch = content[trim_end - 1]; + if (ch != ' ' && ch != '\t') + { + break; + } + trim_end--; + } + + out.append(content.substr(cursor, trim_end - cursor)); + out.append(newline_suffix); + + if (!has_newline) + { + break; + } + + cursor = newline_pos + 1; + } + + return out; + } + } std::string RangeFormatting::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentRangeFormattingProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::DocumentRangeFormattingParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(params.textDocument.uri); + + protocol::LSPArray edits; + if (content) + { + auto start_offset = static_cast(text::ToOffset(params.range.start, *content)); + auto end_offset = static_cast(text::ToOffset(params.range.end, *content)); + if (start_offset > end_offset) + { + std::swap(start_offset, end_offset); + } + start_offset = std::min(start_offset, content->size()); + end_offset = std::min(end_offset, content->size()); + + std::string_view slice(*content); + slice = slice.substr(start_offset, end_offset - start_offset); + + const bool trim_trailing = params.options.trimTrailingWhitespace.value_or(true); + std::string formatted = trim_trailing ? TrimTrailingWhitespace(slice) : std::string(slice); + + if (formatted != slice) + { + protocol::LSPObject edit; + edit["range"] = codec::ToLSPAny(params.range); + edit["newText"] = protocol::string(std::move(formatted)); + edits.emplace_back(std::move(edit)); + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(edits)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/references.cppm b/lsp-server/src/provider/text_document/references.cppm index 79eeb01..9739132 100644 --- a/lsp-server/src/provider/text_document/references.cppm +++ b/lsp-server/src/provider/text_document/references.cppm @@ -1,7 +1,5 @@ module; -#include - export module lsp.provider.text_document.references; import tree_sitter; import spdlog; @@ -12,13 +10,15 @@ import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; import lsp.language.symbol; -import lsp.manager.document; +import lsp.utils.text_coordinates; extern "C" { } -namespace transform = lsp::codec; +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -32,163 +32,185 @@ export namespace lsp::provider::text_document std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; private: - std::vector BuildReferencesResponse(const protocol::ReferenceParams& params, ExecutionContext& context); - std::vector FindReferences(const protocol::DocumentUri& uri, const std::string& identifier, bool include_declaration, ExecutionContext& context); - std::string GetIdentifierAtPosition(const protocol::DocumentUri& uri, const protocol::Position& position, ExecutionContext& context); - void FindReferencesInNode(TSNode node, const std::string& identifier, const std::string& content, const protocol::DocumentUri& uri, std::vector& locations, bool include_declaration); - bool IsDefinitionNode(TSNode node, const std::string& identifier, const std::string& content); - bool IsReferenceNode(TSNode node, const std::string& identifier, const std::string& content); + static constexpr const char* kIdentifierNodeType = "identifier"; + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context); + + std::optional ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context); + + static protocol::Range ToRange(const language::ast::Location& loc); + static protocol::Location ToLocation(const protocol::DocumentUri& uri, const language::ast::Location& loc); }; - namespace detail - { - constexpr const char* kIdentifier = "identifier"; - constexpr const char* kCall = "call"; - constexpr const char* kAttribute = "attribute"; - constexpr const char* kFunctionDefinition = "function_definition_statement"; - constexpr const char* kFunctionDeclaration = "function_declaration_statement"; - constexpr const char* kClassDefinition = "class_definition_statement"; - constexpr const char* kVarStatement = "var_statement"; - constexpr const char* kConstStatement = "const_statement"; - constexpr const char* kMethodWithImplementation = "method_with_implementation"; - constexpr const char* kAssignmentExpression = "assignment_expression"; - } - - using namespace detail; - } namespace lsp::provider::text_document { - - - - - std::string References::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::string References::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentReferencesProvider: Providing response for method {}", request.method); + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::ReferenceParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + std::vector locations; + + auto ident = GetIdentifierAtPosition(params.textDocument.uri, params.position, context); + if (ident) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.textDocument.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.textDocument.uri); + + if (table && semantic) + { + if (auto symbol_id = ResolveSymbolId(params.textDocument.uri, ident->text, ident->location, context)) + { + const auto& refs = semantic->references().references(*symbol_id); + locations.reserve(refs.size() + (params.context.includeDeclaration ? 1U : 0U)); + + std::unordered_set seen; + + for (const auto& ref : refs) + { + std::uint64_t key = (static_cast(ref.location.start_offset) << 32) | + static_cast(ref.location.end_offset); + if (!seen.insert(key).second) + { + continue; + } + + locations.push_back(ToLocation(params.textDocument.uri, ref.location)); + } + + if (params.context.includeDeclaration) + { + if (const auto* symbol = table->definition(*symbol_id)) + { + locations.push_back(ToLocation(params.textDocument.uri, symbol->selection_range())); + } + } + } + } + } + protocol::ResponseMessage response; response.id = request.id; - response.result = transform::ToLSPAny(std::vector{}); + response.result = codec::ToLSPAny(locations); - std::optional json = transform::Serialize(response); + auto json = codec::Serialize(response); if (!json.has_value()) return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); return json.value(); } - std::vector References::BuildReferencesResponse(const protocol::ReferenceParams& params, ExecutionContext& context) - { - spdlog::trace("{}: Processing references request for URI='{}', Position=({}, {})", - GetProviderName(), - params.textDocument.uri, - params.position.line, - params.position.character); - - std::string identifier = GetIdentifierAtPosition( - params.textDocument.uri, params.position, context); - - if (identifier.empty()) - { - spdlog::info("{}: No identifier at position", GetProviderName()); - return {}; - } - - spdlog::debug("{}: Looking for references of '{}'", GetProviderName(), identifier); - - auto locations = FindReferences(params.textDocument.uri, identifier, params.context.includeDeclaration, context); - spdlog::info("{}: Found {} references", GetProviderName(), locations.size()); - - return locations; - } - - std::vector References::FindReferences(const protocol::DocumentUri& uri, const std::string& identifier, bool include_declaration, ExecutionContext& context) - { - std::vector locations; - - auto& hub = context.GetManagerHub(); - auto content = hub.documents().GetContent(uri); - auto tree = hub.parser().GetTree(uri); - - if (!content.has_value() || !tree) - { - spdlog::warn("{}: Document not found or no syntax tree: {}", GetProviderName(), uri); - return locations; - } - - TSNode root = ts_tree_root_node(tree); - FindReferencesInNode(root, identifier, *content, uri, locations, include_declaration); - - return locations; - } - - std::string References::GetIdentifierAtPosition(const protocol::DocumentUri& uri, const protocol::Position& position, ExecutionContext& context) + std::optional References::GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) { auto& hub = context.GetManagerHub(); auto content = hub.documents().GetContent(uri); auto tree = hub.parser().GetTree(uri); if (!content.has_value() || !tree) - return ""; - - size_t byte_offset = 0; - size_t current_line = 0; - size_t current_col = 0; - - for (size_t i = 0; i < content->length(); i++) - { - if (current_line == position.line && current_col == position.character) - { - byte_offset = i; - break; - } - - if ((*content)[i] == '\n') - { - current_line++; - current_col = 0; - } - else - { - current_col++; - } - } + return std::nullopt; + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); TSNode root = ts_tree_root_node(tree); - TSNode node = ts_node_descendant_for_byte_range(root, byte_offset, byte_offset); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); while (!ts_node_is_null(node)) { const char* node_type = ts_node_type(node); - if (strcmp(node_type, kIdentifier) == 0) + if (std::string_view(node_type) == kIdentifierNodeType) { uint32_t start = ts_node_start_byte(node); uint32_t end = ts_node_end_byte(node); - return content->substr(start, end - start); + if (start >= content->size() || end > content->size() || start >= end) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = content->substr(start, end - start); + info.location.start_line = position.line; + info.location.end_line = position.line; + info.location.start_column = position.character; + info.location.end_column = position.character; + info.location.start_offset = static_cast(byte_offset); + info.location.end_offset = static_cast(byte_offset); + return info; } node = ts_node_parent(node); } - return ""; + return std::nullopt; } - void References::FindReferencesInNode(TSNode /*node*/, - const std::string& /*identifier*/, - const std::string& /*content*/, - const protocol::DocumentUri& /*uri*/, - std::vector& /*locations*/, - bool /*include_declaration*/) + std::optional References::ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context) { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (table && semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(identifier, location); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(identifier); + if (!matches.empty()) + { + return matches.front(); + } + } + + return std::nullopt; } - bool References::IsDefinitionNode(TSNode /*node*/, const std::string& /*identifier*/, const std::string& /*content*/) + protocol::Range References::ToRange(const language::ast::Location& loc) { - return false; + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; } - bool References::IsReferenceNode(TSNode /*node*/, const std::string& /*identifier*/, const std::string& /*content*/) + protocol::Location References::ToLocation(const protocol::DocumentUri& uri, const language::ast::Location& loc) { - return false; + protocol::Location location; + location.uri = uri; + location.range = ToRange(loc); + return location; } } diff --git a/lsp-server/src/provider/text_document/rename.cppm b/lsp-server/src/provider/text_document/rename.cppm index 5a687f0..55b1925 100644 --- a/lsp-server/src/provider/text_document/rename.cppm +++ b/lsp-server/src/provider/text_document/rename.cppm @@ -1,8 +1,5 @@ module; -#include -#include - export module lsp.provider.text_document.rename; import tree_sitter; import spdlog; @@ -12,8 +9,11 @@ import std; import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; +import lsp.language.ast; +import lsp.language.semantic; import lsp.language.symbol; import lsp.manager.manager_hub; +import lsp.utils.text_coordinates; extern "C" { } @@ -36,7 +36,10 @@ export namespace lsp::provider::text_document std::optional BuildRenameResponse(const protocol::RenameParams& params, ExecutionContext& context); // 查找所有需要重命名的位置 - std::vector FindRenameLocations(const protocol::DocumentUri& uri, const std::string& old_name, ExecutionContext& context); + std::vector FindRenameLocations(const protocol::DocumentUri& uri, + const protocol::Position& position, + const std::string& old_name, + ExecutionContext& context); // 获取位置处的标识符和范围 std::string GetIdentifierAtPosition(const protocol::DocumentUri& uri, const protocol::Position& position, ExecutionContext& context); @@ -50,6 +53,9 @@ export namespace lsp::provider::text_document // 在节点中查找所有引用 void FindIdentifiersInNode(TSNode node, const std::string& identifier, const std::string& content, std::vector& ranges); + + static protocol::Range ToRange(const language::ast::Location& loc); + static protocol::Location ToLocation(const protocol::DocumentUri& uri, const language::ast::Location& loc); }; namespace detail @@ -160,9 +166,32 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + constexpr char ToLowerAscii(char ch) + { + if (ch >= 'A' && ch <= 'Z') + { + return static_cast(ch - 'A' + 'a'); + } + return ch; + } - + constexpr bool IsAsciiDigit(char ch) + { + return ch >= '0' && ch <= '9'; + } + + constexpr bool IsAsciiAlpha(char ch) + { + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); + } + + constexpr bool IsAsciiAlnum(char ch) + { + return IsAsciiAlpha(ch) || IsAsciiDigit(ch); + } + } std::string Rename::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { @@ -234,7 +263,7 @@ namespace lsp::provider::text_document spdlog::debug("{}: Renaming '{}' to '{}'", GetProviderName(), old_name, params.newName); // 查找所有需要重命名的位置 - auto locations = FindRenameLocations(params.textDocument.uri, old_name, context); + auto locations = FindRenameLocations(params.textDocument.uri, params.position, old_name, context); if (locations.empty()) { @@ -273,7 +302,10 @@ namespace lsp::provider::text_document return workspace_edit; } - std::vector Rename::FindRenameLocations(const protocol::DocumentUri& uri, const std::string& old_name, ExecutionContext& context) + std::vector Rename::FindRenameLocations(const protocol::DocumentUri& uri, + const protocol::Position& position, + const std::string& old_name, + ExecutionContext& context) { std::vector locations; @@ -281,6 +313,48 @@ namespace lsp::provider::text_document auto content = hub.documents().GetContent(uri); auto tree = hub.parser().GetTree(uri); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (content.has_value() && table && semantic) + { + language::ast::Location loc{}; + loc.start_line = loc.end_line = position.line; + loc.start_column = loc.end_column = position.character; + loc.start_offset = loc.end_offset = utils::text_coordinates::ToOffset(position, *content); + + auto resolved = semantic->name_resolver().ResolveNameAtLocation(old_name, loc); + if (resolved.IsResolved()) + { + const auto& refs = semantic->references().references(resolved.symbol_id); + locations.reserve(refs.size() + 1); + + std::unordered_set seen; + for (const auto& ref : refs) + { + std::uint64_t key = (static_cast(ref.location.start_offset) << 32) | + static_cast(ref.location.end_offset); + if (!seen.insert(key).second) + { + continue; + } + + locations.push_back(ToLocation(uri, ref.location)); + } + + if (const auto* symbol = table->definition(resolved.symbol_id)) + { + locations.push_back(ToLocation(uri, symbol->selection_range())); + } + + spdlog::debug("{}: Found {} resolved occurrences of '{}' in document", + GetProviderName(), + locations.size(), + old_name); + return locations; + } + } + if (!content.has_value() || !tree) { spdlog::warn("{}: Document not found or no syntax tree: {}", @@ -316,7 +390,7 @@ namespace lsp::provider::text_document const char* node_type = ts_node_type(node); // 处理标识符节点 - if (strcmp(node_type, kIdentifier) == 0) + if (std::string_view(node_type) == kIdentifier) { uint32_t start = ts_node_start_byte(node); uint32_t end = ts_node_end_byte(node); @@ -368,39 +442,19 @@ namespace lsp::provider::text_document return ""; } - // 计算字节位置 - size_t byte_offset = 0; - size_t current_line = 0; - size_t current_col = 0; - - for (size_t i = 0; i < content->length(); i++) - { - if (current_line == position.line) - { - if (current_col == position.character) - { - byte_offset = i; - break; - } - current_col++; - } - - if ((*content)[i] == '\n') - { - current_line++; - current_col = 0; - } - } + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); // 获取该位置的节点 TSNode root = ts_tree_root_node(tree); - TSNode node = ts_node_descendant_for_byte_range(root, byte_offset, byte_offset); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); // 向上查找标识符节点 while (!ts_node_is_null(node)) { const char* node_type = ts_node_type(node); - if (strcmp(node_type, kIdentifier) == 0) + if (std::string_view(node_type) == kIdentifier) { uint32_t start = ts_node_start_byte(node); uint32_t end = ts_node_end_byte(node); @@ -424,6 +478,24 @@ namespace lsp::provider::text_document return ""; } + protocol::Range Rename::ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + protocol::Location Rename::ToLocation(const protocol::DocumentUri& uri, const language::ast::Location& loc) + { + protocol::Location location; + location.uri = uri; + location.range = ToRange(loc); + return location; + } + bool Rename::CanRename(const std::string& identifier) { // 空标识符不能重命名 @@ -434,7 +506,7 @@ namespace lsp::provider::text_document // 转换为小写以进行不区分大小写的比较 std::string lower_id = identifier; - std::transform(lower_id.begin(), lower_id.end(), lower_id.begin(), ::tolower); + std::transform(lower_id.begin(), lower_id.end(), lower_id.begin(), ToLowerAscii); // 不能重命名关键字 if (kReservedKeywords.find(lower_id) != kReservedKeywords.end()) @@ -459,7 +531,7 @@ namespace lsp::provider::text_document // 检查是否是保留关键字 std::string lower_name = new_name; - std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower); + std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ToLowerAscii); if (kReservedKeywords.find(lower_name) != kReservedKeywords.end()) { @@ -469,7 +541,7 @@ namespace lsp::provider::text_document // TSF标识符规则:以字母或下划线开头,后跟字母、数字或下划线 // 第一个字符必须是字母或下划线 - if (!std::isalpha(new_name[0]) && new_name[0] != '_') + if (!IsAsciiAlpha(new_name[0]) && new_name[0] != '_') { return false; } @@ -478,7 +550,7 @@ namespace lsp::provider::text_document for (size_t i = 1; i < new_name.length(); i++) { char c = new_name[i]; - if (!std::isalnum(c) && c != '_') + if (!IsAsciiAlnum(c) && c != '_') { return false; } @@ -495,21 +567,21 @@ namespace lsp::provider::text_document if (ts_node_is_null(parent)) return false; - const char* parent_type = ts_node_type(parent); + const std::string_view parent_type = ts_node_type(parent); // 检查是否是函数/类/变量定义 - if (strcmp(parent_type, kFunctionDefinition) == 0 || - strcmp(parent_type, kFunctionDeclaration) == 0 || - strcmp(parent_type, kClassDefinition) == 0 || - strcmp(parent_type, kVarStatement) == 0 || - strcmp(parent_type, kStaticStatement) == 0 || - strcmp(parent_type, kGlobalStatement) == 0 || - strcmp(parent_type, kConstStatement) == 0 || - strcmp(parent_type, kMethodDeclaration) == 0 || - strcmp(parent_type, kMethodDeclarationOnly) == 0 || - strcmp(parent_type, kMethodWithModifier) == 0 || - strcmp(parent_type, kMethodWithImplementation) == 0 || - strcmp(parent_type, kPropertyDeclaration) == 0) + if (parent_type == kFunctionDefinition || + parent_type == kFunctionDeclaration || + parent_type == kClassDefinition || + parent_type == kVarStatement || + parent_type == kStaticStatement || + parent_type == kGlobalStatement || + parent_type == kConstStatement || + parent_type == kMethodDeclaration || + parent_type == kMethodDeclarationOnly || + parent_type == kMethodWithModifier || + parent_type == kMethodWithImplementation || + parent_type == kPropertyDeclaration) { // 检查是否是名称字段 TSNode name_node = ts_node_child_by_field_name(parent, "name", 4); @@ -520,7 +592,7 @@ namespace lsp::provider::text_document } // 检查是否是赋值表达式的左侧(变量定义) - if (strcmp(parent_type, kAssignmentExpression) == 0) + if (parent_type == kAssignmentExpression) { TSNode left_node = ts_node_child_by_field_name(parent, "left", 4); if (!ts_node_is_null(left_node)) @@ -539,9 +611,9 @@ namespace lsp::provider::text_document } // 检查是否是变量声明 - if (strcmp(parent_type, kVarDeclaration) == 0 || - strcmp(parent_type, kStaticDeclaration) == 0 || - strcmp(parent_type, kGlobalDeclaration) == 0) + if (parent_type == kVarDeclaration || + parent_type == kStaticDeclaration || + parent_type == kGlobalDeclaration) { return true; } diff --git a/lsp-server/src/provider/text_document/selection_range.cppm b/lsp-server/src/provider/text_document/selection_range.cppm index 8e71f64..dbc60fd 100644 --- a/lsp-server/src/provider/text_document/selection_range.cppm +++ b/lsp-server/src/provider/text_document/selection_range.cppm @@ -2,14 +2,19 @@ module; export module lsp.provider.text_document.selection_range; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.utils.text_coordinates; import lsp.provider.base.interface; +namespace codec = lsp::codec; + export namespace lsp::provider::text_document { class SelectionRange : public AutoRegisterProvider @@ -25,19 +30,147 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace utils = lsp::utils; - + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } - std::string SelectionRange::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + protocol::Range ToRange(TSNode node) + { + TSPoint start = ts_node_start_point(node); + TSPoint end = ts_node_end_point(node); + + protocol::Range range; + range.start.line = start.row; + range.start.character = start.column; + range.end.line = end.row; + range.end.character = end.column; + return range; + } + + std::optional BuildSelectionRangeObject(TSTree* tree, + const protocol::string& content, + const protocol::Position& position) + { + if (!tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + if (ts_node_is_null(node)) + { + return std::nullopt; + } + + std::vector ranges; + ranges.reserve(16); + + protocol::Range last_range{}; + bool has_last = false; + + while (!ts_node_is_null(node)) + { + if (ts_node_is_named(node)) + { + auto range = ToRange(node); + if (!has_last || + range.start.line != last_range.start.line || + range.start.character != last_range.start.character || + range.end.line != last_range.end.line || + range.end.character != last_range.end.character) + { + ranges.push_back(range); + last_range = range; + has_last = true; + } + } + node = ts_node_parent(node); + } + + if (ranges.empty()) + { + protocol::Range fallback; + fallback.start = position; + fallback.end = position; + ranges.push_back(fallback); + } + + protocol::LSPObject current; + for (auto it = ranges.rbegin(); it != ranges.rend(); ++it) + { + protocol::LSPObject next; + next["range"] = ToRangeObject(*it); + if (!current.empty()) + { + next["parent"] = current; + } + current = std::move(next); + } + + return current; + } + } + + std::string SelectionRange::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("TextDocumentSelectionRangeProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + auto params = codec::FromLSPAny.template operator()(request.params.value()); + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(params.textDocument.uri); + auto tree = hub.parser().GetTree(params.textDocument.uri); + + protocol::LSPArray result; + result.reserve(params.positions.size()); + + for (const auto& position : params.positions) + { + if (content && tree) + { + if (auto selection = BuildSelectionRangeObject(tree, *content, position)) + { + result.emplace_back(std::move(*selection)); + continue; + } + } + result.emplace_back(protocol::LSPObject{}); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/semantic_tokens.cppm b/lsp-server/src/provider/text_document/semantic_tokens.cppm index 3af620c..c43ee23 100644 --- a/lsp-server/src/provider/text_document/semantic_tokens.cppm +++ b/lsp-server/src/provider/text_document/semantic_tokens.cppm @@ -2,6 +2,7 @@ module; export module lsp.provider.text_document.semantic_tokens; +import tree_sitter; import spdlog; import std; @@ -10,6 +11,10 @@ import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.symbol; +import lsp.language.semantic; +import lsp.utils.string; export namespace lsp::provider::text_document { @@ -40,36 +45,446 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; + namespace symbol = lsp::language::symbol; - + struct Token + { + protocol::uinteger line = 0; + protocol::uinteger start = 0; + protocol::uinteger length = 0; + protocol::uinteger type = 0; + protocol::uinteger modifiers = 0; + }; + + enum class TokenTypeIndex : protocol::uinteger + { + Namespace = 0, + Class = 1, + Method = 2, + Function = 3, + Property = 4, + Variable = 5, + Parameter = 6, + Keyword = 7, + Comment = 8, + String = 9, + Number = 10, + Operator = 11, + }; + + enum class ModifierIndex : protocol::uinteger + { + Declaration = 0, + Definition = 1, + Static = 2, + Readonly = 3, + Modification = 4, + }; + + constexpr protocol::uinteger ModifierBit(ModifierIndex index) + { + return protocol::uinteger{ 1 } << static_cast(index); + } + + struct ClassifiedSymbol + { + protocol::uinteger type = 0; + protocol::uinteger modifiers = 0; + }; + + std::optional ClassifySymbol(const symbol::Symbol& sym, bool is_definition, bool is_write) + { + protocol::uinteger modifiers = 0; + if (is_definition) + { + modifiers |= ModifierBit(ModifierIndex::Definition); + modifiers |= ModifierBit(ModifierIndex::Declaration); + } + + if (is_write) + { + modifiers |= ModifierBit(ModifierIndex::Modification); + } + + protocol::uinteger type = static_cast(TokenTypeIndex::Variable); + + if (sym.Is()) + { + type = static_cast(TokenTypeIndex::Namespace); + } + else if (sym.Is()) + { + type = static_cast(TokenTypeIndex::Class); + } + else if (sym.Is()) + { + type = static_cast(TokenTypeIndex::Method); + if (const auto* method = sym.As(); method && method->is_static) + { + modifiers |= ModifierBit(ModifierIndex::Static); + } + } + else if (sym.Is()) + { + type = static_cast(TokenTypeIndex::Function); + } + else if (sym.Is()) + { + type = static_cast(TokenTypeIndex::Property); + } + else if (sym.Is()) + { + type = static_cast(TokenTypeIndex::Property); + if (const auto* field = sym.As(); field && field->is_static) + { + modifiers |= ModifierBit(ModifierIndex::Static); + } + } + else if (sym.Is()) + { + const auto* variable = sym.As(); + if (variable && variable->storage == symbol::VariableScope::kParameter) + { + type = static_cast(TokenTypeIndex::Parameter); + } + else + { + type = static_cast(TokenTypeIndex::Variable); + } + + if (variable && (variable->storage == symbol::VariableScope::kStatic || variable->storage == symbol::VariableScope::kGlobal)) + { + modifiers |= ModifierBit(ModifierIndex::Static); + } + } + else if (sym.Is()) + { + type = static_cast(TokenTypeIndex::Variable); + modifiers |= ModifierBit(ModifierIndex::Readonly); + } + + return ClassifiedSymbol{ .type = type, .modifiers = modifiers }; + } + + std::uint64_t LocationKey(const language::ast::Location& location) + { + return (static_cast(location.start_offset) << 32) | static_cast(location.end_offset); + } + + bool AddToken(std::vector& out, + std::unordered_set& seen, + const ClassifiedSymbol& classified, + const language::ast::Location& location) + { + if (location.end_line != location.start_line) + { + return false; + } + + if (location.end_column <= location.start_column) + { + return false; + } + + if (!seen.insert(LocationKey(location)).second) + { + return false; + } + + Token token; + token.line = location.start_line; + token.start = location.start_column; + token.length = location.end_column - location.start_column; + token.type = classified.type; + token.modifiers = classified.modifiers; + out.push_back(token); + return true; + } + + std::vector CollectTokensForDocument(const protocol::DocumentUri& uri, ExecutionContext& context) + { + std::vector tokens; + std::unordered_set seen; + + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (!table) + { + return tokens; + } + + for (const auto& wrapper : table->all_definitions()) + { + const auto& sym = wrapper.get(); + auto classified = ClassifySymbol(sym, true, false); + if (classified) + { + AddToken(tokens, seen, *classified, sym.selection_range()); + } + + if (!semantic) + { + continue; + } + + const auto& refs = semantic->references().references(sym.id()); + for (const auto& ref : refs) + { + auto ref_classified = ClassifySymbol(sym, false, ref.is_write); + if (ref_classified) + { + AddToken(tokens, seen, *ref_classified, ref.location); + } + } + } + + std::sort(tokens.begin(), tokens.end(), [](const Token& a, const Token& b) { + if (a.line != b.line) + { + return a.line < b.line; + } + if (a.start != b.start) + { + return a.start < b.start; + } + if (a.length != b.length) + { + return a.length < b.length; + } + if (a.type != b.type) + { + return a.type < b.type; + } + return a.modifiers < b.modifiers; + }); + + return tokens; + } + + protocol::SemanticTokens EncodeTokens(const std::vector& tokens, + const std::optional& version) + { + protocol::SemanticTokens result; + if (version) + { + result.resultId = std::to_string(*version); + } + + result.data.reserve(tokens.size() * 5); + + protocol::uinteger prev_line = 0; + protocol::uinteger prev_start = 0; + + for (const auto& token : tokens) + { + protocol::uinteger delta_line = token.line - prev_line; + protocol::uinteger delta_start = delta_line == 0 ? token.start - prev_start : token.start; + + result.data.push_back(delta_line); + result.data.push_back(delta_start); + result.data.push_back(token.length); + result.data.push_back(token.type); + result.data.push_back(token.modifiers); + + prev_line = token.line; + prev_start = token.start; + } + + return result; + } + + std::vector FilterTokensByRange(const std::vector& tokens, const protocol::Range& range) + { + std::vector filtered; + + const auto start_line = range.start.line; + const auto start_char = range.start.character; + const auto end_line = range.end.line; + const auto end_char = range.end.character; + + for (const auto& token : tokens) + { + if (token.line < start_line || token.line > end_line) + { + continue; + } + + if (token.line == start_line && token.start + token.length <= start_char) + { + continue; + } + + if (token.line == end_line && token.start >= end_char) + { + continue; + } + + filtered.push_back(token); + } + + return filtered; + } + + struct CachedTokens + { + std::string result_id; + std::vector data; + }; + + std::mutex g_cache_mutex; + std::unordered_map g_cache; + + void UpdateCache(const protocol::DocumentUri& uri, const protocol::SemanticTokens& tokens) + { + if (!tokens.resultId) + { + return; + } + + std::lock_guard lock(g_cache_mutex); + g_cache[uri] = CachedTokens{ + .result_id = *tokens.resultId, + .data = tokens.data, + }; + } + + } // namespace std::string SemanticTokensRange::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentSemanticTokensRangeProvider: Providing response for method {}", request.method); - return BuildErrorResponseMessage(request, protocol::ErrorCodes::MethodNotFound, "Semantic tokens range not implemented"); + + if (!request.params.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::SemanticTokensRangeParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto version = hub.documents().GetVersion(params.textDocument.uri); + + auto tokens = CollectTokensForDocument(params.textDocument.uri, context); + auto filtered = FilterTokensByRange(tokens, params.range); + auto encoded = EncodeTokens(filtered, version); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(encoded); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } - - - - std::string SemanticTokensFull::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("SemanticTokensFullProvider: Providing response for method {}", request.method); - return BuildErrorResponseMessage(request, protocol::ErrorCodes::MethodNotFound, "Semantic tokens full not implemented"); + + if (!request.params.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::SemanticTokensParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto version = hub.documents().GetVersion(params.textDocument.uri); + + auto tokens = CollectTokensForDocument(params.textDocument.uri, context); + auto encoded = EncodeTokens(tokens, version); + UpdateCache(params.textDocument.uri, encoded); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(encoded); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } - - - - std::string SemanticTokensFullDelta::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("SemanticTokensFullDeltaProvider: Providing response for method {}", request.method); - return BuildErrorResponseMessage(request, protocol::ErrorCodes::MethodNotFound, "Semantic tokens delta not implemented"); + + if (!request.params.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + protocol::SemanticTokensDeltaParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + auto version = hub.documents().GetVersion(params.textDocument.uri); + + auto tokens = CollectTokensForDocument(params.textDocument.uri, context); + auto encoded = EncodeTokens(tokens, version); + + protocol::ResponseMessage response; + response.id = request.id; + + std::optional cached; + { + std::lock_guard lock(g_cache_mutex); + auto it = g_cache.find(params.textDocument.uri); + if (it != g_cache.end()) + { + cached = it->second; + } + } + + if (!cached || cached->result_id != params.previousResultId) + { + UpdateCache(params.textDocument.uri, encoded); + response.result = codec::ToLSPAny(encoded); + } + else + { + protocol::SemanticTokensDelta delta; + delta.resultId = encoded.resultId; + protocol::SemanticTokensEdit edit; + edit.start = 0; + edit.deleteCount = static_cast(cached->data.size()); + edit.data = encoded.data; + delta.edits = std::vector{ std::move(edit) }; + + { + std::lock_guard lock(g_cache_mutex); + g_cache[params.textDocument.uri] = CachedTokens{ + .result_id = delta.resultId.value_or(""), + .data = encoded.data, + }; + } + + response.result = codec::ToLSPAny(delta); + } + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/signature_help.cppm b/lsp-server/src/provider/text_document/signature_help.cppm index a4bc73b..506fbe6 100644 --- a/lsp-server/src/provider/text_document/signature_help.cppm +++ b/lsp-server/src/provider/text_document/signature_help.cppm @@ -1,7 +1,7 @@ module; - export module lsp.provider.text_document.signature_help; +import tree_sitter; import spdlog; import std; @@ -9,6 +9,11 @@ import std; import lsp.protocol; import lsp.codec.facade; import lsp.provider.base.interface; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; +import lsp.utils.text_coordinates; export namespace lsp::provider::text_document { @@ -25,19 +30,424 @@ export namespace lsp::provider::text_document namespace lsp::provider::text_document { - + namespace + { + namespace codec = lsp::codec; - + static constexpr const char* kIdentifierNodeType = "identifier"; + + constexpr bool IsAsciiSpace(char ch) + { + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f' || ch == '\v'; + } + + constexpr bool IsAsciiDigit(char ch) + { + return ch >= '0' && ch <= '9'; + } + + constexpr bool IsAsciiAlpha(char ch) + { + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); + } + + constexpr bool IsAsciiAlnum(char ch) + { + return IsAsciiAlpha(ch) || IsAsciiDigit(ch); + } + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + std::optional GetIdentifierAtOffset(const protocol::DocumentUri& uri, + std::size_t byte_offset, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + if (std::string_view(ts_node_type(node)) == kIdentifierNodeType) + { + const uint32_t start = ts_node_start_byte(node); + const uint32_t end = ts_node_end_byte(node); + + if (start >= content->size() || end > content->size() || start >= end) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = content->substr(start, end - start); + + const TSPoint start_point = ts_node_start_point(node); + const TSPoint end_point = ts_node_end_point(node); + info.location.start_line = start_point.row; + info.location.start_column = start_point.column; + info.location.end_line = end_point.row; + info.location.end_column = end_point.column; + info.location.start_offset = start; + info.location.end_offset = end; + return info; + } + + node = ts_node_parent(node); + } + + return std::nullopt; + } + + struct CallContext + { + std::size_t open_paren = std::string::npos; + std::size_t identifier_offset = std::string::npos; + protocol::uinteger active_parameter = 0; + }; + + std::optional FindCallContext(const std::string& content, std::size_t cursor_offset) + { + if (content.empty()) + { + return std::nullopt; + } + + cursor_offset = std::min(cursor_offset, content.size()); + + int depth = 0; + std::size_t open_paren = std::string::npos; + + for (std::size_t i = cursor_offset; i > 0; --i) + { + const char c = content[i - 1]; + if (c == ')') + { + ++depth; + continue; + } + + if (c == '(') + { + if (depth == 0) + { + open_paren = i - 1; + break; + } + --depth; + continue; + } + + if (depth == 0 && (c == ';' || c == '\n')) + { + break; + } + } + + if (open_paren == std::string::npos) + { + return std::nullopt; + } + + std::size_t end = open_paren; + while (end > 0 && IsAsciiSpace(content[end - 1])) + { + --end; + } + + std::size_t start = end; + while (start > 0) + { + const char c = content[start - 1]; + if (IsAsciiAlnum(c) || c == '_') + { + --start; + continue; + } + break; + } + + if (start == end) + { + return std::nullopt; + } + + protocol::uinteger active_parameter = 0; + int call_depth = 0; + for (std::size_t i = open_paren + 1; i < cursor_offset; ++i) + { + const char c = content[i]; + if (c == '(') + { + ++call_depth; + continue; + } + if (c == ')') + { + if (call_depth == 0) + { + break; + } + --call_depth; + continue; + } + if (c == ',' && call_depth == 0) + { + ++active_parameter; + } + } + + return CallContext{ + .open_paren = open_paren, + .identifier_offset = start, + .active_parameter = active_parameter, + }; + } + + std::string BuildParameterLabel(const language::symbol::Parameter& parameter) + { + std::string label = parameter.name; + if (parameter.type && !parameter.type->empty()) + { + label += ": " + *parameter.type; + } + if (parameter.default_value && !parameter.default_value->empty()) + { + label += " = " + *parameter.default_value; + } + return label; + } + + protocol::SignatureInformation BuildSignature(const language::symbol::Symbol& symbol, + const std::vector& parameters, + const std::optional& return_type) + { + protocol::SignatureInformation signature; + signature.label = symbol.name() + "("; + for (std::size_t i = 0; i < parameters.size(); ++i) + { + if (i > 0) + { + signature.label += ", "; + } + signature.label += BuildParameterLabel(parameters[i]); + } + signature.label += ")"; + + if (return_type && !return_type->empty()) + { + signature.label += ": " + *return_type; + } + + std::vector lsp_params; + lsp_params.reserve(parameters.size()); + for (const auto& param : parameters) + { + protocol::ParammeterInformation info; + info.label = BuildParameterLabel(param); + info.documentation = ""; + lsp_params.push_back(std::move(info)); + } + signature.parameters = std::move(lsp_params); + return signature; + } + + std::optional BuildSignatureForSymbolId(const language::symbol::SymbolTable& table, + language::symbol::SymbolId symbol_id) + { + const auto* symbol = table.definition(symbol_id); + if (!symbol) + { + return std::nullopt; + } + + using namespace language::symbol; + + if (symbol->Is()) + { + const auto* fn = symbol->As(); + return BuildSignature(*symbol, fn->parameters, fn->return_type); + } + if (symbol->Is()) + { + const auto* method = symbol->As(); + return BuildSignature(*symbol, method->parameters, method->return_type); + } + if (symbol->Is()) + { + const auto* property = symbol->As(); + std::vector params; + return BuildSignature(*symbol, params, property->type); + } + return std::nullopt; + } + + std::optional ResolveCallTarget(const protocol::DocumentUri& uri, + const IdentifierInfo& callee, + std::size_t identifier_offset, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + const auto* table = hub.symbols().GetSymbolTable(uri); + if (!semantic || !table) + { + return std::nullopt; + } + + const auto& resolver = semantic->name_resolver(); + + if (identifier_offset > 0) + { + auto content = hub.documents().GetContent(uri); + if (content.has_value()) + { + std::size_t dot_pos = identifier_offset; + while (dot_pos > 0 && IsAsciiSpace((*content)[dot_pos - 1])) + { + --dot_pos; + } + + if (dot_pos > 0 && (*content)[dot_pos - 1] == '.') + { + std::size_t qualifier_end = dot_pos - 1; + while (qualifier_end > 0 && IsAsciiSpace((*content)[qualifier_end - 1])) + { + --qualifier_end; + } + + std::size_t qualifier_start = qualifier_end; + while (qualifier_start > 0) + { + const char c = (*content)[qualifier_start - 1]; + if (IsAsciiAlnum(c) || c == '_') + { + --qualifier_start; + continue; + } + break; + } + + if (qualifier_start < qualifier_end) + { + auto qualifier_ident = GetIdentifierAtOffset(uri, qualifier_start, context); + if (qualifier_ident) + { + auto qualifier_result = resolver.ResolveNameAtLocation(qualifier_ident->text, qualifier_ident->location); + if (qualifier_result.IsResolved()) + { + auto member = resolver.ResolveMemberAccess(qualifier_result.symbol_id, callee.text); + if (member.IsResolved()) + { + return member.symbol_id; + } + } + } + } + } + } + } + + auto result = resolver.ResolveNameAtLocation(callee.text, callee.location); + if (result.IsResolved()) + { + return result.symbol_id; + } + + auto matches = table->FindSymbolsByName(callee.text); + if (!matches.empty()) + { + return matches.front(); + } + + return std::nullopt; + } + + } // namespace std::string SignatureHelp::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentSignatureHelpProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + const auto position_params = + codec::FromLSPAny.template operator()(request.params.value()); + + auto& hub = context.GetManagerHub(); + const auto content = hub.documents().GetContent(position_params.textDocument.uri); + + std::optional signature_help; + + if (content.has_value()) + { + const std::size_t cursor_offset = utils::text_coordinates::ToOffset(position_params.position, *content); + auto call_context = FindCallContext(*content, cursor_offset); + if (call_context) + { + auto callee_ident = GetIdentifierAtOffset(position_params.textDocument.uri, call_context->identifier_offset, context); + if (callee_ident) + { + const auto* table = hub.symbols().GetSymbolTable(position_params.textDocument.uri); + if (table) + { + auto target_id = ResolveCallTarget(position_params.textDocument.uri, + *callee_ident, + call_context->identifier_offset, + context); + if (target_id) + { + protocol::SignatureHelp result; + if (auto signature = BuildSignatureForSymbolId(*table, *target_id)) + { + signature->activeParameter = call_context->active_parameter; + result.signatures.push_back(std::move(*signature)); + result.activeSignature = 0; + + if (result.signatures.front().parameters.has_value()) + { + auto count = result.signatures.front().parameters->size(); + if (count > 0) + { + result.activeParameter = std::min(call_context->active_parameter, + static_cast(count - 1)); + } + } + + signature_help = std::move(result); + } + } + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(signature_help); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/text_document/type_definition.cppm b/lsp-server/src/provider/text_document/type_definition.cppm index d2aaad8..889d0dd 100644 --- a/lsp-server/src/provider/text_document/type_definition.cppm +++ b/lsp-server/src/provider/text_document/type_definition.cppm @@ -1,14 +1,22 @@ module; - export module lsp.provider.text_document.type_definition; +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; +import lsp.utils.string; + +namespace codec = lsp::codec; export namespace lsp::provider::text_document { @@ -20,24 +28,346 @@ export namespace lsp::provider::text_document TypeDefinition() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; + + private: + static constexpr const char* kIdentifierNodeType = "identifier"; + + struct IdentifierInfo + { + std::string text; + language::ast::Location location; + }; + + static std::optional GetIdentifierAtPosition(const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context); + static std::optional ResolveSymbolId(const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context); + static std::optional ExtractTypeName(const language::symbol::Symbol& symbol); + static std::optional ResolveClassType(const protocol::DocumentUri& current_uri, + const std::string& type_name, + ExecutionContext& context); + static std::optional ResolveClassType(const protocol::DocumentUri& current_uri, + language::symbol::SymbolId class_id, + ExecutionContext& context); + static std::string UnqualifyTypeName(std::string type_name); + static protocol::Range ToRange(const language::ast::Location& loc); }; } namespace lsp::provider::text_document { - - - - std::string TypeDefinition::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TextDocumentTypeDefinitionProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + const auto position_params = + codec::FromLSPAny.template operator()(request.params.value()); + + std::optional location; + + auto ident = GetIdentifierAtPosition(position_params.textDocument.uri, position_params.position, context); + if (ident) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(position_params.textDocument.uri); + const auto* semantic = hub.symbols().GetSemanticModel(position_params.textDocument.uri); + + if (table && semantic) + { + if (auto symbol_id = ResolveSymbolId(position_params.textDocument.uri, ident->text, ident->location, context)) + { + if (const auto* symbol = table->definition(*symbol_id)) + { + if (symbol->Is()) + { + location = ResolveClassType(position_params.textDocument.uri, *symbol_id, context); + } + else + { + auto type = semantic->GetSymbolType(*symbol_id); + while (type) + { + switch (type->kind()) + { + case language::semantic::TypeKind::kOptional: + type = type->As()->inner_type_ptr(); + continue; + case language::semantic::TypeKind::kArray: + type = type->As()->element_type_ptr(); + continue; + case language::semantic::TypeKind::kFunction: + type = type->As()->return_type_ptr(); + continue; + default: + break; + } + break; + } + + if (type && type->kind() == language::semantic::TypeKind::kClass) + { + const auto* class_type = type->As(); + location = ResolveClassType(position_params.textDocument.uri, class_type->class_id(), context); + } + + if (!location) + { + if (auto type_name = ExtractTypeName(*symbol)) + { + location = ResolveClassType(position_params.textDocument.uri, *type_name, context); + } + } + } + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(location); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); + } + + std::optional TypeDefinition::GetIdentifierAtPosition( + const protocol::DocumentUri& uri, + const protocol::Position& position, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(uri); + auto tree = hub.parser().GetTree(uri); + + if (!content.has_value() || !tree) + { + return std::nullopt; + } + + std::size_t byte_offset = utils::text_coordinates::ToOffset(position, *content); + TSNode root = ts_tree_root_node(tree); + TSNode node = ts_node_descendant_for_byte_range(root, + static_cast(byte_offset), + static_cast(byte_offset)); + + while (!ts_node_is_null(node)) + { + if (std::string_view(ts_node_type(node)) == kIdentifierNodeType) + { + uint32_t start = ts_node_start_byte(node); + uint32_t end = ts_node_end_byte(node); + if (start >= content->size() || end > content->size() || start >= end) + { + return std::nullopt; + } + + IdentifierInfo info; + info.text = content->substr(start, end - start); + info.location.start_line = position.line; + info.location.end_line = position.line; + info.location.start_column = position.character; + info.location.end_column = position.character; + info.location.start_offset = static_cast(byte_offset); + info.location.end_offset = static_cast(byte_offset); + return info; + } + + node = ts_node_parent(node); + } + + return std::nullopt; + } + + std::optional TypeDefinition::ResolveSymbolId( + const protocol::DocumentUri& uri, + const std::string& identifier, + const language::ast::Location& location, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(uri); + const auto* semantic = hub.symbols().GetSemanticModel(uri); + + if (table && semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(identifier, location); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + if (table) + { + auto matches = table->FindSymbolsByName(identifier); + if (!matches.empty()) + { + return matches.front(); + } + } + + return std::nullopt; + } + + std::optional TypeDefinition::ExtractTypeName(const language::symbol::Symbol& symbol) + { + using namespace language::symbol; + + if (symbol.Is()) + { + return symbol.As()->type; + } + if (symbol.Is()) + { + return symbol.As()->type; + } + if (symbol.Is()) + { + return symbol.As()->type; + } + if (symbol.Is()) + { + return symbol.As()->type; + } + if (symbol.Is()) + { + return symbol.As()->return_type; + } + if (symbol.Is()) + { + return symbol.As()->return_type; + } + + return std::nullopt; + } + + std::optional TypeDefinition::ResolveClassType( + const protocol::DocumentUri& current_uri, + language::symbol::SymbolId class_id, + ExecutionContext& context) + { + auto& symbols = context.GetManagerHub().symbols(); + if (const auto* table = symbols.GetSymbolTable(current_uri)) + { + if (const auto* def = table->definition(class_id)) + { + protocol::Location loc; + loc.uri = current_uri; + loc.range = ToRange(def->selection_range()); + return loc; + } + } + + return std::nullopt; + } + + std::optional TypeDefinition::ResolveClassType( + const protocol::DocumentUri& current_uri, + const std::string& type_name, + ExecutionContext& context) + { + auto& hub = context.GetManagerHub(); + auto& symbols = hub.symbols(); + + const std::string class_name = UnqualifyTypeName(type_name); + + if (const auto* table = symbols.GetSymbolTable(current_uri)) + { + auto matches = table->FindSymbolsByName(class_name); + for (auto id : matches) + { + if (const auto* def = table->definition(id)) + { + if (def->kind() == protocol::SymbolKind::Class) + { + protocol::Location loc; + loc.uri = current_uri; + loc.range = ToRange(def->selection_range()); + return loc; + } + } + } + + for (const auto& wrapper : table->all_definitions()) + { + const auto& def = wrapper.get(); + if (def.kind() != protocol::SymbolKind::Class) + { + continue; + } + if (!utils::IEquals(def.name(), class_name)) + { + continue; + } + + protocol::Location loc; + loc.uri = current_uri; + loc.range = ToRange(def.selection_range()); + return loc; + } + } + + const std::string query_lower = utils::ToLower(class_name); + auto indexed = symbols.QueryIndexedSymbols(protocol::SymbolKind::Class, std::nullopt); + for (const auto& item : indexed) + { + if (!utils::IEquals(item.name, query_lower)) + { + continue; + } + + if (const auto* table = symbols.GetSymbolTable(item.uri)) + { + if (const auto* def = table->definition(item.id)) + { + protocol::Location loc; + loc.uri = item.uri; + loc.range = ToRange(def->selection_range()); + return loc; + } + } + } + + return std::nullopt; + } + + std::string TypeDefinition::UnqualifyTypeName(std::string type_name) + { + type_name = utils::Trim(std::move(type_name)); + if (type_name.empty()) + { + return type_name; + } + + auto dot_pos = type_name.find_last_of('.'); + if (dot_pos != std::string::npos && dot_pos + 1 < type_name.size()) + { + return utils::Trim(type_name.substr(dot_pos + 1)); + } + + return type_name; + } + + protocol::Range TypeDefinition::ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; } } diff --git a/lsp-server/src/provider/type_hierarchy/subtypes.cppm b/lsp-server/src/provider/type_hierarchy/subtypes.cppm index 16b5334..3bf8348 100644 --- a/lsp-server/src/provider/type_hierarchy/subtypes.cppm +++ b/lsp-server/src/provider/type_hierarchy/subtypes.cppm @@ -7,8 +7,14 @@ import spdlog; import std; import lsp.protocol; +import lsp.protocol.types; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::type_hierarchy { @@ -16,7 +22,7 @@ export namespace lsp::provider::type_hierarchy { public: static constexpr std::string_view kMethod = "typeHierarchy/subtypes"; - static constexpr std::string_view kProviderName = "WorkspaceSubtypes"; + static constexpr std::string_view kProviderName = "TypeHierarchySubtypes"; Subtypes() = default; std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; @@ -25,19 +31,210 @@ export namespace lsp::provider::type_hierarchy namespace lsp::provider::type_hierarchy { - + namespace + { + namespace codec = lsp::codec; - + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + std::optional ParseSymbolId(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto it = obj.find("symbolId"); + if (it == obj.end()) + { + return std::nullopt; + } + + if (it->second.Is()) + { + const auto& text = it->second.Get(); + try + { + return static_cast(std::stoull(text)); + } + catch (const std::exception&) + { + return std::nullopt; + } + } + + if (it->second.Is()) + { + return static_cast(it->second.Get()); + } + + if (it->second.Is()) + { + auto value = it->second.Get(); + if (value < 0) + { + return std::nullopt; + } + return static_cast(value); + } + + return std::nullopt; + } + + std::optional ResolveSymbolIdFromItem(const protocol::TypeHierarchyItem& item, + ExecutionContext& context) + { + if (item.data) + { + if (auto id = ParseSymbolId(*item.data)) + { + return id; + } + } + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(item.uri); + const auto* table = hub.symbols().GetSymbolTable(item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(item.uri); + if (!content.has_value() || !table) + { + return std::nullopt; + } + + language::ast::Location loc{}; + loc.start_line = item.selectionRange.start.line; + loc.start_column = item.selectionRange.start.character; + loc.end_line = item.selectionRange.start.line; + loc.end_column = item.selectionRange.start.character; + + auto offset = utils::text_coordinates::ToOffset(item.selectionRange.start, *content); + loc.start_offset = static_cast(offset); + loc.end_offset = static_cast(offset); + + if (auto symbol_id = table->FindSymbolAt(loc)) + { + return symbol_id; + } + + if (semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(item.name, loc); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + auto matches = table->FindSymbolsByName(item.name); + for (auto id : matches) + { + const auto* symbol = table->definition(id); + if (symbol && symbol->Is()) + { + return id; + } + } + + return std::nullopt; + } + + protocol::LSPObject BuildTypeHierarchyItem(const protocol::DocumentUri& uri, + const language::symbol::Symbol& symbol) + { + protocol::LSPObject item; + item["name"] = protocol::string(symbol.name()); + item["kind"] = static_cast(symbol.kind()); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(uri); + item["range"] = ToRangeObject(ToRange(symbol.range())); + item["selectionRange"] = ToRangeObject(ToRange(symbol.selection_range())); + + protocol::LSPObject data; + data["uri"] = protocol::string(uri); + data["symbolId"] = protocol::string(std::to_string(symbol.id())); + item["data"] = protocol::LSPAny(std::move(data)); + + return item; + } + } std::string Subtypes::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { - spdlog::debug("WorkspaceSubtypesProvider: Providing response for method {}", request.method); + spdlog::debug("TypeHierarchySubtypesProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::TypeHierarchySubtypesParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::LSPArray result; + + auto symbol_id = ResolveSymbolIdFromItem(params.item, context); + if (symbol_id) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.item.uri); + + if (table && semantic) + { + std::unordered_set seen; + for (auto derived_id : semantic->inheritance().derived_classes(*symbol_id)) + { + if (!seen.insert(derived_id).second) + { + continue; + } + + const auto* derived_symbol = table->definition(derived_id); + if (!derived_symbol || !derived_symbol->Is()) + { + continue; + } + + result.emplace_back(BuildTypeHierarchyItem(params.item.uri, *derived_symbol)); + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/type_hierarchy/supertypes.cppm b/lsp-server/src/provider/type_hierarchy/supertypes.cppm index 0f7f027..2c3ae06 100644 --- a/lsp-server/src/provider/type_hierarchy/supertypes.cppm +++ b/lsp-server/src/provider/type_hierarchy/supertypes.cppm @@ -7,8 +7,14 @@ import spdlog; import std; import lsp.protocol; +import lsp.protocol.types; import lsp.codec.facade; +import lsp.manager.manager_hub; +import lsp.language.ast; +import lsp.language.semantic; +import lsp.language.symbol; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::type_hierarchy { @@ -25,19 +31,210 @@ export namespace lsp::provider::type_hierarchy namespace lsp::provider::type_hierarchy { - + namespace + { + namespace codec = lsp::codec; - + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + + std::optional ParseSymbolId(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto it = obj.find("symbolId"); + if (it == obj.end()) + { + return std::nullopt; + } + + if (it->second.Is()) + { + const auto& text = it->second.Get(); + try + { + return static_cast(std::stoull(text)); + } + catch (const std::exception&) + { + return std::nullopt; + } + } + + if (it->second.Is()) + { + return static_cast(it->second.Get()); + } + + if (it->second.Is()) + { + auto value = it->second.Get(); + if (value < 0) + { + return std::nullopt; + } + return static_cast(value); + } + + return std::nullopt; + } + + std::optional ResolveSymbolIdFromItem(const protocol::TypeHierarchyItem& item, + ExecutionContext& context) + { + if (item.data) + { + if (auto id = ParseSymbolId(*item.data)) + { + return id; + } + } + + auto& hub = context.GetManagerHub(); + auto content = hub.documents().GetContent(item.uri); + const auto* table = hub.symbols().GetSymbolTable(item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(item.uri); + if (!content.has_value() || !table) + { + return std::nullopt; + } + + language::ast::Location loc{}; + loc.start_line = item.selectionRange.start.line; + loc.start_column = item.selectionRange.start.character; + loc.end_line = item.selectionRange.start.line; + loc.end_column = item.selectionRange.start.character; + + auto offset = utils::text_coordinates::ToOffset(item.selectionRange.start, *content); + loc.start_offset = static_cast(offset); + loc.end_offset = static_cast(offset); + + if (auto symbol_id = table->FindSymbolAt(loc)) + { + return symbol_id; + } + + if (semantic) + { + auto resolved = semantic->name_resolver().ResolveNameAtLocation(item.name, loc); + if (resolved.IsResolved()) + { + return resolved.symbol_id; + } + } + + auto matches = table->FindSymbolsByName(item.name); + for (auto id : matches) + { + const auto* symbol = table->definition(id); + if (symbol && symbol->Is()) + { + return id; + } + } + + return std::nullopt; + } + + protocol::LSPObject BuildTypeHierarchyItem(const protocol::DocumentUri& uri, + const language::symbol::Symbol& symbol) + { + protocol::LSPObject item; + item["name"] = protocol::string(symbol.name()); + item["kind"] = static_cast(symbol.kind()); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(uri); + item["range"] = ToRangeObject(ToRange(symbol.range())); + item["selectionRange"] = ToRangeObject(ToRange(symbol.selection_range())); + + protocol::LSPObject data; + data["uri"] = protocol::string(uri); + data["symbolId"] = protocol::string(std::to_string(symbol.id())); + item["data"] = protocol::LSPAny(std::move(data)); + + return item; + } + } std::string Supertypes::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("TypeHierarchySupertypesProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::TypeHierarchySupertypesParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::LSPArray result; + + auto symbol_id = ResolveSymbolIdFromItem(params.item, context); + if (symbol_id) + { + auto& hub = context.GetManagerHub(); + const auto* table = hub.symbols().GetSymbolTable(params.item.uri); + const auto* semantic = hub.symbols().GetSemanticModel(params.item.uri); + + if (table && semantic) + { + std::unordered_set seen; + for (auto base_id : semantic->inheritance().base_classes(*symbol_id)) + { + if (!seen.insert(base_id).second) + { + continue; + } + + const auto* base_symbol = table->definition(base_id); + if (!base_symbol || !base_symbol->Is()) + { + continue; + } + + result.emplace_back(BuildTypeHierarchyItem(params.item.uri, *base_symbol)); + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/window/log_message.cppm b/lsp-server/src/provider/window/log_message.cppm index 65f23cd..25434e8 100644 --- a/lsp-server/src/provider/window/log_message.cppm +++ b/lsp-server/src/provider/window/log_message.cppm @@ -25,17 +25,21 @@ export namespace lsp::provider::window namespace lsp::provider::window { - - - - void LogMessage::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WindowLogMessageProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value() || !notification.params->Is()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + const auto& obj = notification.params->Get(); + auto msg_it = obj.find("message"); + if (msg_it != obj.end() && msg_it->second.Is()) + { + spdlog::info("window/logMessage: {}", msg_it->second.Get()); + } } } diff --git a/lsp-server/src/provider/window/show_document.cppm b/lsp-server/src/provider/window/show_document.cppm index c8e52d0..6134c54 100644 --- a/lsp-server/src/provider/window/show_document.cppm +++ b/lsp-server/src/provider/window/show_document.cppm @@ -25,18 +25,40 @@ export namespace lsp::provider::window namespace lsp::provider::window { - - - + namespace + { + namespace codec = lsp::codec; + } std::string ShowDocument::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WindowShowDocumentProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + const auto& params = request.params->Get(); + auto uri_it = params.find("uri"); + if (uri_it != params.end() && uri_it->second.Is()) + { + spdlog::debug("window/showDocument: uri={}", uri_it->second.Get()); + } + + protocol::LSPObject result; + result["success"] = false; + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/window/show_message.cppm b/lsp-server/src/provider/window/show_message.cppm index 3de2b80..b48ab29 100644 --- a/lsp-server/src/provider/window/show_message.cppm +++ b/lsp-server/src/provider/window/show_message.cppm @@ -26,18 +26,22 @@ export namespace lsp::provider::window namespace lsp::provider::window { - - - - void ShowMessage::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WindowShowMessageProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value() || !notification.params->Is()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + const auto& obj = notification.params->Get(); + auto msg_it = obj.find("message"); + if (msg_it != obj.end() && msg_it->second.Is()) + { + spdlog::info("window/showMessage: {}", msg_it->second.Get()); + } } } diff --git a/lsp-server/src/provider/window/show_message_request.cppm b/lsp-server/src/provider/window/show_message_request.cppm index bf45ceb..f310090 100644 --- a/lsp-server/src/provider/window/show_message_request.cppm +++ b/lsp-server/src/provider/window/show_message_request.cppm @@ -25,18 +25,48 @@ export namespace lsp::provider::window namespace lsp::provider::window { - - - + namespace + { + namespace codec = lsp::codec; + } std::string ShowMessageRequest::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WindowShowMessageRequestProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + const auto& obj = request.params->Get(); + auto msg_it = obj.find("message"); + if (msg_it != obj.end() && msg_it->second.Is()) + { + spdlog::info("window/showMessageRequest: {}", msg_it->second.Get()); + } + + protocol::LSPAny result(std::nullptr_t{}); + auto actions_it = obj.find("actions"); + if (actions_it != obj.end() && actions_it->second.Is()) + { + const auto& actions = actions_it->second.Get(); + if (!actions.empty() && actions.front().Is()) + { + result = actions.front().Get(); + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = std::move(result); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/window/work_done_progress_create.cppm b/lsp-server/src/provider/window/work_done_progress_create.cppm index 8e5b065..71f0f67 100644 --- a/lsp-server/src/provider/window/work_done_progress_create.cppm +++ b/lsp-server/src/provider/window/work_done_progress_create.cppm @@ -25,18 +25,37 @@ export namespace lsp::provider::window namespace lsp::provider::window { - - - + namespace + { + namespace codec = lsp::codec; + } std::string WorkDoneProgressCreate::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WindowWorkDoneProgressCreateProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response - return "{}"; // Placeholder response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } + + const auto& obj = request.params->Get(); + auto token_it = obj.find("token"); + if (token_it == obj.end()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing token"); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/apply_edit.cppm b/lsp-server/src/provider/workspace/apply_edit.cppm index a2291e3..77bb085 100644 --- a/lsp-server/src/provider/workspace/apply_edit.cppm +++ b/lsp-server/src/provider/workspace/apply_edit.cppm @@ -8,7 +8,9 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +import lsp.utils.text_coordinates; export namespace lsp::provider::workspace { @@ -25,19 +27,406 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + namespace codec = lsp::codec; + namespace text = lsp::utils::text_coordinates; - + struct ParsedTextEdit + { + protocol::Range range; + protocol::string new_text; + }; + + std::optional GetUInteger(const protocol::LSPAny& any) + { + if (any.Is()) + { + return any.Get(); + } + if (any.Is()) + { + auto value = any.Get(); + if (value < 0) + { + return std::nullopt; + } + return static_cast(value); + } + return std::nullopt; + } + + std::optional ParsePosition(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto line_it = obj.find("line"); + auto character_it = obj.find("character"); + if (line_it == obj.end() || character_it == obj.end()) + { + return std::nullopt; + } + + auto line = GetUInteger(line_it->second); + auto character = GetUInteger(character_it->second); + if (!line || !character) + { + return std::nullopt; + } + + protocol::Position pos; + pos.line = *line; + pos.character = *character; + return pos; + } + + std::optional ParseRange(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto start_it = obj.find("start"); + auto end_it = obj.find("end"); + if (start_it == obj.end() || end_it == obj.end()) + { + return std::nullopt; + } + + auto start = ParsePosition(start_it->second); + auto end = ParsePosition(end_it->second); + if (!start || !end) + { + return std::nullopt; + } + + protocol::Range range; + range.start = *start; + range.end = *end; + return range; + } + + std::optional ParseTextEdit(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& obj = any.Get(); + auto range_it = obj.find("range"); + auto new_text_it = obj.find("newText"); + if (range_it == obj.end() || new_text_it == obj.end()) + { + return std::nullopt; + } + + auto range = ParseRange(range_it->second); + if (!range) + { + return std::nullopt; + } + + if (!new_text_it->second.Is()) + { + return std::nullopt; + } + + ParsedTextEdit edit; + edit.range = *range; + edit.new_text = new_text_it->second.Get(); + return edit; + } + + bool IsAfter(const protocol::Position& lhs, const protocol::Position& rhs) + { + if (lhs.line != rhs.line) + { + return lhs.line > rhs.line; + } + return lhs.character > rhs.character; + } + + bool SortDescending(const ParsedTextEdit& lhs, const ParsedTextEdit& rhs) + { + if (IsAfter(lhs.range.start, rhs.range.start)) + { + return true; + } + if (IsAfter(rhs.range.start, lhs.range.start)) + { + return false; + } + + if (IsAfter(lhs.range.end, rhs.range.end)) + { + return true; + } + if (IsAfter(rhs.range.end, lhs.range.end)) + { + return false; + } + + return lhs.new_text > rhs.new_text; + } + + std::string UriToPath(const std::string& uri) + { + std::string path = uri; + if (path.starts_with("file://")) + { + path = path.substr(7); + } + +#ifdef _WIN32 + if (!path.empty() && path[0] == '/') + { + path = path.substr(1); + } + std::replace(path.begin(), path.end(), '/', '\\'); +#endif + + std::string decoded; + decoded.reserve(path.size()); + for (std::size_t i = 0; i < path.size(); ++i) + { + if (path[i] == '%' && i + 2 < path.size()) + { + std::string hex = path.substr(i + 1, 2); + char ch = static_cast(std::stoi(hex, nullptr, 16)); + decoded.push_back(ch); + i += 2; + continue; + } + + if (path[i] == '+') + { + decoded.push_back(' '); + continue; + } + + decoded.push_back(path[i]); + } + return decoded; + } + + const protocol::LSPObject* GetObjectPtrField(const protocol::LSPObject& obj, std::string_view key) + { + auto it = obj.find(std::string(key)); + if (it == obj.end() || !it->second.Is()) + { + return nullptr; + } + return &it->second.Get(); + } + + protocol::LSPObject BuildApplyEditResult(bool applied, std::optional failure_reason) + { + protocol::LSPObject result; + result["applied"] = applied; + if (!applied && failure_reason) + { + result["failureReason"] = protocol::string(std::move(*failure_reason)); + } + return result; + } + + bool ApplyEditsToString(std::string& content, std::vector edits) + { + if (edits.empty()) + { + return true; + } + + std::sort(edits.begin(), edits.end(), SortDescending); + + for (const auto& edit : edits) + { + auto start = static_cast(text::ToOffset(edit.range.start, content)); + auto end = static_cast(text::ToOffset(edit.range.end, content)); + if (start > end || start > content.size() || end > content.size()) + { + return false; + } + content.replace(start, end - start, edit.new_text); + } + + return true; + } + + bool ApplyEditsToOpenDocument(lsp::manager::ManagerHub& hub, + const protocol::DocumentUri& uri, + std::vector edits) + { + if (edits.empty()) + { + return true; + } + + auto version = hub.documents().GetVersion(uri); + protocol::DidChangeTextDocumentParams change; + change.textDocument.uri = uri; + change.textDocument.version = version.value_or(0) + 1; + + std::sort(edits.begin(), edits.end(), SortDescending); + change.contentChanges.reserve(edits.size()); + for (const auto& edit : edits) + { + protocol::TextDocumentContentChangeEvent event; + event.range = edit.range; + event.text = edit.new_text; + change.contentChanges.emplace_back(std::move(event)); + } + + hub.documents().UpdateDocument(change); + return true; + } + } std::string ApplyEdit::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceApplyEditProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + const auto& params_obj = request.params->Get(); + const auto* edit_obj = GetObjectPtrField(params_obj, "edit"); + + if (!edit_obj) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing edit"); + } + + const auto* changes_obj = GetObjectPtrField(*edit_obj, "changes"); + if (!changes_obj) + { + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(BuildApplyEditResult(false, "Only WorkspaceEdit.changes is supported")); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); + } + + bool applied = true; + std::optional failure_reason; + + auto& hub = context.GetManagerHub(); + + for (const auto& [uri, edits_any] : *changes_obj) + { + if (!edits_any.Is()) + { + applied = false; + failure_reason = std::format("Invalid edits array for {}", uri); + break; + } + + std::vector edits; + const auto& edits_array = edits_any.Get(); + edits.reserve(edits_array.size()); + + for (const auto& edit_any : edits_array) + { + auto edit = ParseTextEdit(edit_any); + if (!edit) + { + applied = false; + failure_reason = std::format("Invalid edit entry for {}", uri); + break; + } + edits.emplace_back(std::move(*edit)); + } + + if (!applied) + { + break; + } + + const bool is_open = hub.documents().GetVersion(uri).has_value(); + if (!is_open) + { + std::filesystem::path path; + try + { + path = std::filesystem::path(UriToPath(uri)); + } + catch (const std::exception& e) + { + applied = false; + failure_reason = std::format("Invalid uri {}: {}", uri, e.what()); + break; + } + + std::ifstream input(path, std::ios::binary); + if (!input) + { + applied = false; + failure_reason = std::format("Failed to open file {}", path.string()); + break; + } + + std::string content((std::istreambuf_iterator(input)), {}); + if (!ApplyEditsToString(content, std::move(edits))) + { + applied = false; + failure_reason = std::format("Failed to apply edits to {}", uri); + break; + } + + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) + { + applied = false; + failure_reason = std::format("Failed to write file {}", path.string()); + break; + } + + output.write(content.data(), static_cast(content.size())); + if (!output) + { + applied = false; + failure_reason = std::format("Failed to write file {}", path.string()); + break; + } + + hub.symbols().IndexWorkspaceFiles({ uri }); + continue; + } + + if (!ApplyEditsToOpenDocument(hub, uri, std::move(edits))) + { + applied = false; + failure_reason = std::format("Failed to apply edits to open document {}", uri); + break; + } + } + + protocol::LSPObject result = BuildApplyEditResult(applied, std::move(failure_reason)); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/code_lens_refresh.cppm b/lsp-server/src/provider/workspace/code_lens_refresh.cppm index 84db689..2a97496 100644 --- a/lsp-server/src/provider/workspace/code_lens_refresh.cppm +++ b/lsp-server/src/provider/workspace/code_lens_refresh.cppm @@ -25,19 +25,24 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string CodeLensRefresh::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceCodeLensRefreshProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); - return "{}"; // Placeholder response + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/configuration.cppm b/lsp-server/src/provider/workspace/configuration.cppm index 017eceb..ccc0f89 100644 --- a/lsp-server/src/provider/workspace/configuration.cppm +++ b/lsp-server/src/provider/workspace/configuration.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,19 +26,138 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + namespace codec = lsp::codec; - + std::optional GetStringField(const protocol::LSPObject& obj, std::string_view key) + { + auto it = obj.find(std::string(key)); + if (it == obj.end() || !it->second.Is()) + { + return std::nullopt; + } + return it->second.Get(); + } - std::string Configuration::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) + std::vector SplitSection(std::string_view section) + { + std::vector parts; + std::size_t cursor = 0; + while (cursor < section.size()) + { + auto pos = section.find('.', cursor); + if (pos == std::string_view::npos) + { + parts.push_back(section.substr(cursor)); + break; + } + parts.push_back(section.substr(cursor, pos - cursor)); + cursor = pos + 1; + } + parts.erase(std::remove_if(parts.begin(), parts.end(), [](std::string_view part) { + return part.empty(); + }), + parts.end()); + return parts; + } + + protocol::LSPAny LookupSection(const protocol::LSPAny& settings, + const std::optional& section) + { + if (!section || section->empty()) + { + return settings; + } + + const protocol::LSPAny* current = &settings; + for (auto key : SplitSection(*section)) + { + if (!current->Is()) + { + return protocol::LSPAny(std::nullptr_t{}); + } + + const auto& obj = current->Get(); + auto it = obj.find(std::string(key)); + if (it == obj.end()) + { + return protocol::LSPAny(std::nullptr_t{}); + } + current = &it->second; + } + + return *current; + } + + const protocol::LSPArray* ParseItems(const protocol::LSPAny& params) + { + if (!params.Is()) + { + return nullptr; + } + + const auto& obj = params.Get(); + auto it = obj.find("items"); + if (it == obj.end() || !it->second.Is()) + { + return nullptr; + } + + return &it->second.Get(); + } + } + + std::string Configuration::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) { spdlog::debug("WorkspaceConfigurationProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + const auto* items = ParseItems(request.params.value()); + if (!items) + { + spdlog::warn("{}: Invalid params (missing items)", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing items"); + } + + auto settings = context.GetManagerHub().GetConfiguration(); + + protocol::LSPArray result; + result.reserve(items->size()); + for (const auto& item_any : *items) + { + if (!item_any.Is()) + { + result.emplace_back(protocol::LSPAny(std::nullptr_t{})); + continue; + } + + const auto& item_obj = item_any.Get(); + auto section = GetStringField(item_obj, "section"); + + if (settings.Is()) + { + result.emplace_back(protocol::LSPAny(std::nullptr_t{})); + continue; + } + + result.emplace_back(LookupSection(settings, section)); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(result)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/diagnostic.cppm b/lsp-server/src/provider/workspace/diagnostic.cppm index ca6d32a..b23be6e 100644 --- a/lsp-server/src/provider/workspace/diagnostic.cppm +++ b/lsp-server/src/provider/workspace/diagnostic.cppm @@ -1,15 +1,20 @@ module; - export module lsp.provider.workspace.diagnostic; + +import tree_sitter; import spdlog; import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.manager.manager_hub; import lsp.provider.base.interface; +namespace transform = lsp::codec; + export namespace lsp::provider::workspace { class Diagnostic : public AutoRegisterProvider @@ -25,19 +30,124 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - - std::string Diagnostic::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceDiagnosticProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::WorkspaceDiagnosticParams params = + transform::FromLSPAny.template operator()(request.params.value()); + + std::unordered_map previous; + previous.reserve(params.previousResultIds.size()); + for (const auto& item : params.previousResultIds) + { + previous.emplace(item.uri, item.value); + } + + auto& hub = context.GetManagerHub(); + auto uris = hub.documents().GetAllDocumentUris(); + + protocol::LSPArray item_reports; + item_reports.reserve(uris.size()); + + for (const auto& uri : uris) + { + auto content_opt = hub.documents().GetContent(uri); + auto version_opt = hub.documents().GetVersion(uri); + + std::optional result_id; + if (version_opt.has_value()) + { + result_id = std::to_string(version_opt.value()); + } + + if (auto it = previous.find(uri); it != previous.end() && result_id && it->second == *result_id) + { + protocol::LSPObject unchanged; + unchanged["kind"] = protocol::string("unchanged"); + unchanged["uri"] = uri; + if (version_opt.has_value()) + { + unchanged["version"] = version_opt.value(); + } + unchanged["resultId"] = *result_id; + item_reports.emplace_back(std::move(unchanged)); + continue; + } + + protocol::LSPObject full; + full["kind"] = protocol::string("full"); + full["uri"] = uri; + if (version_opt.has_value()) + { + full["version"] = version_opt.value(); + } + if (result_id.has_value()) + { + full["resultId"] = *result_id; + } + + auto* tree = hub.parser().GetTree(uri); + if (tree && content_opt.has_value()) + { + language::ast::Deserializer deserializer; + auto errors = deserializer.DiagnoseSyntax(ts_tree_root_node(tree), content_opt.value()); + + protocol::LSPArray diagnostics; + diagnostics.reserve(errors.size()); + for (const auto& error : errors) + { + protocol::Diagnostic diagnostic; + diagnostic.range.start.line = error.location.start_line; + diagnostic.range.start.character = error.location.start_column; + diagnostic.range.end.line = error.location.end_line; + diagnostic.range.end.character = error.location.end_column; + + switch (error.severity) + { + case language::ast::ErrorSeverity::Warning: + diagnostic.severity = protocol::DiagnosticSeverity::Warning; + break; + case language::ast::ErrorSeverity::Fatal: + case language::ast::ErrorSeverity::Error: + default: + diagnostic.severity = protocol::DiagnosticSeverity::Error; + break; + } + + diagnostic.source = "tsl"; + diagnostic.message = error.message; + diagnostics.emplace_back(transform::ToLSPAny(diagnostic)); + } + + full["items"] = std::move(diagnostics); + } + else + { + full["items"] = protocol::LSPArray{}; + } + + item_reports.emplace_back(std::move(full)); + } + + protocol::LSPObject report; + report["items"] = std::move(item_reports); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(report)); + + std::optional json = transform::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/diagnostic_refresh.cppm b/lsp-server/src/provider/workspace/diagnostic_refresh.cppm index a37a2a9..5c31e19 100644 --- a/lsp-server/src/provider/workspace/diagnostic_refresh.cppm +++ b/lsp-server/src/provider/workspace/diagnostic_refresh.cppm @@ -25,19 +25,24 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string DiagnosticRefresh::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceDiagnosticRefreshProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); - return "{}"; // Placeholder response + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/did_change_configuration.cppm b/lsp-server/src/provider/workspace/did_change_configuration.cppm index 1097713..86d1a69 100644 --- a/lsp-server/src/provider/workspace/did_change_configuration.cppm +++ b/lsp-server/src/provider/workspace/did_change_configuration.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,17 +26,36 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } void DidChangeConfiguration::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceDidChangeConfigurationProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + try + { + auto params = + codec::FromLSPAny.template operator()(notification.params.value()); + + context.GetManagerHub().SetConfiguration(params.settings); + + if (params.settings.Is()) + { + spdlog::debug("{}: Received {} setting key(s)", GetProviderName(), params.settings.Get().size()); + } + } + catch (const std::exception& e) + { + spdlog::warn("{}: Invalid params: {}", GetProviderName(), e.what()); + } } } diff --git a/lsp-server/src/provider/workspace/did_change_watched_files.cppm b/lsp-server/src/provider/workspace/did_change_watched_files.cppm index fb9960f..113b000 100644 --- a/lsp-server/src/provider/workspace/did_change_watched_files.cppm +++ b/lsp-server/src/provider/workspace/did_change_watched_files.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,17 +26,106 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + struct ParsedFileChanges + { + std::vector upserts; + std::vector deletes; + }; - + std::optional GetInteger(const protocol::LSPAny& any) + { + if (any.Is()) + { + return any.Get(); + } + if (any.Is()) + { + return static_cast(any.Get()); + } + return std::nullopt; + } - void DidChangeWatchedFiles::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) + ParsedFileChanges ParseFileChanges(const protocol::LSPAny& params) + { + ParsedFileChanges parsed; + if (!params.Is()) + { + return parsed; + } + + const auto& obj = params.Get(); + auto changes_it = obj.find("changes"); + if (changes_it == obj.end() || !changes_it->second.Is()) + { + return parsed; + } + + const auto& changes = changes_it->second.Get(); + parsed.upserts.reserve(changes.size()); + parsed.deletes.reserve(changes.size()); + + for (const auto& change_any : changes) + { + if (!change_any.Is()) + { + continue; + } + + const auto& change = change_any.Get(); + auto uri_it = change.find("uri"); + auto type_it = change.find("type"); + if (uri_it == change.end() || type_it == change.end() || + !uri_it->second.Is()) + { + continue; + } + + auto type_value = GetInteger(type_it->second); + if (!type_value.has_value()) + { + continue; + } + + switch (static_cast(*type_value)) + { + case protocol::FileChangeType::Created: + case protocol::FileChangeType::Changed: + parsed.upserts.push_back(uri_it->second.Get()); + break; + case protocol::FileChangeType::Deleted: + parsed.deletes.push_back(uri_it->second.Get()); + break; + default: + break; + } + } + + return parsed; + } + } + + void DidChangeWatchedFiles::HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) { spdlog::debug("WorkspaceDidChangeWatchedFilesProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + auto parsed = ParseFileChanges(notification.params.value()); + auto& hub = context.GetManagerHub(); + + if (!parsed.deletes.empty()) + { + hub.symbols().RemoveWorkspaceFiles(parsed.deletes); + } + if (!parsed.upserts.empty()) + { + hub.symbols().IndexWorkspaceFiles(parsed.upserts); + } } } diff --git a/lsp-server/src/provider/workspace/did_change_workspace_folders.cppm b/lsp-server/src/provider/workspace/did_change_workspace_folders.cppm index 7e7f0c2..dd10f2a 100644 --- a/lsp-server/src/provider/workspace/did_change_workspace_folders.cppm +++ b/lsp-server/src/provider/workspace/did_change_workspace_folders.cppm @@ -25,17 +25,235 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + std::string PathToUri(const std::filesystem::path& path) + { + auto absolute = std::filesystem::absolute(path).generic_string(); +#ifdef _WIN32 + std::replace(absolute.begin(), absolute.end(), '\\', '/'); +#endif + if (!absolute.starts_with("/")) + absolute = "/" + absolute; + return "file://" + absolute; + } - + std::string UriToPath(const std::string& uri) + { + std::string path = uri; + if (path.starts_with("file://")) + path = path.substr(7); + +#ifdef _WIN32 + if (!path.empty() && path[0] == '/') + path = path.substr(1); + std::replace(path.begin(), path.end(), '/', '\\'); +#endif + + std::string decoded; + decoded.reserve(path.size()); + for (std::size_t i = 0; i < path.size(); ++i) + { + if (path[i] == '%' && i + 2 < path.size()) + { + std::string hex = path.substr(i + 1, 2); + char ch = static_cast(std::stoi(hex, nullptr, 16)); + decoded.push_back(ch); + i += 2; + } + else if (path[i] == '+') + { + decoded.push_back(' '); + } + else + { + decoded.push_back(path[i]); + } + } + return decoded; + } + + const protocol::LSPObject* GetObjectField(const protocol::LSPObject& obj, std::string_view key) + { + auto it = obj.find(std::string(key)); + if (it == obj.end() || !it->second.Is()) + { + return nullptr; + } + return &it->second.Get(); + } + + std::vector ParseWorkspaceFolders(const protocol::LSPAny& any) + { + std::vector folders; + if (!any.Is()) + { + return folders; + } + + const auto& items = any.Get(); + folders.reserve(items.size()); + + for (const auto& item_any : items) + { + if (!item_any.Is()) + { + continue; + } + + const auto& item = item_any.Get(); + auto uri_it = item.find("uri"); + if (uri_it == item.end() || !uri_it->second.Is()) + { + continue; + } + + protocol::WorkspaceFolder folder{}; + folder.uri = uri_it->second.Get(); + + auto name_it = item.find("name"); + if (name_it != item.end() && name_it->second.Is()) + { + folder.name = name_it->second.Get(); + } + + folders.emplace_back(std::move(folder)); + } + + return folders; + } + + std::vector EnumerateWorkspaceFiles(const protocol::DocumentUri& workspace_uri) + { + std::vector uris; + + std::filesystem::path workspace_path; + try + { + workspace_path = std::filesystem::path(UriToPath(workspace_uri)); + } + catch (const std::exception&) + { + return uris; + } + + if (!std::filesystem::exists(workspace_path)) + { + return uris; + } + + auto options = std::filesystem::directory_options::follow_directory_symlink | + std::filesystem::directory_options::skip_permission_denied; + + for (const auto& entry : std::filesystem::recursive_directory_iterator(workspace_path, options)) + { + if (!entry.is_regular_file()) + { + continue; + } + + auto ext = entry.path().extension().string(); + if (ext != ".tsl" && ext != ".tsf") + { + continue; + } + + uris.emplace_back(PathToUri(entry.path())); + } + + return uris; + } + } void DidChangeWorkspaceFolders::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceDidChangeWorkspaceFoldersProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value() || !notification.params->Is()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + const auto& params_obj = notification.params->Get(); + const auto* event_obj = GetObjectField(params_obj, "event"); + if (!event_obj) + { + return; + } + + auto added_it = event_obj->find("added"); + auto removed_it = event_obj->find("removed"); + + std::vector added; + std::vector removed; + if (added_it != event_obj->end()) + { + added = ParseWorkspaceFolders(added_it->second); + } + if (removed_it != event_obj->end()) + { + removed = ParseWorkspaceFolders(removed_it->second); + } + + if (added.empty() && removed.empty()) + { + return; + } + + auto& hub = context.GetManagerHub(); + hub.RemoveWorkspaceFolders(removed); + hub.AddWorkspaceFolders(added); + auto& scheduler = context.GetScheduler(); + + for (const auto& folder : removed) + { + auto task_id = std::format("Remove workspace folder: {}", folder.uri); + scheduler.Submit( + task_id, + [&hub, uri = folder.uri]() -> std::optional { + auto uris = EnumerateWorkspaceFiles(uri); + if (!uris.empty()) + { + hub.symbols().RemoveWorkspaceFiles(uris); + } + return std::format("Removed {} workspace file(s)", uris.size()); + }, + [](const std::optional& result, bool cancelled) { + if (cancelled) + { + spdlog::debug("Workspace folder removal task cancelled"); + } + else if (result) + { + spdlog::info("{}", *result); + } + }); + } + + for (const auto& folder : added) + { + auto task_id = std::format("Index workspace folder: {}", folder.uri); + scheduler.Submit( + task_id, + [&hub, uri = folder.uri]() -> std::optional { + auto uris = EnumerateWorkspaceFiles(uri); + if (!uris.empty()) + { + hub.symbols().IndexWorkspaceFiles(uris); + } + return std::format("Indexed {} workspace file(s)", uris.size()); + }, + [](const std::optional& result, bool cancelled) { + if (cancelled) + { + spdlog::debug("Workspace folder indexing task cancelled"); + } + else if (result) + { + spdlog::info("{}", *result); + } + }); + } } } diff --git a/lsp-server/src/provider/workspace/did_create_files.cppm b/lsp-server/src/provider/workspace/did_create_files.cppm index da4aa8f..dc3941a 100644 --- a/lsp-server/src/provider/workspace/did_create_files.cppm +++ b/lsp-server/src/provider/workspace/did_create_files.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,17 +26,59 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + std::vector ParseCreateFileUris(const protocol::LSPAny& params) + { + std::vector uris; + if (!params.Is()) + { + return uris; + } - + const auto& obj = params.Get(); + auto files_it = obj.find("files"); + if (files_it == obj.end() || !files_it->second.Is()) + { + return uris; + } - void DidCreateFiles::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) + const auto& files = files_it->second.Get(); + uris.reserve(files.size()); + for (const auto& file_any : files) + { + if (!file_any.Is()) + { + continue; + } + const auto& file_obj = file_any.Get(); + auto uri_it = file_obj.find("uri"); + if (uri_it == file_obj.end() || !uri_it->second.Is()) + { + continue; + } + uris.push_back(uri_it->second.Get()); + } + return uris; + } + } + + void DidCreateFiles::HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) { spdlog::debug("WorkspaceDidCreateFilesProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + auto uris = ParseCreateFileUris(notification.params.value()); + if (uris.empty()) + { + return; + } + + context.GetManagerHub().symbols().IndexWorkspaceFiles(uris); } } diff --git a/lsp-server/src/provider/workspace/did_delete_files.cppm b/lsp-server/src/provider/workspace/did_delete_files.cppm index 915ebaf..100f56e 100644 --- a/lsp-server/src/provider/workspace/did_delete_files.cppm +++ b/lsp-server/src/provider/workspace/did_delete_files.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,17 +26,59 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + std::vector ParseDeleteFileUris(const protocol::LSPAny& params) + { + std::vector uris; + if (!params.Is()) + { + return uris; + } - + const auto& obj = params.Get(); + auto files_it = obj.find("files"); + if (files_it == obj.end() || !files_it->second.Is()) + { + return uris; + } - void DidDeleteFiles::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) + const auto& files = files_it->second.Get(); + uris.reserve(files.size()); + for (const auto& file_any : files) + { + if (!file_any.Is()) + { + continue; + } + const auto& file_obj = file_any.Get(); + auto uri_it = file_obj.find("uri"); + if (uri_it == file_obj.end() || !uri_it->second.Is()) + { + continue; + } + uris.push_back(uri_it->second.Get()); + } + return uris; + } + } + + void DidDeleteFiles::HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) { spdlog::debug("WorkspaceDidDeleteFilesProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + auto uris = ParseDeleteFileUris(notification.params.value()); + if (uris.empty()) + { + return; + } + + context.GetManagerHub().symbols().RemoveWorkspaceFiles(uris); } } diff --git a/lsp-server/src/provider/workspace/did_rename_files.cppm b/lsp-server/src/provider/workspace/did_rename_files.cppm index 1c58ac1..9741c27 100644 --- a/lsp-server/src/provider/workspace/did_rename_files.cppm +++ b/lsp-server/src/provider/workspace/did_rename_files.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,17 +26,64 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + std::vector> ParseRenameFiles(const protocol::LSPAny& params) + { + std::vector> files; + if (!params.Is()) + { + return files; + } - + const auto& obj = params.Get(); + auto files_it = obj.find("files"); + if (files_it == obj.end() || !files_it->second.Is()) + { + return files; + } - void DidRenameFiles::HandleNotification(const protocol::NotificationMessage& notification, [[maybe_unused]] ExecutionContext& context) + const auto& entries = files_it->second.Get(); + files.reserve(entries.size()); + for (const auto& entry_any : entries) + { + if (!entry_any.Is()) + { + continue; + } + const auto& entry_obj = entry_any.Get(); + auto old_it = entry_obj.find("oldUri"); + auto new_it = entry_obj.find("newUri"); + if (old_it == entry_obj.end() || new_it == entry_obj.end()) + { + continue; + } + if (!old_it->second.Is() || !new_it->second.Is()) + { + continue; + } + files.emplace_back(old_it->second.Get(), new_it->second.Get()); + } + return files; + } + } + + void DidRenameFiles::HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) { spdlog::debug("WorkspaceDidRenameFilesProvider: Handling notification for method {}", notification.method); - // TODO: Implement the actual notification handling logic - // 1. Parse notification parameters - // 2. Update appropriate services/state - // 3. Trigger any necessary side effects + if (!notification.params.has_value()) + { + spdlog::warn("{}: Missing params in notification", GetProviderName()); + return; + } + + auto files = ParseRenameFiles(notification.params.value()); + if (files.empty()) + { + return; + } + + context.GetManagerHub().symbols().RenameWorkspaceFiles(files); } } diff --git a/lsp-server/src/provider/workspace/execute_command.cppm b/lsp-server/src/provider/workspace/execute_command.cppm index ea0ab3b..f165581 100644 --- a/lsp-server/src/provider/workspace/execute_command.cppm +++ b/lsp-server/src/provider/workspace/execute_command.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,19 +26,123 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - + namespace + { + namespace codec = lsp::codec; - + std::optional GetStringArg(const std::optional>& args, + std::size_t index) + { + if (!args || index >= args->size()) + { + return std::nullopt; + } + + const auto& any = (*args)[index]; + if (!any.Is()) + { + return std::nullopt; + } + + return any.Get(); + } + + std::vector GetStringArrayArg(const std::optional>& args, + std::size_t index) + { + std::vector result; + if (!args || index >= args->size()) + { + return result; + } + + const auto& any = (*args)[index]; + if (!any.Is()) + { + return result; + } + + const auto& array = any.Get(); + result.reserve(array.size()); + for (const auto& item : array) + { + if (item.Is()) + { + result.push_back(item.Get()); + } + } + + return result; + } + } std::string ExecuteCommand::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceExecuteCommandProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + protocol::ExecuteCommandParams params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::ResponseMessage response; + response.id = request.id; + + if (params.command == "tsl.noop") + { + response.result = protocol::LSPAny(std::nullptr_t{}); + } + else if (params.command == "tsl.loadWorkspace") + { + auto uri = GetStringArg(params.arguments, 0); + if (!uri) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing workspace uri argument"); + } + + auto& hub = context.GetManagerHub(); + auto& scheduler = context.GetScheduler(); + auto task_id = std::format("ExecuteCommand load workspace: {}", *uri); + scheduler.Submit(task_id, [&hub, uri = *uri]() -> std::optional { + hub.symbols().LoadWorkspace(uri); + return std::string("ok"); + }); + + response.result = protocol::LSPAny(protocol::string("scheduled")); + } + else if (params.command == "tsl.indexFiles") + { + auto uris = GetStringArrayArg(params.arguments, 0); + if (uris.empty()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing uri list argument"); + } + + auto& hub = context.GetManagerHub(); + auto& scheduler = context.GetScheduler(); + auto task_id = std::format("ExecuteCommand index files: {}", uris.size()); + scheduler.Submit(task_id, [&hub, uris = std::move(uris)]() -> std::optional { + hub.symbols().IndexWorkspaceFiles(uris); + return std::string("ok"); + }); + + response.result = protocol::LSPAny(protocol::string("scheduled")); + } + else + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Unsupported command"); + } + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/inlay_hint_refresh.cppm b/lsp-server/src/provider/workspace/inlay_hint_refresh.cppm index b12001b..3691bb9 100644 --- a/lsp-server/src/provider/workspace/inlay_hint_refresh.cppm +++ b/lsp-server/src/provider/workspace/inlay_hint_refresh.cppm @@ -25,19 +25,24 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string InlayHintRefresh::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceInlayHintRefreshProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); - return "{}"; // Placeholder response + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/inline_value_refresh.cppm b/lsp-server/src/provider/workspace/inline_value_refresh.cppm index c617b0d..d4e333f 100644 --- a/lsp-server/src/provider/workspace/inline_value_refresh.cppm +++ b/lsp-server/src/provider/workspace/inline_value_refresh.cppm @@ -25,19 +25,24 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string InlineValueRefresh::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceInlineValueRefreshProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); - return "{}"; // Placeholder response + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/semantic_tokens_refresh.cppm b/lsp-server/src/provider/workspace/semantic_tokens_refresh.cppm index 794ec8c..30b458d 100644 --- a/lsp-server/src/provider/workspace/semantic_tokens_refresh.cppm +++ b/lsp-server/src/provider/workspace/semantic_tokens_refresh.cppm @@ -25,19 +25,24 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string SemanticTokensRefresh::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceSemanticTokensRefreshProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); - return "{}"; // Placeholder response + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/symbol.cppm b/lsp-server/src/provider/workspace/symbol.cppm index d1ca80a..8bcfbe6 100644 --- a/lsp-server/src/provider/workspace/symbol.cppm +++ b/lsp-server/src/provider/workspace/symbol.cppm @@ -1,6 +1,5 @@ module; - export module lsp.provider.workspace.symbol; import spdlog; @@ -8,6 +7,13 @@ import std; import lsp.provider.base.interface; import lsp.protocol; +import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.symbol; +import lsp.manager.manager_hub; +import lsp.utils.string; + +namespace codec = lsp::codec; export namespace lsp::provider::workspace { @@ -20,19 +26,107 @@ export namespace lsp::provider::workspace std::string ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) override; + + private: + static protocol::Range ToRange(const language::ast::Location& loc); }; } namespace lsp::provider::workspace { - - - - std::string Symbol::ProvideResponse(const protocol::RequestMessage& request, - [[maybe_unused]] ExecutionContext& context) + ExecutionContext& context) { - spdlog::warn("{} disabled: method {} not implemented", GetProviderName(), request.method); - return BuildErrorResponseMessage(request, protocol::ErrorCodes::MethodNotFound, "workspace/symbol not supported"); + spdlog::debug("{} handling request {}", GetProviderName(), request.method); + + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Invalid params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Invalid params"); + } + + const auto& params = request.params->Get(); + auto query_it = params.find("query"); + if (query_it == params.end() || !query_it->second.Is()) + { + spdlog::warn("{}: Missing query in params", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing query"); + } + + const std::string query = query_it->second.Get(); + const std::string query_lower = utils::ToLower(query); + + auto& hub = context.GetManagerHub(); + auto& symbols = hub.symbols(); + + std::vector result; + + auto append_matches = [&](protocol::SymbolKind kind) { + auto indexed = symbols.QueryIndexedSymbols(kind, std::nullopt); + for (const auto& item : indexed) + { + if (!query_lower.empty()) + { + auto name_lower = utils::ToLower(item.name); + if (name_lower.find(query_lower) == std::string::npos) + { + continue; + } + } + + protocol::WorkspaceSymbol ws; + ws.name = item.name; + ws.kind = item.kind; + + protocol::Location loc; + loc.uri = item.uri; + + if (const auto* table = symbols.GetSymbolTable(item.uri)) + { + if (const auto* def = table->definition(item.id)) + { + loc.range = ToRange(def->selection_range()); + } + } + + ws.location = std::move(loc); + + protocol::LSPObject data; + data["uri"] = protocol::string(item.uri); + data["symbolId"] = protocol::string(std::to_string(item.id)); + ws.data = protocol::LSPAny(std::move(data)); + result.push_back(std::move(ws)); + } + }; + + append_matches(protocol::SymbolKind::Module); + append_matches(protocol::SymbolKind::Class); + append_matches(protocol::SymbolKind::Function); + + std::sort(result.begin(), result.end(), [](const protocol::WorkspaceSymbol& a, const protocol::WorkspaceSymbol& b) { + return utils::ICompare(a.name, b.name) < 0; + }); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = codec::ToLSPAny(result); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); + } + + protocol::Range Symbol::ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; } } // namespace lsp::provider::workspace diff --git a/lsp-server/src/provider/workspace/will_create_files.cppm b/lsp-server/src/provider/workspace/will_create_files.cppm index 93820bf..fefe60c 100644 --- a/lsp-server/src/provider/workspace/will_create_files.cppm +++ b/lsp-server/src/provider/workspace/will_create_files.cppm @@ -25,19 +25,34 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string WillCreateFiles::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceWillCreateFilesProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + [[maybe_unused]] auto params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/will_delete_files.cppm b/lsp-server/src/provider/workspace/will_delete_files.cppm index 70fcb52..b405ea7 100644 --- a/lsp-server/src/provider/workspace/will_delete_files.cppm +++ b/lsp-server/src/provider/workspace/will_delete_files.cppm @@ -25,19 +25,34 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string WillDeleteFiles::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceWillDeleteFilesProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + [[maybe_unused]] auto params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/will_rename_files.cppm b/lsp-server/src/provider/workspace/will_rename_files.cppm index eeeec3f..ee36ada 100644 --- a/lsp-server/src/provider/workspace/will_rename_files.cppm +++ b/lsp-server/src/provider/workspace/will_rename_files.cppm @@ -25,19 +25,34 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string WillRenameFiles::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceWillRenameFilesProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value()) + { + spdlog::warn("{}: Missing params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Missing params"); + } - return "{}"; // Placeholder response + [[maybe_unused]] auto params = + codec::FromLSPAny.template operator()(request.params.value()); + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::nullptr_t{}); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/provider/workspace/workspace_folders.cppm b/lsp-server/src/provider/workspace/workspace_folders.cppm index 69c2944..57f528c 100644 --- a/lsp-server/src/provider/workspace/workspace_folders.cppm +++ b/lsp-server/src/provider/workspace/workspace_folders.cppm @@ -8,6 +8,7 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace @@ -25,19 +26,36 @@ export namespace lsp::provider::workspace namespace lsp::provider::workspace { - - - + namespace + { + namespace codec = lsp::codec; + } std::string WorkspaceFolders::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceWorkspaceFoldersProvider: Providing response for method {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + auto workspace_folders = context.GetManagerHub().GetWorkspaceFolders(); - return "{}"; // Placeholder response + protocol::LSPArray folders; + folders.reserve(workspace_folders.size()); + for (const auto& folder : workspace_folders) + { + protocol::LSPObject obj; + obj["uri"] = protocol::string(folder.uri); + obj["name"] = protocol::string(folder.name); + folders.emplace_back(std::move(obj)); + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(folders)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + return json.value(); } } diff --git a/lsp-server/src/provider/workspace_symbol/resolve.cppm b/lsp-server/src/provider/workspace_symbol/resolve.cppm index a92dde4..383a192 100644 --- a/lsp-server/src/provider/workspace_symbol/resolve.cppm +++ b/lsp-server/src/provider/workspace_symbol/resolve.cppm @@ -8,6 +8,9 @@ import std; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import lsp.language.symbol; +import lsp.manager.manager_hub; import lsp.provider.base.interface; export namespace lsp::provider::workspace_symbol @@ -25,19 +28,130 @@ export namespace lsp::provider::workspace_symbol namespace lsp::provider::workspace_symbol { - + namespace + { + namespace codec = lsp::codec; - + std::optional GetStringField(const protocol::LSPObject& obj, std::string_view key) + { + auto it = obj.find(std::string(key)); + if (it == obj.end() || !it->second.Is()) + { + return std::nullopt; + } + return it->second.Get(); + } + + std::optional ParseSymbolId(const protocol::LSPAny& any) + { + if (any.Is()) + { + return any.Get(); + } + + if (any.Is()) + { + auto value = any.Get(); + if (value < 0) + { + return std::nullopt; + } + return static_cast(value); + } + + if (any.Is()) + { + try + { + return std::stoull(any.Get()); + } + catch (const std::exception&) + { + return std::nullopt; + } + } + + return std::nullopt; + } + + std::optional ExtractSymbolId(const protocol::LSPObject& symbol) + { + auto data_it = symbol.find("data"); + if (data_it == symbol.end() || !data_it->second.Is()) + { + return std::nullopt; + } + + const auto& data = data_it->second.Get(); + auto id_it = data.find("symbolId"); + if (id_it == data.end()) + { + id_it = data.find("id"); + } + + if (id_it == data.end()) + { + return std::nullopt; + } + + return ParseSymbolId(id_it->second); + } + + protocol::Range ToRange(const language::ast::Location& loc) + { + protocol::Range range; + range.start.line = loc.start_line; + range.start.character = loc.start_column; + range.end.line = loc.end_line; + range.end.character = loc.end_column; + return range; + } + } std::string Resolve::ProvideResponse(const protocol::RequestMessage& request, [[maybe_unused]] ExecutionContext& context) { spdlog::debug("WorkspaceSymbolResolve request {}", request.method); - // TODO: Implement the actual request handling logic - // 1. Parse request parameters - // 2. Process the request using appropriate services - // 3. Return formatted response + if (!request.params.has_value() || !request.params->Is()) + { + spdlog::warn("{}: Invalid params in request", GetProviderName()); + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InvalidParams, "Invalid params"); + } - return "{}"; // Placeholder response + protocol::LSPObject symbol = request.params->Get(); + + auto location_it = symbol.find("location"); + if (location_it != symbol.end() && location_it->second.Is()) + { + auto location = location_it->second.Get(); + auto uri = GetStringField(location, "uri"); + if (uri && location.find("range") == location.end()) + { + if (auto symbol_id = ExtractSymbolId(symbol)) + { + auto& hub = context.GetManagerHub(); + if (const auto* table = hub.symbols().GetSymbolTable(*uri)) + { + if (const auto* def = table->definition(static_cast(*symbol_id))) + { + location["range"] = codec::ToLSPAny(ToRange(def->selection_range())); + location_it->second = protocol::LSPAny(std::move(location)); + } + } + } + } + } + + protocol::ResponseMessage response; + response.id = request.id; + response.result = protocol::LSPAny(std::move(symbol)); + + auto json = codec::Serialize(response); + if (!json.has_value()) + { + return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); + } + + return json.value(); } } diff --git a/lsp-server/src/utils/args_parser.cppm b/lsp-server/src/utils/args_parser.cppm index 4cc5d95..98de145 100644 --- a/lsp-server/src/utils/args_parser.cppm +++ b/lsp-server/src/utils/args_parser.cppm @@ -1,8 +1,5 @@ module; -#include -#include - export module lsp.utils.args_parser; import spdlog; @@ -55,6 +52,10 @@ namespace lsp::utils // Default to stderr so LSP stdio (stdout) stays clean. config_.use_stderr = true; + constexpr std::string_view kLogFilePrefix = "--log-file="; + constexpr std::string_view kThreadsPrefix = "--threads="; + constexpr std::string_view kInterpreterPrefix = "--interpreter="; + for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; @@ -81,18 +82,18 @@ namespace lsp::utils config_.use_stderr = true; else if (arg == "--log-stdout") config_.use_stderr = false; - else if (arg.find("--log-file=") == 0) - config_.log_file = arg.substr(std::strlen("--log-file=")); + else if (arg.starts_with(kLogFilePrefix)) + config_.log_file = arg.substr(kLogFilePrefix.size()); else if (arg == "--use-stdio") config_.use_stderr = true; - else if (arg.find("--threads=") == 0) + else if (arg.starts_with(kThreadsPrefix)) { - auto value = arg.substr(std::strlen("--threads=")); + auto value = arg.substr(kThreadsPrefix.size()); config_.thread_count = std::max(1, static_cast(std::stoi(value))); } - else if (arg.find("--interpreter=") == 0) + else if (arg.starts_with(kInterpreterPrefix)) { - config_.interpreter_path = arg.substr(std::strlen("--interpreter=")); + config_.interpreter_path = arg.substr(kInterpreterPrefix.size()); } } diff --git a/lsp-server/test/test_provider/CMakeLists.txt b/lsp-server/test/test_provider/CMakeLists.txt index 6d0def0..bcd4df1 100644 --- a/lsp-server/test/test_provider/CMakeLists.txt +++ b/lsp-server/test/test_provider/CMakeLists.txt @@ -23,6 +23,7 @@ set(SOURCES fixtures.cppm completion_test.cppm json_flow_test.cppm + json_provider_coverage_test.cppm definitions_test.cppm provider_misc_test.cppm provider_surface_test.cppm @@ -49,6 +50,7 @@ target_sources( ${CMAKE_CURRENT_SOURCE_DIR}/fixtures.cppm ${CMAKE_CURRENT_SOURCE_DIR}/completion_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/json_flow_test.cppm + ${CMAKE_CURRENT_SOURCE_DIR}/json_provider_coverage_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/definitions_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/provider_misc_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/provider_surface_test.cppm @@ -121,6 +123,8 @@ target_sources( ../../src/protocol/types.cppm ../../src/protocol/protocol.cppm ../../src/provider/base/interface.cppm + ../../src/provider/base/registry.cppm + ../../src/provider/manifest.cppm ../../src/provider/text_document/completion.cppm ../../src/provider/text_document/definition.cppm ../../src/provider/text_document/did_open.cppm diff --git a/lsp-server/test/test_provider/fixtures/code_action_missing_semicolon.tsl b/lsp-server/test/test_provider/fixtures/code_action_missing_semicolon.tsl new file mode 100644 index 0000000..cfb3a71 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/code_action_missing_semicolon.tsl @@ -0,0 +1,3 @@ +var target: integer; +target := 1 +target := target + 1 diff --git a/lsp-server/test/test_provider/fixtures/color_literals.tsl b/lsp-server/test/test_provider/fixtures/color_literals.tsl new file mode 100644 index 0000000..d81cc57 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/color_literals.tsl @@ -0,0 +1,8 @@ +var red: string; +red := "#ff0000"; + +var green: string; +green := "#00FF00"; + +var with_alpha: string; +with_alpha := "#11223344"; diff --git a/lsp-server/test/test_provider/fixtures/inlay_hint_case.tsl b/lsp-server/test/test_provider/fixtures/inlay_hint_case.tsl new file mode 100644 index 0000000..5ba38b8 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/inlay_hint_case.tsl @@ -0,0 +1,2 @@ +var count := 1; +var name := "alpha"; diff --git a/lsp-server/test/test_provider/fixtures/type_hierarchy_unit.tsf b/lsp-server/test/test_provider/fixtures/type_hierarchy_unit.tsf new file mode 100644 index 0000000..2e66c66 --- /dev/null +++ b/lsp-server/test/test_provider/fixtures/type_hierarchy_unit.tsf @@ -0,0 +1,43 @@ +unit TypeHierarchyUnit; +interface + +type Base = class +public + function BaseMethod(): integer; +end; + +type Mid = class(Base) +public + function MidMethod(): integer; +end; + +type Derived = class(Mid) +public + function DerivedMethod(): integer; +end; + +implementation + +function Base.BaseMethod(): integer; +begin + return 0; +end; + +function Mid.MidMethod(): integer; +begin + return 0; +end; + +function Derived.DerivedMethod(): integer; +begin + return 0; +end; + +procedure TestTypeHierarchy(); +var d: Derived; +begin + d := new Derived; +end; + +end. + diff --git a/lsp-server/test/test_provider/json_provider_coverage_test.cppm b/lsp-server/test/test_provider/json_provider_coverage_test.cppm new file mode 100644 index 0000000..cb683ba --- /dev/null +++ b/lsp-server/test/test_provider/json_provider_coverage_test.cppm @@ -0,0 +1,1433 @@ +module; + +export module lsp.test.provider.json_provider_coverage; + +import std; + +import lsp.test.framework; + +import lsp.codec.facade; +import lsp.core.dispacther; +import lsp.language.ast; +import lsp.manager.manager_hub; +import lsp.protocol; +import lsp.provider.manifest; +import lsp.scheduler.async_executor; +import lsp.test.provider.fixtures; +import tree_sitter; + +export namespace lsp::test::provider +{ + class JsonProviderCoverageTests + { + public: + static void Register(TestRunner& runner); + + private: + static TestResult TestAllProvidersJsonCoverage(); + }; +} + +namespace lsp::test::provider +{ + namespace + { + namespace codec = lsp::codec; + namespace provider = lsp::provider; + + struct SeededRequestParams + { + std::optional code_lens; + std::optional document_link; + std::optional inlay_hint; + std::optional call_hierarchy_incoming_item; + std::optional call_hierarchy_outgoing_item; + std::optional type_hierarchy_item; + std::optional code_action; + std::optional workspace_symbol; + }; + + struct ProviderEnv + { + scheduler::AsyncExecutor scheduler{ 1 }; + manager::ManagerHub hub{}; + core::RequestDispatcher dispatcher{}; + + ProviderEnv() + { + hub.Initialize(); + dispatcher.SetRequestScheduler(&scheduler); + dispatcher.SetManagerHub(&hub); + provider::RegisterAllProviders(dispatcher); + } + }; + + template + std::string SerializeOrThrow(const T& obj) + { + auto json = codec::Serialize(obj); + assertTrue(json.has_value(), "Failed to serialize LSP JSON"); + return json.value(); + } + + template + T DeserializeOrThrow(const std::string& json) + { + auto parsed = codec::Deserialize(json); + assertTrue(parsed.has_value(), "Failed to deserialize LSP JSON"); + return parsed.value(); + } + + protocol::Position FindPosition(const std::string& content, const std::string& marker, bool after_marker = false) + { + auto pos = content.find(marker); + assertTrue(pos != std::string::npos, "Marker not found in fixture"); + + protocol::Position result{}; + for (std::size_t i = 0; i < pos; ++i) + { + if (content[i] == '\n') + { + result.line++; + result.character = 0; + } + else + { + result.character++; + } + } + + if (after_marker) + { + result.character += static_cast(marker.size()); + } + + return result; + } + + protocol::LSPObject ToTextDocument(const std::string& uri) + { + return protocol::LSPObject{ { "uri", uri } }; + } + + protocol::LSPObject ToPosition(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::LSPObject ToRange(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPosition(range.start) }, + { "end", ToPosition(range.end) }, + }; + } + + protocol::Range FullDocumentRange(const std::string& content) + { + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + + protocol::uinteger line_count = 0; + protocol::uinteger last_line_len = 0; + for (char ch : content) + { + if (ch == '\n') + { + line_count++; + last_line_len = 0; + } + else + { + last_line_len++; + } + } + + range.end.line = line_count; + range.end.character = last_line_len; + return range; + } + + std::optional FirstArrayItem(const protocol::LSPAny& any) + { + if (!any.Is()) + { + return std::nullopt; + } + + const auto& array = any.Get(); + if (array.empty()) + { + return std::nullopt; + } + + return array.front(); + } + + protocol::LSPAny BuildCompletionResolveItem(const std::string& uri) + { + protocol::LSPObject data; + data["ctx"] = "new"; + data["class"] = "Widget"; + data["unit"] = "MainUnit"; + data["uri"] = uri; + + protocol::LSPObject item; + item["label"] = "Widget"; + item["data"] = std::move(data); + return protocol::LSPAny(std::move(item)); + } + + std::optional BuildRequestParams(std::string_view method, + const SeededRequestParams& seeded, + const std::string& main_uri, + const std::string& main_content, + const std::string& rename_uri, + const std::string& rename_content, + const std::string& code_action_uri, + const std::string& code_action_content, + const protocol::LSPArray& code_action_diagnostics) + { + if (method == "textDocument/completion") + { + auto completion_pos = FindPosition(main_content, "new Wid", true); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(completion_pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "completionItem/resolve") + { + return BuildCompletionResolveItem(main_uri); + } + + if (method == "textDocument/definition") + { + auto def_pos = FindPosition(main_content, "UnitFunc(1);", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(def_pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/hover") + { + auto hover_pos = FindPosition(main_content, "UnitFunc(1);", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(hover_pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/signatureHelp") + { + auto sig_pos = FindPosition(main_content, "UnitFunc(1);", false); + sig_pos.character += static_cast(std::string("UnitFunc(").size()); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(sig_pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/references") + { + auto pos = FindPosition(rename_content, "target := target", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(rename_uri); + params["position"] = ToPosition(pos); + params["context"] = protocol::LSPObject{ { "includeDeclaration", true } }; + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/linkedEditingRange") + { + auto pos = FindPosition(rename_content, "target := target", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(rename_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/documentHighlight") + { + auto pos = FindPosition(rename_content, "target := target", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(rename_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/prepareRename") + { + auto pos = FindPosition(rename_content, "target := target", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(rename_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/rename") + { + auto pos = FindPosition(rename_content, "target := target", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(rename_uri); + params["position"] = ToPosition(pos); + params["newName"] = "renamed_target"; + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/documentSymbol") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/symbol") + { + protocol::LSPObject params; + params["query"] = protocol::string("Workspace"); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/semanticTokens/full") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/semanticTokens/full/delta") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["previousResultId"] = "0"; + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/semanticTokens/range") + { + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + range.end.line = 5; + range.end.character = 0; + + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["range"] = ToRange(range); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/codeAction") + { + auto range = FullDocumentRange(code_action_content); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(code_action_uri); + params["range"] = ToRange(range); + params["context"] = protocol::LSPObject{ + { "diagnostics", code_action_diagnostics }, + }; + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/typeDefinition") + { + auto pos = FindPosition(main_content, "obj: Widget", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/implementation") + { + auto pos = FindPosition(main_content, "UnitFunc(1);", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/documentLink") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/codeLens") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "codeLens/resolve") + { + if (seeded.code_lens) + { + return *seeded.code_lens; + } + + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + range.end.line = 0; + range.end.character = 0; + + protocol::LSPObject data; + data["kind"] = protocol::string("references"); + data["count"] = static_cast(2); + + protocol::LSPObject lens; + lens["range"] = ToRange(range); + lens["data"] = protocol::LSPAny(std::move(data)); + return protocol::LSPAny(std::move(lens)); + } + + if (method == "documentLink/resolve") + { + if (seeded.document_link) + { + return *seeded.document_link; + } + + auto pos = FindPosition(main_content, "WorkspaceUnit", false); + protocol::Range range; + range.start = pos; + range.end = pos; + range.end.character += static_cast(std::string("WorkspaceUnit").size()); + + protocol::LSPObject data; + data["kind"] = protocol::string("unit"); + data["name"] = protocol::string("WorkspaceUnit"); + data["baseUri"] = protocol::string(main_uri); + + protocol::LSPObject link; + link["range"] = ToRange(range); + link["data"] = protocol::LSPAny(std::move(data)); + return protocol::LSPAny(std::move(link)); + } + + if (method == "textDocument/foldingRange") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/selectionRange") + { + auto pos = FindPosition(main_content, "UnitFunc(1);", false); + protocol::LSPArray positions; + positions.emplace_back(ToPosition(pos)); + + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["positions"] = std::move(positions); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/prepareCallHierarchy") + { + auto pos = FindPosition(main_content, "UnitFunc(1);", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "callHierarchy/incomingCalls") + { + if (seeded.call_hierarchy_incoming_item) + { + protocol::LSPObject params; + params["item"] = *seeded.call_hierarchy_incoming_item; + return protocol::LSPAny(std::move(params)); + } + + auto pos = FindPosition(main_content, "UnitFunc(1);", false); + + protocol::Range range{}; + range.start = pos; + range.end = pos; + range.end.character += static_cast(std::string("UnitFunc").size()); + + protocol::LSPObject item; + item["name"] = protocol::string("UnitFunc"); + item["kind"] = static_cast(protocol::SymbolKind::Function); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(main_uri); + item["range"] = ToRange(range); + item["selectionRange"] = ToRange(range); + + protocol::LSPObject params; + params["item"] = protocol::LSPAny(std::move(item)); + return protocol::LSPAny(std::move(params)); + } + + if (method == "callHierarchy/outgoingCalls") + { + if (seeded.call_hierarchy_outgoing_item) + { + protocol::LSPObject params; + params["item"] = *seeded.call_hierarchy_outgoing_item; + return protocol::LSPAny(std::move(params)); + } + + auto pos = FindPosition(main_content, "TestDefinitions();", false); + + protocol::Range range{}; + range.start = pos; + range.end = pos; + range.end.character += static_cast(std::string("TestDefinitions").size()); + + protocol::LSPObject item; + item["name"] = protocol::string("TestDefinitions"); + item["kind"] = static_cast(protocol::SymbolKind::Function); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(main_uri); + item["range"] = ToRange(range); + item["selectionRange"] = ToRange(range); + + protocol::LSPObject params; + params["item"] = protocol::LSPAny(std::move(item)); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/prepareTypeHierarchy") + { + auto pos = FindPosition(main_content, "Widget = class", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "typeHierarchy/supertypes") + { + if (seeded.type_hierarchy_item) + { + protocol::LSPObject params; + params["item"] = *seeded.type_hierarchy_item; + return protocol::LSPAny(std::move(params)); + } + + auto pos = FindPosition(main_content, "Widget = class", false); + + protocol::Range range{}; + range.start = pos; + range.end = pos; + range.end.character += static_cast(std::string("Widget").size()); + + protocol::LSPObject item; + item["name"] = protocol::string("Widget"); + item["kind"] = static_cast(protocol::SymbolKind::Class); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(main_uri); + item["range"] = ToRange(range); + item["selectionRange"] = ToRange(range); + + protocol::LSPObject params; + params["item"] = protocol::LSPAny(std::move(item)); + return protocol::LSPAny(std::move(params)); + } + + if (method == "typeHierarchy/subtypes") + { + if (seeded.type_hierarchy_item) + { + protocol::LSPObject params; + params["item"] = *seeded.type_hierarchy_item; + return protocol::LSPAny(std::move(params)); + } + + auto pos = FindPosition(main_content, "Widget = class", false); + + protocol::Range range{}; + range.start = pos; + range.end = pos; + range.end.character += static_cast(std::string("Widget").size()); + + protocol::LSPObject item; + item["name"] = protocol::string("Widget"); + item["kind"] = static_cast(protocol::SymbolKind::Class); + item["tags"] = protocol::LSPArray{}; + item["uri"] = protocol::string(main_uri); + item["range"] = ToRange(range); + item["selectionRange"] = ToRange(range); + + protocol::LSPObject params; + params["item"] = protocol::LSPAny(std::move(item)); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/inlayHint") + { + auto range = FullDocumentRange(main_content); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["range"] = ToRange(range); + return protocol::LSPAny(std::move(params)); + } + + if (method == "inlayHint/resolve") + { + if (seeded.inlay_hint) + { + return *seeded.inlay_hint; + } + + protocol::Position pos{}; + pos.line = 0; + pos.character = 0; + + protocol::LSPObject data; + data["detail"] = protocol::string("param: int"); + + protocol::LSPObject hint; + hint["position"] = ToPosition(pos); + hint["label"] = protocol::string("param:"); + hint["kind"] = static_cast(protocol::InlayHintKind::Parameter); + hint["data"] = protocol::LSPAny(std::move(data)); + return protocol::LSPAny(std::move(hint)); + } + + if (method == "textDocument/documentColor") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/colorPresentation") + { + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + range.end.line = 0; + range.end.character = 7; + + protocol::LSPObject color; + color["red"] = protocol::decimal(1.0); + color["green"] = protocol::decimal(0.0); + color["blue"] = protocol::decimal(0.0); + color["alpha"] = protocol::decimal(1.0); + + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["color"] = std::move(color); + params["range"] = ToRange(range); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/diagnostic") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(code_action_uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/diagnostic") + { + protocol::LSPObject params; + params["previousResultIds"] = protocol::LSPArray{}; + return protocol::LSPAny(std::move(params)); + } + + if (method == "codeAction/resolve") + { + if (seeded.code_action) + { + return *seeded.code_action; + } + + protocol::LSPObject action; + action["title"] = protocol::string("Fix"); + action["kind"] = protocol::string(protocol::CodeActionKindLiterals::QuickFix); + action["data"] = protocol::LSPAny(protocol::LSPObject{ { "kind", protocol::string("quickfix") } }); + return protocol::LSPAny(std::move(action)); + } + + if (method == "workspaceSymbol/resolve") + { + if (seeded.workspace_symbol) + { + return *seeded.workspace_symbol; + } + + protocol::LSPObject symbol; + symbol["name"] = protocol::string("WorkspaceUnit"); + symbol["kind"] = static_cast(protocol::SymbolKind::Module); + symbol["location"] = protocol::LSPObject{ + { "uri", protocol::string(main_uri) }, + { "range", ToRange(FullDocumentRange(main_content)) }, + }; + return protocol::LSPAny(std::move(symbol)); + } + + if (method == "workspace/executeCommand") + { + protocol::LSPObject params; + params["command"] = protocol::string("tsl.noop"); + params["arguments"] = protocol::LSPArray{}; + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/willCreateFiles") + { + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + protocol::LSPArray files; + files.emplace_back(protocol::LSPObject{ + { "uri", file_uri }, + }); + + protocol::LSPObject params; + params["files"] = std::move(files); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/willDeleteFiles") + { + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + protocol::LSPArray files; + files.emplace_back(protocol::LSPObject{ + { "uri", file_uri }, + }); + + protocol::LSPObject params; + params["files"] = std::move(files); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/willRenameFiles") + { + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + protocol::LSPArray files; + files.emplace_back(protocol::LSPObject{ + { "oldUri", file_uri }, + { "newUri", file_uri }, + }); + + protocol::LSPObject params; + params["files"] = std::move(files); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/formatting") + { + protocol::LSPObject options; + options["tabSize"] = static_cast(4); + options["insertSpaces"] = true; + + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["options"] = std::move(options); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/rangeFormatting") + { + protocol::LSPObject options; + options["tabSize"] = static_cast(4); + options["insertSpaces"] = true; + + auto range = FullDocumentRange(main_content); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["range"] = ToRange(range); + params["options"] = std::move(options); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/onTypeFormatting") + { + protocol::LSPObject options; + options["tabSize"] = static_cast(4); + options["insertSpaces"] = true; + + auto pos = FindPosition(main_content, "return", false); + + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(pos); + params["ch"] = protocol::string(";"); + params["options"] = std::move(options); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/inlineValue") + { + protocol::LSPObject context; + context["frameId"] = static_cast(0); + context["stoppedLocation"] = ToRange(FullDocumentRange(main_content)); + + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["range"] = ToRange(FullDocumentRange(main_content)); + params["context"] = std::move(context); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/moniker") + { + auto pos = FindPosition(main_content, "UnitFunc(1);", false); + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(main_uri); + params["position"] = ToPosition(pos); + return protocol::LSPAny(std::move(params)); + } + + if (method == "client/registerCapability") + { + protocol::LSPArray registrations; + registrations.emplace_back(protocol::LSPObject{ + { "id", protocol::string("reg_1") }, + { "method", protocol::string("workspace/didChangeConfiguration") }, + }); + + protocol::LSPObject params; + params["registrations"] = std::move(registrations); + return protocol::LSPAny(std::move(params)); + } + + if (method == "client/unregisterCapability") + { + protocol::LSPArray unregistrations; + unregistrations.emplace_back(protocol::LSPObject{ + { "id", protocol::string("reg_1") }, + { "method", protocol::string("workspace/didChangeConfiguration") }, + }); + + protocol::LSPObject params; + params["unregistrations"] = std::move(unregistrations); + return protocol::LSPAny(std::move(params)); + } + + if (method == "window/workDoneProgress/create") + { + protocol::LSPObject params; + params["token"] = protocol::string("progress_token"); + return protocol::LSPAny(std::move(params)); + } + + if (method == "window/showMessageRequest") + { + protocol::LSPArray actions; + actions.emplace_back(protocol::LSPObject{ + { "title", protocol::string("OK") }, + }); + + protocol::LSPObject params; + params["type"] = static_cast(protocol::MessageType::Info); + params["message"] = protocol::string("Test showMessageRequest"); + params["actions"] = std::move(actions); + return protocol::LSPAny(std::move(params)); + } + + if (method == "window/showDocument") + { + protocol::LSPObject params; + params["uri"] = protocol::string(main_uri); + params["takeFocus"] = true; + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/configuration") + { + protocol::LSPArray items; + items.emplace_back(protocol::LSPObject{ + { "scopeUri", protocol::string(main_uri) }, + { "section", protocol::string("tsl") }, + }); + + protocol::LSPObject params; + params["items"] = std::move(items); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/applyEdit") + { + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + range.end = range.start; + + protocol::LSPObject text_edit; + text_edit["range"] = ToRange(range); + text_edit["newText"] = protocol::string(""); + + protocol::LSPArray edits; + edits.emplace_back(std::move(text_edit)); + + protocol::LSPObject changes; + changes[main_uri] = protocol::LSPAny(std::move(edits)); + + protocol::LSPObject edit; + edit["changes"] = std::move(changes); + + protocol::LSPObject params; + params["edit"] = std::move(edit); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/workspaceFolders") + { + return protocol::LSPAny(protocol::LSPObject{}); + } + + if (method == "workspace/codeLens/refresh" || + method == "workspace/diagnostic/refresh" || + method == "workspace/inlayHint/refresh" || + method == "workspace/inlineValue/refresh" || + method == "workspace/semanticTokens/refresh") + { + return protocol::LSPAny(protocol::LSPObject{}); + } + + return std::nullopt; + } + + std::optional BuildNotificationParams(std::string_view method, + const std::string& uri, + const std::string& content, + int version) + { + if (method == "textDocument/didOpen") + { + protocol::LSPObject params; + params["textDocument"] = protocol::LSPObject{ + { "uri", uri }, + { "languageId", "tsl" }, + { "version", version }, + { "text", content }, + }; + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/didChange") + { + protocol::LSPObject change; + protocol::Range full_range{}; + full_range.start.line = 0; + full_range.start.character = 0; + full_range.end.line = 9999; + full_range.end.character = 0; + change["range"] = ToRange(full_range); + change["text"] = content; + + protocol::LSPArray changes; + changes.emplace_back(std::move(change)); + + protocol::LSPObject params; + params["textDocument"] = protocol::LSPObject{ + { "uri", uri }, + { "version", version }, + }; + params["contentChanges"] = std::move(changes); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/didClose") + { + protocol::LSPObject params; + params["textDocument"] = ToTextDocument(uri); + return protocol::LSPAny(std::move(params)); + } + + if (method == "$/setTrace") + { + protocol::LSPObject params; + params["value"] = protocol::TraceValueLiterals::Off; + return protocol::LSPAny(std::move(params)); + } + + if (method == "$/cancelRequest") + { + protocol::LSPObject params; + params["id"] = "json_cancel_me"; + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/didCreateFiles") + { + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + protocol::LSPArray files; + files.emplace_back(protocol::LSPObject{ + { "uri", file_uri }, + }); + + protocol::LSPObject params; + params["files"] = std::move(files); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/didDeleteFiles") + { + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + protocol::LSPArray files; + files.emplace_back(protocol::LSPObject{ + { "uri", file_uri }, + }); + + protocol::LSPObject params; + params["files"] = std::move(files); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/didRenameFiles") + { + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + protocol::LSPArray files; + files.emplace_back(protocol::LSPObject{ + { "oldUri", file_uri }, + { "newUri", file_uri }, + }); + + protocol::LSPObject params; + params["files"] = std::move(files); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/didChangeWatchedFiles") + { + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + protocol::LSPObject change; + change["uri"] = file_uri; + change["type"] = static_cast(protocol::FileChangeType::Changed); + + protocol::LSPArray changes; + changes.emplace_back(std::move(change)); + + protocol::LSPObject params; + params["changes"] = std::move(changes); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/didChangeConfiguration") + { + protocol::LSPObject tsl; + tsl["format"] = true; + + protocol::LSPObject settings; + settings["tsl"] = std::move(tsl); + + protocol::LSPObject params; + params["settings"] = std::move(settings); + return protocol::LSPAny(std::move(params)); + } + + if (method == "workspace/didChangeWorkspaceFolders") + { + auto workspace_uri = ToUri(FixturePath("workspace")); + + protocol::LSPArray added; + added.emplace_back(protocol::LSPObject{ + { "uri", workspace_uri }, + { "name", "workspace" }, + }); + + protocol::LSPObject event; + event["added"] = std::move(added); + event["removed"] = protocol::LSPArray{}; + + protocol::LSPObject params; + params["event"] = std::move(event); + return protocol::LSPAny(std::move(params)); + } + + if (method == "window/logMessage") + { + protocol::LSPObject params; + params["type"] = static_cast(protocol::MessageType::Log); + params["message"] = protocol::string("Test logMessage"); + return protocol::LSPAny(std::move(params)); + } + + if (method == "window/showMessage") + { + protocol::LSPObject params; + params["type"] = static_cast(protocol::MessageType::Info); + params["message"] = protocol::string("Test showMessage"); + return protocol::LSPAny(std::move(params)); + } + + if (method == "telemetry/event") + { + protocol::LSPObject params; + params["event"] = protocol::string("test_event"); + params["value"] = static_cast(1); + return protocol::LSPAny(std::move(params)); + } + + if (method == "textDocument/publishDiagnostics") + { + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + range.end.line = 0; + range.end.character = 1; + + protocol::LSPArray diagnostics; + diagnostics.emplace_back(protocol::LSPObject{ + { "range", ToRange(range) }, + { "message", protocol::string("Test diagnostic") }, + }); + + protocol::LSPObject params; + params["uri"] = protocol::string(uri); + params["version"] = static_cast(version); + params["diagnostics"] = std::move(diagnostics); + return protocol::LSPAny(std::move(params)); + } + + if (method == "initialized") + { + return protocol::LSPAny(protocol::LSPObject{}); + } + + if (method == "exit") + { + return protocol::LSPAny(protocol::LSPObject{}); + } + + return std::nullopt; + } + + protocol::LSPArray BuildDiagnosticsFromSyntaxErrors(TSTree* tree, const std::string& content) + { + protocol::LSPArray diagnostics; + if (!tree) + { + return diagnostics; + } + + auto errors = language::ast::Deserializer::DiagnoseSyntax(ts_tree_root_node(tree), content); + diagnostics.reserve(errors.size()); + for (const auto& error : errors) + { + protocol::LSPObject diagnostic; + protocol::Range range{}; + range.start.line = error.location.start_line; + range.start.character = error.location.start_column; + range.end.line = error.location.end_line; + range.end.character = error.location.end_column; + + diagnostic["range"] = ToRange(range); + diagnostic["message"] = error.message; + diagnostics.emplace_back(std::move(diagnostic)); + } + return diagnostics; + } + } + + void JsonProviderCoverageTests::Register(TestRunner& runner) + { + runner.addTest("json provider coverage (all providers)", TestAllProvidersJsonCoverage); + } + + TestResult JsonProviderCoverageTests::TestAllProvidersJsonCoverage() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto workspace_uri = ToUri(FixturePath("workspace")); + + auto main_path = FixturePath("main_unit.tsf"); + auto main_content = ReadTextFile(main_path); + auto main_uri = ToUri(main_path); + + auto type_hierarchy_path = FixturePath("type_hierarchy_unit.tsf"); + auto type_hierarchy_content = ReadTextFile(type_hierarchy_path); + auto type_hierarchy_uri = ToUri(type_hierarchy_path); + + auto inlay_hint_path = FixturePath("inlay_hint_case.tsl"); + auto inlay_hint_content = ReadTextFile(inlay_hint_path); + auto inlay_hint_uri = ToUri(inlay_hint_path); + + auto rename_path = FixturePath("rename_case.tsl"); + auto rename_content = ReadTextFile(rename_path); + auto rename_uri = ToUri(rename_path); + + auto code_action_path = FixturePath("code_action_missing_semicolon.tsl"); + auto code_action_content = ReadTextFile(code_action_path); + auto code_action_uri = ToUri(code_action_path); + + { + protocol::LSPArray folders; + folders.emplace_back(protocol::LSPObject{ + { "uri", workspace_uri }, + { "name", "workspace" }, + }); + + protocol::LSPObject init_params; + init_params["trace"] = protocol::string(protocol::TraceValueLiterals::Off); + init_params["workspaceFolders"] = std::move(folders); + + protocol::RequestMessage init_request; + init_request.id = "init"; + init_request.method = "initialize"; + init_request.params = protocol::LSPAny(std::move(init_params)); + + auto init_json = SerializeOrThrow(init_request); + auto parsed_init = DeserializeOrThrow(init_json); + + auto init_response_json = env.dispatcher.Dispatch(parsed_init); + auto init_response_any = codec::Deserialize(init_response_json); + assertTrue(init_response_any.has_value(), "Initialize response should be valid JSON"); + env.scheduler.WaitAll(); + } + + { + protocol::NotificationMessage initialized; + initialized.method = "initialized"; + initialized.params = protocol::LSPAny(protocol::LSPObject{}); + auto json = SerializeOrThrow(initialized); + auto parsed = DeserializeOrThrow(json); + env.dispatcher.Dispatch(parsed); + } + + auto send_notification = [&](std::string_view method, + const std::string& uri, + const std::string& content, + int version) { + protocol::NotificationMessage notification; + notification.method = std::string(method); + auto params = BuildNotificationParams(method, uri, content, version); + assertTrue(params.has_value(), "Missing notification params builder for: " + std::string(method)); + notification.params = *params; + + auto json = SerializeOrThrow(notification); + auto parsed = DeserializeOrThrow(json); + env.dispatcher.Dispatch(parsed); + }; + + send_notification("textDocument/didOpen", main_uri, main_content, 1); + send_notification("textDocument/didOpen", type_hierarchy_uri, type_hierarchy_content, 1); + send_notification("textDocument/didOpen", inlay_hint_uri, inlay_hint_content, 1); + send_notification("textDocument/didOpen", rename_uri, rename_content, 1); + send_notification("textDocument/didOpen", code_action_uri, code_action_content, 1); + send_notification("textDocument/didChange", main_uri, main_content, 2); + + auto code_action_tree = env.hub.parser().GetTree(code_action_uri); + auto code_action_diagnostics = BuildDiagnosticsFromSyntaxErrors(code_action_tree, code_action_content); + + env.scheduler.Submit("json_cancel_me", []() -> std::optional { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + return std::string("done"); + }); + + { + auto methods = env.dispatcher.GetSupportedNotifications(); + std::sort(methods.begin(), methods.end()); + + for (const auto& method : methods) + { + if (method == "exit" || + method == "textDocument/didOpen" || + method == "textDocument/didChange" || + method == "textDocument/didClose" || + method == "initialized") + { + continue; + } + + protocol::NotificationMessage notification; + notification.method = method; + + auto params = BuildNotificationParams(method, main_uri, main_content, 1); + assertTrue(params.has_value(), "Missing notification params builder for: " + method); + notification.params = *params; + + try + { + auto json = SerializeOrThrow(notification); + auto parsed = DeserializeOrThrow(json); + env.dispatcher.Dispatch(parsed); + } + catch (const std::exception& e) + { + throw std::runtime_error("Notification method failed: " + method + " (" + e.what() + ")"); + } + } + } + + env.scheduler.WaitAll(); + + SeededRequestParams seeded; + auto dispatch_request_result = [&](std::string_view method, protocol::LSPAny params) -> std::optional { + protocol::RequestMessage request; + request.id = protocol::string("seed_" + std::string(method)); + request.method = std::string(method); + request.params = std::move(params); + + try + { + auto json = SerializeOrThrow(request); + auto parsed = DeserializeOrThrow(json); + auto response_json = env.dispatcher.Dispatch(parsed); + + auto response = codec::Deserialize(response_json); + if (!response.has_value() || !response->result.has_value()) + { + return std::nullopt; + } + return response->result.value(); + } + catch (const std::exception&) + { + return std::nullopt; + } + }; + + if (auto result = dispatch_request_result("textDocument/codeLens", + protocol::LSPAny(protocol::LSPObject{ + { "textDocument", ToTextDocument(main_uri) }, + }))) + { + seeded.code_lens = FirstArrayItem(*result); + } + + if (auto result = dispatch_request_result("textDocument/documentLink", + protocol::LSPAny(protocol::LSPObject{ + { "textDocument", ToTextDocument(main_uri) }, + }))) + { + seeded.document_link = FirstArrayItem(*result); + } + + if (auto result = dispatch_request_result("textDocument/inlayHint", + protocol::LSPAny(protocol::LSPObject{ + { "textDocument", ToTextDocument(inlay_hint_uri) }, + { "range", ToRange(FullDocumentRange(inlay_hint_content)) }, + }))) + { + seeded.inlay_hint = FirstArrayItem(*result); + } + + auto call_incoming_pos = FindPosition(main_content, "UnitFunc(1);", false); + if (auto result = dispatch_request_result("textDocument/prepareCallHierarchy", + protocol::LSPAny(protocol::LSPObject{ + { "textDocument", ToTextDocument(main_uri) }, + { "position", ToPosition(call_incoming_pos) }, + }))) + { + seeded.call_hierarchy_incoming_item = FirstArrayItem(*result); + } + + auto call_outgoing_pos = FindPosition(main_content, "TestDefinitions();", false); + if (auto result = dispatch_request_result("textDocument/prepareCallHierarchy", + protocol::LSPAny(protocol::LSPObject{ + { "textDocument", ToTextDocument(main_uri) }, + { "position", ToPosition(call_outgoing_pos) }, + }))) + { + seeded.call_hierarchy_outgoing_item = FirstArrayItem(*result); + } + + auto type_pos = FindPosition(type_hierarchy_content, "Mid = class", false); + if (auto result = dispatch_request_result("textDocument/prepareTypeHierarchy", + protocol::LSPAny(protocol::LSPObject{ + { "textDocument", ToTextDocument(type_hierarchy_uri) }, + { "position", ToPosition(type_pos) }, + }))) + { + seeded.type_hierarchy_item = FirstArrayItem(*result); + } + + if (auto result = dispatch_request_result("workspace/symbol", + protocol::LSPAny(protocol::LSPObject{ + { "query", protocol::string("Workspace") }, + }))) + { + seeded.workspace_symbol = FirstArrayItem(*result); + } + + if (auto result = dispatch_request_result("textDocument/codeAction", + protocol::LSPAny(protocol::LSPObject{ + { "textDocument", ToTextDocument(code_action_uri) }, + { "range", ToRange(FullDocumentRange(code_action_content)) }, + { "context", protocol::LSPObject{ { "diagnostics", code_action_diagnostics } } }, + }))) + { + seeded.code_action = FirstArrayItem(*result); + } + + { + auto methods = env.dispatcher.GetSupportedRequests(); + std::sort(methods.begin(), methods.end()); + + protocol::integer request_counter = 1; + for (const auto& method : methods) + { + if (method == "initialize" || method == "shutdown") + { + continue; + } + + protocol::RequestMessage request; + request.id = "req_" + std::to_string(++request_counter); + request.method = method; + + auto params = BuildRequestParams(method, + seeded, + main_uri, + main_content, + rename_uri, + rename_content, + code_action_uri, + code_action_content, + code_action_diagnostics); + assertTrue(params.has_value(), "Missing request params builder for: " + method); + request.params = *params; + + try + { + auto json = SerializeOrThrow(request); + auto parsed = DeserializeOrThrow(json); + auto response_json = env.dispatcher.Dispatch(parsed); + auto response = codec::Deserialize(response_json); + assertTrue(response.has_value(), "Request response should deserialize"); + assertFalse(response->error.has_value(), "Request should not return error for: " + method); + } + catch (const std::exception& e) + { + throw std::runtime_error("Request method failed: " + method + " (" + e.what() + ")"); + } + } + } + + send_notification("textDocument/didClose", main_uri, "", 0); + send_notification("textDocument/didClose", rename_uri, "", 0); + send_notification("textDocument/didClose", code_action_uri, "", 0); + + { + protocol::RequestMessage shutdown; + shutdown.id = "shutdown"; + shutdown.method = "shutdown"; + shutdown.params = protocol::LSPAny(protocol::LSPObject{}); + + auto json = SerializeOrThrow(shutdown); + auto parsed = DeserializeOrThrow(json); + auto response_json = env.dispatcher.Dispatch(parsed); + auto response_any = codec::Deserialize(response_json); + assertTrue(response_any.has_value(), "Shutdown response should be valid JSON"); + } + + env.scheduler.WaitAll(); + return result; + } +} diff --git a/lsp-server/test/test_provider/provider_misc_test.cppm b/lsp-server/test/test_provider/provider_misc_test.cppm index 9a87d24..2d9c89a 100644 --- a/lsp-server/test/test_provider/provider_misc_test.cppm +++ b/lsp-server/test/test_provider/provider_misc_test.cppm @@ -12,12 +12,71 @@ import lsp.provider.initialized.initialized; import lsp.provider.text_document.did_open; import lsp.provider.text_document.did_change; import lsp.provider.text_document.did_close; +import lsp.provider.text_document.code_action; +import lsp.provider.text_document.code_lens; +import lsp.provider.text_document.color_presentation; +import lsp.provider.text_document.document_color; +import lsp.provider.text_document.document_highlight; +import lsp.provider.text_document.document_symbol; +import lsp.provider.text_document.folding_range; +import lsp.provider.text_document.hover; +import lsp.provider.text_document.implementation; +import lsp.provider.text_document.prepare_call_hierarchy; +import lsp.provider.text_document.prepare_type_hierarchy; +import lsp.provider.text_document.prepare_rename; import lsp.provider.text_document.rename; +import lsp.provider.text_document.linked_editing_range; import lsp.provider.text_document.references; +import lsp.provider.text_document.selection_range; import lsp.provider.text_document.semantic_tokens; -import lsp.provider.workspace.symbol; +import lsp.provider.text_document.signature_help; +import lsp.provider.text_document.type_definition; +import lsp.provider.text_document.diagnostic; +import lsp.provider.text_document.document_link; +import lsp.provider.text_document.inlay_hint; +import lsp.provider.text_document.formatting; +import lsp.provider.text_document.range_formatting; +import lsp.provider.text_document.on_type_formatting; +import lsp.provider.text_document.inline_value; +import lsp.provider.text_document.moniker; +import lsp.provider.call_hierarchy.incoming_calls; +import lsp.provider.call_hierarchy.outgoing_calls; +import lsp.provider.type_hierarchy.subtypes; +import lsp.provider.type_hierarchy.supertypes; +import lsp.provider.code_action.resolve; +import lsp.provider.code_lens.resolve; +import lsp.provider.document_link.resolve; +import lsp.provider.inlay_hint.resolve; import lsp.provider.client.register_capability; import lsp.provider.client.unregister_capability; +import lsp.provider.window.work_done_progress_create; +import lsp.provider.window.show_message_request; +import lsp.provider.window.show_document; +import lsp.provider.window.log_message; +import lsp.provider.window.show_message; +import lsp.provider.telemetry.event; +import lsp.provider.workspace.symbol; +import lsp.provider.workspace.diagnostic; +import lsp.provider.workspace.did_create_files; +import lsp.provider.workspace.did_delete_files; +import lsp.provider.workspace.did_rename_files; +import lsp.provider.workspace.did_change_watched_files; +import lsp.provider.workspace.did_change_configuration; +import lsp.provider.workspace.did_change_workspace_folders; +import lsp.provider.workspace.configuration; +import lsp.provider.workspace.apply_edit; +import lsp.provider.workspace.workspace_folders; +import lsp.provider.workspace.code_lens_refresh; +import lsp.provider.workspace.diagnostic_refresh; +import lsp.provider.workspace.inlay_hint_refresh; +import lsp.provider.workspace.inline_value_refresh; +import lsp.provider.workspace.semantic_tokens_refresh; +import lsp.provider.workspace.execute_command; +import lsp.provider.workspace.will_create_files; +import lsp.provider.workspace.will_delete_files; +import lsp.provider.workspace.will_rename_files; +import lsp.provider.workspace_symbol.resolve; +import lsp.provider.text_document.publish_diagnostics; import lsp.provider.shutdown.shutdown; import lsp.provider.cancel_request.cancel_request; import lsp.provider.trace.set_trace; @@ -28,6 +87,8 @@ import lsp.manager.symbol; import lsp.scheduler.async_executor; import lsp.protocol; import lsp.codec.facade; +import lsp.language.ast; +import tree_sitter; import lsp.test.provider.fixtures; export namespace lsp::test::provider @@ -41,13 +102,64 @@ export namespace lsp::test::provider static TestResult TestInitializeProvider(); static TestResult TestInitializedNotification(); static TestResult TestDidOpenDidChangeDidClose(); + static TestResult TestHoverProvider(); static TestResult TestRenameProvider(); + static TestResult TestPrepareRenameProvider(); static TestResult TestRenameInvalidName(); + static TestResult TestLinkedEditingRangeProvider(); static TestResult TestReferencesProvider(); + static TestResult TestDocumentHighlightProvider(); + static TestResult TestImplementationProvider(); + static TestResult TestTypeDefinitionProvider(); + static TestResult TestDocumentSymbolProvider(); + static TestResult TestDocumentLinkProvider(); + static TestResult TestDocumentLinkResolveProvider(); + static TestResult TestFoldingRangeProvider(); + static TestResult TestSelectionRangeProvider(); + static TestResult TestDocumentColorProvider(); + static TestResult TestColorPresentationProvider(); + static TestResult TestTextDocumentDiagnosticProvider(); + static TestResult TestInlayHintProvider(); + static TestResult TestInlayHintResolveProvider(); + static TestResult TestCodeLensProvider(); + static TestResult TestCodeLensResolveProvider(); + static TestResult TestPrepareCallHierarchyProvider(); + static TestResult TestCallHierarchyIncomingCallsProvider(); + static TestResult TestCallHierarchyOutgoingCallsProvider(); + static TestResult TestPrepareTypeHierarchyProvider(); + static TestResult TestTypeHierarchySupertypesProvider(); + static TestResult TestTypeHierarchySubtypesProvider(); + static TestResult TestDidCreateFilesProvider(); + static TestResult TestDidDeleteFilesProvider(); + static TestResult TestDidRenameFilesProvider(); + static TestResult TestDidChangeWatchedFilesProvider(); + static TestResult TestDidChangeConfigurationProvider(); + static TestResult TestDidChangeWorkspaceFoldersProvider(); static TestResult TestWorkspaceSymbolProvider(); + static TestResult TestWorkspaceDiagnosticProvider(); + static TestResult TestWorkspaceConfigurationProvider(); + static TestResult TestWorkspaceApplyEditProvider(); + static TestResult TestWorkspaceWorkspaceFoldersProvider(); + static TestResult TestWorkspaceRefreshProviders(); + static TestResult TestClientCapabilityProviders(); + static TestResult TestWindowWorkDoneProgressCreateProvider(); + static TestResult TestWindowShowMessageRequestProvider(); + static TestResult TestWindowShowDocumentProvider(); + static TestResult TestWindowMessageNotifications(); + static TestResult TestTelemetryEventNotification(); + static TestResult TestPublishDiagnosticsNotification(); static TestResult TestSemanticTokensProvider(); - static TestResult TestRegisterCapabilityProvider(); - static TestResult TestUnregisterCapabilityProvider(); + static TestResult TestSignatureHelpProvider(); + static TestResult TestCodeActionProvider(); + static TestResult TestCodeActionResolveProvider(); + static TestResult TestDocumentFormattingProvider(); + static TestResult TestDocumentRangeFormattingProvider(); + static TestResult TestDocumentOnTypeFormattingProvider(); + static TestResult TestInlineValueProvider(); + static TestResult TestMonikerProvider(); + static TestResult TestExecuteCommandProvider(); + static TestResult TestWillFileOperationsProviders(); + static TestResult TestWorkspaceSymbolResolveProvider(); static TestResult TestShutdownProvider(); static TestResult TestCancelRequestProvider(); static TestResult TestSetTraceProvider(); @@ -107,6 +219,78 @@ namespace lsp::test::provider return result; } + protocol::LSPObject ToPositionObject(const protocol::Position& pos) + { + return protocol::LSPObject{ + { "line", static_cast(pos.line) }, + { "character", static_cast(pos.character) }, + }; + } + + protocol::LSPObject ToRangeObject(const protocol::Range& range) + { + return protocol::LSPObject{ + { "start", ToPositionObject(range.start) }, + { "end", ToPositionObject(range.end) }, + }; + } + + protocol::Range FullDocumentRange(const std::string& content) + { + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + + protocol::uinteger line_count = 0; + protocol::uinteger last_line_len = 0; + for (char ch : content) + { + if (ch == '\n') + { + line_count++; + last_line_len = 0; + } + else + { + last_line_len++; + } + } + + range.end.line = line_count; + range.end.character = last_line_len; + return range; + } + + std::optional GetUInteger(const protocol::LSPAny& any) + { + if (any.Is()) + { + return any.Get(); + } + if (any.Is()) + { + return static_cast(any.Get()); + } + return std::nullopt; + } + + std::optional GetDecimal(const protocol::LSPAny& any) + { + if (any.Is()) + { + return any.Get(); + } + if (any.Is()) + { + return static_cast(any.Get()); + } + if (any.Is()) + { + return static_cast(any.Get()); + } + return std::nullopt; + } + void OpenDocument(manager::ManagerHub& hub, const std::string& uri, const std::string& text, int version) { protocol::DidOpenTextDocumentParams open_params; @@ -123,13 +307,64 @@ namespace lsp::test::provider runner.addTest("initialize provider", TestInitializeProvider); runner.addTest("initialized notification", TestInitializedNotification); runner.addTest("didOpen/didChange/didClose", TestDidOpenDidChangeDidClose); + runner.addTest("hover provider", TestHoverProvider); runner.addTest("rename provider", TestRenameProvider); + runner.addTest("prepareRename provider", TestPrepareRenameProvider); runner.addTest("rename invalid name", TestRenameInvalidName); + runner.addTest("linkedEditingRange provider", TestLinkedEditingRangeProvider); runner.addTest("references provider", TestReferencesProvider); + runner.addTest("documentHighlight provider", TestDocumentHighlightProvider); + runner.addTest("implementation provider", TestImplementationProvider); + runner.addTest("typeDefinition provider", TestTypeDefinitionProvider); + runner.addTest("documentSymbol provider", TestDocumentSymbolProvider); + runner.addTest("documentLink provider", TestDocumentLinkProvider); + runner.addTest("documentLink resolve provider", TestDocumentLinkResolveProvider); + runner.addTest("foldingRange provider", TestFoldingRangeProvider); + runner.addTest("selectionRange provider", TestSelectionRangeProvider); + runner.addTest("documentColor provider", TestDocumentColorProvider); + runner.addTest("colorPresentation provider", TestColorPresentationProvider); + runner.addTest("textDocument diagnostic provider", TestTextDocumentDiagnosticProvider); + runner.addTest("inlayHint provider", TestInlayHintProvider); + runner.addTest("inlayHint resolve provider", TestInlayHintResolveProvider); + runner.addTest("codeLens provider", TestCodeLensProvider); + runner.addTest("codeLens resolve provider", TestCodeLensResolveProvider); + runner.addTest("prepareCallHierarchy provider", TestPrepareCallHierarchyProvider); + runner.addTest("callHierarchy incomingCalls provider", TestCallHierarchyIncomingCallsProvider); + runner.addTest("callHierarchy outgoingCalls provider", TestCallHierarchyOutgoingCallsProvider); + runner.addTest("prepareTypeHierarchy provider", TestPrepareTypeHierarchyProvider); + runner.addTest("typeHierarchy supertypes provider", TestTypeHierarchySupertypesProvider); + runner.addTest("typeHierarchy subtypes provider", TestTypeHierarchySubtypesProvider); + runner.addTest("didCreateFiles notification", TestDidCreateFilesProvider); + runner.addTest("didDeleteFiles notification", TestDidDeleteFilesProvider); + runner.addTest("didRenameFiles notification", TestDidRenameFilesProvider); + runner.addTest("didChangeWatchedFiles notification", TestDidChangeWatchedFilesProvider); + runner.addTest("didChangeConfiguration notification", TestDidChangeConfigurationProvider); + runner.addTest("didChangeWorkspaceFolders notification", TestDidChangeWorkspaceFoldersProvider); runner.addTest("workspace symbol provider", TestWorkspaceSymbolProvider); + runner.addTest("workspace diagnostic provider", TestWorkspaceDiagnosticProvider); + runner.addTest("workspace configuration provider", TestWorkspaceConfigurationProvider); + runner.addTest("workspace applyEdit provider", TestWorkspaceApplyEditProvider); + runner.addTest("workspace workspaceFolders provider", TestWorkspaceWorkspaceFoldersProvider); + runner.addTest("workspace refresh providers", TestWorkspaceRefreshProviders); + runner.addTest("client capability providers", TestClientCapabilityProviders); + runner.addTest("window workDoneProgressCreate provider", TestWindowWorkDoneProgressCreateProvider); + runner.addTest("window showMessageRequest provider", TestWindowShowMessageRequestProvider); + runner.addTest("window showDocument provider", TestWindowShowDocumentProvider); + runner.addTest("window message notifications", TestWindowMessageNotifications); + runner.addTest("telemetry event notification", TestTelemetryEventNotification); + runner.addTest("publish diagnostics notification", TestPublishDiagnosticsNotification); runner.addTest("semantic tokens provider", TestSemanticTokensProvider); - runner.addTest("register capability provider", TestRegisterCapabilityProvider); - runner.addTest("unregister capability provider", TestUnregisterCapabilityProvider); + runner.addTest("signature help provider", TestSignatureHelpProvider); + runner.addTest("code action provider", TestCodeActionProvider); + runner.addTest("codeAction/resolve provider", TestCodeActionResolveProvider); + runner.addTest("document formatting provider", TestDocumentFormattingProvider); + runner.addTest("document range formatting provider", TestDocumentRangeFormattingProvider); + runner.addTest("document onType formatting provider", TestDocumentOnTypeFormattingProvider); + runner.addTest("inline value provider", TestInlineValueProvider); + runner.addTest("moniker provider", TestMonikerProvider); + runner.addTest("workspace executeCommand provider", TestExecuteCommandProvider); + runner.addTest("workspace will file operations providers", TestWillFileOperationsProviders); + runner.addTest("workspaceSymbol/resolve provider", TestWorkspaceSymbolResolveProvider); runner.addTest("shutdown provider", TestShutdownProvider); runner.addTest("cancel request provider", TestCancelRequestProvider); runner.addTest("setTrace provider", TestSetTraceProvider); @@ -184,6 +419,102 @@ namespace lsp::test::provider "Initialize should enable completion resolve"); } + assertTrue(capabilities.find("definitionProvider") != capabilities.end(), "Initialize should enable definitionProvider"); + assertTrue(capabilities.find("typeDefinitionProvider") != capabilities.end(), "Initialize should enable typeDefinitionProvider"); + assertTrue(capabilities.find("implementationProvider") != capabilities.end(), "Initialize should enable implementationProvider"); + assertTrue(capabilities.find("hoverProvider") != capabilities.end(), "Initialize should enable hoverProvider"); + assertTrue(capabilities.find("signatureHelpProvider") != capabilities.end(), "Initialize should enable signatureHelpProvider"); + assertTrue(capabilities.find("codeActionProvider") != capabilities.end(), "Initialize should enable codeActionProvider"); + auto call_hierarchy_it = capabilities.find("callHierarchyProvider"); + assertTrue(call_hierarchy_it != capabilities.end(), "Initialize should enable callHierarchyProvider"); + if (call_hierarchy_it != capabilities.end()) + { + assertTrue(call_hierarchy_it->second.Is() && call_hierarchy_it->second.Get(), + "callHierarchyProvider should be enabled"); + } + auto type_hierarchy_it = capabilities.find("typeHierarchyProvider"); + assertTrue(type_hierarchy_it != capabilities.end(), "Initialize should enable typeHierarchyProvider"); + if (type_hierarchy_it != capabilities.end()) + { + assertTrue(type_hierarchy_it->second.Is() && type_hierarchy_it->second.Get(), + "typeHierarchyProvider should be enabled"); + } + assertTrue(capabilities.find("referencesProvider") != capabilities.end(), "Initialize should enable referencesProvider"); + assertTrue(capabilities.find("documentHighlightProvider") != capabilities.end(), "Initialize should enable documentHighlightProvider"); + assertTrue(capabilities.find("documentSymbolProvider") != capabilities.end(), "Initialize should enable documentSymbolProvider"); + assertTrue(capabilities.find("workspaceSymbolProvider") != capabilities.end(), "Initialize should enable workspaceSymbolProvider"); + assertTrue(capabilities.find("semanticTokensProvider") != capabilities.end(), "Initialize should enable semanticTokensProvider"); + auto document_link_it = capabilities.find("documentLinkProvider"); + assertTrue(document_link_it != capabilities.end(), "Initialize should enable documentLinkProvider"); + if (document_link_it != capabilities.end() && document_link_it->second.Is()) + { + const auto& document_link = document_link_it->second.Get(); + auto resolve_it = document_link.find("resolveProvider"); + assertTrue(resolve_it != document_link.end(), "documentLinkProvider should include resolveProvider"); + assertTrue(resolve_it->second.Is() && resolve_it->second.Get(), + "documentLinkProvider should enable resolveProvider"); + } + assertTrue(capabilities.find("foldingRangeProvider") != capabilities.end(), "Initialize should enable foldingRangeProvider"); + assertTrue(capabilities.find("selectionRangeProvider") != capabilities.end(), "Initialize should enable selectionRangeProvider"); + auto inlay_hint_it = capabilities.find("inlayHintProvider"); + assertTrue(inlay_hint_it != capabilities.end(), "Initialize should enable inlayHintProvider"); + if (inlay_hint_it != capabilities.end() && inlay_hint_it->second.Is()) + { + const auto& inlay_hint = inlay_hint_it->second.Get(); + auto resolve_it = inlay_hint.find("resolveProvider"); + assertTrue(resolve_it != inlay_hint.end(), "inlayHintProvider should include resolveProvider"); + assertTrue(resolve_it->second.Is() && resolve_it->second.Get(), + "inlayHintProvider should enable resolveProvider"); + } + + auto code_lens_it = capabilities.find("codeLensProvider"); + assertTrue(code_lens_it != capabilities.end(), "Initialize should enable codeLensProvider"); + if (code_lens_it != capabilities.end() && code_lens_it->second.Is()) + { + const auto& code_lens = code_lens_it->second.Get(); + auto resolve_it = code_lens.find("resolveProvider"); + assertTrue(resolve_it != code_lens.end(), "codeLensProvider should include resolveProvider"); + assertTrue(resolve_it->second.Is() && resolve_it->second.Get(), + "codeLensProvider should enable resolveProvider"); + } + + auto rename_it = capabilities.find("renameProvider"); + assertTrue(rename_it != capabilities.end(), "Initialize should enable renameProvider"); + if (rename_it != capabilities.end() && rename_it->second.Is()) + { + const auto& rename = rename_it->second.Get(); + auto prepare_it = rename.find("prepareProvider"); + assertTrue(prepare_it != rename.end(), "renameProvider should include prepareProvider"); + assertTrue(prepare_it->second.Is() && prepare_it->second.Get(), + "renameProvider should enable prepareProvider"); + } + + auto workspace_it = capabilities.find("workspace"); + assertTrue(workspace_it != capabilities.end(), "Initialize should include workspace capabilities"); + if (workspace_it != capabilities.end() && workspace_it->second.Is()) + { + const auto& workspace = workspace_it->second.Get(); + auto file_ops_it = workspace.find("fileOperations"); + assertTrue(file_ops_it != workspace.end(), "workspace should include fileOperations"); + assertTrue(file_ops_it->second.Is(), "fileOperations should be an object"); + const auto& file_ops = file_ops_it->second.Get(); + + auto did_create_it = file_ops.find("didCreate"); + assertTrue(did_create_it != file_ops.end(), "fileOperations should include didCreate"); + assertTrue(did_create_it->second.Is() && did_create_it->second.Get(), + "fileOperations.didCreate should be enabled"); + + auto did_delete_it = file_ops.find("didDelete"); + assertTrue(did_delete_it != file_ops.end(), "fileOperations should include didDelete"); + assertTrue(did_delete_it->second.Is() && did_delete_it->second.Get(), + "fileOperations.didDelete should be enabled"); + + auto did_rename_it = file_ops.find("didRename"); + assertTrue(did_rename_it != file_ops.end(), "fileOperations should include didRename"); + assertTrue(did_rename_it->second.Is() && did_rename_it->second.Get(), + "fileOperations.didRename should be enabled"); + } + env.scheduler.WaitAll(); auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Module); bool found_workspace = std::any_of(indexed.begin(), indexed.end(), [](const manager::Symbol::IndexedSymbol& item) { @@ -196,6 +527,37 @@ namespace lsp::test::provider return result; } + TestResult ProviderMiscTests::TestHoverProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::HoverParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "UnitFunc(1);"); + + protocol::RequestMessage request; + request.id = "hover"; + request.method = "textDocument/hover"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::Hover provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "Hover should return result"); + assertFalse(response.result->Is(), "Hover result should not be null"); + + auto hover = codec::FromLSPAny.template operator()(response.result.value()); + assertTrue(hover.contents.value.find("UnitFunc") != std::string::npos, "Hover contents should mention UnitFunc"); + assertTrue(hover.range.has_value(), "Hover should include range"); + return result; + } + TestResult ProviderMiscTests::TestInitializedNotification() { TestResult result{ "", true, "ok" }; @@ -297,6 +659,38 @@ namespace lsp::test::provider return result; } + TestResult ProviderMiscTests::TestPrepareRenameProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("rename_case.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::PrepareRenameParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "target := target"); + + protocol::RequestMessage request; + request.id = "prep"; + request.method = "textDocument/prepareRename"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::PrepareRename provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "PrepareRename should return result"); + assertFalse(response.result->Is(), "PrepareRename result should not be null"); + + auto range = codec::FromLSPAny.template operator()(response.result.value()); + assertEqual(std::uint32_t(1), range.start.line, "PrepareRename should return range on assignment line"); + assertEqual(std::uint32_t(0), range.start.character, "PrepareRename should start at identifier"); + assertEqual(std::uint32_t(6), range.end.character, "PrepareRename should cover full identifier"); + return result; + } + TestResult ProviderMiscTests::TestRenameInvalidName() { TestResult result{ "", true, "ok" }; @@ -327,6 +721,37 @@ namespace lsp::test::provider return result; } + TestResult ProviderMiscTests::TestLinkedEditingRangeProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("rename_case.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::LinkedEditingRangeParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "target := target"); + + protocol::RequestMessage request; + request.id = "linked_editing"; + request.method = "textDocument/linkedEditingRange"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::LinkedEditingRange provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "LinkedEditingRange should not return error"); + assertTrue(response.result.has_value(), "LinkedEditingRange should return result"); + + auto linked = codec::FromLSPAny.template operator()(response.result.value()); + assertEqual(std::size_t(3), linked.ranges.size(), "LinkedEditingRange should include occurrences (incl decl)"); + return result; + } + TestResult ProviderMiscTests::TestReferencesProvider() { TestResult result{ "", true, "ok" }; @@ -351,7 +776,1610 @@ namespace lsp::test::provider auto json = provider.ProvideResponse(request, env.context); auto response = ParseResponse(json); auto locations = codec::FromLSPAny.template operator()>(response.result.value()); - assertEqual(std::size_t(0), locations.size(), "References provider currently returns empty list"); + assertEqual(std::size_t(3), locations.size(), "References provider should return all occurrences (incl decl)"); + return result; + } + + TestResult ProviderMiscTests::TestDocumentHighlightProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("rename_case.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::DocumentHighlightParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "target := target"); + + protocol::RequestMessage request; + request.id = "hl"; + request.method = "textDocument/documentHighlight"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::DocumentHighlight provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + auto highlights = codec::FromLSPAny.template operator()>(response.result.value()); + + std::size_t reads = 0; + std::size_t writes = 0; + std::size_t texts = 0; + for (const auto& highlight : highlights) + { + switch (highlight.kind) + { + case protocol::DocumentHighlightKind::Read: + ++reads; + break; + case protocol::DocumentHighlightKind::Write: + ++writes; + break; + case protocol::DocumentHighlightKind::Text: + ++texts; + break; + } + } + + assertEqual(std::size_t(1), reads, "DocumentHighlight should include one read reference"); + assertEqual(std::size_t(1), writes, "DocumentHighlight should include one write reference"); + assertEqual(std::size_t(1), texts, "DocumentHighlight should include declaration"); + return result; + } + + TestResult ProviderMiscTests::TestImplementationProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::TextDocumentPositionParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "function UnitFunc(a: integer): integer;"); + params.position.character += static_cast(std::string("function ").size()); + + protocol::RequestMessage request; + request.id = "impl_func"; + request.method = "textDocument/implementation"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::Implementation provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "Implementation should not return error"); + assertTrue(response.result.has_value(), "Implementation should return result"); + + auto location = codec::FromLSPAny.template operator()>(response.result.value()); + assertTrue(location.has_value(), "Implementation should resolve function implementation"); + + auto expected = FindPosition(content, "function UnitFunc(a: integer): integer;\nbegin"); + expected.character += static_cast(std::string("function ").size()); + + assertEqual(uri, location->uri, "Implementation location URI mismatch"); + assertEqual(expected.line, location->range.start.line, "Implementation start line mismatch"); + assertEqual(expected.character, location->range.start.character, "Implementation start character mismatch"); + + protocol::TextDocumentPositionParams method_params; + method_params.textDocument.uri = uri; + method_params.position = FindPosition(content, "function Foo(x: integer): integer;"); + method_params.position.character += static_cast(std::string("function ").size()); + + protocol::RequestMessage method_request; + method_request.id = "impl_method"; + method_request.method = "textDocument/implementation"; + method_request.params = codec::ToLSPAny(method_params); + + auto method_json = provider.ProvideResponse(method_request, env.context); + auto method_response = ParseResponse(method_json); + assertFalse(method_response.error.has_value(), "Method implementation should not return error"); + assertTrue(method_response.result.has_value(), "Method implementation should return result"); + + auto method_location = + codec::FromLSPAny.template operator()>(method_response.result.value()); + assertTrue(method_location.has_value(), "Implementation should resolve method implementation"); + + auto method_expected = FindPosition(content, "function Widget.Foo(x: integer): integer;\nbegin"); + method_expected.character += static_cast(std::string("function ").size()); + + assertEqual(uri, method_location->uri, "Method implementation location URI mismatch"); + assertEqual(method_expected.line, method_location->range.start.line, "Method implementation start line mismatch"); + assertEqual(method_expected.character, + method_location->range.start.character, + "Method implementation start character mismatch"); + + return result; + } + + TestResult ProviderMiscTests::TestTypeDefinitionProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::TextDocumentPositionParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "obj: Widget"); + + protocol::RequestMessage request; + request.id = "type_def"; + request.method = "textDocument/typeDefinition"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::TypeDefinition provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "TypeDefinition should not return error"); + assertTrue(response.result.has_value(), "TypeDefinition should return result"); + + auto location = codec::FromLSPAny.template operator()>(response.result.value()); + assertTrue(location.has_value(), "TypeDefinition should resolve class type definition"); + + auto expected = FindPosition(content, "type Widget = class"); + expected.character += static_cast(std::string("type ").size()); + + assertEqual(uri, location->uri, "TypeDefinition location URI mismatch"); + assertEqual(expected.line, location->range.start.line, "TypeDefinition start line mismatch"); + assertEqual(expected.character, location->range.start.character, "TypeDefinition start character mismatch"); + return result; + } + + TestResult ProviderMiscTests::TestDocumentSymbolProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::LSPObject params; + params["textDocument"] = protocol::LSPObject{ { "uri", uri } }; + + protocol::RequestMessage request; + request.id = "doc_symbols"; + request.method = "textDocument/documentSymbol"; + request.params = protocol::LSPAny(params); + + ::lsp::provider::text_document::DocumentSymbol provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + auto symbols = codec::FromLSPAny.template operator()>(response.result.value()); + assertTrue(!symbols.empty(), "DocumentSymbol should return symbols"); + + auto unit_it = std::find_if(symbols.begin(), symbols.end(), [](const protocol::DocumentSymbol& symbol) { + return symbol.name == "MainUnit"; + }); + assertTrue(unit_it != symbols.end(), "DocumentSymbol should include unit symbol"); + assertTrue(unit_it->children.has_value(), "Unit symbol should include children"); + + const auto& children = unit_it->children.value(); + bool found_widget = std::any_of(children.begin(), children.end(), [](const protocol::DocumentSymbol& symbol) { + return symbol.name == "Widget" && symbol.kind == protocol::SymbolKind::Class; + }); + bool found_unit_func = std::any_of(children.begin(), children.end(), [](const protocol::DocumentSymbol& symbol) { + return symbol.name == "UnitFunc" && symbol.kind == protocol::SymbolKind::Function; + }); + + assertTrue(found_widget, "Unit children should include Widget"); + assertTrue(found_unit_func, "Unit children should include UnitFunc"); + return result; + } + + TestResult ProviderMiscTests::TestDocumentLinkProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace"))); + env.hub.symbols().LoadSystemLibrary(FixturePath("system")); + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::DocumentLinkParams params; + params.textDocument.uri = uri; + + protocol::RequestMessage request; + request.id = "doc_link"; + request.method = "textDocument/documentLink"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::DocumentLink provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "DocumentLink should return result"); + assertTrue(response.result->Is(), "DocumentLink result should be array"); + + const auto& links = response.result->Get(); + bool found_workspace = false; + bool found_system = false; + for (const auto& link_any : links) + { + if (!link_any.Is()) + { + continue; + } + const auto& link = link_any.Get(); + auto target_it = link.find("target"); + if (target_it == link.end() || !target_it->second.Is()) + { + continue; + } + const auto& target = target_it->second.Get(); + if (target.find("WorkspaceUnit.tsf") != std::string::npos) + { + found_workspace = true; + } + if (target.find("SystemUnit.tsf") != std::string::npos) + { + found_system = true; + } + } + + assertTrue(found_workspace, "DocumentLink should resolve WorkspaceUnit"); + assertTrue(found_system, "DocumentLink should resolve SystemUnit"); + return result; + } + + TestResult ProviderMiscTests::TestDocumentLinkResolveProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace"))); + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + auto pos = FindPosition(content, "WorkspaceUnit"); + protocol::Range range; + range.start = pos; + range.end = pos; + range.end.character += static_cast(std::string("WorkspaceUnit").size()); + + protocol::LSPObject data; + data["kind"] = protocol::string("unit"); + data["name"] = protocol::string("WorkspaceUnit"); + data["baseUri"] = protocol::string(uri); + + protocol::LSPObject link; + link["range"] = codec::ToLSPAny(range); + link["data"] = protocol::LSPAny(std::move(data)); + + protocol::RequestMessage request; + request.id = "doc_link_resolve"; + request.method = "documentLink/resolve"; + request.params = protocol::LSPAny(std::move(link)); + + ::lsp::provider::document_link::Resolve provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "DocumentLink resolve should return result"); + assertTrue(response.result->Is(), "DocumentLink resolve result should be object"); + + const auto& resolved = response.result->Get(); + auto target_it = resolved.find("target"); + assertTrue(target_it != resolved.end(), "DocumentLink resolve should set target"); + assertTrue(target_it->second.Is(), "DocumentLink target should be string"); + assertTrue(target_it->second.Get().find("WorkspaceUnit.tsf") != std::string::npos, + "DocumentLink resolve should target WorkspaceUnit"); + return result; + } + + TestResult ProviderMiscTests::TestFoldingRangeProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::FoldingRangeParams params; + params.textDocument.uri = uri; + + protocol::RequestMessage request; + request.id = "folding"; + request.method = "textDocument/foldingRange"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::FoldingRange provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "FoldingRange should return result"); + assertTrue(response.result->Is(), "FoldingRange result should be array"); + + const auto& ranges = response.result->Get(); + assertTrue(!ranges.empty(), "FoldingRange should include ranges"); + + bool has_span = false; + for (const auto& range_any : ranges) + { + if (!range_any.Is()) + { + continue; + } + const auto& range_obj = range_any.Get(); + auto start_it = range_obj.find("startLine"); + auto end_it = range_obj.find("endLine"); + if (start_it == range_obj.end() || end_it == range_obj.end()) + { + continue; + } + auto start_line = GetUInteger(start_it->second); + auto end_line = GetUInteger(end_it->second); + if (start_line && end_line && *end_line > *start_line) + { + has_span = true; + break; + } + } + + assertTrue(has_span, "FoldingRange should include multi-line spans"); + return result; + } + + TestResult ProviderMiscTests::TestSelectionRangeProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::SelectionRangeParams params; + params.textDocument.uri = uri; + params.positions.push_back(FindPosition(content, "UnitFunc(1);")); + + protocol::RequestMessage request; + request.id = "select"; + request.method = "textDocument/selectionRange"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::SelectionRange provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "SelectionRange should return result"); + assertTrue(response.result->Is(), "SelectionRange result should be array"); + + const auto& ranges = response.result->Get(); + assertEqual(std::size_t(1), ranges.size(), "SelectionRange should return one entry"); + assertTrue(ranges[0].Is(), "SelectionRange entry should be object"); + + const auto& range_obj = ranges[0].Get(); + auto range_it = range_obj.find("range"); + assertTrue(range_it != range_obj.end(), "SelectionRange entry should include range"); + assertTrue(range_it->second.Is(), "SelectionRange range should be object"); + return result; + } + + TestResult ProviderMiscTests::TestDocumentColorProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("color_literals.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::DocumentColorParams params; + params.textDocument.uri = uri; + + protocol::RequestMessage request; + request.id = "doc_color"; + request.method = "textDocument/documentColor"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::DocumentColor provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "DocumentColor should not return error"); + assertTrue(response.result.has_value(), "DocumentColor should return result"); + assertTrue(response.result->Is(), "DocumentColor result should be array"); + + const auto& infos = response.result->Get(); + assertTrue(!infos.empty(), "DocumentColor should return matches"); + + auto red_pos = FindPosition(content, "#ff0000"); + bool found_red = false; + + for (const auto& info_any : infos) + { + if (!info_any.Is()) + { + continue; + } + + const auto& info = info_any.Get(); + auto range_it = info.find("range"); + auto color_it = info.find("color"); + if (range_it == info.end() || color_it == info.end()) + { + continue; + } + if (!range_it->second.Is() || !color_it->second.Is()) + { + continue; + } + + const auto& range = range_it->second.Get(); + auto start_it = range.find("start"); + if (start_it == range.end() || !start_it->second.Is()) + { + continue; + } + + const auto& start = start_it->second.Get(); + auto line_it = start.find("line"); + auto ch_it = start.find("character"); + if (line_it == start.end() || ch_it == start.end()) + { + continue; + } + + auto line = GetUInteger(line_it->second); + auto ch = GetUInteger(ch_it->second); + if (!line || !ch) + { + continue; + } + + if (*line != red_pos.line || *ch != red_pos.character) + { + continue; + } + + const auto& color = color_it->second.Get(); + auto red_it = color.find("red"); + auto green_it = color.find("green"); + auto blue_it = color.find("blue"); + auto alpha_it = color.find("alpha"); + assertTrue(red_it != color.end() && green_it != color.end() && blue_it != color.end() && alpha_it != color.end(), + "DocumentColor should include color components"); + + auto red = GetDecimal(red_it->second); + auto green = GetDecimal(green_it->second); + auto blue = GetDecimal(blue_it->second); + auto alpha = GetDecimal(alpha_it->second); + assertTrue(red && green && blue && alpha, "DocumentColor color components should be numeric"); + assertTrue(std::abs(*red - 1.0) < 1e-6, "DocumentColor should parse red channel"); + assertTrue(std::abs(*green) < 1e-6, "DocumentColor should parse green channel"); + assertTrue(std::abs(*blue) < 1e-6, "DocumentColor should parse blue channel"); + assertTrue(std::abs(*alpha - 1.0) < 1e-6, "DocumentColor should parse alpha channel"); + + found_red = true; + break; + } + + assertTrue(found_red, "DocumentColor should include #ff0000"); + return result; + } + + TestResult ProviderMiscTests::TestColorPresentationProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("color_literals.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + auto pos = FindPosition(content, "#ff0000"); + protocol::Range range; + range.start = pos; + range.end = pos; + range.end.character += 7; + + protocol::ColorPresentationParams params; + params.textDocument.uri = uri; + params.range = range; + params.color.red = 1.0; + params.color.green = 0.0; + params.color.blue = 0.0; + params.color.alpha = 1.0; + + protocol::RequestMessage request; + request.id = "color_presentation"; + request.method = "textDocument/colorPresentation"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::ColorPresentation provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "ColorPresentation should not return error"); + assertTrue(response.result.has_value(), "ColorPresentation should return result"); + assertTrue(response.result->Is(), "ColorPresentation result should be array"); + + const auto& presentations = response.result->Get(); + assertTrue(!presentations.empty(), "ColorPresentation should return entries"); + assertTrue(presentations[0].Is(), "ColorPresentation entry should be object"); + + const auto& entry = presentations[0].Get(); + auto label_it = entry.find("label"); + assertTrue(label_it != entry.end(), "ColorPresentation should include label"); + assertTrue(label_it->second.Is(), "ColorPresentation label should be string"); + assertEqual(std::string("#ff0000"), label_it->second.Get(), "ColorPresentation should format hex"); + + auto edit_it = entry.find("textEdit"); + assertTrue(edit_it != entry.end(), "ColorPresentation should include textEdit"); + assertTrue(edit_it->second.Is(), "ColorPresentation textEdit should be object"); + + const auto& edit_obj = edit_it->second.Get(); + auto new_text_it = edit_obj.find("newText"); + assertTrue(new_text_it != edit_obj.end(), "ColorPresentation textEdit should include newText"); + assertTrue(new_text_it->second.Is(), "ColorPresentation textEdit.newText should be string"); + assertEqual(std::string("#ff0000"), new_text_it->second.Get(), "ColorPresentation textEdit should match label"); + return result; + } + + TestResult ProviderMiscTests::TestTextDocumentDiagnosticProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("code_action_missing_semicolon.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::DiagnosticParams params; + params.textDocument.uri = uri; + + protocol::RequestMessage request; + request.id = "doc_diag"; + request.method = "textDocument/diagnostic"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::Diagnostic provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "Diagnostic should not return error"); + assertTrue(response.result.has_value(), "Diagnostic should return result"); + assertTrue(response.result->Is(), "Diagnostic result should be object"); + + const auto& report = response.result->Get(); + auto kind_it = report.find("kind"); + assertTrue(kind_it != report.end(), "Diagnostic report should include kind"); + assertTrue(kind_it->second.Is(), "Diagnostic kind should be string"); + assertEqual(std::string("full"), kind_it->second.Get(), "Diagnostic kind should be full"); + + auto items_it = report.find("items"); + assertTrue(items_it != report.end(), "Diagnostic report should include items"); + assertTrue(items_it->second.Is(), "Diagnostic items should be array"); + assertTrue(!items_it->second.Get().empty(), "Diagnostic items should not be empty"); + return result; + } + + TestResult ProviderMiscTests::TestInlayHintProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::LSPObject params; + params["textDocument"] = protocol::LSPObject{ { "uri", uri } }; + params["range"] = ToRangeObject(FullDocumentRange(content)); + + protocol::RequestMessage request; + request.id = "inlay"; + request.method = "textDocument/inlayHint"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::text_document::InlayHint provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "InlayHint should return result"); + assertTrue(response.result->Is(), "InlayHint result should be array"); + + const auto& hints = response.result->Get(); + bool found_param = false; + for (const auto& hint_any : hints) + { + if (!hint_any.Is()) + { + continue; + } + const auto& hint = hint_any.Get(); + auto label_it = hint.find("label"); + auto kind_it = hint.find("kind"); + if (label_it == hint.end() || kind_it == hint.end()) + { + continue; + } + if (!label_it->second.Is()) + { + continue; + } + auto kind = GetUInteger(kind_it->second); + if (!kind) + { + continue; + } + if (label_it->second.Get() == "a:" && + *kind == static_cast(protocol::InlayHintKind::Parameter)) + { + found_param = true; + break; + } + } + + assertTrue(found_param, "InlayHint should include UnitFunc parameter name hint"); + + auto type_path = FixturePath("inlay_hint_case.tsl"); + auto type_content = ReadTextFile(type_path); + auto type_uri = ToUri(type_path); + OpenDocument(env.hub, type_uri, type_content, 1); + + protocol::LSPObject type_params; + type_params["textDocument"] = protocol::LSPObject{ { "uri", type_uri } }; + type_params["range"] = ToRangeObject(FullDocumentRange(type_content)); + + protocol::RequestMessage type_request; + type_request.id = "inlay_type"; + type_request.method = "textDocument/inlayHint"; + type_request.params = protocol::LSPAny(std::move(type_params)); + + auto type_json = provider.ProvideResponse(type_request, env.context); + auto type_response = ParseResponse(type_json); + assertTrue(type_response.result.has_value(), "InlayHint (type) should return result"); + assertTrue(type_response.result->Is(), "InlayHint (type) result should be array"); + + const auto& type_hints = type_response.result->Get(); + bool found_type = false; + for (const auto& hint_any : type_hints) + { + if (!hint_any.Is()) + { + continue; + } + const auto& hint = hint_any.Get(); + auto kind_it = hint.find("kind"); + auto label_it = hint.find("label"); + if (kind_it == hint.end() || label_it == hint.end()) + { + continue; + } + auto kind = GetUInteger(kind_it->second); + if (!kind) + { + continue; + } + if (*kind == static_cast(protocol::InlayHintKind::Type)) + { + found_type = true; + break; + } + } + + assertTrue(found_type, "InlayHint should include type hints for inferred variables"); + return result; + } + + TestResult ProviderMiscTests::TestInlayHintResolveProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::LSPObject data; + data["detail"] = protocol::string("param: int"); + + protocol::Position position{}; + position.line = 0; + position.character = 0; + + protocol::LSPObject hint; + hint["position"] = ToPositionObject(position); + hint["label"] = protocol::string("param:"); + hint["kind"] = static_cast(protocol::InlayHintKind::Parameter); + hint["data"] = protocol::LSPAny(std::move(data)); + + protocol::RequestMessage request; + request.id = "inlay_resolve"; + request.method = "inlayHint/resolve"; + request.params = protocol::LSPAny(std::move(hint)); + + ::lsp::provider::inlay_hint::Resolve provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "InlayHint resolve should return result"); + assertTrue(response.result->Is(), "InlayHint resolve result should be object"); + + const auto& resolved = response.result->Get(); + auto tooltip_it = resolved.find("tooltip"); + assertTrue(tooltip_it != resolved.end(), "InlayHint resolve should set tooltip"); + assertTrue(tooltip_it->second.Is(), "InlayHint tooltip should be string"); + assertEqual(std::string("param: int"), tooltip_it->second.Get(), + "InlayHint tooltip should use detail"); + return result; + } + + TestResult ProviderMiscTests::TestCodeLensProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::CodeLensParams params; + params.textDocument.uri = uri; + + protocol::RequestMessage request; + request.id = "codelens"; + request.method = "textDocument/codeLens"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::CodeLens provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "CodeLens should return result"); + assertTrue(response.result->Is(), "CodeLens result should be array"); + + const auto& lenses = response.result->Get(); + bool found = false; + for (const auto& lens_any : lenses) + { + if (!lens_any.Is()) + { + continue; + } + const auto& lens = lens_any.Get(); + auto data_it = lens.find("data"); + if (data_it == lens.end() || !data_it->second.Is()) + { + continue; + } + const auto& data = data_it->second.Get(); + auto kind_it = data.find("kind"); + auto name_it = data.find("name"); + if (kind_it == data.end() || name_it == data.end()) + { + continue; + } + if (!kind_it->second.Is() || !name_it->second.Is()) + { + continue; + } + if (kind_it->second.Get() == "references" && + name_it->second.Get() == "UnitFunc") + { + found = true; + break; + } + } + + assertTrue(found, "CodeLens should include reference lens for UnitFunc"); + return result; + } + + TestResult ProviderMiscTests::TestCodeLensResolveProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + range.end.line = 0; + range.end.character = 0; + + protocol::LSPObject data; + data["kind"] = protocol::string("references"); + data["count"] = static_cast(2); + + protocol::LSPObject lens; + lens["range"] = ToRangeObject(range); + lens["data"] = protocol::LSPAny(std::move(data)); + + protocol::RequestMessage request; + request.id = "codelens_resolve"; + request.method = "codeLens/resolve"; + request.params = protocol::LSPAny(std::move(lens)); + + ::lsp::provider::code_lens::Resolve provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "CodeLens resolve should return result"); + assertTrue(response.result->Is(), "CodeLens resolve result should be object"); + + const auto& resolved = response.result->Get(); + auto command_it = resolved.find("command"); + assertTrue(command_it != resolved.end(), "CodeLens resolve should set command"); + assertTrue(command_it->second.Is(), "CodeLens command should be object"); + + const auto& command = command_it->second.Get(); + auto title_it = command.find("title"); + auto cmd_it = command.find("command"); + assertTrue(title_it != command.end(), "CodeLens command should include title"); + assertTrue(cmd_it != command.end(), "CodeLens command should include command"); + assertTrue(title_it->second.Is(), "CodeLens title should be string"); + assertTrue(cmd_it->second.Is(), "CodeLens command should be string"); + assertEqual(std::string("2 references"), title_it->second.Get(), "CodeLens title should use count"); + assertEqual(std::string("tsl.showReferences"), cmd_it->second.Get(), "CodeLens command should match"); + return result; + } + + TestResult ProviderMiscTests::TestPrepareCallHierarchyProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::CallHierarchyParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "UnitFunc(1);"); + + protocol::RequestMessage request; + request.id = "call_prepare"; + request.method = "textDocument/prepareCallHierarchy"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::PrepareCallHierarchy provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "PrepareCallHierarchy should return result"); + assertTrue(response.result->Is(), "PrepareCallHierarchy result should be array"); + + const auto& items = response.result->Get(); + assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item"); + + assertTrue(items[0].Is(), "PrepareCallHierarchy item should be object"); + const auto& item = items[0].Get(); + + auto name_it = item.find("name"); + assertTrue(name_it != item.end(), "CallHierarchyItem should include name"); + assertTrue(name_it->second.Is(), "CallHierarchyItem name should be string"); + assertEqual(std::string("UnitFunc"), name_it->second.Get(), "PrepareCallHierarchy should target UnitFunc"); + + auto data_it = item.find("data"); + assertTrue(data_it != item.end(), "CallHierarchyItem should include data"); + assertTrue(data_it->second.Is(), "CallHierarchyItem data should be object"); + const auto& data = data_it->second.Get(); + auto symbol_id_it = data.find("symbolId"); + assertTrue(symbol_id_it != data.end(), "CallHierarchyItem data should include symbolId"); + assertTrue(symbol_id_it->second.Is(), "CallHierarchyItem symbolId should be string"); + assertTrue(!symbol_id_it->second.Get().empty(), "CallHierarchyItem symbolId should not be empty"); + + return result; + } + + TestResult ProviderMiscTests::TestCallHierarchyIncomingCallsProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::CallHierarchyParams prepare_params; + prepare_params.textDocument.uri = uri; + prepare_params.position = FindPosition(content, "UnitFunc(1);"); + + protocol::RequestMessage prepare_request; + prepare_request.id = "call_prepare_in"; + prepare_request.method = "textDocument/prepareCallHierarchy"; + prepare_request.params = codec::ToLSPAny(prepare_params); + + ::lsp::provider::text_document::PrepareCallHierarchy prepare_provider; + auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context); + auto prepare_response = ParseResponse(prepare_json); + assertTrue(prepare_response.result.has_value(), "PrepareCallHierarchy should return result"); + assertTrue(prepare_response.result->Is(), "PrepareCallHierarchy result should be array"); + + const auto& items = prepare_response.result->Get(); + assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item"); + assertTrue(items[0].Is(), "PrepareCallHierarchy item should be object"); + + protocol::LSPObject params; + params["item"] = items[0]; + + protocol::RequestMessage request; + request.id = "call_incoming"; + request.method = "callHierarchy/incomingCalls"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::call_hierarchy::IncomingCalls provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "IncomingCalls should return result"); + assertTrue(response.result->Is(), "IncomingCalls result should be array"); + + const auto& calls = response.result->Get(); + bool found = false; + for (const auto& call_any : calls) + { + if (!call_any.Is()) + { + continue; + } + const auto& call = call_any.Get(); + auto from_it = call.find("from"); + auto ranges_it = call.find("fromRanges"); + if (from_it == call.end() || ranges_it == call.end()) + { + continue; + } + if (!from_it->second.Is()) + { + continue; + } + const auto& from = from_it->second.Get(); + auto name_it = from.find("name"); + if (name_it == from.end() || !name_it->second.Is()) + { + continue; + } + if (name_it->second.Get() == "TestDefinitions") + { + found = true; + assertTrue(ranges_it->second.Is(), "IncomingCalls fromRanges should be array"); + break; + } + } + + assertTrue(found, "IncomingCalls should include TestDefinitions -> UnitFunc"); + return result; + } + + TestResult ProviderMiscTests::TestCallHierarchyOutgoingCallsProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::CallHierarchyParams prepare_params; + prepare_params.textDocument.uri = uri; + prepare_params.position = FindPosition(content, "TestDefinitions();"); + + protocol::RequestMessage prepare_request; + prepare_request.id = "call_prepare_out"; + prepare_request.method = "textDocument/prepareCallHierarchy"; + prepare_request.params = codec::ToLSPAny(prepare_params); + + ::lsp::provider::text_document::PrepareCallHierarchy prepare_provider; + auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context); + auto prepare_response = ParseResponse(prepare_json); + assertTrue(prepare_response.result.has_value(), "PrepareCallHierarchy should return result"); + assertTrue(prepare_response.result->Is(), "PrepareCallHierarchy result should be array"); + + const auto& items = prepare_response.result->Get(); + assertTrue(!items.empty(), "PrepareCallHierarchy should return at least one item"); + assertTrue(items[0].Is(), "PrepareCallHierarchy item should be object"); + + protocol::LSPObject params; + params["item"] = items[0]; + + protocol::RequestMessage request; + request.id = "call_outgoing"; + request.method = "callHierarchy/outgoingCalls"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::call_hierarchy::OutgoingCalls provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "OutgoingCalls should return result"); + assertTrue(response.result->Is(), "OutgoingCalls result should be array"); + + const auto& calls = response.result->Get(); + bool found = false; + for (const auto& call_any : calls) + { + if (!call_any.Is()) + { + continue; + } + const auto& call = call_any.Get(); + auto to_it = call.find("to"); + if (to_it == call.end() || !to_it->second.Is()) + { + continue; + } + const auto& to = to_it->second.Get(); + auto name_it = to.find("name"); + if (name_it == to.end() || !name_it->second.Is()) + { + continue; + } + if (name_it->second.Get() == "UnitFunc") + { + found = true; + break; + } + } + + assertTrue(found, "OutgoingCalls should include TestDefinitions -> UnitFunc"); + return result; + } + + TestResult ProviderMiscTests::TestPrepareTypeHierarchyProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("type_hierarchy_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::TypeHierarchyPrepareParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "Derived = class(Mid)"); + + protocol::RequestMessage request; + request.id = "type_prepare"; + request.method = "textDocument/prepareTypeHierarchy"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::PrepareTypeHierarchy provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "PrepareTypeHierarchy should return result"); + assertTrue(response.result->Is(), "PrepareTypeHierarchy result should be array"); + + const auto& items = response.result->Get(); + assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item"); + assertTrue(items[0].Is(), "TypeHierarchyItem should be object"); + + const auto& item = items[0].Get(); + + auto name_it = item.find("name"); + assertTrue(name_it != item.end(), "TypeHierarchyItem should include name"); + assertTrue(name_it->second.Is(), "TypeHierarchyItem name should be string"); + assertEqual(std::string("Derived"), name_it->second.Get(), "PrepareTypeHierarchy should target Derived"); + + auto data_it = item.find("data"); + assertTrue(data_it != item.end(), "TypeHierarchyItem should include data"); + assertTrue(data_it->second.Is(), "TypeHierarchyItem data should be object"); + const auto& data = data_it->second.Get(); + auto symbol_id_it = data.find("symbolId"); + assertTrue(symbol_id_it != data.end(), "TypeHierarchyItem data should include symbolId"); + assertTrue(symbol_id_it->second.Is(), "TypeHierarchyItem symbolId should be string"); + assertTrue(!symbol_id_it->second.Get().empty(), "TypeHierarchyItem symbolId should not be empty"); + + return result; + } + + TestResult ProviderMiscTests::TestTypeHierarchySupertypesProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("type_hierarchy_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::TypeHierarchyPrepareParams prepare_params; + prepare_params.textDocument.uri = uri; + prepare_params.position = FindPosition(content, "Derived = class(Mid)"); + + protocol::RequestMessage prepare_request; + prepare_request.id = "type_prepare_super"; + prepare_request.method = "textDocument/prepareTypeHierarchy"; + prepare_request.params = codec::ToLSPAny(prepare_params); + + ::lsp::provider::text_document::PrepareTypeHierarchy prepare_provider; + auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context); + auto prepare_response = ParseResponse(prepare_json); + assertTrue(prepare_response.result.has_value(), "PrepareTypeHierarchy should return result"); + assertTrue(prepare_response.result->Is(), "PrepareTypeHierarchy result should be array"); + + const auto& items = prepare_response.result->Get(); + assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item"); + assertTrue(items[0].Is(), "PrepareTypeHierarchy item should be object"); + + protocol::LSPObject params; + params["item"] = items[0]; + + protocol::RequestMessage request; + request.id = "type_supertypes"; + request.method = "typeHierarchy/supertypes"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::type_hierarchy::Supertypes provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "Supertypes should return result"); + assertTrue(response.result->Is(), "Supertypes result should be array"); + + const auto& types = response.result->Get(); + bool found = false; + for (const auto& type_any : types) + { + if (!type_any.Is()) + { + continue; + } + const auto& type_item = type_any.Get(); + auto name_it = type_item.find("name"); + if (name_it == type_item.end() || !name_it->second.Is()) + { + continue; + } + if (name_it->second.Get() == "Mid") + { + found = true; + break; + } + } + + assertTrue(found, "Supertypes should include Derived -> Mid"); + return result; + } + + TestResult ProviderMiscTests::TestTypeHierarchySubtypesProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("type_hierarchy_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::TypeHierarchyPrepareParams prepare_params; + prepare_params.textDocument.uri = uri; + prepare_params.position = FindPosition(content, "Mid = class(Base)"); + + protocol::RequestMessage prepare_request; + prepare_request.id = "type_prepare_sub"; + prepare_request.method = "textDocument/prepareTypeHierarchy"; + prepare_request.params = codec::ToLSPAny(prepare_params); + + ::lsp::provider::text_document::PrepareTypeHierarchy prepare_provider; + auto prepare_json = prepare_provider.ProvideResponse(prepare_request, env.context); + auto prepare_response = ParseResponse(prepare_json); + assertTrue(prepare_response.result.has_value(), "PrepareTypeHierarchy should return result"); + assertTrue(prepare_response.result->Is(), "PrepareTypeHierarchy result should be array"); + + const auto& items = prepare_response.result->Get(); + assertTrue(!items.empty(), "PrepareTypeHierarchy should return at least one item"); + assertTrue(items[0].Is(), "PrepareTypeHierarchy item should be object"); + + protocol::LSPObject params; + params["item"] = items[0]; + + protocol::RequestMessage request; + request.id = "type_subtypes"; + request.method = "typeHierarchy/subtypes"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::type_hierarchy::Subtypes provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertTrue(response.result.has_value(), "Subtypes should return result"); + assertTrue(response.result->Is(), "Subtypes result should be array"); + + const auto& types = response.result->Get(); + bool found = false; + for (const auto& type_any : types) + { + if (!type_any.Is()) + { + continue; + } + const auto& type_item = type_any.Get(); + auto name_it = type_item.find("name"); + if (name_it == type_item.end() || !name_it->second.Is()) + { + continue; + } + if (name_it->second.Get() == "Derived") + { + found = true; + break; + } + } + + assertTrue(found, "Subtypes should include Mid -> Derived"); + return result; + } + + namespace + { + struct TempDirGuard + { + std::filesystem::path path; + ~TempDirGuard() + { + if (path.empty()) + { + return; + } + std::error_code ec; + std::filesystem::remove_all(path, ec); + } + }; + + std::filesystem::path MakeTempDir(std::string_view prefix) + { + auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + std::filesystem::path dir = std::filesystem::temp_directory_path() / + (std::string(prefix) + "-" + std::to_string(now)); + std::filesystem::create_directories(dir); + return dir; + } + + void WriteTextFile(const std::filesystem::path& path, const std::string& content) + { + std::filesystem::create_directories(path.parent_path()); + std::ofstream file(path, std::ios::binary); + if (!file.is_open()) + { + throw std::runtime_error("Failed to write file: " + path.string()); + } + file << content; + } + + std::string SimpleWorkspaceFunction(std::string_view name) + { + std::string result; + result += "function "; + result += name; + result += "(): integer;\n\n"; + result += "function "; + result += name; + result += "(): integer;\n"; + result += "begin\n"; + result += " return 1;\n"; + result += "end;\n"; + return result; + } + + protocol::LSPAny BuildDidCreateFilesParams(const std::vector& uris) + { + protocol::LSPArray files; + files.reserve(uris.size()); + for (const auto& uri : uris) + { + protocol::LSPObject file; + file["uri"] = protocol::string(uri); + files.emplace_back(std::move(file)); + } + protocol::LSPObject params; + params["files"] = std::move(files); + return protocol::LSPAny(std::move(params)); + } + + protocol::LSPAny BuildDidDeleteFilesParams(const std::vector& uris) + { + return BuildDidCreateFilesParams(uris); + } + + protocol::LSPAny BuildDidRenameFilesParams(const std::vector>& files) + { + protocol::LSPArray entries; + entries.reserve(files.size()); + for (const auto& [old_uri, new_uri] : files) + { + protocol::LSPObject entry; + entry["oldUri"] = protocol::string(old_uri); + entry["newUri"] = protocol::string(new_uri); + entries.emplace_back(std::move(entry)); + } + protocol::LSPObject params; + params["files"] = std::move(entries); + return protocol::LSPAny(std::move(params)); + } + + protocol::LSPAny BuildDidChangeWatchedFilesParams(const std::vector>& changes) + { + protocol::LSPArray entries; + entries.reserve(changes.size()); + for (const auto& [uri, type] : changes) + { + protocol::LSPObject entry; + entry["uri"] = protocol::string(uri); + entry["type"] = type; + entries.emplace_back(std::move(entry)); + } + protocol::LSPObject params; + params["changes"] = std::move(entries); + return protocol::LSPAny(std::move(params)); + } + } + + TestResult ProviderMiscTests::TestDidCreateFilesProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto dir = MakeTempDir("tsl-didCreateFiles"); + TempDirGuard guard{ dir }; + + env.hub.symbols().LoadWorkspace(ToUri(dir)); + + auto file_path = dir / "created_script.tsl"; + WriteTextFile(file_path, SimpleWorkspaceFunction("CreatedFunc")); + auto file_uri = ToUri(file_path); + + protocol::NotificationMessage notification; + notification.method = "workspace/didCreateFiles"; + notification.params = BuildDidCreateFilesParams({ file_uri }); + + ::lsp::provider::workspace::DidCreateFiles provider; + provider.HandleNotification(notification, env.context); + + auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function); + bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) { + return item.name == "CreatedFunc" && item.uri == file_uri; + }); + assertTrue(found, "didCreateFiles should index CreatedFunc"); + return result; + } + + TestResult ProviderMiscTests::TestDidDeleteFilesProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto dir = MakeTempDir("tsl-didDeleteFiles"); + TempDirGuard guard{ dir }; + + auto file_path = dir / "delete_script.tsl"; + WriteTextFile(file_path, SimpleWorkspaceFunction("DeletedFunc")); + auto file_uri = ToUri(file_path); + + env.hub.symbols().LoadWorkspace(ToUri(dir)); + + { + auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function); + bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) { + return item.name == "DeletedFunc" && item.uri == file_uri; + }); + assertTrue(found, "Workspace should index DeletedFunc before deletion"); + } + + std::filesystem::remove(file_path); + + protocol::NotificationMessage notification; + notification.method = "workspace/didDeleteFiles"; + notification.params = BuildDidDeleteFilesParams({ file_uri }); + + ::lsp::provider::workspace::DidDeleteFiles provider; + provider.HandleNotification(notification, env.context); + + auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function); + bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) { + return item.name == "DeletedFunc"; + }); + assertFalse(found, "didDeleteFiles should remove DeletedFunc from index"); + return result; + } + + TestResult ProviderMiscTests::TestDidRenameFilesProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto dir = MakeTempDir("tsl-didRenameFiles"); + TempDirGuard guard{ dir }; + + auto old_path = dir / "old_name.tsl"; + auto new_path = dir / "new_name.tsl"; + WriteTextFile(old_path, SimpleWorkspaceFunction("RenamedFunc")); + auto old_uri = ToUri(old_path); + auto new_uri = ToUri(new_path); + + env.hub.symbols().LoadWorkspace(ToUri(dir)); + + std::filesystem::rename(old_path, new_path); + + protocol::NotificationMessage notification; + notification.method = "workspace/didRenameFiles"; + notification.params = BuildDidRenameFilesParams({ { old_uri, new_uri } }); + + ::lsp::provider::workspace::DidRenameFiles provider; + provider.HandleNotification(notification, env.context); + + auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function); + bool found_old = false; + bool found_new = false; + for (const auto& item : indexed) + { + if (item.name != "RenamedFunc") + { + continue; + } + if (item.uri == old_uri) + { + found_old = true; + } + if (item.uri == new_uri) + { + found_new = true; + } + } + + assertFalse(found_old, "didRenameFiles should remove old URI entry"); + assertTrue(found_new, "didRenameFiles should index new URI entry"); + return result; + } + + TestResult ProviderMiscTests::TestDidChangeWatchedFilesProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto dir = MakeTempDir("tsl-didChangeWatchedFiles"); + TempDirGuard guard{ dir }; + + auto file_path = dir / "watched_script.tsl"; + WriteTextFile(file_path, SimpleWorkspaceFunction("WatchedFunc")); + auto file_uri = ToUri(file_path); + + env.hub.symbols().LoadWorkspace(ToUri(dir)); + + { + auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function); + bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) { + return item.name == "WatchedFunc" && item.uri == file_uri; + }); + assertTrue(found, "Workspace should index WatchedFunc before change"); + } + + WriteTextFile(file_path, SimpleWorkspaceFunction("WatchedChangedFunc")); + + protocol::NotificationMessage change_notification; + change_notification.method = "workspace/didChangeWatchedFiles"; + change_notification.params = BuildDidChangeWatchedFilesParams({ { file_uri, 2 } }); + + ::lsp::provider::workspace::DidChangeWatchedFiles provider; + provider.HandleNotification(change_notification, env.context); + + { + auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function); + bool found_old = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) { + return item.name == "WatchedFunc"; + }); + bool found_new = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) { + return item.name == "WatchedChangedFunc" && item.uri == file_uri; + }); + assertFalse(found_old, "didChangeWatchedFiles should remove WatchedFunc after reindex"); + assertTrue(found_new, "didChangeWatchedFiles should reindex WatchedChangedFunc"); + } + + std::filesystem::remove(file_path); + + protocol::NotificationMessage delete_notification; + delete_notification.method = "workspace/didChangeWatchedFiles"; + delete_notification.params = BuildDidChangeWatchedFilesParams({ { file_uri, 3 } }); + + provider.HandleNotification(delete_notification, env.context); + + { + auto indexed = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Function); + bool found = std::any_of(indexed.begin(), indexed.end(), [&](const manager::Symbol::IndexedSymbol& item) { + return item.name == "WatchedChangedFunc"; + }); + assertFalse(found, "didChangeWatchedFiles should remove WatchedChangedFunc on delete"); + } + + return result; + } + + TestResult ProviderMiscTests::TestDidChangeConfigurationProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::LSPObject tsl_settings; + tsl_settings["format"] = true; + + protocol::LSPObject settings; + settings["tsl"] = std::move(tsl_settings); + + protocol::LSPObject params; + params["settings"] = std::move(settings); + + protocol::NotificationMessage notification; + notification.method = "workspace/didChangeConfiguration"; + notification.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::workspace::DidChangeConfiguration provider; + provider.HandleNotification(notification, env.context); + + auto stored = env.hub.GetConfiguration(); + assertTrue(stored.Is(), "didChangeConfiguration should store object settings"); + const auto& stored_obj = stored.Get(); + assertTrue(stored_obj.contains("tsl"), "stored settings should include tsl section"); + + assertTrue(env.events.empty(), "didChangeConfiguration should not trigger lifecycle events"); + return result; + } + + TestResult ProviderMiscTests::TestDidChangeWorkspaceFoldersProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto workspace_uri = ToUri(FixturePath("workspace")); + + assertTrue(env.hub.symbols().GetWorkspaceSymbolTables().empty(), "Workspace symbols should start empty"); + + protocol::LSPArray added; + added.emplace_back(protocol::LSPObject{ + { "uri", workspace_uri }, + { "name", "workspace" }, + }); + + protocol::LSPObject event; + event["added"] = std::move(added); + event["removed"] = protocol::LSPArray{}; + + protocol::LSPObject params; + params["event"] = std::move(event); + + protocol::NotificationMessage add_notification; + add_notification.method = "workspace/didChangeWorkspaceFolders"; + add_notification.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::workspace::DidChangeWorkspaceFolders provider; + provider.HandleNotification(add_notification, env.context); + env.scheduler.WaitAll(); + + assertFalse(env.hub.symbols().GetWorkspaceSymbolTables().empty(), "didChangeWorkspaceFolders should index workspace folder"); + assertEqual(static_cast(1), env.hub.GetWorkspaceFolders().size(), "didChangeWorkspaceFolders should track added folders"); + + protocol::LSPArray removed; + removed.emplace_back(protocol::LSPObject{ + { "uri", workspace_uri }, + { "name", "workspace" }, + }); + + protocol::LSPObject remove_event; + remove_event["added"] = protocol::LSPArray{}; + remove_event["removed"] = std::move(removed); + + protocol::LSPObject remove_params; + remove_params["event"] = std::move(remove_event); + + protocol::NotificationMessage remove_notification; + remove_notification.method = "workspace/didChangeWorkspaceFolders"; + remove_notification.params = protocol::LSPAny(std::move(remove_params)); + + provider.HandleNotification(remove_notification, env.context); + env.scheduler.WaitAll(); + + assertTrue(env.hub.symbols().GetWorkspaceSymbolTables().empty(), "didChangeWorkspaceFolders should remove workspace folder symbols"); + assertTrue(env.hub.GetWorkspaceFolders().empty(), "didChangeWorkspaceFolders should track removed folders"); return result; } @@ -360,8 +2388,10 @@ namespace lsp::test::provider TestResult result{ "", true, "ok" }; ProviderEnv env; + env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace"))); + protocol::LSPObject params; - params["query"] = "Widget"; + params["query"] = "Workspace"; protocol::RequestMessage request; request.id = "ws_symbol"; @@ -371,10 +2401,510 @@ namespace lsp::test::provider ::lsp::provider::workspace::Symbol provider; auto json = provider.ProvideResponse(request, env.context); auto response = ParseResponse(json); - assertTrue(response.error.has_value(), "Workspace symbol should return error"); - assertEqual(static_cast(protocol::ErrorCodes::MethodNotFound), - static_cast(response.error->code), - "Workspace symbol should return MethodNotFound"); + assertFalse(response.error.has_value(), "Workspace symbol should not return error"); + assertTrue(response.result.has_value(), "Workspace symbol should return result"); + + auto symbols = codec::FromLSPAny.template operator()>(response.result.value()); + assertTrue(!symbols.empty(), "Workspace symbol should return matches"); + + bool found_unit = std::any_of(symbols.begin(), symbols.end(), [](const protocol::WorkspaceSymbol& symbol) { + return symbol.name == "WorkspaceUnit"; + }); + bool found_func = std::any_of(symbols.begin(), symbols.end(), [](const protocol::WorkspaceSymbol& symbol) { + return symbol.name == "WorkspaceFunc"; + }); + assertTrue(found_unit, "Workspace symbol should include WorkspaceUnit"); + assertTrue(found_func, "Workspace symbol should include WorkspaceFunc"); + return result; + } + + TestResult ProviderMiscTests::TestWorkspaceDiagnosticProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto error_path = FixturePath("code_action_missing_semicolon.tsl"); + auto error_content = ReadTextFile(error_path); + auto error_uri = ToUri(error_path); + OpenDocument(env.hub, error_uri, error_content, 1); + + auto ok_path = FixturePath("main_unit.tsf"); + auto ok_content = ReadTextFile(ok_path); + auto ok_uri = ToUri(ok_path); + OpenDocument(env.hub, ok_uri, ok_content, 1); + + protocol::WorkspaceDiagnosticParams params; + + protocol::RequestMessage request; + request.id = "ws_diag"; + request.method = "workspace/diagnostic"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::workspace::Diagnostic provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "Workspace diagnostic should not return error"); + assertTrue(response.result.has_value(), "Workspace diagnostic should return result"); + assertTrue(response.result->Is(), "Workspace diagnostic result should be object"); + + const auto& report = response.result->Get(); + auto items_it = report.find("items"); + assertTrue(items_it != report.end(), "Workspace diagnostic report should include items"); + assertTrue(items_it->second.Is(), "Workspace diagnostic items should be array"); + + bool found_error = false; + for (const auto& item_any : items_it->second.Get()) + { + if (!item_any.Is()) + { + continue; + } + const auto& item = item_any.Get(); + auto uri_it = item.find("uri"); + if (uri_it == item.end() || !uri_it->second.Is()) + { + continue; + } + if (uri_it->second.Get() != error_uri) + { + continue; + } + + found_error = true; + auto kind_it = item.find("kind"); + assertTrue(kind_it != item.end(), "Workspace diagnostic item should include kind"); + assertTrue(kind_it->second.Is(), "Workspace diagnostic kind should be string"); + assertEqual(std::string("full"), kind_it->second.Get(), "Workspace diagnostic kind should be full"); + + auto diags_it = item.find("items"); + assertTrue(diags_it != item.end(), "Workspace diagnostic item should include items"); + assertTrue(diags_it->second.Is(), "Workspace diagnostic items should be array"); + assertTrue(!diags_it->second.Get().empty(), + "Workspace diagnostic should include diagnostics for error document"); + break; + } + + assertTrue(found_error, "Workspace diagnostic should include opened documents"); + return result; + } + + TestResult ProviderMiscTests::TestWorkspaceConfigurationProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::LSPObject nested; + nested["b"] = protocol::string("x"); + + protocol::LSPObject tsl; + tsl["foo"] = static_cast(42); + tsl["nested"] = std::move(nested); + + protocol::LSPObject settings; + settings["tsl"] = std::move(tsl); + + protocol::LSPObject dc_params; + dc_params["settings"] = std::move(settings); + + protocol::NotificationMessage did_change; + did_change.method = "workspace/didChangeConfiguration"; + did_change.params = protocol::LSPAny(std::move(dc_params)); + + ::lsp::provider::workspace::DidChangeConfiguration dc_provider; + dc_provider.HandleNotification(did_change, env.context); + + protocol::LSPArray items; + items.emplace_back(protocol::LSPObject{ + { "scopeUri", protocol::string(ToUri(FixturePath("main_unit.tsf"))) }, + { "section", protocol::string("tsl.foo") }, + }); + items.emplace_back(protocol::LSPObject{ + { "section", protocol::string("tsl.nested.b") }, + }); + items.emplace_back(protocol::LSPObject{ + { "section", protocol::string("missing") }, + }); + + protocol::LSPObject params; + params["items"] = std::move(items); + + protocol::RequestMessage request; + request.id = "cfg"; + request.method = "workspace/configuration"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::workspace::Configuration provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "workspace/configuration should not return error"); + assertTrue(response.result.has_value(), "workspace/configuration should return result"); + assertTrue(response.result->Is(), "workspace/configuration result should be array"); + + const auto& values = response.result->Get(); + assertEqual(static_cast(3), values.size(), "workspace/configuration result size should match items"); + assertTrue(values[0].Is(), "workspace/configuration should resolve tsl.foo"); + assertEqual(static_cast(42), values[0].Get(), "tsl.foo should equal 42"); + assertTrue(values[1].Is(), "workspace/configuration should resolve tsl.nested.b"); + assertEqual(std::string("x"), values[1].Get(), "tsl.nested.b should equal x"); + assertTrue(values[2].Is(), "workspace/configuration should return null for missing section"); + return result; + } + + TestResult ProviderMiscTests::TestWorkspaceApplyEditProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto uri = ToUri(FixturePath("inlay_hint_case.tsl")); + std::string content = "var count := 1;\n"; + OpenDocument(env.hub, uri, content, 1); + + auto pos = FindPosition(content, "1"); + protocol::Range range; + range.start = pos; + range.end = pos; + range.end.character = pos.character + 1; + + protocol::LSPObject edit_item; + edit_item["range"] = ToRangeObject(range); + edit_item["newText"] = protocol::string("2"); + + protocol::LSPArray edits; + edits.emplace_back(std::move(edit_item)); + + protocol::LSPObject changes; + changes[uri] = protocol::LSPAny(std::move(edits)); + + protocol::LSPObject edit; + edit["changes"] = std::move(changes); + + protocol::LSPObject params; + params["edit"] = std::move(edit); + + protocol::RequestMessage request; + request.id = "apply"; + request.method = "workspace/applyEdit"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::workspace::ApplyEdit provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "workspace/applyEdit should not return error"); + assertTrue(response.result.has_value(), "workspace/applyEdit should return result"); + assertTrue(response.result->Is(), "workspace/applyEdit result should be object"); + + const auto& obj = response.result->Get(); + auto applied_it = obj.find("applied"); + assertTrue(applied_it != obj.end(), "workspace/applyEdit result should include applied"); + assertTrue(applied_it->second.Is(), "workspace/applyEdit applied should be bool"); + assertTrue(applied_it->second.Get(), "workspace/applyEdit should return applied=true"); + + auto updated = env.hub.documents().GetContent(uri); + assertTrue(updated.has_value(), "workspace/applyEdit should update open document"); + assertEqual(std::string("var count := 2;\n"), updated.value(), "workspace/applyEdit should apply edits"); + return result; + } + + TestResult ProviderMiscTests::TestWorkspaceWorkspaceFoldersProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::WorkspaceFolder folder; + folder.uri = ToUri(FixturePath("workspace")); + folder.name = "workspace"; + env.hub.SetWorkspaceFolders({ folder }); + + protocol::RequestMessage request; + request.id = "folders"; + request.method = "workspace/workspaceFolders"; + request.params = std::nullopt; + + ::lsp::provider::workspace::WorkspaceFolders provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "workspace/workspaceFolders should not return error"); + assertTrue(response.result.has_value(), "workspace/workspaceFolders should return result"); + assertTrue(response.result->Is(), "workspace/workspaceFolders result should be array"); + + const auto& folders = response.result->Get(); + assertEqual(static_cast(1), folders.size(), "workspace/workspaceFolders should return configured folders"); + assertTrue(folders.front().Is(), "workspace/workspaceFolders item should be object"); + const auto& item = folders.front().Get(); + auto uri_it = item.find("uri"); + auto name_it = item.find("name"); + assertTrue(uri_it != item.end(), "workspace/workspaceFolders item should include uri"); + assertTrue(uri_it->second.Is(), "workspace/workspaceFolders uri should be string"); + assertEqual(folder.uri, uri_it->second.Get(), "workspace/workspaceFolders uri should match"); + assertTrue(name_it != item.end(), "workspace/workspaceFolders item should include name"); + assertTrue(name_it->second.Is(), "workspace/workspaceFolders name should be string"); + assertEqual(folder.name, name_it->second.Get(), "workspace/workspaceFolders name should match"); + return result; + } + + TestResult ProviderMiscTests::TestWorkspaceRefreshProviders() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto check_null_result = [&](auto& provider, std::string_view method) { + protocol::RequestMessage request; + request.id = "refresh"; + request.method = std::string(method); + request.params = std::nullopt; + + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), std::string(method) + " should not return error"); + assertTrue(response.result.has_value(), std::string(method) + " should return result"); + assertTrue(response.result->template Is(), std::string(method) + " result should be null"); + }; + + ::lsp::provider::workspace::CodeLensRefresh code_lens; + check_null_result(code_lens, "workspace/codeLens/refresh"); + + ::lsp::provider::workspace::DiagnosticRefresh diagnostic; + check_null_result(diagnostic, "workspace/diagnostic/refresh"); + + ::lsp::provider::workspace::InlayHintRefresh inlay; + check_null_result(inlay, "workspace/inlayHint/refresh"); + + ::lsp::provider::workspace::InlineValueRefresh inline_value; + check_null_result(inline_value, "workspace/inlineValue/refresh"); + + ::lsp::provider::workspace::SemanticTokensRefresh semantic; + check_null_result(semantic, "workspace/semanticTokens/refresh"); + + return result; + } + + TestResult ProviderMiscTests::TestClientCapabilityProviders() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + { + protocol::LSPArray registrations; + registrations.emplace_back(protocol::LSPObject{ + { "id", protocol::string("reg_1") }, + { "method", protocol::string("workspace/didChangeConfiguration") }, + }); + + protocol::LSPObject params; + params["registrations"] = std::move(registrations); + + protocol::RequestMessage request; + request.id = "reg"; + request.method = "client/registerCapability"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::client::RegisterCapability provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "client/registerCapability should not return error"); + assertTrue(response.result.has_value(), "client/registerCapability should return result"); + assertTrue(response.result->Is(), "client/registerCapability result should be null"); + } + + { + protocol::LSPArray unregistrations; + unregistrations.emplace_back(protocol::LSPObject{ + { "id", protocol::string("reg_1") }, + { "method", protocol::string("workspace/didChangeConfiguration") }, + }); + + protocol::LSPObject params; + params["unregistrations"] = std::move(unregistrations); + + protocol::RequestMessage request; + request.id = "unreg"; + request.method = "client/unregisterCapability"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::client::UnregisterCapability provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "client/unregisterCapability should not return error"); + assertTrue(response.result.has_value(), "client/unregisterCapability should return result"); + assertTrue(response.result->Is(), "client/unregisterCapability result should be null"); + } + + return result; + } + + TestResult ProviderMiscTests::TestWindowWorkDoneProgressCreateProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::LSPObject params; + params["token"] = protocol::string("progress_token"); + + protocol::RequestMessage request; + request.id = "progress"; + request.method = "window/workDoneProgress/create"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::window::WorkDoneProgressCreate provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "window/workDoneProgress/create should not return error"); + assertTrue(response.result.has_value(), "window/workDoneProgress/create should return result"); + assertTrue(response.result->Is(), "window/workDoneProgress/create result should be null"); + return result; + } + + TestResult ProviderMiscTests::TestWindowShowMessageRequestProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::LSPArray actions; + actions.emplace_back(protocol::LSPObject{ + { "title", protocol::string("OK") }, + }); + + protocol::LSPObject params; + params["type"] = static_cast(protocol::MessageType::Info); + params["message"] = protocol::string("Test showMessageRequest"); + params["actions"] = std::move(actions); + + protocol::RequestMessage request; + request.id = "msgreq"; + request.method = "window/showMessageRequest"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::window::ShowMessageRequest provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "window/showMessageRequest should not return error"); + assertTrue(response.result.has_value(), "window/showMessageRequest should return result"); + assertTrue(response.result->Is(), "window/showMessageRequest result should be object"); + + const auto& obj = response.result->Get(); + auto title_it = obj.find("title"); + assertTrue(title_it != obj.end(), "window/showMessageRequest result should include title"); + assertTrue(title_it->second.Is(), "window/showMessageRequest title should be string"); + assertEqual(std::string("OK"), title_it->second.Get(), "window/showMessageRequest should return first action"); + return result; + } + + TestResult ProviderMiscTests::TestWindowShowDocumentProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::LSPObject params; + params["uri"] = protocol::string(ToUri(FixturePath("main_unit.tsf"))); + params["takeFocus"] = true; + + protocol::RequestMessage request; + request.id = "showdoc"; + request.method = "window/showDocument"; + request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::window::ShowDocument provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "window/showDocument should not return error"); + assertTrue(response.result.has_value(), "window/showDocument should return result"); + assertTrue(response.result->Is(), "window/showDocument result should be object"); + + const auto& obj = response.result->Get(); + auto success_it = obj.find("success"); + assertTrue(success_it != obj.end(), "window/showDocument result should include success"); + assertTrue(success_it->second.Is(), "window/showDocument success should be bool"); + assertFalse(success_it->second.Get(), "window/showDocument should return success=false"); + return result; + } + + TestResult ProviderMiscTests::TestWindowMessageNotifications() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + { + protocol::LSPObject params; + params["type"] = static_cast(protocol::MessageType::Log); + params["message"] = protocol::string("Test logMessage"); + + protocol::NotificationMessage notification; + notification.method = "window/logMessage"; + notification.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::window::LogMessage provider; + provider.HandleNotification(notification, env.context); + } + + { + protocol::LSPObject params; + params["type"] = static_cast(protocol::MessageType::Info); + params["message"] = protocol::string("Test showMessage"); + + protocol::NotificationMessage notification; + notification.method = "window/showMessage"; + notification.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::window::ShowMessage provider; + provider.HandleNotification(notification, env.context); + } + + assertTrue(env.events.empty(), "window message notifications should not trigger lifecycle events"); + return result; + } + + TestResult ProviderMiscTests::TestTelemetryEventNotification() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + protocol::LSPObject params; + params["event"] = protocol::string("test_event"); + params["value"] = static_cast(1); + + protocol::NotificationMessage notification; + notification.method = "telemetry/event"; + notification.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::telemetry::Event provider; + provider.HandleNotification(notification, env.context); + + assertTrue(env.events.empty(), "telemetry/event should not trigger lifecycle events"); + return result; + } + + TestResult ProviderMiscTests::TestPublishDiagnosticsNotification() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto uri = ToUri(FixturePath("main_unit.tsf")); + + protocol::Range range{}; + range.start.line = 0; + range.start.character = 0; + range.end.line = 0; + range.end.character = 1; + + protocol::LSPArray diagnostics; + diagnostics.emplace_back(protocol::LSPObject{ + { "range", ToRangeObject(range) }, + { "message", protocol::string("Test diagnostic") }, + }); + + protocol::LSPObject params; + params["uri"] = protocol::string(uri); + params["version"] = static_cast(1); + params["diagnostics"] = std::move(diagnostics); + + protocol::NotificationMessage notification; + notification.method = "textDocument/publishDiagnostics"; + notification.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::text_document::PublishDiagnostics provider; + provider.HandleNotification(notification, env.context); + + assertTrue(env.events.empty(), "publishDiagnostics should not trigger lifecycle events"); return result; } @@ -383,63 +2913,574 @@ namespace lsp::test::provider TestResult result{ "", true, "ok" }; ProviderEnv env; - protocol::LSPObject params; - params["textDocument"] = protocol::LSPObject{ - { "uri", ToUri(FixturePath("rename_case.tsl")) } - }; - params["range"] = protocol::LSPObject{ - { "start", protocol::LSPObject{ { "line", 0 }, { "character", 0 } } }, - { "end", protocol::LSPObject{ { "line", 0 }, { "character", 1 } } } - }; + auto path = FixturePath("rename_case.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); protocol::RequestMessage request; request.id = "sem"; - request.method = "textDocument/semanticTokens/range"; + request.method = "textDocument/semanticTokens/full"; + + protocol::SemanticTokensParams params; + params.textDocument.uri = uri; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::SemanticTokensFull provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + assertFalse(response.error.has_value(), "Semantic tokens should not return error"); + assertTrue(response.result.has_value(), "Semantic tokens should return result"); + + auto tokens = codec::FromLSPAny.template operator()(response.result.value()); + assertTrue(tokens.resultId.has_value(), "Semantic tokens should include resultId"); + assertTrue(!tokens.data.empty(), "Semantic tokens data should not be empty"); + assertTrue(tokens.data.size() % 5 == 0, "Semantic tokens data should be in 5-tuples"); + return result; + } + + TestResult ProviderMiscTests::TestSignatureHelpProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::TextDocumentPositionParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "UnitFunc(1);"); + params.position.character += static_cast(std::string("UnitFunc(").size()); + + protocol::RequestMessage request; + request.id = "sig"; + request.method = "textDocument/signatureHelp"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::SignatureHelp provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "Signature help should not return error"); + assertTrue(response.result.has_value(), "Signature help should return result"); + + auto sig_help = codec::FromLSPAny.template operator()>(response.result.value()); + assertTrue(sig_help.has_value(), "Signature help should return data"); + assertTrue(!sig_help->signatures.empty(), "Signature help should include signatures"); + assertTrue(sig_help->signatures.front().label.find("UnitFunc") != std::string::npos, + "Signature help label should mention UnitFunc"); + assertTrue(sig_help->activeSignature.has_value(), "Signature help should include activeSignature"); + assertTrue(sig_help->activeParameter.has_value(), "Signature help should include activeParameter"); + return result; + } + + TestResult ProviderMiscTests::TestCodeActionProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("code_action_missing_semicolon.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + auto tree = env.hub.parser().GetTree(uri); + assertTrue(tree != nullptr, "Parser should produce syntax tree"); + + auto errors = language::ast::Deserializer::DiagnoseSyntax(ts_tree_root_node(tree), content); + assertTrue(!errors.empty(), "Fixture should produce syntax errors"); + + protocol::LSPArray diagnostics; + for (const auto& error : errors) + { + protocol::LSPObject diagnostic; + diagnostic["range"] = protocol::LSPObject{ + { "start", protocol::LSPObject{ { "line", static_cast(error.location.start_line) }, + { "character", static_cast(error.location.start_column) } } }, + { "end", protocol::LSPObject{ { "line", static_cast(error.location.end_line) }, + { "character", static_cast(error.location.end_column) } } }, + }; + diagnostic["message"] = error.message; + diagnostics.emplace_back(std::move(diagnostic)); + } + + protocol::LSPObject params; + params["textDocument"] = protocol::LSPObject{ { "uri", uri } }; + params["range"] = protocol::LSPObject{ + { "start", protocol::LSPObject{ { "line", 0 }, { "character", 0 } } }, + { "end", protocol::LSPObject{ { "line", 9999 }, { "character", 0 } } }, + }; + params["context"] = protocol::LSPObject{ { "diagnostics", std::move(diagnostics) } }; + + protocol::RequestMessage request; + request.id = "code_action"; + request.method = "textDocument/codeAction"; request.params = protocol::LSPAny(params); - ::lsp::provider::text_document::SemanticTokensRange provider; + ::lsp::provider::text_document::CodeAction provider; auto json = provider.ProvideResponse(request, env.context); auto response = ParseResponse(json); - assertTrue(response.error.has_value(), "Semantic tokens range should return error"); - assertEqual(static_cast(protocol::ErrorCodes::MethodNotFound), - static_cast(response.error->code), - "Semantic tokens range should return MethodNotFound"); + + assertFalse(response.error.has_value(), "Code action should not return error"); + assertTrue(response.result.has_value(), "Code action should return result"); + assertTrue(response.result->Is(), "Code action result should be an array"); + + const auto& actions = response.result->Get(); + assertTrue(!actions.empty(), "Code action should return actions"); + + bool found_insert = false; + bool found_fix_all = false; + + for (const auto& action_any : actions) + { + if (!action_any.Is()) + { + continue; + } + + const auto& action = action_any.Get(); + auto title_it = action.find("title"); + auto kind_it = action.find("kind"); + if (title_it == action.end() || kind_it == action.end()) + { + continue; + } + + if (!title_it->second.Is() || !kind_it->second.Is()) + { + continue; + } + + const auto& title = title_it->second.Get(); + const auto& kind = kind_it->second.Get(); + + if (kind == protocol::CodeActionKindLiterals::QuickFix && title.find("Insert") != std::string::npos) + { + found_insert = true; + } + if (kind == protocol::CodeActionKindLiterals::SourceFixAll && title.find("semicolons") != std::string::npos) + { + found_fix_all = true; + } + } + + assertTrue(found_insert, "Code action should include insert fix"); + assertTrue(found_fix_all, "Code action should include fixAll missing semicolons"); return result; } - TestResult ProviderMiscTests::TestRegisterCapabilityProvider() + TestResult ProviderMiscTests::TestCodeActionResolveProvider() { TestResult result{ "", true, "ok" }; ProviderEnv env; - protocol::RegistrationParams params; - protocol::RequestMessage request; - request.id = "reg"; - request.method = "client/registerCapability"; - request.params = codec::ToLSPAny(params); + protocol::LSPObject action; + action["title"] = protocol::string("Resolve me"); + action["kind"] = protocol::string(protocol::CodeActionKindLiterals::QuickFix); + action["data"] = protocol::LSPAny(protocol::LSPObject{ + { "kind", protocol::string("noop") }, + }); - ::lsp::provider::client::RegisterCapability provider; + protocol::RequestMessage request; + request.id = "code_action_resolve"; + request.method = "codeAction/resolve"; + request.params = protocol::LSPAny(std::move(action)); + + ::lsp::provider::code_action::Resolve provider; auto json = provider.ProvideResponse(request, env.context); auto response = ParseResponse(json); - assertTrue(response.result == std::nullopt, "Register capability should return null"); + + assertFalse(response.error.has_value(), "CodeAction resolve should not return error"); + assertTrue(response.result.has_value(), "CodeAction resolve should return result"); + assertTrue(response.result->Is(), "CodeAction resolve result should be object"); + + const auto& resolved = response.result->Get(); + auto title_it = resolved.find("title"); + assertTrue(title_it != resolved.end(), "Resolved code action should include title"); + assertTrue(title_it->second.Is(), "Resolved code action title should be string"); + assertEqual(std::string("Resolve me"), title_it->second.Get(), "Resolved title should match"); + return result; } - TestResult ProviderMiscTests::TestUnregisterCapabilityProvider() + TestResult ProviderMiscTests::TestDocumentFormattingProvider() { TestResult result{ "", true, "ok" }; ProviderEnv env; - protocol::UnregistrationParams params; + auto uri = ToUri(FixturePath("inlay_hint_case.tsl")); + std::string content = "var count := 1; \nvar name := \"alpha\";\n"; + OpenDocument(env.hub, uri, content, 1); + + protocol::DocumentFormattingParams params; + params.textDocument.uri = uri; + params.options.tabSize = 4; + params.options.insertSpaces = true; + protocol::RequestMessage request; - request.id = "unreg"; - request.method = "client/unregisterCapability"; + request.id = "fmt"; + request.method = "textDocument/formatting"; request.params = codec::ToLSPAny(params); - ::lsp::provider::client::UnregisterCapability provider; + ::lsp::provider::text_document::Formatting provider; auto json = provider.ProvideResponse(request, env.context); auto response = ParseResponse(json); - assertTrue(response.result == std::nullopt, "Unregister capability should return null"); + + assertFalse(response.error.has_value(), "Formatting should not return error"); + assertTrue(response.result.has_value(), "Formatting should return result"); + assertTrue(response.result->Is(), "Formatting result should be array"); + + const auto& edits = response.result->Get(); + assertTrue(!edits.empty(), "Formatting should return edits when content differs"); + assertTrue(edits.front().Is(), "Formatting edit should be object"); + + const auto& edit = edits.front().Get(); + auto new_text_it = edit.find("newText"); + assertTrue(new_text_it != edit.end(), "Formatting edit should include newText"); + assertTrue(new_text_it->second.Is(), "Formatting newText should be string"); + const auto& new_text = new_text_it->second.Get(); + assertTrue(new_text.find("1; ") == std::string::npos, "Formatting should trim trailing whitespace"); + return result; + } + + TestResult ProviderMiscTests::TestDocumentRangeFormattingProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto uri = ToUri(FixturePath("inlay_hint_case.tsl")); + std::string content = "var count := 1; \nvar name := \"alpha\";\n"; + OpenDocument(env.hub, uri, content, 1); + + protocol::DocumentRangeFormattingParams params; + params.textDocument.uri = uri; + params.range.start.line = 0; + params.range.start.character = 0; + params.range.end.line = 0; + params.range.end.character = 9999; + params.options.tabSize = 4; + params.options.insertSpaces = true; + + protocol::RequestMessage request; + request.id = "range_fmt"; + request.method = "textDocument/rangeFormatting"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::RangeFormatting provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "Range formatting should not return error"); + assertTrue(response.result.has_value(), "Range formatting should return result"); + assertTrue(response.result->Is(), "Range formatting result should be array"); + + const auto& edits = response.result->Get(); + assertTrue(!edits.empty(), "Range formatting should return edits"); + assertTrue(edits.front().Is(), "Range formatting edit should be object"); + + const auto& edit = edits.front().Get(); + auto new_text_it = edit.find("newText"); + assertTrue(new_text_it != edit.end(), "Range formatting edit should include newText"); + assertTrue(new_text_it->second.Is(), "Range formatting newText should be string"); + const auto& new_text = new_text_it->second.Get(); + assertTrue(new_text.find("1; ") == std::string::npos, "Range formatting should trim trailing whitespace"); + return result; + } + + TestResult ProviderMiscTests::TestDocumentOnTypeFormattingProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto uri = ToUri(FixturePath("inlay_hint_case.tsl")); + std::string content = "var count := 1; \nvar name := \"alpha\";\n"; + OpenDocument(env.hub, uri, content, 1); + + protocol::DocumentOnTypeFormattingParams params; + params.textDocument.uri = uri; + params.position.line = 0; + params.position.character = 0; + params.ch = ";"; + params.options.tabSize = 4; + params.options.insertSpaces = true; + + protocol::RequestMessage request; + request.id = "on_type_fmt"; + request.method = "textDocument/onTypeFormatting"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::OnTypeFormatting provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "OnType formatting should not return error"); + assertTrue(response.result.has_value(), "OnType formatting should return result"); + assertTrue(response.result->Is(), "OnType formatting result should be array"); + + const auto& edits = response.result->Get(); + assertTrue(!edits.empty(), "OnType formatting should return edits"); + assertTrue(edits.front().Is(), "OnType formatting edit should be object"); + + const auto& edit = edits.front().Get(); + auto new_text_it = edit.find("newText"); + assertTrue(new_text_it != edit.end(), "OnType formatting edit should include newText"); + assertTrue(new_text_it->second.Is(), "OnType formatting newText should be string"); + assertEqual(std::string(""), new_text_it->second.Get(), "OnType formatting should delete whitespace"); + return result; + } + + TestResult ProviderMiscTests::TestInlineValueProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("inlay_hint_case.tsl"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::InlineValueParams params; + params.textDocument.uri = uri; + params.range.start.line = 0; + params.range.start.character = 0; + params.range.end.line = 9999; + params.range.end.character = 0; + params.context.frameId = 0; + params.context.stoppedLocation = params.range; + + protocol::RequestMessage request; + request.id = "inline_value"; + request.method = "textDocument/inlineValue"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::InlineValue provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "InlineValue should not return error"); + assertTrue(response.result.has_value(), "InlineValue should return result"); + assertTrue(response.result->Is(), "InlineValue result should be array"); + return result; + } + + TestResult ProviderMiscTests::TestMonikerProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto path = FixturePath("main_unit.tsf"); + auto content = ReadTextFile(path); + auto uri = ToUri(path); + OpenDocument(env.hub, uri, content, 1); + + protocol::MonikerParams params; + params.textDocument.uri = uri; + params.position = FindPosition(content, "UnitFunc(a: integer): integer;"); + + protocol::RequestMessage request; + request.id = "moniker"; + request.method = "textDocument/moniker"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::text_document::Moniker provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "Moniker should not return error"); + assertTrue(response.result.has_value(), "Moniker should return result"); + assertTrue(response.result->Is(), "Moniker result should be array"); + + const auto& monikers = response.result->Get(); + assertTrue(!monikers.empty(), "Moniker should return at least one entry"); + assertTrue(monikers.front().Is(), "Moniker entry should be object"); + + const auto& moniker = monikers.front().Get(); + auto scheme_it = moniker.find("scheme"); + auto ident_it = moniker.find("identifier"); + assertTrue(scheme_it != moniker.end(), "Moniker should include scheme"); + assertTrue(ident_it != moniker.end(), "Moniker should include identifier"); + assertTrue(scheme_it->second.Is(), "Moniker scheme should be string"); + assertTrue(ident_it->second.Is(), "Moniker identifier should be string"); + assertEqual(std::string("tsl"), scheme_it->second.Get(), "Moniker scheme should be tsl"); + assertTrue(ident_it->second.Get().find(uri) != std::string::npos, "Moniker identifier should include uri"); + + return result; + } + + TestResult ProviderMiscTests::TestExecuteCommandProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + { + protocol::ExecuteCommandParams params; + params.command = "tsl.noop"; + + protocol::RequestMessage request; + request.id = "exec_noop"; + request.method = "workspace/executeCommand"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::workspace::ExecuteCommand provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "ExecuteCommand noop should not return error"); + assertTrue(response.result.has_value(), "ExecuteCommand noop should return result"); + assertTrue(response.result->Is(), "ExecuteCommand noop should return null"); + } + + { + auto workspace_uri = ToUri(FixturePath("workspace")); + + protocol::ExecuteCommandParams params; + params.command = "tsl.loadWorkspace"; + params.arguments = std::vector{ protocol::string(workspace_uri) }; + + protocol::RequestMessage request; + request.id = "exec_load_ws"; + request.method = "workspace/executeCommand"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::workspace::ExecuteCommand provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "ExecuteCommand loadWorkspace should not return error"); + assertTrue(response.result.has_value(), "ExecuteCommand loadWorkspace should return result"); + assertTrue(response.result->Is(), "ExecuteCommand loadWorkspace result should be string"); + assertEqual(std::string("scheduled"), response.result->Get(), "ExecuteCommand should schedule workspace load"); + + env.scheduler.WaitAll(); + auto modules = env.hub.symbols().QueryIndexedSymbols(protocol::SymbolKind::Module); + assertTrue(!modules.empty(), "Workspace load should populate module index"); + } + + return result; + } + + TestResult ProviderMiscTests::TestWillFileOperationsProviders() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + auto file_uri = ToUri(FixturePath("workspace/workspace_script.tsl")); + + { + protocol::CreateFilesParams params; + params.files = std::vector{ { .uri = file_uri } }; + + protocol::RequestMessage request; + request.id = "will_create"; + request.method = "workspace/willCreateFiles"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::workspace::WillCreateFiles provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "WillCreateFiles should not return error"); + assertTrue(response.result.has_value(), "WillCreateFiles should return result"); + assertTrue(response.result->Is(), "WillCreateFiles should return null"); + } + + { + protocol::DeleteFilesParams params; + params.files = std::vector{ { .uri = file_uri } }; + + protocol::RequestMessage request; + request.id = "will_delete"; + request.method = "workspace/willDeleteFiles"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::workspace::WillDeleteFiles provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "WillDeleteFiles should not return error"); + assertTrue(response.result.has_value(), "WillDeleteFiles should return result"); + assertTrue(response.result->Is(), "WillDeleteFiles should return null"); + } + + { + protocol::RenameFilesParams params; + params.files = std::vector{ { .oldUri = file_uri, .newUri = file_uri } }; + + protocol::RequestMessage request; + request.id = "will_rename"; + request.method = "workspace/willRenameFiles"; + request.params = codec::ToLSPAny(params); + + ::lsp::provider::workspace::WillRenameFiles provider; + auto json = provider.ProvideResponse(request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "WillRenameFiles should not return error"); + assertTrue(response.result.has_value(), "WillRenameFiles should return result"); + assertTrue(response.result->Is(), "WillRenameFiles should return null"); + } + + return result; + } + + TestResult ProviderMiscTests::TestWorkspaceSymbolResolveProvider() + { + TestResult result{ "", true, "ok" }; + ProviderEnv env; + + env.hub.symbols().LoadWorkspace(ToUri(FixturePath("workspace"))); + + protocol::LSPObject params; + params["query"] = protocol::string("Workspace"); + + protocol::RequestMessage query_request; + query_request.id = "ws_symbol_resolve_seed"; + query_request.method = "workspace/symbol"; + query_request.params = protocol::LSPAny(std::move(params)); + + ::lsp::provider::workspace::Symbol symbol_provider; + auto query_json = symbol_provider.ProvideResponse(query_request, env.context); + auto query_response = ParseResponse(query_json); + assertFalse(query_response.error.has_value(), "Workspace symbol should not return error"); + assertTrue(query_response.result.has_value(), "Workspace symbol should return result"); + + auto symbols = codec::FromLSPAny.template operator()>(query_response.result.value()); + assertTrue(!symbols.empty(), "Workspace symbol should return symbols"); + + auto symbol_any = codec::ToLSPAny(symbols.front()); + assertTrue(symbol_any.Is(), "Workspace symbol should serialize to object"); + + auto symbol_obj = symbol_any.Get(); + auto location_it = symbol_obj.find("location"); + assertTrue(location_it != symbol_obj.end(), "Workspace symbol should include location"); + assertTrue(location_it->second.Is(), "Workspace symbol location should be object"); + + auto location_obj = location_it->second.Get(); + location_obj.erase("range"); + location_it->second = protocol::LSPAny(std::move(location_obj)); + + protocol::RequestMessage resolve_request; + resolve_request.id = "ws_symbol_resolve"; + resolve_request.method = "workspaceSymbol/resolve"; + resolve_request.params = protocol::LSPAny(std::move(symbol_obj)); + + ::lsp::provider::workspace_symbol::Resolve provider; + auto json = provider.ProvideResponse(resolve_request, env.context); + auto response = ParseResponse(json); + + assertFalse(response.error.has_value(), "workspaceSymbol/resolve should not return error"); + assertTrue(response.result.has_value(), "workspaceSymbol/resolve should return result"); + assertTrue(response.result->Is(), "workspaceSymbol/resolve result should be object"); + + const auto& resolved = response.result->Get(); + auto resolved_location_it = resolved.find("location"); + assertTrue(resolved_location_it != resolved.end(), "Resolved symbol should include location"); + assertTrue(resolved_location_it->second.Is(), "Resolved location should be object"); + const auto& resolved_location = resolved_location_it->second.Get(); + assertTrue(resolved_location.find("range") != resolved_location.end(), "Resolved location should include range"); + return result; } diff --git a/lsp-server/test/test_provider/provider_surface_test.cppm b/lsp-server/test/test_provider/provider_surface_test.cppm index 87bd3c8..4b3afe3 100644 --- a/lsp-server/test/test_provider/provider_surface_test.cppm +++ b/lsp-server/test/test_provider/provider_surface_test.cppm @@ -13,8 +13,6 @@ import lsp.scheduler.async_executor; import lsp.test.provider.fixtures; import lsp.provider.cancel_request.cancel_request; -import lsp.provider.client.register_capability; -import lsp.provider.client.unregister_capability; import lsp.provider.code_action.resolve; import lsp.provider.code_lens.resolve; import lsp.provider.completion_item.resolve; @@ -24,38 +22,40 @@ import lsp.provider.initialize.initialize; import lsp.provider.initialized.initialized; import lsp.provider.inlay_hint.resolve; import lsp.provider.shutdown.shutdown; -import lsp.provider.telemetry.event; import lsp.provider.trace.set_trace; import lsp.provider.call_hierarchy.incoming_calls; import lsp.provider.call_hierarchy.outgoing_calls; import lsp.provider.type_hierarchy.supertypes; import lsp.provider.type_hierarchy.subtypes; -import lsp.provider.window.log_message; -import lsp.provider.window.show_document; -import lsp.provider.window.show_message; -import lsp.provider.window.show_message_request; -import lsp.provider.window.work_done_progress_create; -import lsp.provider.workspace.apply_edit; -import lsp.provider.workspace.code_lens_refresh; -import lsp.provider.workspace.configuration; +import lsp.provider.client.register_capability; +import lsp.provider.client.unregister_capability; import lsp.provider.workspace.diagnostic; -import lsp.provider.workspace.diagnostic_refresh; import lsp.provider.workspace.did_change_configuration; import lsp.provider.workspace.did_change_watched_files; import lsp.provider.workspace.did_change_workspace_folders; import lsp.provider.workspace.did_create_files; import lsp.provider.workspace.did_delete_files; import lsp.provider.workspace.did_rename_files; -import lsp.provider.workspace.execute_command; +import lsp.provider.workspace.configuration; +import lsp.provider.workspace.apply_edit; +import lsp.provider.workspace.workspace_folders; +import lsp.provider.workspace.code_lens_refresh; +import lsp.provider.workspace.diagnostic_refresh; import lsp.provider.workspace.inlay_hint_refresh; import lsp.provider.workspace.inline_value_refresh; import lsp.provider.workspace.semantic_tokens_refresh; +import lsp.provider.workspace.execute_command; import lsp.provider.workspace.symbol; import lsp.provider.workspace.will_create_files; import lsp.provider.workspace.will_delete_files; import lsp.provider.workspace.will_rename_files; -import lsp.provider.workspace.workspace_folders; import lsp.provider.workspace_symbol.resolve; +import lsp.provider.window.work_done_progress_create; +import lsp.provider.window.show_message_request; +import lsp.provider.window.show_document; +import lsp.provider.window.log_message; +import lsp.provider.window.show_message; +import lsp.provider.telemetry.event; import lsp.provider.text_document.code_action; import lsp.provider.text_document.code_lens; import lsp.provider.text_document.color_presentation; @@ -81,7 +81,6 @@ import lsp.provider.text_document.on_type_formatting; import lsp.provider.text_document.prepare_call_hierarchy; import lsp.provider.text_document.prepare_rename; import lsp.provider.text_document.prepare_type_hierarchy; -import lsp.provider.text_document.publish_diagnostics; import lsp.provider.text_document.range_formatting; import lsp.provider.text_document.references; import lsp.provider.text_document.rename; @@ -89,6 +88,7 @@ import lsp.provider.text_document.selection_range; import lsp.provider.text_document.semantic_tokens; import lsp.provider.text_document.signature_help; import lsp.provider.text_document.type_definition; +import lsp.provider.text_document.publish_diagnostics; export namespace lsp::test::provider { @@ -234,32 +234,35 @@ namespace lsp::test::provider CheckProviderMetadata("inlayHint/resolve", "InlayHintResolve"); CheckProviderMetadata("workspaceSymbol/resolve", "WorkspaceSymbResolve"); CheckProviderMetadata("typeHierarchy/supertypes", "TypeHierarchySupertypes"); - CheckProviderMetadata("typeHierarchy/subtypes", "WorkspaceSubtypes"); + CheckProviderMetadata("typeHierarchy/subtypes", "TypeHierarchySubtypes"); CheckProviderMetadata("callHierarchy/incomingCalls", "CallHierarchyIncomingCalls"); CheckProviderMetadata("callHierarchy/outgoingCalls", "CallHierarchyOutgoingCalls"); - CheckProviderMetadata("workspace/applyEdit", "WorkspaceApplyEdit"); - CheckProviderMetadata("workspace/configuration", "WorkspaceConfiguration"); CheckProviderMetadata("workspace/diagnostic", "WorkspaceDiagnostic"); - CheckProviderMetadata("workspace/diagnostic/refresh", - "WorkspaceDiagnosticRefresh"); CheckProviderMetadata("workspace/executeCommand", "WorkspaceExecuteCommand"); - CheckProviderMetadata("workspace/workspaceFolders", - "WorkspaceWorkspaceFolders"); CheckProviderMetadata("workspace/willCreateFiles", "WorkspaceWillCreateFiles"); CheckProviderMetadata("workspace/willDeleteFiles", "WorkspaceWillDeleteFiles"); CheckProviderMetadata("workspace/willRenameFiles", "WorkspaceWillRenameFiles"); CheckProviderMetadata("workspace/symbol", "WorkSpaceSymbol"); - CheckProviderMetadata("workspace/semanticTokens/refresh", - "WorkspaceSemanticTokensRefresh"); - CheckProviderMetadata("workspace/inlineValue/refresh", - "WorkspaceInlineValueRefresh"); - CheckProviderMetadata("workspace/inlayHint/refresh", - "WorkspaceInlayHintRefresh"); - CheckProviderMetadata("workspace/codeLens/refresh", - "WorkspaceCodeLensRefresh"); + CheckProviderMetadata("workspace/configuration", "WorkspaceConfiguration"); + CheckProviderMetadata("workspace/applyEdit", "WorkspaceApplyEdit"); + CheckProviderMetadata("workspace/workspaceFolders", "WorkspaceWorkspaceFolders"); + CheckProviderMetadata("workspace/codeLens/refresh", "WorkspaceCodeLensRefresh"); + CheckProviderMetadata("workspace/diagnostic/refresh", "WorkspaceDiagnosticRefresh"); + CheckProviderMetadata("workspace/inlayHint/refresh", "WorkspaceInlayHintRefresh"); + CheckProviderMetadata("workspace/inlineValue/refresh", "WorkspaceInlineValueRefresh"); + CheckProviderMetadata("workspace/semanticTokens/refresh", "WorkspaceSemanticTokensRefresh"); + CheckProviderMetadata("client/registerCapability", "ClientRegisterCapability"); + CheckProviderMetadata("client/unregisterCapability", "ClientUnregisterCapability"); + CheckProviderMetadata("window/workDoneProgress/create", "WindowWorkDoneProgressCreate"); + CheckProviderMetadata("window/showMessageRequest", "WindowShowMessageRequest"); + CheckProviderMetadata("window/showDocument", "WindowShowDocument"); + CheckProviderMetadata("window/logMessage", "WindowLogMessage"); + CheckProviderMetadata("window/showMessage", "WindowShowMessage"); + CheckProviderMetadata("telemetry/event", "TelemetryEvent"); + CheckProviderMetadata("textDocument/publishDiagnostics", "TextDocumentPublishDiagnostics"); CheckProviderMetadata("workspace/didChangeConfiguration", "WorkspaceDidChangeConfiguration"); CheckProviderMetadata("workspace/didChangeWatchedFiles", @@ -269,15 +272,6 @@ namespace lsp::test::provider CheckProviderMetadata("workspace/didCreateFiles", "WorkspaceDidCreateFiles"); CheckProviderMetadata("workspace/didDeleteFiles", "WorkspaceDidDeleteFiles"); CheckProviderMetadata("workspace/didRenameFiles", "WorkspaceDidRenameFiles"); - CheckProviderMetadata("window/showMessageRequest", - "WindowShowMessageRequest"); - CheckProviderMetadata("window/showDocument", "WindowShowDocument"); - CheckProviderMetadata("window/workDoneProgress/create", - "WindowWorkDoneProgressCreate"); - CheckProviderMetadata("client/registerCapability", - "ClientRegisterCapability"); - CheckProviderMetadata("client/unregisterCapability", - "ClientUnregisterCapability"); CheckProviderMetadata("initialized", "Initialized"); CheckProviderMetadata("exit", "Exit"); CheckProviderMetadata("$/cancelRequest", "CancelRequest"); @@ -285,11 +279,6 @@ namespace lsp::test::provider CheckProviderMetadata("textDocument/didOpen", "TextDocumentDidOpen"); CheckProviderMetadata("textDocument/didChange", "TextDocumentDidChange"); CheckProviderMetadata("textDocument/didClose", "TextDocumentDidClose"); - CheckProviderMetadata("textDocument/publishDiagnostics", - "TextDocumentPublishDiagnostics"); - CheckProviderMetadata("window/showMessage", "WindowShowMessage"); - CheckProviderMetadata("window/logMessage", "WindowLogMessage"); - CheckProviderMetadata("telemetry/event", "TelemetryEvent"); return result; } @@ -340,25 +329,25 @@ namespace lsp::test::provider CheckRequestResponse(); CheckRequestResponse(); CheckRequestResponse(); - CheckRequestResponse(); - CheckRequestResponse(); CheckRequestResponse(); - CheckRequestResponse(); CheckRequestResponse(); - CheckRequestResponse(); CheckRequestResponse(); CheckRequestResponse(); CheckRequestResponse(); CheckRequestResponse(); - CheckRequestResponse(); - CheckRequestResponse(); - CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); CheckRequestResponse(); - CheckRequestResponse(); - CheckRequestResponse(); - CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); CheckRequestResponse(); CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); + CheckRequestResponse(); return result; } @@ -369,16 +358,16 @@ namespace lsp::test::provider CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); - CheckNotificationHandler(std::nullopt); - CheckNotificationHandler(std::nullopt); - CheckNotificationHandler(std::nullopt); - CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); + CheckNotificationHandler(std::nullopt); return result; } diff --git a/lsp-server/test/test_provider/server_json_test.cppm b/lsp-server/test/test_provider/server_json_test.cppm index 4cf1875..995448c 100644 --- a/lsp-server/test/test_provider/server_json_test.cppm +++ b/lsp-server/test/test_provider/server_json_test.cppm @@ -95,9 +95,9 @@ namespace lsp::test::provider return 0; } - std::vector ParseResponses(const std::string& data) + std::vector ParseBodies(const std::string& data) { - std::vector responses; + std::vector bodies; std::size_t pos = 0; while (pos < data.size()) { @@ -120,11 +120,10 @@ namespace lsp::test::provider break; } - auto body = data.substr(body_start, length); - responses.push_back(DeserializeResponseOrThrow(body)); + bodies.push_back(data.substr(body_start, length)); pos = body_start + length; } - return responses; + return bodies; } protocol::CompletionItem BuildResolveItem(const std::string& uri) @@ -263,13 +262,46 @@ namespace lsp::test::provider std::filesystem::remove(input_path); std::filesystem::remove(output_path); - auto responses = ParseResponses(output); + auto bodies = ParseBodies(output); std::unordered_map by_id; - for (const auto& response : responses) + bool saw_diagnostics = false; + + for (const auto& body : bodies) { - if (response.id.has_value()) + auto any = codec::Deserialize(body); + if (!any.has_value() || !any->Is()) { - by_id[codec::debug::GetIdString(response.id.value())] = response; + continue; + } + + const auto& obj = any->Get(); + const bool has_id = obj.contains("id"); + const bool has_method = obj.contains("method"); + const bool has_result = obj.contains("result"); + const bool has_error = obj.contains("error"); + + if (has_id && (has_result || has_error)) + { + auto response = DeserializeResponseOrThrow(body); + if (response.id.has_value()) + { + by_id[codec::debug::GetIdString(response.id.value())] = std::move(response); + } + continue; + } + + if (has_method && !has_id) + { + auto notification = codec::Deserialize(body); + if (notification && notification->method == "textDocument/publishDiagnostics" && notification->params.has_value()) + { + const auto& diag_params = notification->params->Get(); + auto uri_it = diag_params.find("uri"); + if (uri_it != diag_params.end() && uri_it->second.Is() && uri_it->second.Get() == uri) + { + saw_diagnostics = true; + } + } } } @@ -292,6 +324,7 @@ namespace lsp::test::provider auto expected = FindPosition(content, "function UnitFunc", false); assertTrue(location.range.start.line == expected.line, "Definition should resolve in document"); + assertTrue(saw_diagnostics, "Server should publish diagnostics after didOpen"); assertTrue(!by_id["5"].error.has_value(), "Shutdown response should not contain error"); return result; } diff --git a/lsp-server/test/test_provider/test_main.cppm b/lsp-server/test/test_provider/test_main.cppm index 7107cad..d022573 100644 --- a/lsp-server/test/test_provider/test_main.cppm +++ b/lsp-server/test/test_provider/test_main.cppm @@ -8,6 +8,7 @@ import lsp.test.framework; import lsp.test.provider.completion; import lsp.test.provider.definitions; import lsp.test.provider.json_flow; +import lsp.test.provider.json_provider_coverage; import lsp.test.provider.misc; import lsp.test.provider.surface; import lsp.test.provider.fixtures; @@ -41,6 +42,8 @@ export int Run(int argc, char** argv) lsp::test::provider::DefinitionTests::Register(runner); std::cout << " - JSON flow tests" << std::endl; lsp::test::provider::JsonFlowTests::Register(runner); + std::cout << " - JSON provider coverage tests" << std::endl; + lsp::test::provider::JsonProviderCoverageTests::Register(runner); std::cout << " - Other provider tests" << std::endl; lsp::test::provider::ProviderMiscTests::Register(runner); std::cout << " - Provider surface tests" << std::endl; diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index d119db8..cb3a3ef 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode' import * as fs from 'fs' import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node' -let client: LanguageClient +let client: LanguageClient | undefined function findInSystemPath(binary: string): string | null { const pathDirs = process.env.PATH?.split(path.delimiter) || [] @@ -37,6 +37,48 @@ export function activate(context: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration('tsl') let serverArguments = config.get('server.arguments') || [] + context.subscriptions.push( + vscode.commands.registerCommand('tsl.showReferences', async (data?: any) => { + if (!data || typeof data !== 'object') { + vscode.window.showErrorMessage('TSL: invalid reference payload') + return + } + + const uriValue = (data as any).uri + const positionValue = (data as any).position + if (typeof uriValue !== 'string' || !positionValue || typeof positionValue !== 'object') { + vscode.window.showErrorMessage('TSL: missing uri/position in reference payload') + return + } + + const line = (positionValue as any).line + const character = (positionValue as any).character + if (typeof line !== 'number' || typeof character !== 'number') { + vscode.window.showErrorMessage('TSL: invalid position in reference payload') + return + } + + if (client && !client.isRunning()) { + await client.start() + } + + const uri = vscode.Uri.parse(uriValue) + const position = new vscode.Position(line, character) + const locations = await vscode.commands.executeCommand( + 'vscode.executeReferenceProvider', + uri, + position + ) + + if (!locations || locations.length === 0) { + vscode.window.showInformationMessage('TSL: no references found') + return + } + + await vscode.commands.executeCommand('editor.action.showReferences', uri, position, locations) + }) + ) + const serverBinary = process.platform === 'win32' ? 'tsl-server.exe' : 'tsl-server' let serverExe = findExecutable( context, @@ -86,7 +128,10 @@ export function activate(context: vscode.ExtensionContext) { const clientOptions: LanguageClientOptions = { documentSelector: [{ scheme: 'file', language: 'tsl' }], synchronize: { - fileEvents: vscode.workspace.createFileSystemWatcher('**/*.tsl') + fileEvents: [ + vscode.workspace.createFileSystemWatcher('**/*.tsl'), + vscode.workspace.createFileSystemWatcher('**/*.tsf') + ] } }