From daf83e66a6ede2f7a5e93ee7e3a67178da3ceb3f Mon Sep 17 00:00:00 2001 From: eidheim Date: Fri, 5 Jan 2018 17:49:43 +0100 Subject: [PATCH] Added initial support for the language server protocol --- README.md | 2 + src/CMakeLists.txt | 1 + src/filesystem.cc | 30 ++ src/filesystem.h | 4 + src/notebook.cc | 15 + src/project.cc | 10 + src/project.h | 7 + src/project_build.cc | 24 +- src/project_build.h | 4 + src/selection_dialog.cc | 4 +- src/source.cc | 2 + src/source_language_protocol.cc | 760 ++++++++++++++++++++++++++++++++ src/source_language_protocol.h | 107 +++++ 13 files changed, 961 insertions(+), 9 deletions(-) create mode 100644 src/source_language_protocol.cc create mode 100644 src/source_language_protocol.h diff --git a/README.md b/README.md index 0975ff7..1044d69 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ towards libclang with speed, stability, and ease of use in mind. * Git support through libgit2 * Fast C++ autocompletion * Keyword and buffer autocompletion for other file types +* Experimental language server protocol support that is enabled if `[language identifier]-language-server` executable is found. This executable can be a symbolic link to one of your installed language server binaries. +See [language-server-protocol/specification.md](https://github.com/Microsoft/language-server-protocol/blob/gh-pages/specification.md) for a table of the currently defined language identifiers. * Tooltips showing type information and doxygen documentation (C++) * Rename refactoring across files (C++) * Highlighting of similar types (C++) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5468a80..a0a934b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ set(JUCI_SHARED_FILES source.cc source_clang.cc source_diff.cc + source_language_protocol.cc source_spellcheck.cc terminal.cc usages_clang.cc diff --git a/src/filesystem.cc b/src/filesystem.cc index c16c0c4..d8b8505 100644 --- a/src/filesystem.cc +++ b/src/filesystem.cc @@ -214,3 +214,33 @@ boost::filesystem::path filesystem::get_executable(const boost::filesystem::path return executable_name; } + +// Based on https://stackoverflow.com/a/11295568 +const std::vector &filesystem::get_executable_search_paths() { + static std::vector result; + if(!result.empty()) + return result; + + const std::string env = getenv("PATH"); + const char delimiter = ':'; + + size_t previous = 0; + size_t pos; + while((pos = env.find(delimiter, previous)) != std::string::npos) { + result.emplace_back(env.substr(previous, pos - previous)); + previous = pos + 1; + } + result.emplace_back(env.substr(previous)); + + return result; +} + +boost::filesystem::path filesystem::find_executable(const std::string &executable_name) { + for(auto &path: get_executable_search_paths()) { + boost::system::error_code ec; + auto executable_path=path/executable_name; + if(boost::filesystem::exists(executable_path, ec)) + return executable_path; + } + return boost::filesystem::path(); +} diff --git a/src/filesystem.h b/src/filesystem.h index 70a8a3f..1112128 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -33,4 +33,8 @@ public: /// Return executable with latest version in filename on systems that is lacking executable_name symbolic link static boost::filesystem::path get_executable(const boost::filesystem::path &executable_name) noexcept; + + static const std::vector &get_executable_search_paths(); + + static boost::filesystem::path find_executable(const std::string &executable_name); }; diff --git a/src/notebook.cc b/src/notebook.cc index 2574fae..76bdc9c 100644 --- a/src/notebook.cc +++ b/src/notebook.cc @@ -7,6 +7,7 @@ #include "filesystem.h" #include "selection_dialog.h" #include "source_clang.h" +#include "source_language_protocol.h" #include "gtksourceview-3.0/gtksourceview/gtksourcemap.h" Notebook::TabLabel::TabLabel(const boost::filesystem::path &path, std::function on_close) { @@ -144,8 +145,22 @@ void Notebook::open(const boost::filesystem::path &file_path_, size_t notebook_i auto last_view=get_current_view(); auto language=Source::guess_language(file_path); + + std::string language_protocol_language_id; + if(language) { + language_protocol_language_id=language->get_id(); + if(language_protocol_language_id=="js") { + if(file_path.extension()==".ts") + language_protocol_language_id="typescript"; + else + language_protocol_language_id="javascript"; + } + } + if(language && (language->get_id()=="chdr" || language->get_id()=="cpphdr" || language->get_id()=="c" || language->get_id()=="cpp" || language->get_id()=="objc")) source_views.emplace_back(new Source::ClangView(file_path, language)); + else if(language && !language_protocol_language_id.empty() && !filesystem::find_executable(language_protocol_language_id+"-language-server").empty()) + source_views.emplace_back(new Source::LanguageProtocolView(file_path, language, language_protocol_language_id)); else source_views.emplace_back(new Source::GenericView(file_path, language)); diff --git a/src/project.cc b/src/project.cc index 5bd50cd..2c36382 100644 --- a/src/project.cc +++ b/src/project.cc @@ -146,6 +146,8 @@ std::shared_ptr Project::create() { return std::shared_ptr(new Project::JavaScript(std::move(build))); if(language_id=="html") return std::shared_ptr(new Project::HTML(std::move(build))); + if(language_id=="rust") + return std::shared_ptr(new Project::Rust(std::move(build))); } } else @@ -782,3 +784,11 @@ void Project::HTML::compile_and_run() { g_clear_error(&error); #endif } + +void Project::Rust::compile_and_run() { + std::string command="cargo run"; + Terminal::get().print("Running "+command+"\n"); + Terminal::get().async_process(command, Notebook::get().get_current_view()->file_path.parent_path(), [command](int exit_status) { + Terminal::get().async_print(command+" returned: "+std::to_string(exit_status)+'\n'); + }); +} diff --git a/src/project.h b/src/project.h index 600866c..51869d5 100644 --- a/src/project.h +++ b/src/project.h @@ -137,6 +137,13 @@ namespace Project { void compile_and_run() override; }; + class Rust : public Base { + public: + Rust(std::unique_ptr &&build) : Base(std::move(build)) {} + + void compile_and_run() override; + }; + std::shared_ptr create(); extern std::shared_ptr current; }; diff --git a/src/project_build.cc b/src/project_build.cc index 45de37f..f2d3bc0 100644 --- a/src/project_build.cc +++ b/src/project_build.cc @@ -7,17 +7,29 @@ std::unique_ptr Project::Build::create(const boost::filesystem:: while(true) { if(boost::filesystem::exists(search_path/"CMakeLists.txt")) { - std::unique_ptr cmake(new CMakeBuild(path)); - if(!cmake->project_path.empty()) - return cmake; + std::unique_ptr build(new CMakeBuild(path)); + if(!build->project_path.empty()) + return build; else return std::make_unique(); } if(boost::filesystem::exists(search_path/"meson.build")) { - std::unique_ptr meson(new MesonBuild(path)); - if(!meson->project_path.empty()) - return meson; + std::unique_ptr build(new MesonBuild(path)); + if(!build->project_path.empty()) + return build; + } + + if(boost::filesystem::exists(search_path/"Cargo.toml")) { + std::unique_ptr build(new CargoBuild()); + build->project_path=search_path; + return build; + } + + if(boost::filesystem::exists(search_path/"package.json")) { + std::unique_ptr build(new NpmBuild()); + build->project_path=search_path; + return build; } if(search_path==search_path.root_directory()) diff --git a/src/project_build.h b/src/project_build.h index ea13c74..769fa52 100644 --- a/src/project_build.h +++ b/src/project_build.h @@ -42,4 +42,8 @@ namespace Project { boost::filesystem::path get_executable(const boost::filesystem::path &path) override; }; + + class CargoBuild : public Build {}; + + class NpmBuild : public Build {}; } diff --git a/src/selection_dialog.cc b/src/selection_dialog.cc index 5a75afc..811f6d0 100644 --- a/src/selection_dialog.cc +++ b/src/selection_dialog.cc @@ -220,11 +220,9 @@ SelectionDialog::SelectionDialog(Gtk::TextView *text_view, Glib::RefPtrget_value(list_view_text.column_record.index); auto text=it->get_value(list_view_text.column_record.text); - hide(); on_select(index, text, true); } - else - hide(); + hide(); }; search_entry.signal_activate().connect([this, activate](){ activate(); diff --git a/src/source.cc b/src/source.cc index 941321b..22341f9 100644 --- a/src/source.cc +++ b/src/source.cc @@ -53,6 +53,8 @@ Glib::RefPtr Source::guess_language(const boost::filesystem::path language=language_manager->get_language("makefile"); else if(file_path.extension()==".tcc") language=language_manager->get_language("cpphdr"); + else if(file_path.extension()==".ts") + language=language_manager->get_language("js"); else if(!file_path.has_extension()) { for(auto &part: file_path) { if(part=="include") { diff --git a/src/source_language_protocol.cc b/src/source_language_protocol.cc new file mode 100644 index 0000000..12c40b3 --- /dev/null +++ b/src/source_language_protocol.cc @@ -0,0 +1,760 @@ +#include "source_language_protocol.h" +#include "info.h" +#include "selection_dialog.h" +#include "terminal.h" +#include "project.h" +#include +#include + +const bool output_messages_and_errors=false; + +LanguageProtocol::Client::Client(std::string root_uri_, std::string language_id_) : root_uri(std::move(root_uri_)), language_id(std::move(language_id_)) { + process = std::make_unique(language_id+"-language-server", "", + [this](const char *bytes, size_t n) { + server_message_stream.write(bytes, n); + parse_server_message(); + }, [](const char *bytes, size_t n) { + if(output_messages_and_errors) + Terminal::get().async_print("Error (language server): "+std::string(bytes, n)+'\n', true); + }, true); +} + +std::shared_ptr LanguageProtocol::Client::get(const boost::filesystem::path &file_path, const std::string &language_id) { + std::string root_uri; + auto build=Project::Build::create(file_path); + if(!build->project_path.empty()) + root_uri=build->project_path.string(); + else + root_uri=file_path.parent_path().string(); + + auto cache_id=root_uri+'|'+language_id; + + static std::unordered_map> cache; + static std::mutex mutex; + std::lock_guard lock(mutex); + auto it=cache.find(cache_id); + if(it==cache.end()) + it=cache.emplace(cache_id, std::weak_ptr()).first; + auto instance=it->second.lock(); + if(!instance) + it->second=instance=std::shared_ptr(new Client(root_uri, language_id)); + return instance; +} + +LanguageProtocol::Client::~Client() { + std::condition_variable cv; + std::mutex cv_mutex; + bool cv_notified=false; + write_request("shutdown", "", [this, &cv, &cv_mutex, &cv_notified](const boost::property_tree::ptree &result, bool error) { + if(!error) + this->write_notification("exit", ""); + std::unique_lock lock(cv_mutex); + cv_notified=true; + cv.notify_one(); + }); + + { + std::unique_lock lock(cv_mutex); + if(!cv_notified) + cv.wait(lock); + } + + std::unique_lock lock(timeout_threads_mutex); + for(auto &thread: timeout_threads) + thread.join(); + + int exit_status=-1; + for(size_t c=0;c<20;++c) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + if(process->try_get_exit_status(exit_status)) + break; + } + if(output_messages_and_errors) + std::cout << "Language server exit status: " << exit_status << std::endl; + if(exit_status==-1) + process->kill(); +} + +LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::LanguageProtocolView *view) { + { + std::unique_lock lock(views_mutex); + views.emplace(view); + } + + if(initialized) + return capabilities; + + std::condition_variable cv; + std::mutex cv_mutex; + bool cv_notified=false; + write_request("initialize", "\"processId\":"+std::to_string(process->get_id())+",\"rootUri\":\"file://"+root_uri+"\",\"capabilities\":{\"workspace\":{\"didChangeConfiguration\":{\"dynamicRegistration\":true},\"didChangeWatchedFiles\":{\"dynamicRegistration\":true},\"symbol\":{\"dynamicRegistration\":true},\"executeCommand\":{\"dynamicRegistration\":true}},\"textDocument\":{\"synchronization\":{\"dynamicRegistration\":true,\"willSave\":true,\"willSaveWaitUntil\":true,\"didSave\":true},\"completion\":{\"dynamicRegistration\":true,\"completionItem\":{\"snippetSupport\":true}},\"hover\":{\"dynamicRegistration\":true},\"signatureHelp\":{\"dynamicRegistration\":true},\"definition\":{\"dynamicRegistration\":true},\"references\":{\"dynamicRegistration\":true},\"documentHighlight\":{\"dynamicRegistration\":true},\"documentSymbol\":{\"dynamicRegistration\":true},\"codeAction\":{\"dynamicRegistration\":true},\"codeLens\":{\"dynamicRegistration\":true},\"formatting\":{\"dynamicRegistration\":true},\"rangeFormatting\":{\"dynamicRegistration\":true},\"onTypeFormatting\":{\"dynamicRegistration\":true},\"rename\":{\"dynamicRegistration\":true},\"documentLink\":{\"dynamicRegistration\":true}}},\"initializationOptions\":{\"omitInitBuild\":true},\"trace\":\"off\"", [this, &cv, &cv_mutex, &cv_notified](const boost::property_tree::ptree &result, bool error) { + if(!error) { + auto capabilities_pt=result.find("capabilities"); + if(capabilities_pt!=result.not_found()) + capabilities.text_document_sync=static_cast(capabilities_pt->second.get("textDocumentSync", 0)); + write_notification("initialized", ""); + } + std::unique_lock lock(cv_mutex); + cv_notified=true; + cv.notify_one(); + }); + { + std::unique_lock lock(cv_mutex); + if(!cv_notified) + cv.wait(lock); + } + initialized=true; + return capabilities; +} + +void LanguageProtocol::Client::close(Source::LanguageProtocolView *view) { + std::unique_lock lock(views_mutex); + auto it=views.find(view); + if(it!=views.end()) + views.erase(it); +} + +void LanguageProtocol::Client::parse_server_message() { + if(!header_read) { + std::string line; + while(!header_read && std::getline(server_message_stream, line)) { + if(!line.empty()) { + if(line.back()=='\r') + line.pop_back(); + if(line.compare(0, 16, "Content-Length: ")==0) { + try { + server_message_size=static_cast(std::stoul(line.substr(16))); + } + catch(...) {} + } + } + if(line.empty()) { + server_message_content_pos=server_message_stream.tellg(); + server_message_size+=server_message_content_pos; + header_read=true; + } + } + } + + if(header_read) { + server_message_stream.seekg(0, std::ios::end); + size_t read_size=server_message_stream.tellg(); + std::stringstream tmp; + if(read_size>=server_message_size) { + if(read_size>server_message_size) { + server_message_stream.seekg(server_message_size, std::ios::beg); + server_message_stream.seekp(server_message_size, std::ios::beg); + for(size_t c=server_message_size;c("id", 0); + auto result_it=pt.find("result"); + { + std::unique_lock lock(read_write_mutex); + if(result_it!=pt.not_found()) { + if(message_id) { + auto id_it=handlers.find(message_id); + if(id_it!=handlers.end()) { + auto function=std::move(id_it->second); + lock.unlock(); + function(result_it->second, false); + lock.lock(); + handlers.erase(id_it->first); + } + } + } + else { + auto method_it=pt.find("method"); + if(method_it!=pt.not_found()) { + auto params_it=pt.find("params"); + if(params_it!=pt.not_found()) { + lock.unlock(); + handle_server_request(method_it->second.get_value(""), params_it->second); + lock.lock(); + } + } + } + } + + server_message_stream=std::stringstream(); + header_read=false; + server_message_size=static_cast(-1); + + tmp.seekg(0, std::ios::end); + if(tmp.tellg()>0) { + tmp.seekg(0, std::ios::beg); + server_message_stream << tmp.rdbuf(); + parse_server_message(); + } + } + } +} + +void LanguageProtocol::Client::write_request(const std::string &method, const std::string ¶ms, std::function &&function) { + std::unique_lock lock(read_write_mutex); + if(function) + handlers.emplace(message_id, std::move(function)); + { + std::unique_lock lock(timeout_threads_mutex); + timeout_threads.emplace_back([this] { + for(size_t c=0;c<20;++c) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + std::unique_lock lock(read_write_mutex); + auto id_it=handlers.find(message_id); + if(id_it==handlers.end()) + return; + } + std::unique_lock lock(read_write_mutex); + auto id_it=handlers.find(message_id); + if(id_it!=handlers.end()) { + auto function=std::move(id_it->second); + lock.unlock(); + function(boost::property_tree::ptree(), false); + lock.lock(); + handlers.erase(id_it->first); + } + }); + } + std::string content("{\"jsonrpc\":\"2.0\",\"id\":"+std::to_string(message_id++)+",\"method\":\""+method+"\",\"params\":{"+params+"}}"); + auto message="Content-Length: "+std::to_string(content.size())+"\r\n\r\n"+content; + if(output_messages_and_errors) + std::cout << "Language client: " << content << std::endl; + process->write(message); +} + +void LanguageProtocol::Client::write_notification(const std::string &method, const std::string ¶ms) { + std::unique_lock lock(read_write_mutex); + std::string content("{\"jsonrpc\":\"2.0\",\"method\":\""+method+"\",\"params\":{"+params+"}}"); + auto message="Content-Length: "+std::to_string(content.size())+"\r\n\r\n"+content; + if(output_messages_and_errors) + std::cout << "Language client: " << content << std::endl; + process->write(message); +} + +void LanguageProtocol::Client::handle_server_request(const std::string &method, const boost::property_tree::ptree ¶ms) { + if(method=="textDocument/publishDiagnostics") { + std::vector diagnostics; + auto uri=params.get("uri", ""); + if(!uri.empty()) { + auto diagnostics_pt=params.get_child("diagnostics", boost::property_tree::ptree()); + for(auto it=diagnostics_pt.begin();it!=diagnostics_pt.end();++it) { + auto range_it=it->second.find("range"); + if(range_it!=it->second.not_found()) { + auto start_it=range_it->second.find("start"); + auto end_it=range_it->second.find("end"); + if(start_it!=range_it->second.not_found() && start_it!=range_it->second.not_found()) { + diagnostics.emplace_back(Diagnostic{it->second.get("message", ""), + std::make_pair(Source::Offset(start_it->second.get("line", 0), start_it->second.get("character", 0)), + Source::Offset(end_it->second.get("line", 0), end_it->second.get("character", 0))), + it->second.get("severity", 0), uri}); + } + } + } + std::unique_lock lock(views_mutex); + for(auto view: views) { + if(view->uri==uri) { + view->update_diagnostics(std::move(diagnostics)); + break; + } + } + } + } +} + +Source::LanguageProtocolView::LanguageProtocolView(const boost::filesystem::path &file_path, Glib::RefPtr language, std::string language_id_) + : Source::View(file_path, language), uri("file://"+file_path.string()), language_id(std::move(language_id_)), client(LanguageProtocol::Client::get(file_path, language_id)), autocomplete(this, interactive_completion, last_keyval, false) { + configure(); + get_source_buffer()->set_language(language); + get_source_buffer()->set_highlight_syntax(true); + parsed=true; + + capabilities=client->initialize(this); + if(language_id=="rust") + client->write_notification("workspace/didChangeConfiguration", "\"settings\":{\"rust\":{\"sysroot\":null,\"target\":null,\"rustflags\":null,\"clear_env_rust_log\":true,\"build_lib\":null,\"build_bin\":null,\"cfg_test\":false,\"unstable_features\":false,\"wait_to_build\":500,\"show_warnings\":true,\"goto_def_racer_fallback\":false,\"use_crate_blacklist\":true,\"build_on_save\":false,\"workspace_mode\":false,\"analyze_package\":null,\"features\":[],\"all_features\":false,\"no_default_features\":false}}"); + std::string text=get_buffer()->get_text(); + escape_text(text); + client->write_notification("textDocument/didOpen", "\"textDocument\":{\"uri\":\""+uri+"\",\"languageId\":\""+language_id+"\",\"version\":"+std::to_string(document_version++)+",\"text\":\""+text+"\"}"); + + format_style=[this](bool) { + client->write_request("textDocument/formatting", "\"textDocument\":{\"uri\":\""+uri+"\"},\"options\":{\"tabSize\":2,\"insertSpaces\":true}"); + }; + + // Completion test + get_declaration_location=[this]() { + auto iter=get_buffer()->get_insert()->get_iter(); + std::condition_variable cv; + std::mutex cv_mutex; + bool cv_notified=false; + auto offset=std::make_shared(); + client->write_request("textDocument/definition", "\"textDocument\":{\"uri\":\""+uri+"\"}, \"position\": {\"line\": "+std::to_string(iter.get_line())+", \"character\": "+std::to_string(iter.get_line_offset())+"}", [&cv, &cv_mutex, &cv_notified, offset](const boost::property_tree::ptree &result, bool error) { + if(!error) { + for(auto it=result.begin();it!=result.end();++it) { + auto uri=it->second.get("uri", ""); + if(uri.compare(0, 7, "file://")==0) + uri.erase(0, 7); + auto range=it->second.find("range"); + if(range!=it->second.not_found()) { + auto start=range->second.find("start"); + if(start!=range->second.not_found()) + *offset=Offset(start->second.get("line", 0), start->second.get("character", 0), uri); + } + break; // TODO: can a language server return several definitions? + } + } + std::unique_lock lock(cv_mutex); + cv_notified=true; + cv.notify_one(); + }); + { + std::unique_lock lock(cv_mutex); + if(!cv_notified) + cv.wait(lock); + } + if(!*offset) + Info::get().print("No declaration found"); + return *offset; + }; + + get_buffer()->signal_insert().connect([this](const Gtk::TextBuffer::iterator &start, const Glib::ustring &text_, int bytes) { + std::string content_changes; + if(capabilities.text_document_sync==LanguageProtocol::Capabilities::TextDocumentSync::NONE) + return; + if(capabilities.text_document_sync==LanguageProtocol::Capabilities::TextDocumentSync::INCREMENTAL) { + std::string text=text_; + escape_text(text); + content_changes="{\"range\":{\"start\":{\"line\": "+std::to_string(start.get_line())+",\"character\":"+std::to_string(start.get_line_offset())+"},\"end\":{\"line\":"+std::to_string(start.get_line())+",\"character\":"+std::to_string(start.get_line_offset())+"}},\"text\":\""+text+"\"}"; + } + else { + std::string text=get_buffer()->get_text(); + escape_text(text); + content_changes="{\"text\":\""+text+"\"}"; + } + client->write_notification("textDocument/didChange", "\"textDocument\":{\"uri\":\""+uri+"\",\"version\":"+std::to_string(document_version++)+"},\"contentChanges\":["+content_changes+"]"); + }, false); + + get_buffer()->signal_erase().connect([this](const Gtk::TextBuffer::iterator &start, const Gtk::TextBuffer::iterator &end) { + std::string content_changes; + if(capabilities.text_document_sync==LanguageProtocol::Capabilities::TextDocumentSync::NONE) + return; + if(capabilities.text_document_sync==LanguageProtocol::Capabilities::TextDocumentSync::INCREMENTAL) + content_changes="{\"range\":{\"start\":{\"line\": "+std::to_string(start.get_line())+",\"character\":"+std::to_string(start.get_line_offset())+"},\"end\":{\"line\":"+std::to_string(end.get_line())+",\"character\":"+std::to_string(end.get_line_offset())+"}},\"text\":\"\"}"; + else { + std::string text=get_buffer()->get_text(); + escape_text(text); + content_changes="{\"text\":\""+text+"\"}"; + } + client->write_notification("textDocument/didChange", "\"textDocument\":{\"uri\":\""+uri+"\",\"version\":"+std::to_string(document_version++)+"},\"contentChanges\":["+content_changes+"]"); + }, false); + + goto_next_diagnostic=[this]() { + auto insert_offset=get_buffer()->get_insert()->get_iter().get_offset(); + for(auto offset: diagnostic_offsets) { + if(offset>insert_offset) { + get_buffer()->place_cursor(get_buffer()->get_iter_at_offset(offset)); + scroll_to(get_buffer()->get_insert(), 0.0, 1.0, 0.5); + return; + } + } + if(diagnostic_offsets.size()==0) + Info::get().print("No diagnostics found in current buffer"); + else { + auto iter=get_buffer()->get_iter_at_offset(*diagnostic_offsets.begin()); + get_buffer()->place_cursor(iter); + scroll_to(get_buffer()->get_insert(), 0.0, 1.0, 0.5); + } + }; + + setup_autocomplete(); +} + +Source::LanguageProtocolView::~LanguageProtocolView() { + autocomplete.state=Autocomplete::State::IDLE; + if(autocomplete.thread.joinable()) + autocomplete.thread.join(); + + client->write_notification("textDocument/didClose", "\"textDocument\":{\"uri\":\""+uri+"\"}"); + client->close(this); + + client=nullptr; +} + +void Source::LanguageProtocolView::escape_text(std::string &text) { + for(size_t c=0;c &&diagnostics) { + dispatcher.post([this, diagnostics=std::move(diagnostics)] { + diagnostic_offsets.clear(); + diagnostic_tooltips.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()); + size_t num_warnings=0; + size_t num_errors=0; + size_t num_fix_its=0; + for(auto &diagnostic: diagnostics) { + if(diagnostic.uri==uri) { + int line=diagnostic.offsets.first.line; + if(line<0 || line>=get_buffer()->get_line_count()) + line=get_buffer()->get_line_count()-1; + auto start=get_iter_at_line_end(line); + int index=diagnostic.offsets.first.index; + if(index>=0 && indexget_iter_at_line_offset(line, index); + if(start.ends_line()) { + while(!start.is_start() && start.ends_line()) + start.backward_char(); + } + diagnostic_offsets.emplace(start.get_offset()); + + line=diagnostic.offsets.second.line; + if(line<0 || line>=get_buffer()->get_line_count()) + line=get_buffer()->get_line_count(); + auto end=get_iter_at_line_end(line); + index=diagnostic.offsets.second.index; + if(index>=0 && indexget_iter_at_line_offset(line, index); + + if(start==end) { + if(!end.is_end()) + end.forward_char(); + else + start.forward_char(); + } + + std::string diagnostic_tag_name; + std::string severity_spelling; + if(diagnostic.severity>=2) { + severity_spelling="Warning"; + diagnostic_tag_name="def:warning"; + num_warnings++; + } + else { + severity_spelling="Error"; + diagnostic_tag_name="def:error"; + num_errors++; + } + + auto spelling=diagnostic.spelling; + + auto create_tooltip_buffer=[this, spelling, severity_spelling, diagnostic_tag_name]() { + auto tooltip_buffer=Gtk::TextBuffer::create(get_buffer()->get_tag_table()); + tooltip_buffer->insert_with_tag(tooltip_buffer->get_insert()->get_iter(), severity_spelling, diagnostic_tag_name); + tooltip_buffer->insert_with_tag(tooltip_buffer->get_insert()->get_iter(), ":\n"+spelling, "def:note"); + return tooltip_buffer; + }; + diagnostic_tooltips.emplace_back(create_tooltip_buffer, this, get_buffer()->create_mark(start), get_buffer()->create_mark(end)); + + get_buffer()->apply_tag_by_name(diagnostic_tag_name+"_underline", start, end); + auto iter=get_buffer()->get_insert()->get_iter(); + if(iter.ends_line()) { + auto next_iter=iter; + if(next_iter.forward_char()) + get_buffer()->remove_tag_by_name(diagnostic_tag_name+"_underline", iter, next_iter); + } + } + } + status_diagnostics=std::make_tuple(num_warnings, num_errors, num_fix_its); + if(update_status_diagnostics) + update_status_diagnostics(this); + }); +} + +void Source::LanguageProtocolView::show_diagnostic_tooltips(const Gdk::Rectangle &rectangle) { + diagnostic_tooltips.show(rectangle); +} + +void Source::LanguageProtocolView::show_type_tooltips(const Gdk::Rectangle &rectangle) { + 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; + + auto offset=iter.get_offset(); + client->write_request("textDocument/hover", "\"textDocument\": {\"uri\":\"file://"+file_path.string()+"\"}, \"position\": {\"line\": "+std::to_string(iter.get_line())+", \"character\": "+std::to_string(iter.get_line_offset())+"}", [this, offset](const boost::property_tree::ptree &result, bool error) { + if(!error) { + auto contents=result.get_child("contents", boost::property_tree::ptree()); + auto it=contents.begin(); + if(it!=contents.end()) { + auto value=it->second.get("value", ""); + if(!value.empty()) { + dispatcher.post([this, offset, value=std::move(value)] { + if(offset>=get_buffer()->get_char_count()) + return; + type_tooltips.clear(); + auto create_tooltip_buffer=[this, value=std::move(value)]() { + auto tooltip_buffer=Gtk::TextBuffer::create(get_buffer()->get_tag_table()); + tooltip_buffer->insert_with_tag(tooltip_buffer->get_insert()->get_iter(), "Type: "+value, "def:note"); + + return tooltip_buffer; + }; + + auto iter=get_buffer()->get_iter_at_offset(offset); + auto start=iter; + auto end=iter; + while(((*start>='A' && *start<='Z') || (*start>='a' && *start<='z') || (*start>='0' && *start<='9') || *start=='_') && start.backward_char()) {} + start.forward_char(); + while(((*end>='A' && *end<='Z') || (*end>='a' && *end<='z') || (*end>='0' && *end<='9') || *end=='_') && end.forward_char()) {} + type_tooltips.emplace_back(create_tooltip_buffer, this, get_buffer()->create_mark(start), get_buffer()->create_mark(end)); + type_tooltips.show(); + }); + } + } + } + }); +} + +void Source::LanguageProtocolView::setup_autocomplete() { + non_interactive_completion=[this] { + if(CompletionDialog::get() && CompletionDialog::get()->is_visible()) + return; + autocomplete.run(); + }; + + autocomplete.reparse=[this] { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + }; + + autocomplete.is_continue_key=[](guint keyval) { + if((keyval>='0' && keyval<='9') || (keyval>='a' && keyval<='z') || (keyval>='A' && keyval<='Z') || keyval=='_') + return true; + + return 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; + }; + + autocomplete.run_check=[this]() { + auto iter=get_buffer()->get_insert()->get_iter(); + iter.backward_char(); + if(!is_code_iter(iter)) + return false; + + std::string line=" "+get_line_before(); + const static std::regex dot_or_arrow("^.*[a-zA-Z0-9_\\)\\]\\>](\\.)([a-zA-Z0-9_]*)$"); + const static std::regex colon_colon("^.*::([a-zA-Z0-9_]*)$"); + const static std::regex part_of_symbol("^.*[^a-zA-Z0-9_]+([a-zA-Z0-9_]{3,})$"); + std::smatch sm; + if(std::regex_match(line, sm, dot_or_arrow)) { + { + std::unique_lock lock(autocomplete.prefix_mutex); + autocomplete.prefix=sm[2].str(); + } + if(autocomplete.prefix.size()==0 || autocomplete.prefix[0]<'0' || autocomplete.prefix[0]>'9') + return true; + } + else if(std::regex_match(line, sm, colon_colon)) { + { + std::unique_lock lock(autocomplete.prefix_mutex); + autocomplete.prefix=sm[1].str(); + } + if(autocomplete.prefix.size()==0 || autocomplete.prefix[0]<'0' || autocomplete.prefix[0]>'9') + return true; + } + else if(std::regex_match(line, sm, part_of_symbol)) { + { + std::unique_lock lock(autocomplete.prefix_mutex); + autocomplete.prefix=sm[1].str(); + } + if(autocomplete.prefix.size()==0 || autocomplete.prefix[0]<'0' || autocomplete.prefix[0]>'9') + return true; + } + else if(!interactive_completion) { + auto end_iter=get_buffer()->get_insert()->get_iter(); + auto iter=end_iter; + while(iter.backward_char() && autocomplete.is_continue_key(*iter)) {} + if(iter!=end_iter) + iter.forward_char(); + std::unique_lock lock(autocomplete.prefix_mutex); + autocomplete.prefix=get_buffer()->get_text(iter, end_iter); + return true; + } + + return false; + }; + + autocomplete.before_add_rows=[this] { + status_state="autocomplete..."; + if(update_status_state) + update_status_state(this); + }; + + autocomplete.after_add_rows=[this] { + status_state=""; + if(update_status_state) + update_status_state(this); + }; + + autocomplete.on_add_rows_error=[this] { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + }; + + autocomplete.add_rows=[this](std::string &buffer, int line_number, int column) { + if(autocomplete.state==Autocomplete::State::STARTING) { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + std::condition_variable cv; + std::mutex cv_mutex; + bool cv_notified=false; + client->write_request("textDocument/completion", "\"textDocument\":{\"uri\":\""+uri+"\"}, \"position\": {\"line\": "+std::to_string(line_number-1)+", \"character\": "+std::to_string(column-1)+"}", [this, &cv, &cv_mutex, &cv_notified](const boost::property_tree::ptree &result, bool error) { + if(!error) { + auto begin=result.begin(); // rust language server is bugged + auto end=result.end(); + auto items_it=result.find("items"); // correct + if(items_it!=result.not_found()) { + begin=items_it->second.begin(); + end=items_it->second.end(); + } + for(auto it=begin;it!=end;++it) { + auto label=it->second.get("label", ""); + auto detail=it->second.get("detail", ""); + auto insert=it->second.get("insertText", label); + if(!label.empty()) { + autocomplete.rows.emplace_back(std::move(label)); + autocomplete_comment.emplace_back(std::move(detail)); + autocomplete_insert.emplace_back(std::move(insert)); + } + } + } + std::unique_lock lock(cv_mutex); + cv_notified=true; + cv.notify_one(); + }); + std::unique_lock lock(cv_mutex); + if(!cv_notified) + cv.wait(lock); + } + }; + + signal_key_press_event().connect([this](GdkEventKey *event) { + if((event->keyval==GDK_KEY_Tab || event->keyval==GDK_KEY_ISO_Left_Tab) && (event->state&GDK_SHIFT_MASK)==0) { + if(!autocomplete_marks.empty()) { + auto it=autocomplete_marks.begin(); + auto start=it->first->get_iter(); + auto end=it->second->get_iter(); + if(start==end) + return false; + autocomplete_keep_marks=true; + get_buffer()->select_range(it->first->get_iter(), it->second->get_iter()); + autocomplete_keep_marks=false; + get_buffer()->delete_mark(it->first); + get_buffer()->delete_mark(it->second); + autocomplete_marks.erase(it); + return true; + } + } + return false; + }, false); + + get_buffer()->signal_mark_set().connect([this](const Gtk::TextBuffer::iterator &iterator, const Glib::RefPtr &mark) { + if(mark->get_name() == "insert") { + if(!autocomplete_keep_marks) { + for(auto &pair: autocomplete_marks) { + get_buffer()->delete_mark(pair.first); + get_buffer()->delete_mark(pair.second); + } + autocomplete_marks.clear(); + } + } + }); + + autocomplete.on_show = [this] { + for(auto &pair: autocomplete_marks) { + get_buffer()->delete_mark(pair.first); + get_buffer()->delete_mark(pair.second); + } + autocomplete_marks.clear(); + hide_tooltips(); + }; + + autocomplete.on_hide = [this] { + autocomplete_comment.clear(); + autocomplete_insert.clear(); + }; + + 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) { + Glib::ustring insert=autocomplete_insert[index]; + size_t pos1=0; + std::vector> mark_offsets; + while((pos1=insert.find("${"), pos1)!=Glib::ustring::npos) { + size_t pos2=insert.find(":", pos1+2); + if(pos2!=Glib::ustring::npos) { + size_t pos3=insert.find("}", pos2+1); + if(pos3!=Glib::ustring::npos) { + size_t length=pos3-pos2-1; + insert.erase(pos3, 1); + insert.erase(pos1, pos2-pos1+1); + mark_offsets.emplace_back(pos1, pos1+length); + pos1+=length; + } + else + break; + } + else + break; + } + get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), insert); + for(auto &offset: mark_offsets) { + auto start=CompletionDialog::get()->start_mark->get_iter(); + auto end=start; + start.forward_chars(offset.first); + end.forward_chars(offset.second); + autocomplete_marks.emplace_back(get_buffer()->create_mark(start), get_buffer()->create_mark(end)); + } + if(!autocomplete_marks.empty()) { + auto it=autocomplete_marks.begin(); + autocomplete_keep_marks=true; + get_buffer()->select_range(it->first->get_iter(), it->second->get_iter()); + autocomplete_keep_marks=false; + get_buffer()->delete_mark(it->first); + get_buffer()->delete_mark(it->second); + autocomplete_marks.erase(it); + } + } + else + get_buffer()->insert(CompletionDialog::get()->start_mark->get_iter(), text); + }; + + autocomplete.get_tooltip = [this](unsigned int index) { + return autocomplete_comment[index]; + }; +} diff --git a/src/source_language_protocol.h b/src/source_language_protocol.h new file mode 100644 index 0000000..5e33097 --- /dev/null +++ b/src/source_language_protocol.h @@ -0,0 +1,107 @@ +#pragma once +#include "autocomplete.h" +#include "process.hpp" +#include "source.h" +#include +#include +#include +#include +#include +#include + +namespace Source { + class LanguageProtocolView; +} + +namespace LanguageProtocol { + class Diagnostic { + public: + std::string spelling; + std::pair offsets; + unsigned severity; + std::string uri; + }; + + class Capabilities { + public: + enum class TextDocumentSync { NONE = 0, + FULL, + INCREMENTAL }; + TextDocumentSync text_document_sync; + }; + + class Client { + Client(std::string root_uri, std::string language_id); + std::string root_uri; + std::string language_id; + + Capabilities capabilities; + + std::unordered_set views; + std::mutex views_mutex; + + std::unique_ptr process; + std::mutex read_write_mutex; + + std::stringstream server_message_stream; + size_t server_message_size = static_cast(-1); + size_t server_message_content_pos; + bool header_read = false; + + size_t message_id = 1; + + std::unordered_map> handlers; + std::vector timeout_threads; + std::mutex timeout_threads_mutex; + + public: + static std::shared_ptr get(const boost::filesystem::path &file_path, const std::string &language_id); + + ~Client(); + + bool initialized = false; + Capabilities initialize(Source::LanguageProtocolView *view); + void close(Source::LanguageProtocolView *view); + + void parse_server_message(); + void write_request(const std::string &method, const std::string ¶ms, std::function &&function = nullptr); + void write_notification(const std::string &method, const std::string ¶ms); + void handle_server_request(const std::string &method, const boost::property_tree::ptree ¶ms); + }; +} // namespace LanguageProtocol + +namespace Source { + class LanguageProtocolView : public View { + public: + LanguageProtocolView(const boost::filesystem::path &file_path, Glib::RefPtr language, std::string language_id_); + ~LanguageProtocolView(); + std::string uri; + + void update_diagnostics(std::vector &&diagnostics); + + protected: + void show_diagnostic_tooltips(const Gdk::Rectangle &rectangle) override; + void show_type_tooltips(const Gdk::Rectangle &rectangle) override; + + private: + std::string language_id; + LanguageProtocol::Capabilities capabilities; + + std::shared_ptr client; + + size_t document_version = 1; + + Dispatcher dispatcher; + + void escape_text(std::string &text); + + std::set diagnostic_offsets; + + Autocomplete autocomplete; + void setup_autocomplete(); + std::vector autocomplete_comment; + std::vector autocomplete_insert; + std::list, Glib::RefPtr>> autocomplete_marks; + bool autocomplete_keep_marks = false; + }; +} // namespace Source