diff --git a/src/autocomplete.cpp b/src/autocomplete.cpp index 6893cca..3307826 100644 --- a/src/autocomplete.cpp +++ b/src/autocomplete.cpp @@ -59,19 +59,19 @@ void Autocomplete::run() { if(thread.joinable()) thread.join(); auto iter = view->get_buffer()->get_insert()->get_iter(); - auto line_nr = iter.get_line() + 1; - auto column_nr = iter.get_line_index() + 1; + auto line = iter.get_line(); + auto line_index = iter.get_line_index(); Glib::ustring buffer; if(pass_buffer_and_strip_word) { auto pos = iter.get_offset() - 1; buffer = view->get_buffer()->get_text(); while(pos >= 0 && Source::BaseView::is_token_char(buffer[pos])) { buffer.replace(pos, 1, " "); - column_nr--; + line_index--; pos--; } } - thread = std::thread([this, line_nr, column_nr, buffer = std::move(buffer)] { + thread = std::thread([this, line, line_index, buffer = std::move(buffer)] { auto lock = get_parse_lock(); if(!is_processing()) return; @@ -79,7 +79,7 @@ void Autocomplete::run() { rows.clear(); auto &buffer_raw = const_cast(buffer.raw()); - bool success = add_rows(buffer_raw, line_nr, column_nr); + bool success = add_rows(buffer_raw, line, line_index); if(!is_processing()) return; diff --git a/src/autocomplete.hpp b/src/autocomplete.hpp index f023fae..76586ed 100644 --- a/src/autocomplete.hpp +++ b/src/autocomplete.hpp @@ -48,7 +48,8 @@ public: std::function on_add_rows_error = [] {}; /// The handler is not run in the main loop. Should return false on error. - std::function add_rows = [](std::string &, int, int) { return true; }; + /// Column is line byte index. + std::function add_rows = [](std::string &, int, int) { return true; }; std::function on_show = [] {}; std::function on_hide = [] {}; diff --git a/src/source_clang.cpp b/src/source_clang.cpp index 87d52c4..3f4e32a 100644 --- a/src/source_clang.cpp +++ b/src/source_clang.cpp @@ -948,10 +948,10 @@ Source::ClangViewAutocomplete::ClangViewAutocomplete(const boost::filesystem::pa full_reparse(); }; - autocomplete.add_rows = [this](std::string &buffer, int line_number, int column) { + autocomplete.add_rows = [this](std::string &buffer, int line, int line_index) { if(is_language({"chdr", "cpphdr"})) clangmm::remove_include_guard(buffer); - code_complete_results = std::make_unique(clang_tu->get_code_completions(buffer, line_number, column)); + code_complete_results = std::make_unique(clang_tu->get_code_completions(buffer, line + 1, line_index + 1)); if(!code_complete_results->cx_results) return false; diff --git a/src/source_generic.cpp b/src/source_generic.cpp index d310bc4..83c37c2 100644 --- a/src/source_generic.cpp +++ b/src/source_generic.cpp @@ -243,7 +243,7 @@ void Source::GenericView::setup_autocomplete() { update_status_state(this); }; - autocomplete.add_rows = [this](std::string &buffer, int line_number, int column) { + autocomplete.add_rows = [this](std::string &buffer, int /*line*/, int /*line_index*/) { if(autocomplete.state == Autocomplete::State::starting) { autocomplete_comment.clear(); autocomplete_insert.clear(); diff --git a/src/source_language_protocol.cpp b/src/source_language_protocol.cpp index 549d509..ee1aeba 100644 --- a/src/source_language_protocol.cpp +++ b/src/source_language_protocol.cpp @@ -126,7 +126,7 @@ LanguageProtocol::Client::~Client() { std::promise result_processed; write_request(nullptr, "shutdown", "", [this, &result_processed](const boost::property_tree::ptree &result, bool error) { if(!error) - this->write_notification("exit", ""); + this->write_notification("exit"); result_processed.set_value(); }); result_processed.get_future().get(); @@ -182,7 +182,7 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang process_id = process->get_id(); } write_request( - nullptr, "initialize", "\"processId\":" + std::to_string(process_id) + R"(,"rootUri":")" + LanguageProtocol::escape_text(filesystem::get_uri_from_path(root_path)) + R"(","capabilities": { + nullptr, "initialize", "\"processId\":" + std::to_string(process_id) + ",\"rootUri\":\"" + LanguageProtocol::escape_text(filesystem::get_uri_from_path(root_path)) + R"(","capabilities": { "workspace": { "symbol": { "dynamicRegistration": false } }, @@ -206,6 +206,8 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang } }, "definition": { "dynamicRegistration": false }, + "typeDefinition": { "dynamicRegistration": false }, + "implementation": { "dynamicRegistration": false }, "references": { "dynamicRegistration": false }, "documentHighlight": { "dynamicRegistration": false }, "documentSymbol": { "dynamicRegistration": false }, @@ -219,8 +221,7 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang "codeActionKind": { "valueSet": ["quickfix"] } } } - }, - "offsetEncoding": ["utf-8"] + } }, "initializationOptions": { "checkOnSave": { "enable": true } @@ -239,6 +240,8 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang capabilities.completion = static_cast(capabilities_pt->get_child_optional("completionProvider")); capabilities.signature_help = static_cast(capabilities_pt->get_child_optional("signatureHelpProvider")); capabilities.definition = capabilities_pt->get("definitionProvider", false); + capabilities.type_definition = capabilities_pt->get("typeDefinitionProvider", false); + capabilities.implementation = capabilities_pt->get("implementationProvider", false); capabilities.references = capabilities_pt->get("referencesProvider", false); capabilities.document_highlight = capabilities_pt->get("documentHighlightProvider", false); capabilities.workspace_symbol = capabilities_pt->get("workspaceSymbolProvider", false); @@ -255,9 +258,10 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang } // See https://clangd.llvm.org/extensions.html#utf-8-offsets for documentation on offsetEncoding - capabilities.use_line_index = result.get("offsetEncoding", "") == "utf-8"; + // Disabled for now since rust-analyzer does not seem to support utf-8 byte offsets from clients + // capabilities.use_line_index = result.get("offsetEncoding", "") == "utf-8"; - write_notification("initialized", ""); + write_notification("initialized"); } result_processed.set_value(); }); @@ -419,11 +423,10 @@ void LanguageProtocol::Client::write_request(Source::LanguageProtocolView *view, } }); } - std::string content(R"({"jsonrpc":"2.0","id":)" + std::to_string(message_id++) + R"(,"method":")" + method + R"(","params":{)" + params + "}}"); - auto message = "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content; + std::string content("{\"jsonrpc\":\"2.0\",\"id\":" + std::to_string(message_id++) + ",\"method\":\"" + method + "\",\"params\":{" + params + "}}"); if(Config::get().log.language_server) std::cout << "Language client: " << content << std::endl; - if(!process->write(message)) { + if(!process->write("Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content)) { Terminal::get().async_print("\e[31mError\e[m: could not write to language server. Please close and reopen all project files.\n", true); auto id_it = handlers.find(message_id - 1); if(id_it != handlers.end()) { @@ -438,20 +441,18 @@ void LanguageProtocol::Client::write_request(Source::LanguageProtocolView *view, void LanguageProtocol::Client::write_response(size_t id, const std::string &result) { LockGuard lock(read_write_mutex); - std::string content(R"({"jsonrpc":"2.0","id":)" + std::to_string(id) + R"(,"result":{)" + result + "}}"); - auto message = "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content; + std::string content("{\"jsonrpc\":\"2.0\",\"id\":" + std::to_string(id) + ",\"result\":{" + result + "}}"); if(Config::get().log.language_server) std::cout << "Language client: " << content << std::endl; - process->write(message); + process->write("Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content); } void LanguageProtocol::Client::write_notification(const std::string &method, const std::string ¶ms) { LockGuard lock(read_write_mutex); - std::string content(R"({"jsonrpc":"2.0","method":")" + method + R"(","params":{)" + params + "}}"); - auto message = "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content; + std::string content("{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"params\":{" + params + "}}"); if(Config::get().log.language_server) std::cout << "Language client: " << content << std::endl; - process->write(message); + process->write("Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content); } void LanguageProtocol::Client::handle_server_notification(const std::string &method, const boost::property_tree::ptree ¶ms) { @@ -503,7 +504,7 @@ void Source::LanguageProtocolView::initialize() { this->capabilities = capabilities; set_editable(true); - client->write_notification("textDocument/didOpen", R"("textDocument":{"uri":")" + uri_escaped + R"(","languageId":")" + language_id + R"(","version":)" + std::to_string(document_version++) + R"(,"text":")" + LanguageProtocol::escape_text(get_buffer()->get_text()) + "\"}"); + write_did_open_notification(); if(!initialized) { setup_signals(); @@ -548,7 +549,7 @@ void Source::LanguageProtocolView::close() { autocomplete->thread.join(); } - client->write_notification("textDocument/didClose", R"("textDocument":{"uri":")" + uri_escaped + "\"}"); + write_notification("textDocument/didClose"); client->close(this); client = nullptr; } @@ -558,6 +559,76 @@ Source::LanguageProtocolView::~LanguageProtocolView() { thread_pool.shutdown(true); } +int Source::LanguageProtocolView::get_line_pos(const Gtk::TextIter &iter) { + if(capabilities.use_line_index) + return iter.get_line_index(); + return utf16_code_unit_count(get_line(iter), 0, iter.get_line_index()); +} + +int Source::LanguageProtocolView::get_line_pos(int line, int line_index) { + if(capabilities.use_line_index) + return line_index; + return utf16_code_unit_count(get_line(line), 0, line_index); +} + +Gtk::TextIter Source::LanguageProtocolView::get_iter_at_line_pos(int line, int pos) { + if(capabilities.use_line_index) + return get_iter_at_line_index(line, pos); + return get_iter_at_line_index(line, utf16_code_units_byte_count(get_line(line), pos)); +} + +std::pair Source::LanguageProtocolView::make_position(int line, int character) { + return {"position", "{\"line\":" + std::to_string(line) + ",\"character\":" + std::to_string(character) + "}"}; +} + +std::pair Source::LanguageProtocolView::make_range(const std::pair &start, const std::pair &end) { + return {"range", "{\"start\":{\"line\":" + std::to_string(start.first) + ",\"character\":" + std::to_string(start.second) + "},\"end\":{\"line\":" + std::to_string(end.first) + ",\"character\":" + std::to_string(end.second) + "}}"}; +} + +std::string Source::LanguageProtocolView::to_string(const std::pair ¶m) { + std::string result; + result.reserve(param.first.size() + param.second.size() + 3); + result += '"'; + result += param.first; + result += "\":"; + result += param.second; + return result; +} + +std::string Source::LanguageProtocolView::to_string(const std::vector> ¶ms) { + size_t size = params.empty() ? 0 : params.size() - 1; + for(auto ¶m : params) + size += param.first.size() + param.second.size() + 3; + + std::string result; + result.reserve(size); + for(auto ¶m : params) { + if(!result.empty()) + result += ','; + result += '"'; + result += param.first; + result += "\":"; + result += param.second; + } + return result; +} + +void Source::LanguageProtocolView::write_request(const std::string &method, const std::vector> ¶ms, std::function &&function) { + client->write_request(this, method, "\"textDocument\":{\"uri\":\"" + uri_escaped + "\"}" + (params.empty() ? "" : "," + to_string(params)), std::move(function)); +} + +void Source::LanguageProtocolView::write_notification(const std::string &method) { + client->write_notification(method, "\"textDocument\":{\"uri\":\"" + uri_escaped + "\"}"); +} + +void Source::LanguageProtocolView::write_did_open_notification() { + client->write_notification("textDocument/didOpen", "\"textDocument\":{\"uri\":\"" + uri_escaped + "\",\"version\":" + std::to_string(document_version++) + ",\"languageId\":\"" + language_id + "\",\"text\":\"" + LanguageProtocol::escape_text(get_buffer()->get_text().raw()) + "\"}"); +} + +void Source::LanguageProtocolView::write_did_change_notification(const std::vector> ¶ms) { + client->write_notification("textDocument/didChange", "\"textDocument\":{\"uri\":\"" + uri_escaped + "\",\"version\":" + std::to_string(document_version++) + (params.empty() ? "}" : "}," + to_string(params))); +} + void Source::LanguageProtocolView::rename(const boost::filesystem::path &path) { // Reset view close(); @@ -573,7 +644,7 @@ bool Source::LanguageProtocolView::save() { if(!Source::View::save()) return false; - client->write_notification("textDocument/didSave", R"("textDocument":{"uri":")" + uri_escaped + "\"}"); + write_notification("textDocument/didSave"); update_type_coverage(); @@ -621,20 +692,17 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { std::promise result_processed; std::string method; - std::string params; - std::string options("\"tabSize\":" + std::to_string(tab_size) + ",\"insertSpaces\":" + (tab_char == ' ' ? "true" : "false")); + std::vector> params = {{"options", '{' + to_string({{"tabSize", std::to_string(tab_size)}, {"insertSpaces", tab_char == ' ' ? "true" : "false"}}) + '}'}}; if(get_buffer()->get_has_selection() && capabilities.document_range_formatting) { method = "textDocument/rangeFormatting"; Gtk::TextIter start, end; get_buffer()->get_selection_bounds(start, end); - params = R"("textDocument":{"uri":")" + uri_escaped + R"("},"range":{"start":{"line":)" + std::to_string(start.get_line()) + ",\"character\":" + std::to_string(start.get_line_offset()) + R"(},"end":{"line":)" + std::to_string(end.get_line()) + ",\"character\":" + std::to_string(end.get_line_offset()) + "}},\"options\":{" + options + "}"; + params.emplace_back(make_range({start.get_line(), get_line_pos(start)}, {end.get_line(), get_line_pos(end)})); } - else { + else method = "textDocument/formatting"; - params = R"("textDocument":{"uri":")" + uri_escaped + R"("},"options":{)" + options + "}"; - } - client->write_request(this, method, params, [&text_edits, &result_processed](const boost::property_tree::ptree &result, bool error) { + write_request(method, params, [&text_edits, &result_processed](const boost::property_tree::ptree &result, bool error) { if(!error) { for(auto it = result.begin(); it != result.end(); ++it) { try { @@ -653,7 +721,7 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { if(text_edits.size() == 1 && text_edits[0].range.start.line == 0 && text_edits[0].range.start.character == 0 && (text_edits[0].range.end.line > end_iter.get_line() || - (text_edits[0].range.end.line == end_iter.get_line() && text_edits[0].range.end.character >= end_iter.get_line_offset()))) { + (text_edits[0].range.end.line == end_iter.get_line() && text_edits[0].range.end.character >= get_line_pos(end_iter)))) { replace_text(text_edits[0].new_text); } else { @@ -677,11 +745,50 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { Info::get().print("No declaration found"); return offset; }; - get_declaration_or_implementation_locations = [this]() { - std::vector offsets; - auto offset = get_declaration_location(); - if(offset) - offsets.emplace_back(std::move(offset)); + + // Assumes that capabilities.definition is available when capabilities.implementation is supported + if(capabilities.implementation) { + get_declaration_or_implementation_locations = [this]() { + // Try implementation locations first + auto iter = get_buffer()->get_insert()->get_iter(); + auto offsets = get_implementations(iter); + auto token_iters = get_token_iters(iter); + bool is_implementation = false; + for(auto &offset : offsets) { + if(offset.file_path == file_path && get_iter_at_line_pos(offset.line, offset.index) == token_iters.first) { + is_implementation = true; + break; + } + } + if(offsets.empty() || is_implementation) { + if(auto offset = get_declaration_location()) + return std::vector({std::move(offset)}); + } + return offsets; + }; + } + else { + get_declaration_or_implementation_locations = [this]() { + std::vector offsets; + if(auto offset = get_declaration_location()) + offsets.emplace_back(std::move(offset)); + return offsets; + }; + } + } + if(capabilities.type_definition) { + get_type_declaration_location = [this]() { + auto offset = get_type_declaration(get_buffer()->get_insert()->get_iter()); + if(!offset) + Info::get().print("No type declaration found"); + return offset; + }; + } + if(capabilities.implementation) { + get_implementation_locations = [this]() { + auto offsets = get_implementations(get_buffer()->get_insert()->get_iter()); + if(offsets.empty()) + Info::get().print("No implementation found"); return offsets; }; } @@ -698,7 +805,7 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { else method = "textDocument/documentHighlight"; - client->write_request(this, method, R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(iter.get_line()) + ", \"character\": " + std::to_string(iter.get_line_offset()) + R"(}, "context": {"includeDeclaration": true})", [this, &locations, &result_processed](const boost::property_tree::ptree &result, bool error) { + write_request(method, {make_position(iter.get_line(), get_line_pos(iter)), {"context", "{\"includeDeclaration\":true}"}}, [this, &locations, &result_processed](const boost::property_tree::ptree &result, bool error) { if(!error) { try { for(auto it = result.begin(); it != result.end(); ++it) @@ -830,7 +937,7 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { std::vector changes_vec; std::promise result_processed; if(capabilities.rename) { - client->write_request(this, "textDocument/rename", R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(iter.get_line()) + ", \"character\": " + std::to_string(iter.get_line_offset()) + R"(}, "newName": ")" + text + "\"", [this, &changes_vec, &result_processed](const boost::property_tree::ptree &result, bool error) { + write_request("textDocument/rename", {make_position(iter.get_line(), get_line_pos(iter)), {"newName", '"' + text + '"'}}, [this, &changes_vec, &result_processed](const boost::property_tree::ptree &result, bool error) { if(!error) { boost::filesystem::path project_path; auto build = Project::Build::create(file_path); @@ -867,7 +974,7 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { }); } else { - client->write_request(this, "textDocument/documentHighlight", R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(iter.get_line()) + ", \"character\": " + std::to_string(iter.get_line_offset()) + R"(}, "context": {"includeDeclaration": true})", [this, &changes_vec, &text, &result_processed](const boost::property_tree::ptree &result, bool error) { + write_request("textDocument/documentHighlight", {make_position(iter.get_line(), get_line_pos(iter)), {"context", "{\"includeDeclaration\":true}"}}, [this, &changes_vec, &text, &result_processed](const boost::property_tree::ptree &result, bool error) { if(!error) { try { std::vector edits; @@ -933,7 +1040,7 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { if(changes->text_edits.size() == 1 && changes->text_edits[0].range.start.line == 0 && changes->text_edits[0].range.start.character == 0 && (changes->text_edits[0].range.end.line > end_iter.get_line() || - (changes->text_edits[0].range.end.line == end_iter.get_line() && changes->text_edits[0].range.end.character >= end_iter.get_line_offset()))) { + (changes->text_edits[0].range.end.line == end_iter.get_line() && changes->text_edits[0].range.end.character >= get_line_pos(end_iter)))) { view->replace_text(changes->text_edits[0].new_text); Terminal::get().print(filesystem::get_short_path(view->file_path).string() + ":1:1\n"); @@ -992,7 +1099,7 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { std::vector> methods; std::promise result_processed; - client->write_request(this, "textDocument/documentSymbol", R"("textDocument":{"uri":")" + uri_escaped + "\"}", [&result_processed, &methods](const boost::property_tree::ptree &result, bool error) { + write_request("textDocument/documentSymbol", {}, [&result_processed, &methods](const boost::property_tree::ptree &result, bool error) { if(!error) { std::function parse_result = [&methods, &parse_result](const boost::property_tree::ptree &pt, const std::string &container) { for(auto it = pt.begin(); it != pt.end(); ++it) { @@ -1048,917 +1155,945 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { }; } -void Source::LanguageProtocolView::update_diagnostics_async(std::vector &&diagnostics) { - update_diagnostics_async_count++; - size_t last_count = update_diagnostics_async_count; - if(capabilities.code_action && !diagnostics.empty()) { - dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable { - if(last_count != update_diagnostics_async_count) - return; - std::string range; - std::string diagnostics_string; - for(auto &diagnostic : diagnostics) { - auto start = get_iter_at_line_pos(diagnostic.range.start.line, diagnostic.range.start.character); - auto end = get_iter_at_line_pos(diagnostic.range.end.line, diagnostic.range.end.character); - range = "{\"start\":{\"line\": " + std::to_string(start.get_line()) + ",\"character\":" + std::to_string(start.get_line_offset()) + R"(},"end":{"line":)" + std::to_string(end.get_line()) + ",\"character\":" + std::to_string(end.get_line_offset()) + "}}"; - if(!diagnostics_string.empty()) - diagnostics_string += ','; - diagnostics_string += "{\"range\":" + range + ",\"message\":\"" + LanguageProtocol::escape_text(diagnostic.message) + "\""; - if(diagnostic.severity != 0) - diagnostics_string += ",\"severity\":" + std::to_string(diagnostic.severity); - if(!diagnostic.code.empty()) - diagnostics_string += ",\"code\":\"" + diagnostic.code + "\""; - diagnostics_string += "}"; - } - if(diagnostics.size() != 1) { // Use diagnostic range if only one diagnostic, otherwise use whole buffer - auto start = get_buffer()->begin(); - auto end = get_buffer()->end(); - range = "{\"start\":{\"line\": " + std::to_string(start.get_line()) + ",\"character\":" + std::to_string(start.get_line_offset()) + R"(},"end":{"line":)" + std::to_string(end.get_line()) + ",\"character\":" + std::to_string(end.get_line_offset()) + "}}"; - } +void Source::LanguageProtocolView::setup_signals() { + if(capabilities.text_document_sync == LanguageProtocol::Capabilities::TextDocumentSync::incremental) { + get_buffer()->signal_insert().connect( + [this](const Gtk::TextIter &start, const Glib::ustring &text, int bytes) { + std::pair location = {start.get_line(), get_line_pos(start)}; + write_did_change_notification({{"contentChanges", "[{" + to_string({make_range(location, location), {"text", '"' + LanguageProtocol::escape_text(text.raw()) + '"'}}) + "}]"}}); + }, + false); - auto request = (R"("textDocument":{"uri":")" + uri_escaped + "\"},\"range\":" + range + ",\"context\":{\"diagnostics\":[" + diagnostics_string + "],\"only\":[\"quickfix\"]}"); - thread_pool.push([this, diagnostics = std::move(diagnostics), request = std::move(request), last_count]() mutable { - if(last_count != update_diagnostics_async_count) - return; - std::promise result_processed; - client->write_request(this, "textDocument/codeAction", request, [this, &result_processed, &diagnostics, last_count](const boost::property_tree::ptree &result, bool error) { - if(!error && last_count == update_diagnostics_async_count) { - try { - for(auto it = result.begin(); it != result.end(); ++it) { - auto kind = it->second.get("kind", ""); - if(kind == "quickfix" || kind.empty()) { // Workaround for typescript-language-server (kind.empty()) - auto title = it->second.get("title"); - std::vector quickfix_diagnostics; - if(auto diagnostics_pt = it->second.get_child_optional("diagnostics")) { - for(auto it = diagnostics_pt->begin(); it != diagnostics_pt->end(); ++it) - quickfix_diagnostics.emplace_back(it->second); - } - if(auto changes = it->second.get_child_optional("edit.changes")) { - for(auto file_it = changes->begin(); file_it != changes->end(); ++file_it) { - for(auto edit_it = file_it->second.begin(); edit_it != file_it->second.end(); ++edit_it) { - LanguageProtocol::TextEdit edit(edit_it->second); - if(!quickfix_diagnostics.empty()) { - for(auto &diagnostic : diagnostics) { - for(auto &quickfix_diagnostic : quickfix_diagnostics) { - if(diagnostic.message == quickfix_diagnostic.message && diagnostic.range == quickfix_diagnostic.range) { - auto pair = diagnostic.quickfixes.emplace(title, std::set{}); - pair.first->second.emplace( - edit.new_text, - filesystem::get_path_from_uri(file_it->first).string(), - std::make_pair(Offset(edit.range.start.line, edit.range.start.character), - Offset(edit.range.end.line, edit.range.end.character))); - break; - } - } - } - } - else { // Workaround for language server that does not report quickfix diagnostics - for(auto &diagnostic : diagnostics) { - if(edit.range.start.line == diagnostic.range.start.line) { - auto pair = diagnostic.quickfixes.emplace(title, std::set{}); - pair.first->second.emplace( - edit.new_text, - filesystem::get_path_from_uri(file_it->first).string(), - std::make_pair(Offset(edit.range.start.line, edit.range.start.character), - Offset(edit.range.end.line, edit.range.end.character))); - break; - } - } - } - } - } - } - else { - auto changes_pt = it->second.get_child_optional("edit.documentChanges"); - if(!changes_pt) { // Workaround for typescript-language-server - if(auto arguments_pt = it->second.get_child_optional("arguments")) { - if(!arguments_pt->empty()) - changes_pt = arguments_pt->begin()->second.get_child_optional("documentChanges"); - } - } - if(changes_pt) { - for(auto change_it = changes_pt->begin(); change_it != changes_pt->end(); ++change_it) { - LanguageProtocol::TextDocumentEdit text_document_edit(change_it->second); - for(auto &edit : text_document_edit.edits) { - if(!quickfix_diagnostics.empty()) { - for(auto &diagnostic : diagnostics) { - for(auto &quickfix_diagnostic : quickfix_diagnostics) { - if(diagnostic.message == quickfix_diagnostic.message && diagnostic.range == quickfix_diagnostic.range) { - auto pair = diagnostic.quickfixes.emplace(title, std::set{}); - pair.first->second.emplace( - edit.new_text, - text_document_edit.file, - std::make_pair(Offset(edit.range.start.line, edit.range.start.character), - Offset(edit.range.end.line, edit.range.end.character))); - break; - } - } - } - } - else { // Workaround for language server that does not report quickfix diagnostics - for(auto &diagnostic : diagnostics) { - if(edit.range.start.line == diagnostic.range.start.line) { - auto pair = diagnostic.quickfixes.emplace(title, std::set{}); - pair.first->second.emplace( - edit.new_text, - text_document_edit.file, - std::make_pair(Offset(edit.range.start.line, edit.range.start.character), - Offset(edit.range.end.line, edit.range.end.character))); - break; - } - } - } - } - } - } - } - } - } - } - catch(...) { - } - } - result_processed.set_value(); - }); - result_processed.get_future().get(); - dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable { - if(last_count == update_diagnostics_async_count) { - last_diagnostics = diagnostics; - update_diagnostics(std::move(diagnostics)); - } - }); - }); - }); + get_buffer()->signal_erase().connect( + [this](const Gtk::TextIter &start, const Gtk::TextIter &end) { + write_did_change_notification({{"contentChanges", "[{" + to_string({make_range({start.get_line(), get_line_pos(start)}, {end.get_line(), get_line_pos(end)}), {"text", "\"\""}}) + "}]"}}); + }, + false); } - else { - dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable { - if(last_count == update_diagnostics_async_count) { - last_diagnostics = diagnostics; - update_diagnostics(std::move(diagnostics)); - } + else if(capabilities.text_document_sync == LanguageProtocol::Capabilities::TextDocumentSync::full) { + get_buffer()->signal_changed().connect([this]() { + write_did_change_notification({{"contentChanges", "[{" + to_string({"text", '"' + LanguageProtocol::escape_text(get_buffer()->get_text().raw()) + '"'}) + "}]"}}); }); } } -void Source::LanguageProtocolView::update_diagnostics(std::vector diagnostics) { - diagnostic_offsets.clear(); - diagnostic_tooltips.clear(); - fix_its.clear(); - get_buffer()->remove_tag_by_name("def:warning_underline", get_buffer()->begin(), get_buffer()->end()); - get_buffer()->remove_tag_by_name("def:error_underline", get_buffer()->begin(), get_buffer()->end()); - num_warnings = 0; - num_errors = 0; - num_fix_its = 0; - - for(auto &diagnostic : diagnostics) { - auto start = get_iter_at_line_pos(diagnostic.range.start.line, diagnostic.range.start.character); - auto end = get_iter_at_line_pos(diagnostic.range.end.line, diagnostic.range.end.character); - - if(start == end) { - if(!end.ends_line()) - end.forward_char(); - else - while(start.ends_line() && start.backward_char()) { // Move start so that diagnostic underline is visible - } - } - - bool error = false; - if(diagnostic.severity >= 2) - num_warnings++; - else { - num_errors++; - error = true; - } - num_fix_its += diagnostic.quickfixes.size(); +void Source::LanguageProtocolView::setup_autocomplete() { + autocomplete = std::make_unique(this, interactive_completion, last_keyval, false); - for(auto &quickfix : diagnostic.quickfixes) - fix_its.insert(fix_its.end(), quickfix.second.begin(), quickfix.second.end()); + if(!capabilities.completion) + return; - add_diagnostic_tooltip(start, end, error, [this, diagnostic = std::move(diagnostic)](Tooltip &tooltip) { - if(language_id == "python") { // Python might support markdown in the future - tooltip.insert_with_links_tagged(diagnostic.message); - return; - } - tooltip.insert_markdown(diagnostic.message); + non_interactive_completion = [this] { + if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) + return; + autocomplete->run(); + }; - if(!diagnostic.related_informations.empty()) { - auto link_tag = tooltip.buffer->get_tag_table()->lookup("link"); - for(size_t i = 0; i < diagnostic.related_informations.size(); ++i) { - auto link = filesystem::get_relative_path(diagnostic.related_informations[i].location.file, file_path.parent_path()).string(); - link += ':' + std::to_string(diagnostic.related_informations[i].location.range.start.line + 1); - link += ':' + std::to_string(diagnostic.related_informations[i].location.range.start.character + 1); + autocomplete->reparse = [this] { + autocomplete_rows.clear(); + }; - if(i == 0) - tooltip.buffer->insert_at_cursor("\n\n"); - else - tooltip.buffer->insert_at_cursor("\n"); - tooltip.insert_markdown(diagnostic.related_informations[i].message); - tooltip.buffer->insert_at_cursor(": "); - tooltip.buffer->insert_with_tag(tooltip.buffer->get_insert()->get_iter(), link, link_tag); - } - } + if(capabilities.signature_help) { + // Activate argument completions + get_buffer()->signal_changed().connect( + [this] { + if(!interactive_completion) + return; + if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) + return; + if(!has_focus()) + return; + if(autocomplete_show_arguments) + autocomplete->stop(); + autocomplete_show_arguments = false; + autocomplete_delayed_show_arguments_connection.disconnect(); + autocomplete_delayed_show_arguments_connection = Glib::signal_timeout().connect( + [this]() { + if(get_buffer()->get_has_selection()) + return false; + if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) + return false; + if(!has_focus()) + return false; + if(is_possible_argument()) { + autocomplete->stop(); + autocomplete->run(); + } + return false; + }, + 500); + }, + false); - if(!diagnostic.quickfixes.empty()) { - if(diagnostic.quickfixes.size() == 1) - tooltip.buffer->insert_at_cursor("\n\nFix-it:"); - else - tooltip.buffer->insert_at_cursor("\n\nFix-its:"); - for(auto &quickfix : diagnostic.quickfixes) { - tooltip.buffer->insert_at_cursor("\n"); - tooltip.insert_markdown(quickfix.first); - } - } - }); + // Remove argument completions + signal_key_press_event().connect( + [this](GdkEventKey *event) { + if(autocomplete_show_arguments && CompletionDialog::get() && CompletionDialog::get()->is_visible() && + event->keyval != GDK_KEY_Down && event->keyval != GDK_KEY_Up && + event->keyval != GDK_KEY_Return && event->keyval != GDK_KEY_KP_Enter && + event->keyval != GDK_KEY_ISO_Left_Tab && event->keyval != GDK_KEY_Tab && + (event->keyval < GDK_KEY_Shift_L || event->keyval > GDK_KEY_Hyper_R)) { + get_buffer()->erase(CompletionDialog::get()->start_mark->get_iter(), get_buffer()->get_insert()->get_iter()); + CompletionDialog::get()->hide(); + } + return false; + }, + false); } - for(auto &mark : type_coverage_marks) { - add_diagnostic_tooltip(mark.first->get_iter(), mark.second->get_iter(), false, [](Tooltip &tooltip) { - tooltip.buffer->insert_at_cursor(type_coverage_message); - }); - num_warnings++; + autocomplete->is_restart_key = [this](guint keyval) { + auto iter = get_buffer()->get_insert()->get_iter(); + iter.backward_chars(2); + if(keyval == '.' || (keyval == ':' && *iter == ':')) + return true; + return false; + }; + + std::function is_possible_xml_attribute = [](Gtk::TextIter) { return false; }; + if(is_js) { + autocomplete->is_restart_key = [](guint keyval) { + if(keyval == '.' || keyval == ' ') + return true; + return false; + }; + + is_possible_xml_attribute = [this](Gtk::TextIter iter) { + return (*iter == ' ' || iter.ends_line() || *iter == '/' || (*iter == '>' && iter.backward_char())) && find_open_symbol_backward(iter, iter, '<', '>'); + }; } - status_diagnostics = std::make_tuple(num_warnings, num_errors, num_fix_its); - if(update_status_diagnostics) - update_status_diagnostics(this); -} + autocomplete->run_check = [this, is_possible_xml_attribute]() { + auto prefix_start = get_buffer()->get_insert()->get_iter(); + auto prefix_end = prefix_start; -Gtk::TextIter Source::LanguageProtocolView::get_iter_at_line_pos(int line, int pos) { - if(capabilities.use_line_index) - return get_iter_at_line_index(line, pos); - return get_iter_at_line_index(line, utf16_code_units_byte_count(get_line(line), pos)); -} + auto prev = prefix_start; + prev.backward_char(); + if(!is_code_iter(prev)) + return false; -void Source::LanguageProtocolView::show_type_tooltips(const Gdk::Rectangle &rectangle) { - if(!capabilities.hover) - return; + size_t count = 0; + while(prefix_start.backward_char() && is_token_char(*prefix_start)) + ++count; - Gtk::TextIter iter; - int location_x, location_y; - window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, rectangle.get_x(), rectangle.get_y(), location_x, location_y); - location_x += (rectangle.get_width() - 1) / 2; - get_iter_at_location(iter, location_x, location_y); - Gdk::Rectangle iter_rectangle; - get_iter_location(iter, iter_rectangle); - if(iter.ends_line() && location_x > iter_rectangle.get_x()) - return; + autocomplete_enable_snippets = false; + autocomplete_show_arguments = false; - auto offset = iter.get_offset(); + if(prefix_start != prefix_end && !is_token_char(*prefix_start)) + prefix_start.forward_char(); - static int request_count = 0; - request_count++; - auto current_request = request_count; - client->write_request(this, "textDocument/hover", R"("textDocument": {"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(iter.get_line()) + ", \"character\": " + std::to_string(iter.get_line_offset()) + "}", [this, offset, current_request](const boost::property_tree::ptree &result, bool error) { - if(!error) { - // hover result structure vary significantly from the different language servers - struct Content { - std::string value; - std::string kind; - }; - std::list contents; - auto contents_pt = result.get_child_optional("contents"); - if(!contents_pt) - return; - auto value = contents_pt->get_value(""); - if(!value.empty()) - contents.emplace_back(Content{value, "markdown"}); - else { - auto value_pt = contents_pt->get_optional("value"); - if(value_pt) { - auto kind = contents_pt->get("kind", ""); - if(kind.empty()) - kind = contents_pt->get("language", ""); - contents.emplace_back(Content{*value_pt, kind}); - } - else { - bool first_value = true; - for(auto it = contents_pt->begin(); it != contents_pt->end(); ++it) { - auto value = it->second.get("value", ""); - if(!value.empty()) { - auto kind = it->second.get("kind", ""); - if(kind.empty()) - kind = it->second.get("language", ""); - if(first_value) // Place first value, which most likely is type information, to front (workaround for flow-bin's language server) - contents.emplace_front(Content{value, kind}); - else - contents.emplace_back(Content{value, kind}); - first_value = false; - } - else { - value = it->second.get_value(""); - if(!value.empty()) - contents.emplace_back(Content{value, "markdown"}); - } - } + prev = prefix_start; + prev.backward_char(); + auto prevprev = prev; + if(*prev == '.') { + auto iter = prev; + bool starts_with_num = false; + size_t count = 0; + while(iter.backward_char() && is_token_char(*iter)) { + ++count; + starts_with_num = Glib::Unicode::isdigit(*iter); + } + if((count >= 1 || *iter == ')' || *iter == ']' || *iter == '"' || *iter == '\'' || *iter == '?') && !starts_with_num) { + { + LockGuard lock(autocomplete->prefix_mutex); + autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); } + return true; } - if(!contents.empty()) { - dispatcher.post([this, offset, contents = std::move(contents), current_request]() mutable { - if(current_request != request_count) - return; - if(Notebook::get().get_current_view() != this) - return; - if(offset >= get_buffer()->get_char_count()) - return; - type_tooltips.clear(); + } + else if((prevprev.backward_char() && *prevprev == ':' && *prev == ':')) { + { + LockGuard lock(autocomplete->prefix_mutex); + autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); + } + return true; + } + else if(count >= 3) { // part of symbol + { + LockGuard lock(autocomplete->prefix_mutex); + autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); + } + autocomplete_enable_snippets = true; + return true; + } + if(is_possible_argument()) { + autocomplete_show_arguments = true; + LockGuard lock(autocomplete->prefix_mutex); + autocomplete->prefix = ""; + return true; + } + if(is_possible_xml_attribute(prefix_start)) { + LockGuard lock(autocomplete->prefix_mutex); + autocomplete->prefix = ""; + return true; + } + if(!interactive_completion) { + { + LockGuard lock(autocomplete->prefix_mutex); + autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); + } + auto prevprev = prev; + autocomplete_enable_snippets = !(*prev == '.' || (prevprev.backward_char() && *prevprev == ':' && *prev == ':')); + return true; + } - auto token_iters = get_token_iters(get_buffer()->get_iter_at_offset(offset)); - type_tooltips.emplace_back(this, token_iters.first, token_iters.second, [this, offset, contents = std::move(contents)](Tooltip &tooltip) mutable { - bool first = true; - if(language_id == "python") { // Python might support markdown in the future - for(auto &content : contents) { - if(!first) - tooltip.buffer->insert_at_cursor("\n\n"); - first = false; - if(content.kind == "python") - tooltip.insert_code(content.value, content.kind); - else - tooltip.insert_docstring(content.value); - } - } - else { - for(auto &content : contents) { - if(!first) - tooltip.buffer->insert_at_cursor("\n\n"); - first = false; - if(content.kind == "plaintext" || content.kind.empty()) - tooltip.insert_with_links_tagged(content.value); - else if(content.kind == "markdown") - tooltip.insert_markdown(content.value); - else - tooltip.insert_code(content.value, content.kind); - tooltip.remove_trailing_newlines(); - } - } + return false; + }; -#ifdef JUCI_ENABLE_DEBUG - if(language_id == "rust" && capabilities.definition) { - if(Debug::LLDB::get().is_stopped()) { - Glib::ustring value_type = "Value"; + autocomplete->before_add_rows = [this] { + status_state = "autocomplete..."; + if(update_status_state) + update_status_state(this); + }; - auto token_iters = get_token_iters(get_buffer()->get_iter_at_offset(offset)); - auto offset = get_declaration(token_iters.first); + autocomplete->after_add_rows = [this] { + status_state = ""; + if(update_status_state) + update_status_state(this); + }; - auto variable = get_buffer()->get_text(token_iters.first, token_iters.second); - Glib::ustring debug_value = Debug::LLDB::get().get_value(variable, offset.file_path, offset.line + 1, offset.index + 1); - if(debug_value.empty()) { - debug_value = Debug::LLDB::get().get_return_value(file_path, token_iters.first.get_line() + 1, token_iters.first.get_line_index() + 1); - if(!debug_value.empty()) - value_type = "Return value"; + autocomplete->add_rows = [this](std::string &buffer, int line, int line_index) { + if(autocomplete->state == Autocomplete::State::starting) { + autocomplete_rows.clear(); + std::promise result_processed; + if(autocomplete_show_arguments) { + if(!capabilities.signature_help) + return true; + dispatcher.post([this, line, line_index, &result_processed] { + // Find current parameter number and previously used named parameters + unsigned current_parameter_position = 0; + auto named_parameter_symbol = get_named_parameter_symbol(); + std::set used_named_parameters; + auto iter = get_buffer()->get_insert()->get_iter(); + int para_count = 0; + int square_count = 0; + int angle_count = 0; + int curly_count = 0; + while(iter.backward_char() && backward_to_code(iter)) { + if(para_count == 0 && square_count == 0 && angle_count == 0 && curly_count == 0) { + if(named_parameter_symbol && (*iter == ',' || *iter == '(')) { + auto next = iter; + if(next.forward_char() && forward_to_code(next)) { + auto pair = get_token_iters(next); + if(pair.first != pair.second) { + auto symbol = pair.second; + if(forward_to_code(symbol) && *symbol == static_cast(*named_parameter_symbol)) + used_named_parameters.emplace(get_buffer()->get_text(pair.first, pair.second)); + } } - if(debug_value.empty()) { - auto end = token_iters.second; - while((end.ends_line() || *end == ' ' || *end == '\t') && end.forward_char()) { + } + if(*iter == ',') + ++current_parameter_position; + else if(*iter == '(') + break; + } + if(*iter == '(') + ++para_count; + else if(*iter == ')') + --para_count; + else if(*iter == '[') + ++square_count; + else if(*iter == ']') + --square_count; + else if(*iter == '<') + ++angle_count; + else if(*iter == '>') + --angle_count; + else if(*iter == '{') + ++curly_count; + else if(*iter == '}') + --curly_count; + } + bool using_named_parameters = named_parameter_symbol && !(current_parameter_position > 0 && used_named_parameters.empty()); + + write_request("textDocument/signatureHelp", {make_position(line, get_line_pos(line, line_index))}, [this, &result_processed, current_parameter_position, using_named_parameters, used_named_parameters = std::move(used_named_parameters)](const boost::property_tree::ptree &result, bool error) { + if(!error) { + auto signatures = result.get_child("signatures", boost::property_tree::ptree()); + for(auto signature_it = signatures.begin(); signature_it != signatures.end(); ++signature_it) { + auto parameters = signature_it->second.get_child("parameters", boost::property_tree::ptree()); + unsigned parameter_position = 0; + for(auto parameter_it = parameters.begin(); parameter_it != parameters.end(); ++parameter_it) { + if(parameter_position == current_parameter_position || using_named_parameters) { + auto label = parameter_it->second.get("label", ""); + auto insert = label; + auto documentation = parameter_it->second.get("documentation", ""); + std::string kind; + if(documentation.empty()) { + auto documentation_pt = parameter_it->second.get_child_optional("documentation"); + if(documentation_pt) { + documentation = documentation_pt->get("value", ""); + kind = documentation_pt->get("kind", ""); + } + } + if(documentation == "null") // Python erroneously returns "null" when a parameter is not documented + documentation.clear(); + if(!using_named_parameters || used_named_parameters.find(insert) == used_named_parameters.end()) { + autocomplete->rows.emplace_back(std::move(label)); + autocomplete_rows.emplace_back(AutocompleteRow{std::move(insert), {}, std::move(documentation), std::move(kind)}); + } } - if(*end != '(') { - auto iter = token_iters.first; - auto start = iter; - while(iter.backward_char()) { - if(*iter == '.') { - while(iter.backward_char() && (*iter == ' ' || *iter == '\t' || iter.ends_line())) { + parameter_position++; + } + } + } + result_processed.set_value(); + }); + }); + } + else { + dispatcher.post([this, line, line_index, &result_processed] { + write_request("textDocument/completion", {make_position(line, get_line_pos(line, line_index))}, [this, &result_processed](const boost::property_tree::ptree &result, bool error) { + if(!error) { + boost::property_tree::ptree::const_iterator begin, end; + if(auto items = result.get_child_optional("items")) { + begin = items->begin(); + end = items->end(); + } + else { + begin = result.begin(); + end = result.end(); + } + std::string prefix; + { + LockGuard lock(autocomplete->prefix_mutex); + prefix = autocomplete->prefix; + } + for(auto it = begin; it != end; ++it) { + auto label = it->second.get("label", ""); + if(starts_with(label, prefix)) { + auto detail = it->second.get("detail", ""); + auto documentation = it->second.get("documentation", ""); + std::string documentation_kind; + if(documentation.empty()) { + if(auto documentation_pt = it->second.get_child_optional("documentation")) { + documentation = documentation_pt->get("value", ""); + documentation_kind = documentation_pt->get("kind", ""); + } + } + auto insert = it->second.get("insertText", ""); + if(insert.empty()) + insert = it->second.get("textEdit.newText", ""); + if(insert.empty()) + insert = label; + if(!insert.empty()) { + auto kind = it->second.get("kind", 0); + if(kind >= 2 && kind <= 4 && insert.find('(') == std::string::npos) // If kind is method, function or constructor, but parentheses are missing + insert += "(${1:})"; + autocomplete->rows.emplace_back(std::move(label)); + autocomplete_rows.emplace_back(AutocompleteRow{std::move(insert), std::move(detail), std::move(documentation), std::move(documentation_kind)}); + } + } + } + + if(autocomplete_enable_snippets) { + LockGuard lock(snippets_mutex); + if(snippets) { + for(auto &snippet : *snippets) { + if(starts_with(snippet.prefix, prefix)) { + autocomplete->rows.emplace_back(snippet.prefix); + autocomplete_rows.emplace_back(AutocompleteRow{snippet.body, {}, snippet.description, {}}); + } + } + } + } + } + result_processed.set_value(); + }); + }); + } + result_processed.get_future().get(); + } + return true; + }; + + autocomplete->on_show = [this] { + hide_tooltips(); + }; + + autocomplete->on_hide = [this] { + autocomplete_rows.clear(); + }; + + autocomplete->on_select = [this](unsigned int index, const std::string &text, bool hide_window) { + auto insert = hide_window ? autocomplete_rows[index].insert : text; + + get_buffer()->erase(CompletionDialog::get()->start_mark->get_iter(), get_buffer()->get_insert()->get_iter()); + + // Do not insert function/template parameters if they already exist + { + auto iter = get_buffer()->get_insert()->get_iter(); + if(*iter == '(' || *iter == '<') { + auto bracket_pos = insert.find(*iter); + if(bracket_pos != std::string::npos) + insert.erase(bracket_pos); + } + } + + // Do not instert ?. after ., instead replace . with ?. + if(1 < insert.size() && insert[0] == '?' && insert[1] == '.') { + auto iter = get_buffer()->get_insert()->get_iter(); + auto prev = iter; + if(prev.backward_char() && *prev == '.') { + get_buffer()->erase(prev, iter); + } + } + + if(hide_window) { + if(autocomplete_show_arguments) { + if(auto symbol = get_named_parameter_symbol()) // Do not select named parameters in for instance Python + get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert + *symbol); + else { + get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); + int start_offset = CompletionDialog::get()->start_mark->get_iter().get_offset(); + int end_offset = CompletionDialog::get()->start_mark->get_iter().get_offset() + insert.size(); + get_buffer()->select_range(get_buffer()->get_iter_at_offset(start_offset), get_buffer()->get_iter_at_offset(end_offset)); + } + return; + } + + insert_snippet(CompletionDialog::get()->start_mark->get_iter(), insert); + auto iter = get_buffer()->get_insert()->get_iter(); + if(*iter == ')' && iter.backward_char() && *iter == '(') { // If no arguments, try signatureHelp + last_keyval = '('; + autocomplete->run(); + } + } + else + get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); + }; + + autocomplete->set_tooltip_buffer = [this](unsigned int index) -> std::function { + auto autocomplete = autocomplete_rows[index]; + if(autocomplete.detail.empty() && autocomplete.documentation.empty()) + return nullptr; + return [this, autocomplete = std::move(autocomplete)](Tooltip &tooltip) mutable { + if(language_id == "python") // Python might support markdown in the future + tooltip.insert_docstring(autocomplete.documentation); + else { + if(!autocomplete.detail.empty()) { + tooltip.insert_code(autocomplete.detail, language); + tooltip.remove_trailing_newlines(); + } + if(!autocomplete.documentation.empty()) { + if(tooltip.buffer->size() > 0) + tooltip.buffer->insert_at_cursor("\n\n"); + if(autocomplete.kind == "plaintext" || autocomplete.kind.empty()) + tooltip.insert_with_links_tagged(autocomplete.documentation); + else if(autocomplete.kind == "markdown") + tooltip.insert_markdown(autocomplete.documentation); + else + tooltip.insert_code(autocomplete.documentation, autocomplete.kind); + } + } + }; + }; +} + +void Source::LanguageProtocolView::update_diagnostics_async(std::vector &&diagnostics) { + update_diagnostics_async_count++; + size_t last_count = update_diagnostics_async_count; + if(capabilities.code_action && !diagnostics.empty()) { + dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable { + if(last_count != update_diagnostics_async_count) + return; + std::pair range; + std::string diagnostics_string; + for(auto &diagnostic : diagnostics) { + range = make_range({diagnostic.range.start.line, diagnostic.range.start.character}, {diagnostic.range.end.line, diagnostic.range.end.character}); + std::vector> diagnostic_params = {range}; + diagnostic_params.emplace_back("message", '"' + LanguageProtocol::escape_text(diagnostic.message) + '"'); + if(diagnostic.severity != 0) + diagnostic_params.emplace_back("severity", std::to_string(diagnostic.severity)); + if(!diagnostic.code.empty()) + diagnostic_params.emplace_back("code", '"' + diagnostic.code + '"'); + diagnostics_string += (diagnostics_string.empty() ? "{" : ",{") + to_string(diagnostic_params) + '}'; + } + if(diagnostics.size() != 1) { // Use diagnostic range if only one diagnostic, otherwise use whole buffer + auto start = get_buffer()->begin(); + auto end = get_buffer()->end(); + range = make_range({start.get_line(), get_line_pos(start)}, {end.get_line(), get_line_pos(end)}); + } + std::vector> params = {range, {"context", '{' + to_string({{"diagnostics", '[' + diagnostics_string + ']'}, {"only", "[\"quickfix\"]"}}) + '}'}}; + thread_pool.push([this, diagnostics = std::move(diagnostics), params = std::move(params), last_count]() mutable { + if(last_count != update_diagnostics_async_count) + return; + std::promise result_processed; + write_request("textDocument/codeAction", params, [this, &result_processed, &diagnostics, last_count](const boost::property_tree::ptree &result, bool error) { + if(!error && last_count == update_diagnostics_async_count) { + try { + for(auto it = result.begin(); it != result.end(); ++it) { + auto kind = it->second.get("kind", ""); + if(kind == "quickfix" || kind.empty()) { // Workaround for typescript-language-server (kind.empty()) + auto title = it->second.get("title"); + std::vector quickfix_diagnostics; + if(auto diagnostics_pt = it->second.get_child_optional("diagnostics")) { + for(auto it = diagnostics_pt->begin(); it != diagnostics_pt->end(); ++it) + quickfix_diagnostics.emplace_back(it->second); + } + if(auto changes = it->second.get_child_optional("edit.changes")) { + for(auto file_it = changes->begin(); file_it != changes->end(); ++file_it) { + for(auto edit_it = file_it->second.begin(); edit_it != file_it->second.end(); ++edit_it) { + LanguageProtocol::TextEdit edit(edit_it->second); + if(!quickfix_diagnostics.empty()) { + for(auto &diagnostic : diagnostics) { + for(auto &quickfix_diagnostic : quickfix_diagnostics) { + if(diagnostic.message == quickfix_diagnostic.message && diagnostic.range == quickfix_diagnostic.range) { + auto pair = diagnostic.quickfixes.emplace(title, std::set{}); + pair.first->second.emplace( + edit.new_text, + filesystem::get_path_from_uri(file_it->first).string(), + std::make_pair(Offset(edit.range.start.line, edit.range.start.character), + Offset(edit.range.end.line, edit.range.end.character))); + break; + } + } + } + } + else { // Workaround for language server that does not report quickfix diagnostics + for(auto &diagnostic : diagnostics) { + if(edit.range.start.line == diagnostic.range.start.line) { + auto pair = diagnostic.quickfixes.emplace(title, std::set{}); + pair.first->second.emplace( + edit.new_text, + filesystem::get_path_from_uri(file_it->first).string(), + std::make_pair(Offset(edit.range.start.line, edit.range.start.character), + Offset(edit.range.end.line, edit.range.end.character))); + break; + } + } + } + } + } + } + else { + auto changes_pt = it->second.get_child_optional("edit.documentChanges"); + if(!changes_pt) { // Workaround for typescript-language-server + if(auto arguments_pt = it->second.get_child_optional("arguments")) { + if(!arguments_pt->empty()) + changes_pt = arguments_pt->begin()->second.get_child_optional("documentChanges"); + } + } + if(changes_pt) { + for(auto change_it = changes_pt->begin(); change_it != changes_pt->end(); ++change_it) { + LanguageProtocol::TextDocumentEdit text_document_edit(change_it->second); + for(auto &edit : text_document_edit.edits) { + if(!quickfix_diagnostics.empty()) { + for(auto &diagnostic : diagnostics) { + for(auto &quickfix_diagnostic : quickfix_diagnostics) { + if(diagnostic.message == quickfix_diagnostic.message && diagnostic.range == quickfix_diagnostic.range) { + auto pair = diagnostic.quickfixes.emplace(title, std::set{}); + pair.first->second.emplace( + edit.new_text, + text_document_edit.file, + std::make_pair(Offset(edit.range.start.line, edit.range.start.character), + Offset(edit.range.end.line, edit.range.end.character))); + break; + } + } + } + } + else { // Workaround for language server that does not report quickfix diagnostics + for(auto &diagnostic : diagnostics) { + if(edit.range.start.line == diagnostic.range.start.line) { + auto pair = diagnostic.quickfixes.emplace(title, std::set{}); + pair.first->second.emplace( + edit.new_text, + text_document_edit.file, + std::make_pair(Offset(edit.range.start.line, edit.range.start.character), + Offset(edit.range.end.line, edit.range.end.character))); + break; + } + } + } } } - if(!is_token_char(*iter)) - break; - start = iter; - } - if(is_token_char(*start)) - debug_value = Debug::LLDB::get().get_value(get_buffer()->get_text(start, token_iters.second)); - } - } - if(!debug_value.empty()) { - size_t pos = debug_value.find(" = "); - if(pos != Glib::ustring::npos) { - Glib::ustring::iterator iter; - while(!debug_value.validate(iter)) { - auto next_char_iter = iter; - next_char_iter++; - debug_value.replace(iter, next_char_iter, "?"); } - tooltip.buffer->insert_at_cursor("\n\n" + value_type + ":\n"); - tooltip.insert_code(debug_value.substr(pos + 3, debug_value.size() - (pos + 3) - 1)); } } } } -#endif - }); - type_tooltips.show(); + catch(...) { + } + } + result_processed.set_value(); + }); + result_processed.get_future().get(); + dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable { + if(last_count == update_diagnostics_async_count) { + last_diagnostics = diagnostics; + update_diagnostics(std::move(diagnostics)); + } }); - } - } - }); -} - -void Source::LanguageProtocolView::apply_similar_symbol_tag() { - if(!capabilities.document_highlight && !capabilities.references) - return; - - auto iter = get_buffer()->get_insert()->get_iter(); - std::string method; - if(capabilities.document_highlight) - method = "textDocument/documentHighlight"; - else - method = "textDocument/references"; - - static int request_count = 0; - request_count++; - auto current_request = request_count; - client->write_request(this, method, R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(iter.get_line()) + ", \"character\": " + std::to_string(iter.get_line_offset()) + R"(}, "context": {"includeDeclaration": true})", [this, current_request](const boost::property_tree::ptree &result, bool error) { - if(!error) { - std::vector ranges; - for(auto it = result.begin(); it != result.end(); ++it) { - try { - if(capabilities.document_highlight || it->second.get("uri") == uri) - ranges.emplace_back(it->second.get_child("range")); - } - catch(...) { - } - } - dispatcher.post([this, ranges = std::move(ranges), current_request] { - if(current_request != request_count || !similar_symbol_tag_applied) - return; - get_buffer()->remove_tag(similar_symbol_tag, get_buffer()->begin(), get_buffer()->end()); - for(auto &range : ranges) { - auto start = get_iter_at_line_pos(range.start.line, range.start.character); - auto end = get_iter_at_line_pos(range.end.line, range.end.character); - get_buffer()->apply_tag(similar_symbol_tag, start, end); - } - }); - } - }); -} - -void Source::LanguageProtocolView::apply_clickable_tag(const Gtk::TextIter &iter) { - static int request_count = 0; - request_count++; - auto current_request = request_count; - auto line = iter.get_line(); - auto offset = iter.get_line_offset(); - client->write_request(this, "textDocument/definition", R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(line) + ", \"character\": " + std::to_string(offset) + "}", [this, current_request, line, offset](const boost::property_tree::ptree &result, bool error) { - if(!error && !result.empty()) { - dispatcher.post([this, current_request, line, offset] { - if(current_request != request_count || !clickable_tag_applied) - return; - get_buffer()->remove_tag(clickable_tag, get_buffer()->begin(), get_buffer()->end()); - auto range = get_token_iters(get_iter_at_line_pos(line, offset)); - get_buffer()->apply_tag(clickable_tag, range.first, range.second); }); - } - }); -} - -Source::Offset Source::LanguageProtocolView::get_declaration(const Gtk::TextIter &iter) { - auto offset = std::make_shared(); - std::promise result_processed; - client->write_request(this, "textDocument/definition", R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(iter.get_line()) + ", \"character\": " + std::to_string(iter.get_line_offset()) + "}", [offset, &result_processed](const boost::property_tree::ptree &result, bool error) { - if(!error) { - for(auto it = result.begin(); it != result.end(); ++it) { - try { - LanguageProtocol::Location location(it->second); - offset->file_path = std::move(location.file); - offset->line = location.range.start.line; - offset->index = location.range.start.character; - break; // TODO: can a language server return several definitions? - } - catch(...) { - } - } - } - result_processed.set_value(); - }); - result_processed.get_future().get(); - return *offset; -} - -void Source::LanguageProtocolView::setup_signals() { - if(capabilities.text_document_sync == LanguageProtocol::Capabilities::TextDocumentSync::incremental) { - get_buffer()->signal_insert().connect( - [this](const Gtk::TextIter &start, const Glib::ustring &text, int bytes) { - client->write_notification("textDocument/didChange", R"("textDocument":{"uri":")" + uri_escaped + R"(","version":)" + std::to_string(document_version++) + "},\"contentChanges\":[" + R"({"range":{"start":{"line": )" + std::to_string(start.get_line()) + ",\"character\":" + std::to_string(start.get_line_offset()) + R"(},"end":{"line":)" + std::to_string(start.get_line()) + ",\"character\":" + std::to_string(start.get_line_offset()) + R"(}},"text":")" + LanguageProtocol::escape_text(text) + "\"}" + "]"); - }, - false); - - get_buffer()->signal_erase().connect( - [this](const Gtk::TextIter &start, const Gtk::TextIter &end) { - client->write_notification("textDocument/didChange", R"("textDocument":{"uri":")" + uri_escaped + R"(","version":)" + std::to_string(document_version++) + "},\"contentChanges\":[" + R"({"range":{"start":{"line": )" + std::to_string(start.get_line()) + ",\"character\":" + std::to_string(start.get_line_offset()) + R"(},"end":{"line":)" + std::to_string(end.get_line()) + ",\"character\":" + std::to_string(end.get_line_offset()) + R"(}},"text":""})" + "]"); - }, - false); + }); } - else if(capabilities.text_document_sync == LanguageProtocol::Capabilities::TextDocumentSync::full) { - get_buffer()->signal_changed().connect([this]() { - client->write_notification("textDocument/didChange", R"("textDocument":{"uri":")" + uri_escaped + R"(","version":)" + std::to_string(document_version++) + "},\"contentChanges\":[" + R"({"text":")" + LanguageProtocol::escape_text(get_buffer()->get_text()) + "\"}" + "]"); + else { + dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable { + if(last_count == update_diagnostics_async_count) { + last_diagnostics = diagnostics; + update_diagnostics(std::move(diagnostics)); + } }); } } -void Source::LanguageProtocolView::setup_autocomplete() { - autocomplete = std::make_unique(this, interactive_completion, last_keyval, false); - - if(!capabilities.completion) - return; - - non_interactive_completion = [this] { - if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) - return; - autocomplete->run(); - }; - - autocomplete->reparse = [this] { - autocomplete_rows.clear(); - }; - - if(capabilities.signature_help) { - // Activate argument completions - get_buffer()->signal_changed().connect( - [this] { - if(!interactive_completion) - return; - if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) - return; - if(!has_focus()) - return; - if(autocomplete_show_arguments) - autocomplete->stop(); - autocomplete_show_arguments = false; - autocomplete_delayed_show_arguments_connection.disconnect(); - autocomplete_delayed_show_arguments_connection = Glib::signal_timeout().connect( - [this]() { - if(get_buffer()->get_has_selection()) - return false; - if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) - return false; - if(!has_focus()) - return false; - if(is_possible_argument()) { - autocomplete->stop(); - autocomplete->run(); - } - return false; - }, - 500); - }, - false); - - // Remove argument completions - signal_key_press_event().connect( - [this](GdkEventKey *event) { - if(autocomplete_show_arguments && CompletionDialog::get() && CompletionDialog::get()->is_visible() && - event->keyval != GDK_KEY_Down && event->keyval != GDK_KEY_Up && - event->keyval != GDK_KEY_Return && event->keyval != GDK_KEY_KP_Enter && - event->keyval != GDK_KEY_ISO_Left_Tab && event->keyval != GDK_KEY_Tab && - (event->keyval < GDK_KEY_Shift_L || event->keyval > GDK_KEY_Hyper_R)) { - get_buffer()->erase(CompletionDialog::get()->start_mark->get_iter(), get_buffer()->get_insert()->get_iter()); - CompletionDialog::get()->hide(); - } - return false; - }, - false); - } - - autocomplete->is_restart_key = [this](guint keyval) { - auto iter = get_buffer()->get_insert()->get_iter(); - iter.backward_chars(2); - if(keyval == '.' || (keyval == ':' && *iter == ':')) - return true; - return false; - }; - - std::function is_possible_xml_attribute = [](Gtk::TextIter) { return false; }; - if(is_js) { - autocomplete->is_restart_key = [](guint keyval) { - if(keyval == '.' || keyval == ' ') - return true; - return false; - }; +void Source::LanguageProtocolView::update_diagnostics(std::vector diagnostics) { + diagnostic_offsets.clear(); + diagnostic_tooltips.clear(); + fix_its.clear(); + get_buffer()->remove_tag_by_name("def:warning_underline", get_buffer()->begin(), get_buffer()->end()); + get_buffer()->remove_tag_by_name("def:error_underline", get_buffer()->begin(), get_buffer()->end()); + num_warnings = 0; + num_errors = 0; + num_fix_its = 0; - is_possible_xml_attribute = [this](Gtk::TextIter iter) { - return (*iter == ' ' || iter.ends_line() || *iter == '/' || (*iter == '>' && iter.backward_char())) && find_open_symbol_backward(iter, iter, '<', '>'); - }; - } + for(auto &diagnostic : diagnostics) { + auto start = get_iter_at_line_pos(diagnostic.range.start.line, diagnostic.range.start.character); + auto end = get_iter_at_line_pos(diagnostic.range.end.line, diagnostic.range.end.character); - autocomplete->run_check = [this, is_possible_xml_attribute]() { - auto prefix_start = get_buffer()->get_insert()->get_iter(); - auto prefix_end = prefix_start; + if(start == end) { + if(!end.ends_line()) + end.forward_char(); + else + while(start.ends_line() && start.backward_char()) { // Move start so that diagnostic underline is visible + } + } - auto prev = prefix_start; - prev.backward_char(); - if(!is_code_iter(prev)) - return false; + bool error = false; + if(diagnostic.severity >= 2) + num_warnings++; + else { + num_errors++; + error = true; + } + num_fix_its += diagnostic.quickfixes.size(); - size_t count = 0; - while(prefix_start.backward_char() && is_token_char(*prefix_start)) - ++count; + for(auto &quickfix : diagnostic.quickfixes) + fix_its.insert(fix_its.end(), quickfix.second.begin(), quickfix.second.end()); - autocomplete_enable_snippets = false; - autocomplete_show_arguments = false; + add_diagnostic_tooltip(start, end, error, [this, diagnostic = std::move(diagnostic)](Tooltip &tooltip) { + if(language_id == "python") { // Python might support markdown in the future + tooltip.insert_with_links_tagged(diagnostic.message); + return; + } + tooltip.insert_markdown(diagnostic.message); - if(prefix_start != prefix_end && !is_token_char(*prefix_start)) - prefix_start.forward_char(); + if(!diagnostic.related_informations.empty()) { + auto link_tag = tooltip.buffer->get_tag_table()->lookup("link"); + for(size_t i = 0; i < diagnostic.related_informations.size(); ++i) { + auto link = filesystem::get_relative_path(diagnostic.related_informations[i].location.file, file_path.parent_path()).string(); + link += ':' + std::to_string(diagnostic.related_informations[i].location.range.start.line + 1); + link += ':' + std::to_string(diagnostic.related_informations[i].location.range.start.character + 1); - prev = prefix_start; - prev.backward_char(); - auto prevprev = prev; - if(*prev == '.') { - auto iter = prev; - bool starts_with_num = false; - size_t count = 0; - while(iter.backward_char() && is_token_char(*iter)) { - ++count; - starts_with_num = Glib::Unicode::isdigit(*iter); - } - if((count >= 1 || *iter == ')' || *iter == ']' || *iter == '"' || *iter == '\'' || *iter == '?') && !starts_with_num) { - { - LockGuard lock(autocomplete->prefix_mutex); - autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); + if(i == 0) + tooltip.buffer->insert_at_cursor("\n\n"); + else + tooltip.buffer->insert_at_cursor("\n"); + tooltip.insert_markdown(diagnostic.related_informations[i].message); + tooltip.buffer->insert_at_cursor(": "); + tooltip.buffer->insert_with_tag(tooltip.buffer->get_insert()->get_iter(), link, link_tag); } - return true; - } - } - else if((prevprev.backward_char() && *prevprev == ':' && *prev == ':')) { - { - LockGuard lock(autocomplete->prefix_mutex); - autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); - } - return true; - } - else if(count >= 3) { // part of symbol - { - LockGuard lock(autocomplete->prefix_mutex); - autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); } - autocomplete_enable_snippets = true; - return true; - } - if(is_possible_argument()) { - autocomplete_show_arguments = true; - LockGuard lock(autocomplete->prefix_mutex); - autocomplete->prefix = ""; - return true; - } - if(is_possible_xml_attribute(prefix_start)) { - LockGuard lock(autocomplete->prefix_mutex); - autocomplete->prefix = ""; - return true; - } - if(!interactive_completion) { - { - LockGuard lock(autocomplete->prefix_mutex); - autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); + + if(!diagnostic.quickfixes.empty()) { + if(diagnostic.quickfixes.size() == 1) + tooltip.buffer->insert_at_cursor("\n\nFix-it:"); + else + tooltip.buffer->insert_at_cursor("\n\nFix-its:"); + for(auto &quickfix : diagnostic.quickfixes) { + tooltip.buffer->insert_at_cursor("\n"); + tooltip.insert_markdown(quickfix.first); + } } - auto prevprev = prev; - autocomplete_enable_snippets = !(*prev == '.' || (prevprev.backward_char() && *prevprev == ':' && *prev == ':')); - return true; - } + }); + } - return false; - }; + for(auto &mark : type_coverage_marks) { + add_diagnostic_tooltip(mark.first->get_iter(), mark.second->get_iter(), false, [](Tooltip &tooltip) { + tooltip.buffer->insert_at_cursor(type_coverage_message); + }); + num_warnings++; + } - autocomplete->before_add_rows = [this] { - status_state = "autocomplete..."; - if(update_status_state) - update_status_state(this); - }; + status_diagnostics = std::make_tuple(num_warnings, num_errors, num_fix_its); + if(update_status_diagnostics) + update_status_diagnostics(this); +} - autocomplete->after_add_rows = [this] { - status_state = ""; - if(update_status_state) - update_status_state(this); - }; +void Source::LanguageProtocolView::show_type_tooltips(const Gdk::Rectangle &rectangle) { + if(!capabilities.hover) + return; - autocomplete->add_rows = [this](std::string &buffer, int line_number, int column) { - if(autocomplete->state == Autocomplete::State::starting) { - autocomplete_rows.clear(); - std::promise result_processed; - if(autocomplete_show_arguments) { - if(!capabilities.signature_help) - return true; - dispatcher.post([this, line_number, column, &result_processed] { - // Find current parameter number and previously used named parameters - unsigned current_parameter_position = 0; - auto named_parameter_symbol = get_named_parameter_symbol(); - std::set used_named_parameters; - auto iter = get_buffer()->get_insert()->get_iter(); - int para_count = 0; - int square_count = 0; - int angle_count = 0; - int curly_count = 0; - while(iter.backward_char() && backward_to_code(iter)) { - if(para_count == 0 && square_count == 0 && angle_count == 0 && curly_count == 0) { - if(named_parameter_symbol && (*iter == ',' || *iter == '(')) { - auto next = iter; - if(next.forward_char() && forward_to_code(next)) { - auto pair = get_token_iters(next); - if(pair.first != pair.second) { - auto symbol = pair.second; - if(forward_to_code(symbol) && *symbol == static_cast(*named_parameter_symbol)) - used_named_parameters.emplace(get_buffer()->get_text(pair.first, pair.second)); - } - } - } - if(*iter == ',') - ++current_parameter_position; - else if(*iter == '(') - break; - } - if(*iter == '(') - ++para_count; - else if(*iter == ')') - --para_count; - else if(*iter == '[') - ++square_count; - else if(*iter == ']') - --square_count; - else if(*iter == '<') - ++angle_count; - else if(*iter == '>') - --angle_count; - else if(*iter == '{') - ++curly_count; - else if(*iter == '}') - --curly_count; - } - bool using_named_parameters = named_parameter_symbol && !(current_parameter_position > 0 && used_named_parameters.empty()); + Gtk::TextIter iter; + int location_x, location_y; + window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, rectangle.get_x(), rectangle.get_y(), location_x, location_y); + location_x += (rectangle.get_width() - 1) / 2; + get_iter_at_location(iter, location_x, location_y); + Gdk::Rectangle iter_rectangle; + get_iter_location(iter, iter_rectangle); + if(iter.ends_line() && location_x > iter_rectangle.get_x()) + return; - client->write_request(this, "textDocument/signatureHelp", R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(line_number - 1) + ", \"character\": " + std::to_string(column - 1) + "}", [this, &result_processed, current_parameter_position, using_named_parameters, used_named_parameters = std::move(used_named_parameters)](const boost::property_tree::ptree &result, bool error) { - if(!error) { - auto signatures = result.get_child("signatures", boost::property_tree::ptree()); - for(auto signature_it = signatures.begin(); signature_it != signatures.end(); ++signature_it) { - auto parameters = signature_it->second.get_child("parameters", boost::property_tree::ptree()); - unsigned parameter_position = 0; - for(auto parameter_it = parameters.begin(); parameter_it != parameters.end(); ++parameter_it) { - if(parameter_position == current_parameter_position || using_named_parameters) { - auto label = parameter_it->second.get("label", ""); - auto insert = label; - auto documentation = parameter_it->second.get("documentation", ""); - std::string kind; - if(documentation.empty()) { - auto documentation_pt = parameter_it->second.get_child_optional("documentation"); - if(documentation_pt) { - documentation = documentation_pt->get("value", ""); - kind = documentation_pt->get("kind", ""); - } - } - if(documentation == "null") // Python erroneously returns "null" when a parameter is not documented - documentation.clear(); - if(!using_named_parameters || used_named_parameters.find(insert) == used_named_parameters.end()) { - autocomplete->rows.emplace_back(std::move(label)); - autocomplete_rows.emplace_back(AutocompleteRow{std::move(insert), {}, std::move(documentation), std::move(kind)}); - } - } - parameter_position++; - } - } - } - result_processed.set_value(); - }); - }); - } + auto offset = iter.get_offset(); + + static int request_count = 0; + request_count++; + auto current_request = request_count; + write_request("textDocument/hover", {make_position(iter.get_line(), get_line_pos(iter))}, [this, offset, current_request](const boost::property_tree::ptree &result, bool error) { + if(!error) { + // hover result structure vary significantly from the different language servers + struct Content { + std::string value; + std::string kind; + }; + std::list contents; + auto contents_pt = result.get_child_optional("contents"); + if(!contents_pt) + return; + auto value = contents_pt->get_value(""); + if(!value.empty()) + contents.emplace_back(Content{value, "markdown"}); else { - client->write_request(this, "textDocument/completion", R"("textDocument":{"uri":")" + uri_escaped + R"("}, "position": {"line": )" + std::to_string(line_number - 1) + ", \"character\": " + std::to_string(column - 1) + "}", [this, &result_processed](const boost::property_tree::ptree &result, bool error) { - if(!error) { - boost::property_tree::ptree::const_iterator begin, end; - if(auto items = result.get_child_optional("items")) { - begin = items->begin(); - end = items->end(); + auto value_pt = contents_pt->get_optional("value"); + if(value_pt) { + auto kind = contents_pt->get("kind", ""); + if(kind.empty()) + kind = contents_pt->get("language", ""); + contents.emplace_back(Content{*value_pt, kind}); + } + else { + bool first_value = true; + for(auto it = contents_pt->begin(); it != contents_pt->end(); ++it) { + auto value = it->second.get("value", ""); + if(!value.empty()) { + auto kind = it->second.get("kind", ""); + if(kind.empty()) + kind = it->second.get("language", ""); + if(first_value) // Place first value, which most likely is type information, to front (workaround for flow-bin's language server) + contents.emplace_front(Content{value, kind}); + else + contents.emplace_back(Content{value, kind}); + first_value = false; } else { - begin = result.begin(); - end = result.end(); + value = it->second.get_value(""); + if(!value.empty()) + contents.emplace_back(Content{value, "markdown"}); } - std::string prefix; - { - LockGuard lock(autocomplete->prefix_mutex); - prefix = autocomplete->prefix; + } + } + } + if(!contents.empty()) { + dispatcher.post([this, offset, contents = std::move(contents), current_request]() mutable { + if(current_request != request_count) + return; + if(Notebook::get().get_current_view() != this) + return; + if(offset >= get_buffer()->get_char_count()) + return; + type_tooltips.clear(); + + auto token_iters = get_token_iters(get_buffer()->get_iter_at_offset(offset)); + type_tooltips.emplace_back(this, token_iters.first, token_iters.second, [this, offset, contents = std::move(contents)](Tooltip &tooltip) mutable { + bool first = true; + if(language_id == "python") { // Python might support markdown in the future + for(auto &content : contents) { + if(!first) + tooltip.buffer->insert_at_cursor("\n\n"); + first = false; + if(content.kind == "python") + tooltip.insert_code(content.value, content.kind); + else + tooltip.insert_docstring(content.value); + } } - for(auto it = begin; it != end; ++it) { - auto label = it->second.get("label", ""); - if(starts_with(label, prefix)) { - auto detail = it->second.get("detail", ""); - auto documentation = it->second.get("documentation", ""); - std::string documentation_kind; - if(documentation.empty()) { - if(auto documentation_pt = it->second.get_child_optional("documentation")) { - documentation = documentation_pt->get("value", ""); - documentation_kind = documentation_pt->get("kind", ""); - } - } - auto insert = it->second.get("insertText", ""); - if(insert.empty()) - insert = it->second.get("textEdit.newText", ""); - if(insert.empty()) - insert = label; - if(!insert.empty()) { - auto kind = it->second.get("kind", 0); - if(kind >= 2 && kind <= 4 && insert.find('(') == std::string::npos) // If kind is method, function or constructor, but parentheses are missing - insert += "(${1:})"; - autocomplete->rows.emplace_back(std::move(label)); - autocomplete_rows.emplace_back(AutocompleteRow{std::move(insert), std::move(detail), std::move(documentation), std::move(documentation_kind)}); - } + else { + for(auto &content : contents) { + if(!first) + tooltip.buffer->insert_at_cursor("\n\n"); + first = false; + if(content.kind == "plaintext" || content.kind.empty()) + tooltip.insert_with_links_tagged(content.value); + else if(content.kind == "markdown") + tooltip.insert_markdown(content.value); + else + tooltip.insert_code(content.value, content.kind); + tooltip.remove_trailing_newlines(); } } - if(autocomplete_enable_snippets) { - LockGuard lock(snippets_mutex); - if(snippets) { - for(auto &snippet : *snippets) { - if(starts_with(snippet.prefix, prefix)) { - autocomplete->rows.emplace_back(snippet.prefix); - autocomplete_rows.emplace_back(AutocompleteRow{snippet.body, {}, snippet.description, {}}); +#ifdef JUCI_ENABLE_DEBUG + if(language_id == "rust" && capabilities.definition) { + if(Debug::LLDB::get().is_stopped()) { + Glib::ustring value_type = "Value"; + + auto token_iters = get_token_iters(get_buffer()->get_iter_at_offset(offset)); + auto offset = get_declaration(token_iters.first); + + auto variable = get_buffer()->get_text(token_iters.first, token_iters.second); + Glib::ustring debug_value = Debug::LLDB::get().get_value(variable, offset.file_path, offset.line + 1, offset.index + 1); + if(debug_value.empty()) { + debug_value = Debug::LLDB::get().get_return_value(file_path, token_iters.first.get_line() + 1, token_iters.first.get_line_index() + 1); + if(!debug_value.empty()) + value_type = "Return value"; + } + if(debug_value.empty()) { + auto end = token_iters.second; + while((end.ends_line() || *end == ' ' || *end == '\t') && end.forward_char()) { + } + if(*end != '(') { + auto iter = token_iters.first; + auto start = iter; + while(iter.backward_char()) { + if(*iter == '.') { + while(iter.backward_char() && (*iter == ' ' || *iter == '\t' || iter.ends_line())) { + } + } + if(!is_token_char(*iter)) + break; + start = iter; + } + if(is_token_char(*start)) + debug_value = Debug::LLDB::get().get_value(get_buffer()->get_text(start, token_iters.second)); + } + } + if(!debug_value.empty()) { + size_t pos = debug_value.find(" = "); + if(pos != Glib::ustring::npos) { + Glib::ustring::iterator iter; + while(!debug_value.validate(iter)) { + auto next_char_iter = iter; + next_char_iter++; + debug_value.replace(iter, next_char_iter, "?"); + } + tooltip.buffer->insert_at_cursor("\n\n" + value_type + ":\n"); + tooltip.insert_code(debug_value.substr(pos + 3, debug_value.size() - (pos + 3) - 1)); } } } } - } - result_processed.set_value(); +#endif + }); + type_tooltips.show(); }); } - result_processed.get_future().get(); } - return true; - }; - - autocomplete->on_show = [this] { - hide_tooltips(); - }; - - autocomplete->on_hide = [this] { - autocomplete_rows.clear(); - }; - - autocomplete->on_select = [this](unsigned int index, const std::string &text, bool hide_window) { - auto insert = hide_window ? autocomplete_rows[index].insert : text; + }); +} - get_buffer()->erase(CompletionDialog::get()->start_mark->get_iter(), get_buffer()->get_insert()->get_iter()); +void Source::LanguageProtocolView::apply_similar_symbol_tag() { + if(!capabilities.document_highlight) + return; - // Do not insert function/template parameters if they already exist - { - auto iter = get_buffer()->get_insert()->get_iter(); - if(*iter == '(' || *iter == '<') { - auto bracket_pos = insert.find(*iter); - if(bracket_pos != std::string::npos) - insert.erase(bracket_pos); + auto iter = get_buffer()->get_insert()->get_iter(); + static int request_count = 0; + request_count++; + auto current_request = request_count; + write_request("textDocument/documentHighlight", {make_position(iter.get_line(), get_line_pos(iter)), {"context", "{\"includeDeclaration\":true}"}}, [this, current_request](const boost::property_tree::ptree &result, bool error) { + if(!error) { + std::vector ranges; + for(auto it = result.begin(); it != result.end(); ++it) { + try { + if(capabilities.document_highlight || it->second.get("uri") == uri) + ranges.emplace_back(it->second.get_child("range")); + } + catch(...) { + } } + dispatcher.post([this, ranges = std::move(ranges), current_request] { + if(current_request != request_count || !similar_symbol_tag_applied) + return; + get_buffer()->remove_tag(similar_symbol_tag, get_buffer()->begin(), get_buffer()->end()); + for(auto &range : ranges) { + auto start = get_iter_at_line_pos(range.start.line, range.start.character); + auto end = get_iter_at_line_pos(range.end.line, range.end.character); + get_buffer()->apply_tag(similar_symbol_tag, start, end); + } + }); } + }); +} - // Do not instert ?. after ., instead replace . with ?. - if(1 < insert.size() && insert[0] == '?' && insert[1] == '.') { - auto iter = get_buffer()->get_insert()->get_iter(); - auto prev = iter; - if(prev.backward_char() && *prev == '.') { - get_buffer()->erase(prev, iter); - } +void Source::LanguageProtocolView::apply_clickable_tag(const Gtk::TextIter &iter) { + static int request_count = 0; + request_count++; + auto current_request = request_count; + write_request("textDocument/definition", {make_position(iter.get_line(), get_line_pos(iter))}, [this, current_request, line = iter.get_line(), line_offset = iter.get_line_offset()](const boost::property_tree::ptree &result, bool error) { + if(!error && !result.empty()) { + dispatcher.post([this, current_request, line, line_offset] { + if(current_request != request_count || !clickable_tag_applied) + return; + get_buffer()->remove_tag(clickable_tag, get_buffer()->begin(), get_buffer()->end()); + auto range = get_token_iters(get_iter_at_line_offset(line, line_offset)); + get_buffer()->apply_tag(clickable_tag, range.first, range.second); + }); } + }); +} - if(hide_window) { - if(autocomplete_show_arguments) { - if(auto symbol = get_named_parameter_symbol()) // Do not select named parameters in for instance Python - get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert + *symbol); - else { - get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); - int start_offset = CompletionDialog::get()->start_mark->get_iter().get_offset(); - int end_offset = CompletionDialog::get()->start_mark->get_iter().get_offset() + insert.size(); - get_buffer()->select_range(get_buffer()->get_iter_at_offset(start_offset), get_buffer()->get_iter_at_offset(end_offset)); +Source::Offset Source::LanguageProtocolView::get_declaration(const Gtk::TextIter &iter) { + auto offset = std::make_shared(); + std::promise result_processed; + write_request("textDocument/definition", {make_position(iter.get_line(), get_line_pos(iter))}, [offset, &result_processed](const boost::property_tree::ptree &result, bool error) { + if(!error) { + for(auto it = result.begin(); it != result.end(); ++it) { + try { + LanguageProtocol::Location location(it->second); + offset->file_path = std::move(location.file); + offset->line = location.range.start.line; + offset->index = location.range.start.character; + break; // TODO: can a language server return several definitions? + } + catch(...) { } - return; } + } + result_processed.set_value(); + }); + result_processed.get_future().get(); + return *offset; +} - insert_snippet(CompletionDialog::get()->start_mark->get_iter(), insert); - auto iter = get_buffer()->get_insert()->get_iter(); - if(*iter == ')' && iter.backward_char() && *iter == '(') { // If no arguments, try signatureHelp - last_keyval = '('; - autocomplete->run(); +Source::Offset Source::LanguageProtocolView::get_type_declaration(const Gtk::TextIter &iter) { + auto offset = std::make_shared(); + std::promise result_processed; + write_request("textDocument/typeDefinition", {make_position(iter.get_line(), get_line_pos(iter))}, [offset, &result_processed](const boost::property_tree::ptree &result, bool error) { + if(!error) { + for(auto it = result.begin(); it != result.end(); ++it) { + try { + LanguageProtocol::Location location(it->second); + offset->file_path = std::move(location.file); + offset->line = location.range.start.line; + offset->index = location.range.start.character; + break; // TODO: can a language server return several type definitions? + } + catch(...) { + } } } - else - get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); - }; + result_processed.set_value(); + }); + result_processed.get_future().get(); + return *offset; +} - autocomplete->set_tooltip_buffer = [this](unsigned int index) -> std::function { - auto autocomplete = autocomplete_rows[index]; - if(autocomplete.detail.empty() && autocomplete.documentation.empty()) - return nullptr; - return [this, autocomplete = std::move(autocomplete)](Tooltip &tooltip) mutable { - if(language_id == "python") // Python might support markdown in the future - tooltip.insert_docstring(autocomplete.documentation); - else { - if(!autocomplete.detail.empty()) { - tooltip.insert_code(autocomplete.detail, language); - tooltip.remove_trailing_newlines(); +std::vector Source::LanguageProtocolView::get_implementations(const Gtk::TextIter &iter) { + auto offsets = std::make_shared>(); + std::promise result_processed; + write_request("textDocument/implementation", {make_position(iter.get_line(), get_line_pos(iter))}, [offsets, &result_processed](const boost::property_tree::ptree &result, bool error) { + if(!error) { + for(auto it = result.begin(); it != result.end(); ++it) { + try { + LanguageProtocol::Location location(it->second); + offsets->emplace_back(location.range.start.line, location.range.start.character, location.file); } - if(!autocomplete.documentation.empty()) { - if(tooltip.buffer->size() > 0) - tooltip.buffer->insert_at_cursor("\n\n"); - if(autocomplete.kind == "plaintext" || autocomplete.kind.empty()) - tooltip.insert_with_links_tagged(autocomplete.documentation); - else if(autocomplete.kind == "markdown") - tooltip.insert_markdown(autocomplete.documentation); - else - tooltip.insert_code(autocomplete.documentation, autocomplete.kind); + catch(...) { } } - }; - }; + } + result_processed.set_value(); + }); + result_processed.get_future().get(); + return *offsets; } boost::optional Source::LanguageProtocolView::get_named_parameter_symbol() { @@ -1969,7 +2104,7 @@ boost::optional Source::LanguageProtocolView::get_named_parameter_symbol() void Source::LanguageProtocolView::update_type_coverage() { if(capabilities.type_coverage) { - client->write_request(this, "textDocument/typeCoverage", R"("textDocument": {"uri":")" + uri_escaped + "\"}", [this](const boost::property_tree::ptree &result, bool error) { + write_request("textDocument/typeCoverage", {}, [this](const boost::property_tree::ptree &result, bool error) { if(error) { if(update_type_coverage_retries > 0) { // Retry typeCoverage request, since these requests can fail while waiting for language server to start dispatcher.post([this] { diff --git a/src/source_language_protocol.hpp b/src/source_language_protocol.hpp index fa50fe2..dd6e22e 100644 --- a/src/source_language_protocol.hpp +++ b/src/source_language_protocol.hpp @@ -101,6 +101,8 @@ namespace LanguageProtocol { bool completion = false; bool signature_help = false; bool definition = false; + bool type_definition = false; + bool implementation = false; bool references = false; bool document_highlight = false; bool workspace_symbol = false; @@ -155,7 +157,7 @@ namespace LanguageProtocol { void parse_server_message(); void write_request(Source::LanguageProtocolView *view, const std::string &method, const std::string ¶ms, std::function &&function = nullptr); void write_response(size_t id, const std::string &result); - void write_notification(const std::string &method, const std::string ¶ms); + void write_notification(const std::string &method, const std::string ¶ms = {}); void handle_server_notification(const std::string &method, const boost::property_tree::ptree ¶ms); void handle_server_request(size_t id, const std::string &method, const boost::property_tree::ptree ¶ms); @@ -174,14 +176,31 @@ namespace Source { void rename(const boost::filesystem::path &path) override; bool save() override; - void update_diagnostics_async(std::vector &&diagnostics); - private: - std::atomic update_diagnostics_async_count = {0}; + /// Get line offset depending on if utf-8 byte offsets or utf-16 code units are used + int get_line_pos(const Gtk::TextIter &iter); + /// Get line offset depending on if utf-8 byte offsets or utf-16 code units are used + int get_line_pos(int line, int line_index); + + std::pair make_position(int line, int character); + std::pair make_range(const std::pair &start, const std::pair &end); + std::string to_string(const std::pair ¶m); + std::string to_string(const std::vector> ¶ms); + /// Helper method for calling client->write_request + void write_request(const std::string &method, const std::vector> ¶ms, std::function &&function); + /// Helper method for calling client->write_notification + void write_notification(const std::string &method); + /// Helper method for calling client->write_notification + void write_did_open_notification(); + /// Helper method for calling client->write_notification + void write_did_change_notification(const std::vector> ¶ms); + std::atomic update_diagnostics_async_count = {0}; void update_diagnostics(std::vector diagnostics); public: + void update_diagnostics_async(std::vector &&diagnostics); + Gtk::TextIter get_iter_at_line_pos(int line, int pos) override; std::string uri; @@ -208,14 +227,16 @@ namespace Source { Glib::ThreadPool thread_pool; void setup_navigation_and_refactoring(); + void setup_signals(); + void setup_autocomplete(); void tag_similar_symbols(); Offset get_declaration(const Gtk::TextIter &iter); + Offset get_type_declaration(const Gtk::TextIter &iter); + std::vector get_implementations(const Gtk::TextIter &iter); std::unique_ptr autocomplete; - void setup_signals(); - void setup_autocomplete(); struct AutocompleteRow { std::string insert; diff --git a/src/utility.cpp b/src/utility.cpp index e30c4f8..cc5938e 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -7,31 +7,31 @@ ScopeGuard::~ScopeGuard() { } size_t utf8_character_count(const std::string &text, size_t pos, size_t length) noexcept { - size_t count = 0; + size_t characters = 0; auto size = length == std::string::npos ? text.size() : std::min(pos + length, text.size()); for(; pos < size;) { if(static_cast(text[pos]) <= 0b01111111) { - ++count; + ++characters; ++pos; } else if(static_cast(text[pos]) >= 0b11111000) // Invalid UTF-8 byte ++pos; else if(static_cast(text[pos]) >= 0b11110000) { - ++count; + ++characters; pos += 4; } else if(static_cast(text[pos]) >= 0b11100000) { - ++count; + ++characters; pos += 3; } else if(static_cast(text[pos]) >= 0b11000000) { - ++count; + ++characters; pos += 2; } else // // Invalid start of UTF-8 character ++pos; } - return count; + return characters; } size_t utf16_code_units_byte_count(const std::string &text, size_t code_units, size_t start_pos) { @@ -73,6 +73,34 @@ size_t utf16_code_units_byte_count(const std::string &text, size_t code_units, s return pos - start_pos; } +size_t utf16_code_unit_count(const std::string &text, size_t pos, size_t length) { + size_t code_units = 0; + auto size = length == std::string::npos ? text.size() : std::min(pos + length, text.size()); + for(; pos < size;) { + if(static_cast(text[pos]) <= 0b01111111) { + ++code_units; + ++pos; + } + else if(static_cast(text[pos]) >= 0b11111000) // Invalid UTF-8 byte + ++pos; + else if(static_cast(text[pos]) >= 0b11110000) { + code_units += 2; + pos += 4; + } + else if(static_cast(text[pos]) >= 0b11100000) { + ++code_units; + pos += 3; + } + else if(static_cast(text[pos]) >= 0b11000000) { + ++code_units; + pos += 2; + } + else // // Invalid start of UTF-8 character + ++pos; + } + return code_units; +} + bool starts_with(const char *str, const std::string &test) noexcept { for(size_t i = 0; i < test.size(); ++i) { if(*str == '\0') diff --git a/src/utility.hpp b/src/utility.hpp index 798875f..6faa5d3 100644 --- a/src/utility.hpp +++ b/src/utility.hpp @@ -13,6 +13,8 @@ size_t utf8_character_count(const std::string &text, size_t pos = 0, size_t leng /// Returns number of bytes in the given utf16 code units in text size_t utf16_code_units_byte_count(const std::string &text, size_t code_units, size_t start_pos = 0); +/// Returns number of utf16 code units in the text +size_t utf16_code_unit_count(const std::string &text, size_t pos = 0, size_t length = std::string::npos); bool starts_with(const char *str, const std::string &test) noexcept; bool starts_with(const char *str, const char *test) noexcept; diff --git a/tests/utility_test.cpp b/tests/utility_test.cpp index 47c1553..1c26bcf 100644 --- a/tests/utility_test.cpp +++ b/tests/utility_test.cpp @@ -50,6 +50,25 @@ int main() { g_assert_cmpuint(utf16_code_units_byte_count("test🔥test", 10), ==, 12); // Fire emoji between test words g_assert_cmpuint(utf16_code_units_byte_count("test🔥test", 11), ==, 12); // Fire emoji between test words + g_assert_cmpuint(utf16_code_unit_count("", 0, 0), ==, 0); + g_assert_cmpuint(utf16_code_unit_count("", 0, 2), ==, 0); + g_assert_cmpuint(utf16_code_unit_count("", 2, 2), ==, 0); + g_assert_cmpuint(utf16_code_unit_count("test", 0, 1), ==, 1); + g_assert_cmpuint(utf16_code_unit_count("test", 0, 4), ==, 4); + g_assert_cmpuint(utf16_code_unit_count("test", 0, 10), ==, 4); + g_assert_cmpuint(utf16_code_unit_count("test", 2, 2), ==, 2); + g_assert_cmpuint(utf16_code_unit_count("æøå", 0, 0), ==, 0); + g_assert_cmpuint(utf16_code_unit_count("æøå", 0, 2), ==, 1); + g_assert_cmpuint(utf16_code_unit_count("æøå", 0, 4), ==, 2); + g_assert_cmpuint(utf16_code_unit_count("æøå", 0, 6), ==, 3); + g_assert_cmpuint(utf16_code_unit_count("æøå", 2, 6), ==, 2); + g_assert_cmpuint(utf16_code_unit_count("æøå", 4, 6), ==, 1); + g_assert_cmpuint(utf16_code_unit_count("æøå", 6, 6), ==, 0); + g_assert_cmpuint(utf16_code_unit_count("test🔥test", 0, 0), ==, 0); // Fire emoji between test words + g_assert_cmpuint(utf16_code_unit_count("test🔥test", 0, 4), ==, 4); // Fire emoji between test words + g_assert_cmpuint(utf16_code_unit_count("test🔥test", 0, 8), ==, 6); // Fire emoji between test words + g_assert_cmpuint(utf16_code_unit_count("test🔥test", 0, 12), ==, 10); // Fire emoji between test words + std::string empty; std::string test("test"); std::string testtest("testtest");