From e9044e386b2853ec815315d028d07e1f135b4f2d Mon Sep 17 00:00:00 2001 From: eidheim Date: Wed, 8 Feb 2023 18:26:10 +0100 Subject: [PATCH] Language client: now adds diagnostics to codeAction requests, and supports adding configuration responses. Also now uses buffer completions when completion is not provided by server. --- src/source_generic.cpp | 99 ++++++++++++----------- src/source_generic.hpp | 8 +- src/source_language_protocol.cpp | 131 ++++++++++++++++++++----------- src/source_language_protocol.hpp | 13 ++- 4 files changed, 149 insertions(+), 102 deletions(-) diff --git a/src/source_generic.cpp b/src/source_generic.cpp index c3fafb5..c477d59 100644 --- a/src/source_generic.cpp +++ b/src/source_generic.cpp @@ -8,37 +8,9 @@ #include #include -Source::GenericView::GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language) : BaseView(file_path, language), View(file_path, language, true), autocomplete(this, interactive_completion, last_keyval, false, false) { - if(language) { - auto language_manager = LanguageManager::get_default(); - auto search_paths = language_manager->get_search_path(); - bool found_language_file = false; - boost::filesystem::path language_file; - boost::system::error_code ec; - for(auto &search_path : search_paths) { - boost::filesystem::path p(static_cast(search_path) + '/' + static_cast(language->get_id()) + ".lang"); - if(boost::filesystem::exists(p, ec) && boost::filesystem::is_regular_file(p, ec)) { - language_file = p; - found_language_file = true; - break; - } - } - - if(found_language_file) { - boost::property_tree::ptree pt; - try { - boost::property_tree::xml_parser::read_xml(language_file.string(), pt); - parse_language_file(pt); - } - catch(const std::exception &e) { - Terminal::get().print("\e[31mError\e[m: error parsing language file " + filesystem::get_short_path(language_file).string() + ": " + e.what() + '\n', true); - } - } - } - - setup_buffer_words(); - - setup_autocomplete(); +Source::GenericView::GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language, bool is_generic_view) : BaseView(file_path, language), View(file_path, language, is_generic_view) { + if(is_generic_view) + setup_autocomplete(); } void Source::GenericView::parse_language_file(const boost::property_tree::ptree &pt) { @@ -171,13 +143,44 @@ void Source::GenericView::setup_buffer_words() { } void Source::GenericView::setup_autocomplete() { + autocomplete = std::make_unique(this, interactive_completion, last_keyval, false, false); + + if(language) { + auto language_manager = LanguageManager::get_default(); + auto search_paths = language_manager->get_search_path(); + bool found_language_file = false; + boost::filesystem::path language_file; + boost::system::error_code ec; + for(auto &search_path : search_paths) { + boost::filesystem::path path(static_cast(search_path) + '/' + static_cast(language->get_id()) + ".lang"); + if(boost::filesystem::exists(path, ec) && boost::filesystem::is_regular_file(path, ec)) { + language_file = path; + found_language_file = true; + break; + } + } + + if(found_language_file) { + boost::property_tree::ptree pt; + try { + boost::property_tree::xml_parser::read_xml(language_file.string(), pt); + parse_language_file(pt); + } + catch(const std::exception &e) { + Terminal::get().print("\e[31mError\e[m: error parsing language file " + filesystem::get_short_path(language_file).string() + ": " + e.what() + '\n', true); + } + } + } + + setup_buffer_words(); + non_interactive_completion = [this] { if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) return; - autocomplete.run(); + autocomplete->run(); }; - autocomplete.run_check = [this]() { + autocomplete->run_check = [this]() { auto prefix_start = get_buffer()->get_insert()->get_iter(); auto prefix_end = prefix_start; @@ -189,13 +192,13 @@ void Source::GenericView::setup_autocomplete() { prefix_start.forward_char(); if((count >= 3 && !(*prefix_start >= '0' && *prefix_start <= '9')) || !interactive_completion) { - LockGuard lock(autocomplete.prefix_mutex); - autocomplete.prefix = get_buffer()->get_text(prefix_start, prefix_end); + LockGuard lock(autocomplete->prefix_mutex); + autocomplete->prefix = get_buffer()->get_text(prefix_start, prefix_end); if(interactive_completion) - show_prefix_buffer_word = buffer_words.find(autocomplete.prefix) != buffer_words.end(); + show_prefix_buffer_word = buffer_words.find(autocomplete->prefix) != buffer_words.end(); else { - auto it = buffer_words.find(autocomplete.prefix); + auto it = buffer_words.find(autocomplete->prefix); show_prefix_buffer_word = !(it == buffer_words.end() || it->second == 1); } return true; @@ -204,19 +207,19 @@ void Source::GenericView::setup_autocomplete() { return false; }; - autocomplete.add_rows = [this](std::string & /*buffer*/, int /*line*/, int /*line_index*/) { - if(autocomplete.state == Autocomplete::State::starting) { + autocomplete->add_rows = [this](std::string & /*buffer*/, int /*line*/, int /*line_index*/) { + if(autocomplete->state == Autocomplete::State::starting) { autocomplete_comment.clear(); autocomplete_insert.clear(); std::string prefix; { - LockGuard lock(autocomplete.prefix_mutex); - prefix = autocomplete.prefix; + LockGuard lock(autocomplete->prefix_mutex); + prefix = autocomplete->prefix; } for(auto &keyword : keywords) { if(starts_with(keyword, prefix)) { - autocomplete.rows.emplace_back(keyword); + autocomplete->rows.emplace_back(keyword); autocomplete_insert.emplace_back(keyword); autocomplete_comment.emplace_back(""); } @@ -226,7 +229,7 @@ void Source::GenericView::setup_autocomplete() { if((show_prefix_buffer_word || buffer_word.first.size() > prefix.size()) && starts_with(buffer_word.first, prefix) && keywords.find(buffer_word.first) == keywords.end()) { - autocomplete.rows.emplace_back(buffer_word.first); + autocomplete->rows.emplace_back(buffer_word.first); auto insert = buffer_word.first; boost::replace_all(insert, "$", "\\$"); autocomplete_insert.emplace_back(insert); @@ -238,7 +241,7 @@ void Source::GenericView::setup_autocomplete() { if(snippets) { for(auto &snippet : *snippets) { if(starts_with(snippet.prefix, prefix)) { - autocomplete.rows.emplace_back(snippet.prefix); + autocomplete->rows.emplace_back(snippet.prefix); autocomplete_insert.emplace_back(snippet.body); autocomplete_comment.emplace_back(snippet.description); } @@ -250,16 +253,16 @@ void Source::GenericView::setup_autocomplete() { return true; }; - autocomplete.on_show = [this] { + autocomplete->on_show = [this] { hide_tooltips(); }; - autocomplete.on_hide = [this] { + autocomplete->on_hide = [this] { autocomplete_comment.clear(); autocomplete_insert.clear(); }; - autocomplete.on_select = [this](unsigned int index, const std::string &text, bool hide_window) { + autocomplete->on_select = [this](unsigned int index, const std::string &text, bool hide_window) { get_buffer()->erase(CompletionDialog::get()->start_mark->get_iter(), get_buffer()->get_insert()->get_iter()); if(hide_window) @@ -268,7 +271,7 @@ void Source::GenericView::setup_autocomplete() { get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), text); }; - autocomplete.set_tooltip_buffer = [this](unsigned int index) -> std::function { + autocomplete->set_tooltip_buffer = [this](unsigned int index) -> std::function { auto tooltip_str = autocomplete_comment[index]; if(tooltip_str.empty()) return nullptr; diff --git a/src/source_generic.hpp b/src/source_generic.hpp index 8e1ebbb..9d16ed3 100644 --- a/src/source_generic.hpp +++ b/src/source_generic.hpp @@ -7,7 +7,10 @@ namespace Source { class GenericView : public View { public: - GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language); + GenericView(const boost::filesystem::path &file_path, const Glib::RefPtr &language, bool is_generic_view = true); + + protected: + void setup_autocomplete(); private: void parse_language_file(const boost::property_tree::ptree &pt); @@ -20,9 +23,8 @@ namespace Source { bool show_prefix_buffer_word = false; /// To avoid showing the current word if it is unique in document void setup_buffer_words(); - Autocomplete autocomplete; + std::unique_ptr autocomplete; std::vector autocomplete_comment; std::vector autocomplete_insert; - void setup_autocomplete(); }; } // namespace Source diff --git a/src/source_language_protocol.cpp b/src/source_language_protocol.cpp index ad69eaf..f35183c 100644 --- a/src/source_language_protocol.cpp +++ b/src/source_language_protocol.cpp @@ -52,7 +52,7 @@ LanguageProtocol::Diagnostic::RelatedInformation::RelatedInformation(const JSON LanguageProtocol::Diagnostic::Diagnostic(JSON &&diagnostic) : message(diagnostic.string("message")), range(diagnostic.object("range")), severity(diagnostic.integer_or("severity", 0)), code(diagnostic.string_or("code", "")) { for(auto &related_information : diagnostic.array_or_empty("relatedInformation")) related_informations.emplace_back(related_information); - object = std::make_shared(JSON::make_owner(std::move(diagnostic))); + object = std::make_unique(JSON::make_owner(std::move(diagnostic))); } LanguageProtocol::TextEdit::TextEdit(const JSON &text_edit, std::string new_text_) : range(text_edit.object("range")), new_text(new_text_.empty() ? text_edit.string("newText") : std::move(new_text_)) {} @@ -222,7 +222,8 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize() { "workspace": { "symbol": { "dynamicRegistration": false }, "executeCommand": { "dynamicRegistration": false }, - "workspaceFolders": true + "workspaceFolders": true, + "configuration": true }, "textDocument": { "synchronization": { "dynamicRegistration": false, "didSave": true }, @@ -292,8 +293,21 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize() { capabilities.text_document_sync = static_cast(child->integer_or("change", 0)); } if(auto child = object->child_optional("completionProvider")) { - capabilities.completion = true; - capabilities.completion_resolve = child->boolean_or("resolveProvider", false); + bool is_ltex = false; // Workaround for https://github.com/valentjn/ltex-ls that erroneously reports completionProvider + try { + for(auto &command : object->child("executeCommandProvider").array("commands")) { + if(starts_with(command.string(), "_ltex.")) { + is_ltex = true; + break; + } + } + } + catch(...) { + } + if(!is_ltex) { + capabilities.completion = true; + capabilities.completion_resolve = child->boolean_or("resolveProvider", false); + } } /// Some server capabilities are reported as either boolean or objects auto boolean_or_object = [&object](const std::string &provider) { @@ -540,12 +554,12 @@ void LanguageProtocol::Client::write_notification(const std::string &method, con void LanguageProtocol::Client::handle_server_notification(const std::string &method, JSON &¶ms) { if(method == "textDocument/publishDiagnostics") { - std::vector diagnostics; + std::vector> diagnostics; auto file = filesystem::get_path_from_uri(params.string_or("uri", "")); if(!file.empty()) { for(auto &child : params.array_or_empty("diagnostics")) { try { - diagnostics.emplace_back(std::move(child)); + diagnostics.emplace_back(std::make_shared(std::move(child))); } catch(...) { } @@ -661,12 +675,32 @@ void LanguageProtocol::Client::handle_server_request(const boost::variant &language, std::string language_id_, std::string language_server_) - : Source::BaseView(file_path, language), Source::View(file_path, language), uri(filesystem::get_uri_from_path(file_path)), uri_escaped(JSON::escape_string(uri)), language_id(std::move(language_id_)), language_server(std::move(language_server_)), client(LanguageProtocol::Client::get(file_path, language_id, language_server)) { + : Source::BaseView(file_path, language), Source::GenericView(file_path, language, false), uri(filesystem::get_uri_from_path(file_path)), uri_escaped(JSON::escape_string(uri)), language_id(std::move(language_id_)), language_server(std::move(language_server_)), client(LanguageProtocol::Client::get(file_path, language_id, language_server)) { initialize(); } @@ -1059,9 +1093,7 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { std::unordered_map> file_lines; std::vector> usages; - auto c = static_cast(-1); for(auto &location : locations) { - ++c; usages.emplace_back(Offset(location.range.start.line, location.range.start.character, location.file), std::string()); auto &usage = usages.back(); Source::View *view = nullptr; @@ -1345,8 +1377,19 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { std::promise result_processed; Gtk::TextIter start, end; get_buffer()->get_selection_bounds(start, end); + bool first = true; + std::stringstream ss; + for(auto &diagnostic : last_diagnostics) { + if(get_iter_at_line_pos(diagnostic->range.start.line, diagnostic->range.start.character) <= start && + get_iter_at_line_pos(diagnostic->range.end.line, diagnostic->range.end.character) >= end) { + if(!first) + ss << ','; + ss << *diagnostic->object; + first = false; + } + } std::vector>> results; - write_request("textDocument/codeAction", to_string({make_range({start.get_line(), get_line_pos(start)}, {end.get_line(), get_line_pos(end)}), {"context", "{\"diagnostics\":[]}"}}), [&result_processed, &results](JSON &&result, bool error) { + write_request("textDocument/codeAction", to_string({make_range({start.get_line(), get_line_pos(start)}, {end.get_line(), get_line_pos(end)}), {"context", "{\"diagnostics\":[" + ss.str() + "]}"}}), [&result_processed, &results](JSON &&result, bool error) { if(!error) { for(auto &code_action : result.array_or_empty()) { auto title = code_action.string_or("title", ""); @@ -1491,10 +1534,12 @@ void Source::LanguageProtocolView::setup_signals() { } void Source::LanguageProtocolView::setup_autocomplete() { - autocomplete = std::make_unique(this, interactive_completion, last_keyval, false, true); - - if(!capabilities.completion) + if(!capabilities.completion) { + GenericView::setup_autocomplete(); return; + } + + autocomplete = std::make_unique(this, interactive_completion, last_keyval, false, true); non_interactive_completion = [this] { if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) @@ -1975,7 +2020,7 @@ void Source::LanguageProtocolView::setup_autocomplete() { }; } -void Source::LanguageProtocolView::update_diagnostics_async(std::vector &&diagnostics) { +void Source::LanguageProtocolView::update_diagnostics_async(std::vector> &&diagnostics) { size_t last_count = ++update_diagnostics_async_count; if(capabilities.code_action && !diagnostics.empty()) { dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable { @@ -1986,12 +2031,12 @@ void Source::LanguageProtocolView::update_diagnostics_async(std::vectorobject; first = false; } std::pair range; if(diagnostics.size() == 1) // Use diagnostic range if only one diagnostic, otherwise use whole buffer - range = make_range({diagnostics[0].range.start.line, diagnostics[0].range.start.character}, {diagnostics[0].range.end.line, diagnostics[0].range.end.character}); + range = make_range({diagnostics[0]->range.start.line, diagnostics[0]->range.start.character}, {diagnostics[0]->range.end.line, diagnostics[0]->range.end.character}); else { auto start = get_buffer()->begin(); auto end = get_buffer()->end(); @@ -2027,8 +2072,8 @@ void Source::LanguageProtocolView::update_diagnostics_async(std::vector{}); + if(diagnostic->message == quickfix_diagnostic.message && diagnostic->range == quickfix_diagnostic.range) { + auto pair = diagnostic->quickfixes.emplace(title, std::set{}); pair.first->second.emplace( text_edit.new_text, edit->file, @@ -2041,8 +2086,8 @@ void Source::LanguageProtocolView::update_diagnostics_async(std::vector{}); + if(text_edit.range.start.line == diagnostic->range.start.line) { + auto pair = diagnostic->quickfixes.emplace(title, std::set{}); pair.first->second.emplace( text_edit.new_text, edit->file, @@ -2067,9 +2112,8 @@ void Source::LanguageProtocolView::update_diagnostics_async(std::vector diagnostics) { +void Source::LanguageProtocolView::update_diagnostics(const std::vector> &diagnostics) { diagnostic_offsets.clear(); diagnostic_tooltips.clear(); fix_its.clear(); @@ -2097,8 +2140,8 @@ void Source::LanguageProtocolView::update_diagnostics(std::vectorrange.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()) @@ -2109,47 +2152,47 @@ void Source::LanguageProtocolView::update_diagnostics(std::vector= 2) + if(diagnostic->severity >= 2) num_warnings++; else { num_errors++; error = true; } - num_fix_its += diagnostic.quickfixes.size(); + num_fix_its += diagnostic->quickfixes.size(); - for(auto &quickfix : diagnostic.quickfixes) + for(auto &quickfix : diagnostic->quickfixes) fix_its.insert(fix_its.end(), quickfix.second.begin(), quickfix.second.end()); - add_diagnostic_tooltip(start, end, error, [this, diagnostic = std::move(diagnostic)](Tooltip &tooltip) { + add_diagnostic_tooltip(start, end, error, [this, diagnostic](Tooltip &tooltip) { if(language_id == "python" && !client->pyright) { // pylsp might support markdown in the future - tooltip.insert_with_links_tagged(diagnostic.message); + tooltip.insert_with_links_tagged(diagnostic->message); return; } - tooltip.insert_markdown(diagnostic.message); + tooltip.insert_markdown(diagnostic->message); - if(!diagnostic.related_informations.empty()) { + 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); + 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); 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.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(!diagnostic.quickfixes.empty()) { - if(diagnostic.quickfixes.size() == 1) + 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) { + for(auto &quickfix : diagnostic->quickfixes) { tooltip.buffer->insert_at_cursor("\n"); tooltip.insert_markdown(quickfix.first); } diff --git a/src/source_language_protocol.hpp b/src/source_language_protocol.hpp index db8b71b..7f25d8f 100644 --- a/src/source_language_protocol.hpp +++ b/src/source_language_protocol.hpp @@ -3,7 +3,7 @@ #include "json.hpp" #include "mutex.hpp" #include "process.hpp" -#include "source.hpp" +#include "source_generic.hpp" #include #include #include @@ -83,7 +83,7 @@ namespace LanguageProtocol { std::vector related_informations; std::map> quickfixes; /// Diagnostic object for textDocument/codeAction on new diagnostics - std::shared_ptr object; + std::unique_ptr object; }; class TextEdit { @@ -200,7 +200,7 @@ namespace LanguageProtocol { } // namespace LanguageProtocol namespace Source { - class LanguageProtocolView : public View { + class LanguageProtocolView : public GenericView { friend class LanguageProtocol::Client; public: @@ -231,10 +231,10 @@ namespace Source { void write_did_change_notification(const std::vector> ¶ms); std::atomic update_diagnostics_async_count = {0}; - void update_diagnostics(std::vector diagnostics); + void update_diagnostics(const std::vector> &diagnostics); public: - void update_diagnostics_async(std::vector &&diagnostics); + void update_diagnostics_async(std::vector> &&diagnostics); Gtk::TextIter get_iter_at_line_pos(int line, int pos) override; @@ -298,8 +298,7 @@ namespace Source { /// Used when calling completionItem/resolve. Also increased in autocomplete->on_hide. std::atomic set_tooltip_count = {0}; - /// Used by update_type_coverage only (capabilities.type_coverage == true) - std::vector last_diagnostics; + std::vector> last_diagnostics; sigc::connection update_type_coverage_connection; std::list> type_coverage_marks;