From c7340b709da1b2b8d3eb2e686c7576c34d457a91 Mon Sep 17 00:00:00 2001 From: eidheim Date: Mon, 21 Jun 2021 12:07:37 +0200 Subject: [PATCH] Added JSON::write_json, and some various cleanup --- src/CMakeLists.txt | 1 + src/cmake.cpp | 2 +- src/config.cpp | 13 +- src/files.hpp | 437 ++++++++++++------------ src/filesystem.cpp | 4 +- src/json.cpp | 101 ++++++ src/json.hpp | 19 ++ src/meson.cpp | 2 +- src/project.cpp | 2 +- src/project_build.cpp | 2 +- src/source_base.cpp | 4 +- src/source_language_protocol.cpp | 367 +++++++++----------- src/source_language_protocol.hpp | 16 +- src/usages_clang.cpp | 2 +- src/window.cpp | 20 +- tests/CMakeLists.txt | 6 +- tests/json_test.cpp | 58 ++++ tests/language_protocol_server_test.cpp | 32 +- 18 files changed, 616 insertions(+), 472 deletions(-) create mode 100644 src/json.cpp create mode 100644 src/json.hpp create mode 100644 tests/json_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3585c7f..71cb1d9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,7 @@ set(JUCI_SHARED_FILES filesystem.cpp git.cpp grep.cpp + json.cpp menu.cpp meson.cpp project_build.cpp diff --git a/src/cmake.cpp b/src/cmake.cpp index eb00c17..19fd522 100644 --- a/src/cmake.cpp +++ b/src/cmake.cpp @@ -12,7 +12,7 @@ CMake::CMake(const boost::filesystem::path &path) { const auto find_cmake_project = [](const boost::filesystem::path &file_path) { - std::ifstream input(file_path.string(), std::ofstream::binary); + std::ifstream input(file_path.string(), std::ios::binary); if(input) { std::string line; while(std::getline(input, line)) { diff --git a/src/config.cpp b/src/config.cpp index 5d725b5..fde9cec 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,6 +1,7 @@ #include "config.hpp" #include "files.hpp" #include "filesystem.hpp" +#include "json.hpp" #include "terminal.hpp" #include #include @@ -77,8 +78,16 @@ void Config::update(boost::property_tree::ptree &cfg) { return; cfg_ok &= add_missing_nodes(cfg, default_cfg); cfg_ok &= remove_deprecated_nodes(cfg, default_cfg); - if(!cfg_ok) - boost::property_tree::write_json((home_juci_path / "config" / "config.json").string(), cfg); + if(!cfg_ok) { + auto path = home_juci_path / "config" / "config.json"; + std::ofstream output(path.string(), std::ios::binary); + if(output) { + JSON::write(output, cfg); + output << '\n'; + } + else + std::cerr << "Error writing config file: " << filesystem::get_short_path(path).string() << std::endl; + } } void Config::make_version_dependent_corrections(boost::property_tree::ptree &cfg, const boost::property_tree::ptree &default_cfg, const std::string &version) { diff --git a/src/files.hpp b/src/files.hpp index 92cb638..249a6b9 100644 --- a/src/files.hpp +++ b/src/files.hpp @@ -4,244 +4,243 @@ /// If you add or remove nodes from the default_config_file, increase the juci /// version number (JUCI_VERSION) in ../CMakeLists.txt to automatically apply /// the changes to user's ~/.juci/config/config.json files -const std::string default_config_file = - R"RAW({ - "version": ")RAW" + - std::string(JUCI_VERSION) + - R"RAW(", - "gtk_theme": { - "name_comment": "Use \"\" for default theme, At least these two exist on all systems: Adwaita, Raleigh", - "name": "", - "variant_comment": "Use \"\" for default variant, and \"dark\" for dark theme variant. Note that not all themes support dark variant, but for instance Adwaita does", - "variant": "", - "font_comment": "Set to override theme font, for instance: \"Arial 12\"", - "font": "" - }, - "source": { - "style_comment": "Use \"\" for default style, and for instance juci-dark or juci-dark-blue together with dark gtk_theme variant. Styles from normal gtksourceview install: classic, cobalt, kate, oblivion, solarized-dark, solarized-light, tango", - "style": "juci-light", - "font_comment": "Use \"\" for default font, and for instance \"Monospace 12\" to also set size",)RAW" +const std::string default_config_file = R"RAW({ + "version": ")RAW" + + std::string(JUCI_VERSION) + + R"RAW(", + "gtk_theme": { + "name_comment": "Use \"\" for default theme, At least these two exist on all systems: Adwaita, Raleigh", + "name": "", + "variant_comment": "Use \"\" for default variant, and \"dark\" for dark theme variant. Note that not all themes support dark variant, but for instance Adwaita does", + "variant": "", + "font_comment": "Set to override theme font, for instance: \"Arial 12\"", + "font": "" + }, + "source": { + "style_comment": "Use \"\" for default style, and for instance juci-dark or juci-dark-blue together with dark gtk_theme variant. Styles from normal gtksourceview install: classic, cobalt, kate, oblivion, solarized-dark, solarized-light, tango", + "style": "juci-light", + "font_comment": "Use \"\" for default font, and for instance \"Monospace 12\" to also set size",)RAW" #ifdef __APPLE__ - R"RAW( - "font": "Menlo",)RAW" + R"RAW( + "font": "Menlo",)RAW" #else #ifdef _WIN32 - R"RAW( - "font": "Consolas",)RAW" + R"RAW( + "font": "Consolas",)RAW" #else - R"RAW( - "font": "Monospace",)RAW" + R"RAW( + "font": "Monospace",)RAW" #endif #endif - R"RAW( - "cleanup_whitespace_characters_comment": "Remove trailing whitespace characters on save, and add trailing newline if missing", - "cleanup_whitespace_characters": false, - "show_whitespace_characters_comment": "Determines what kind of whitespaces should be drawn. Use comma-separated list of: space, tab, newline, nbsp, leading, text, trailing or all", - "show_whitespace_characters": "", - "format_style_on_save_comment": "Performs style format on save if supported on language in buffer", - "format_style_on_save": false, - "format_style_on_save_if_style_file_found_comment": "Format style if format file is found, even if format_style_on_save is false", - "format_style_on_save_if_style_file_found": true, - "smart_brackets_comment": "If smart_inserts is enabled, this option is automatically enabled. When inserting an already closed bracket, the cursor might instead be moved, avoiding the need of arrow keys after autocomplete", - "smart_brackets": true, - "smart_inserts_comment": "When for instance inserting (, () gets inserted. Applies to: (), [], \", '. Also enables pressing ; inside an expression before a final ) to insert ; at the end of line, and deletions of empty insertions", - "smart_inserts": true, - "show_map": true, - "map_font_size": "1", - "show_git_diff": true, - "show_background_pattern": true, - "show_right_margin": false, - "right_margin_position": 80, - "spellcheck_language_comment": "Use \"\" to set language from your locale settings", - "spellcheck_language": "en_US", - "auto_tab_char_and_size_comment": "Use false to always use default tab char and size", - "auto_tab_char_and_size": true, - "default_tab_char_comment": "Use \"\t\" for regular tab", - "default_tab_char": " ", - "default_tab_size": 2, - "tab_indents_line": true, - "word_wrap_comment": "Specify language ids that should enable word wrap, for instance: chdr, c, cpphdr, cpp, js, python, or all to enable word wrap for all languages", - "word_wrap": "markdown, latex", - "highlight_current_line": true, - "show_line_numbers": true, - "enable_multiple_cursors": false, - "auto_reload_changed_files": true, - "search_for_selection": true, - "clang_format_style_comment": "IndentWidth, AccessModifierOffset and UseTab are set automatically. See http://clang.llvm.org/docs/ClangFormatStyleOptions.html", - "clang_format_style": "ColumnLimit: 0, NamespaceIndentation: All", - "clang_tidy_enable_comment": "Enable clang-tidy in new C/C++ buffers", - "clang_tidy_enable": false, - "clang_tidy_checks_comment": "In new C/C++ buffers, these checks are appended to the value of 'Checks' in the .clang-tidy file, if any", - "clang_tidy_checks": "", - "clang_usages_threads_comment": "The number of threads used in finding usages in unparsed files. -1 corresponds to the number of cores available, and 0 disables the search", - "clang_usages_threads": -1, - "clang_detailed_preprocessing_record_comment": "Set to true to, at the cost of increased resource use, include all macro definitions and instantiations when parsing new C/C++ buffers. You should reopen buffers and delete build/.usages_clang after changing this option.", - "clang_detailed_preprocessing_record": false, - "debug_place_cursor_at_stop": false - }, - "terminal": { - "history_size": 10000, - "font_comment": "Use \"\" to use source.font with slightly smaller size", - "font": "", - "clear_on_compile": true, - "clear_on_run_command": false, - "hide_entry_on_run_command": true - }, - "project": { - "default_build_path_comment": "Use to insert the project top level directory name", - "default_build_path": "./build", - "debug_build_path_comment": "Use to insert the project top level directory name, and to insert your default_build_path setting.", - "debug_build_path": "/debug", - "cmake": {)RAW" + R"RAW( + "cleanup_whitespace_characters_comment": "Remove trailing whitespace characters on save, and add trailing newline if missing", + "cleanup_whitespace_characters": false, + "show_whitespace_characters_comment": "Determines what kind of whitespaces should be drawn. Use comma-separated list of: space, tab, newline, nbsp, leading, text, trailing or all", + "show_whitespace_characters": "", + "format_style_on_save_comment": "Performs style format on save if supported on language in buffer", + "format_style_on_save": false, + "format_style_on_save_if_style_file_found_comment": "Format style if format file is found, even if format_style_on_save is false", + "format_style_on_save_if_style_file_found": true, + "smart_brackets_comment": "If smart_inserts is enabled, this option is automatically enabled. When inserting an already closed bracket, the cursor might instead be moved, avoiding the need of arrow keys after autocomplete", + "smart_brackets": true, + "smart_inserts_comment": "When for instance inserting (, () gets inserted. Applies to: (), [], \", '. Also enables pressing ; inside an expression before a final ) to insert ; at the end of line, and deletions of empty insertions", + "smart_inserts": true, + "show_map": true, + "map_font_size": 1, + "show_git_diff": true, + "show_background_pattern": true, + "show_right_margin": false, + "right_margin_position": 80, + "spellcheck_language_comment": "Use \"\" to set language from your locale settings", + "spellcheck_language": "en_US", + "auto_tab_char_and_size_comment": "Use false to always use default tab char and size", + "auto_tab_char_and_size": true, + "default_tab_char_comment": "Use \"\t\" for regular tab", + "default_tab_char": " ", + "default_tab_size": 2, + "tab_indents_line": true, + "word_wrap_comment": "Specify language ids that should enable word wrap, for instance: chdr, c, cpphdr, cpp, js, python, or all to enable word wrap for all languages", + "word_wrap": "markdown, latex", + "highlight_current_line": true, + "show_line_numbers": true, + "enable_multiple_cursors": false, + "auto_reload_changed_files": true, + "search_for_selection": true, + "clang_format_style_comment": "IndentWidth, AccessModifierOffset and UseTab are set automatically. See http://clang.llvm.org/docs/ClangFormatStyleOptions.html", + "clang_format_style": "ColumnLimit: 0, NamespaceIndentation: All", + "clang_tidy_enable_comment": "Enable clang-tidy in new C/C++ buffers", + "clang_tidy_enable": false, + "clang_tidy_checks_comment": "In new C/C++ buffers, these checks are appended to the value of 'Checks' in the .clang-tidy file, if any", + "clang_tidy_checks": "", + "clang_usages_threads_comment": "The number of threads used in finding usages in unparsed files. -1 corresponds to the number of cores available, and 0 disables the search", + "clang_usages_threads": -1, + "clang_detailed_preprocessing_record_comment": "Set to true to, at the cost of increased resource use, include all macro definitions and instantiations when parsing new C/C++ buffers. You should reopen buffers and delete build/.usages_clang after changing this option.", + "clang_detailed_preprocessing_record": false, + "debug_place_cursor_at_stop": false + }, + "terminal": { + "history_size": 10000, + "font_comment": "Use \"\" to use source.font with slightly smaller size", + "font": "", + "clear_on_compile": true, + "clear_on_run_command": false, + "hide_entry_on_run_command": true + }, + "project": { + "default_build_path_comment": "Use to insert the project top level directory name", + "default_build_path": "./build", + "debug_build_path_comment": "Use to insert the project top level directory name, and to insert your default_build_path setting.", + "debug_build_path": "/debug", + "cmake": {)RAW" #ifdef _WIN32 - R"RAW( - "command": "cmake -G\"MSYS Makefiles\"",)RAW" + R"RAW( + "command": "cmake -G\"MSYS Makefiles\"",)RAW" #else - R"RAW( - "command": "cmake",)RAW" + R"RAW( + "command": "cmake",)RAW" #endif - R"RAW( - "compile_command": "cmake --build ." - }, - "meson": { - "command": "meson", - "compile_command": "ninja" - }, - "default_build_management_system_comment": "Select which build management system to use when creating a new C or C++ project, for instance \"cmake\" or \"meson\"", - "default_build_management_system": "cmake", - "save_on_compile_or_run": true,)RAW" + R"RAW( + "compile_command": "cmake --build ." + }, + "meson": { + "command": "meson", + "compile_command": "ninja" + }, + "default_build_management_system_comment": "Select which build management system to use when creating a new C or C++ project, for instance \"cmake\" or \"meson\"", + "default_build_management_system": "cmake", + "save_on_compile_or_run": true,)RAW" #ifdef JUCI_USE_UCTAGS - R"RAW( - "ctags_command": "uctags",)RAW" + R"RAW( + "ctags_command": "uctags",)RAW" #else - R"RAW( - "ctags_command": "ctags",)RAW" + R"RAW( + "ctags_command": "ctags",)RAW" #endif - R"RAW( - "grep_command": "grep", - "cargo_command": "cargo", - "python_command": "python -u", - "markdown_command": "grip -b" - }, - "keybindings": { - "preferences": "comma", - "snippets": "", - "commands": "", - "quit": "q", - "file_new_file": "n", - "file_new_folder": "n", - "file_open_file": "o", - "file_open_folder": "o", - "file_reload_file": "", - "file_save": "s", - "file_save_as": "s", - "file_close_file": "w", - "file_close_folder": "", - "file_close_project": "", - "file_print": "", - "edit_undo": "z", - "edit_redo": "z", - "edit_cut": "x", - "edit_cut_lines": "x", - "edit_copy": "c", - "edit_copy_lines": "c", - "edit_paste": "v", - "edit_extend_selection": "a", - "edit_shrink_selection": "a", - "edit_show_or_hide": "", - "edit_find": "f", - "source_spellcheck": "", - "source_spellcheck_clear": "", - "source_spellcheck_next_error": "e", - "source_git_next_diff": "k", - "source_git_show_diff": "k", - "source_indentation_set_buffer_tab": "", - "source_indentation_auto_indent_buffer": "i", - "source_goto_line": "g", - "source_center_cursor": "l", - "source_cursor_history_back": "Left", - "source_cursor_history_forward": "Right", - "source_show_completion_comment": "Add completion keybinding to disable interactive autocompletion", - "source_show_completion": "", - "source_find_file": "p", - "source_find_symbol": "f", - "source_find_pattern": "f", - "source_comments_toggle": "slash", - "source_comments_add_documentation": "slash", - "source_find_documentation": "d", - "source_goto_declaration": "d", - "source_goto_type_declaration": "d", - "source_goto_implementation": "i", - "source_goto_usage": "u", - "source_goto_method": "m", - "source_rename": "r", - "source_implement_method": "m", - "source_goto_next_diagnostic": "e", - "source_apply_fix_its": "space", - "project_set_run_arguments": "", - "project_compile_and_run": "Return", - "project_compile": "Return", - "project_run_command": "Return", - "project_kill_last_running": "Escape", - "project_force_kill_last_running": "Escape", - "debug_set_run_arguments": "", - "debug_start_continue": "y", - "debug_stop": "y", - "debug_kill": "k", - "debug_step_over": "j", - "debug_step_into": "t", - "debug_step_out": "t", - "debug_backtrace": "j", - "debug_show_variables": "b", - "debug_run_command": "Return", - "debug_toggle_breakpoint": "b", - "debug_show_breakpoints": "b", - "debug_goto_stop": "l",)RAW" + R"RAW( + "grep_command": "grep", + "cargo_command": "cargo", + "python_command": "python -u", + "markdown_command": "grip -b" + }, + "keybindings": { + "preferences": "comma", + "snippets": "", + "commands": "", + "quit": "q", + "file_new_file": "n", + "file_new_folder": "n", + "file_open_file": "o", + "file_open_folder": "o", + "file_reload_file": "", + "file_save": "s", + "file_save_as": "s", + "file_close_file": "w", + "file_close_folder": "", + "file_close_project": "", + "file_print": "", + "edit_undo": "z", + "edit_redo": "z", + "edit_cut": "x", + "edit_cut_lines": "x", + "edit_copy": "c", + "edit_copy_lines": "c", + "edit_paste": "v", + "edit_extend_selection": "a", + "edit_shrink_selection": "a", + "edit_show_or_hide": "", + "edit_find": "f", + "source_spellcheck": "", + "source_spellcheck_clear": "", + "source_spellcheck_next_error": "e", + "source_git_next_diff": "k", + "source_git_show_diff": "k", + "source_indentation_set_buffer_tab": "", + "source_indentation_auto_indent_buffer": "i", + "source_goto_line": "g", + "source_center_cursor": "l", + "source_cursor_history_back": "Left", + "source_cursor_history_forward": "Right", + "source_show_completion_comment": "Add completion keybinding to disable interactive autocompletion", + "source_show_completion": "", + "source_find_file": "p", + "source_find_symbol": "f", + "source_find_pattern": "f", + "source_comments_toggle": "slash", + "source_comments_add_documentation": "slash", + "source_find_documentation": "d", + "source_goto_declaration": "d", + "source_goto_type_declaration": "d", + "source_goto_implementation": "i", + "source_goto_usage": "u", + "source_goto_method": "m", + "source_rename": "r", + "source_implement_method": "m", + "source_goto_next_diagnostic": "e", + "source_apply_fix_its": "space", + "project_set_run_arguments": "", + "project_compile_and_run": "Return", + "project_compile": "Return", + "project_run_command": "Return", + "project_kill_last_running": "Escape", + "project_force_kill_last_running": "Escape", + "debug_set_run_arguments": "", + "debug_start_continue": "y", + "debug_stop": "y", + "debug_kill": "k", + "debug_step_over": "j", + "debug_step_into": "t", + "debug_step_out": "t", + "debug_backtrace": "j", + "debug_show_variables": "b", + "debug_run_command": "Return", + "debug_toggle_breakpoint": "b", + "debug_show_breakpoints": "b", + "debug_goto_stop": "l",)RAW" #ifdef __linux - R"RAW( - "window_next_tab": "Tab", - "window_previous_tab": "Tab",)RAW" + R"RAW( + "window_next_tab": "Tab", + "window_previous_tab": "Tab",)RAW" #else - R"RAW( - "window_next_tab": "Right", - "window_previous_tab": "Left",)RAW" + R"RAW( + "window_next_tab": "Right", + "window_previous_tab": "Left",)RAW" #endif - R"RAW( - "window_goto_tab": "", - "window_toggle_split": "", - "window_split_source_buffer": "",)RAW" + R"RAW( + "window_goto_tab": "", + "window_toggle_split": "", + "window_split_source_buffer": "",)RAW" #ifdef __APPLE__ - R"RAW( - "window_toggle_full_screen": "f",)RAW" + R"RAW( + "window_toggle_full_screen": "f",)RAW" #else - R"RAW( - "window_toggle_full_screen": "F11",)RAW" + R"RAW( + "window_toggle_full_screen": "F11",)RAW" #endif - R"RAW( - "window_toggle_directories": "", - "window_toggle_terminal": "", - "window_toggle_menu": "", - "window_toggle_tabs": "", - "window_toggle_zen_mode": "", - "window_clear_terminal": "" - }, - "documentation_searches": { - "clang": { - "separator": "::", - "queries": { - "@empty": "https://www.google.com/search?q=c%2B%2B+", - "std": "https://www.google.com/search?q=site:http://www.cplusplus.com/reference/+", - "boost": "https://www.google.com/search?q=site:http://www.boost.org/doc/libs/1_59_0/+", - "Gtk": "https://www.google.com/search?q=site:https://developer.gnome.org/gtkmm/stable/+", - "@any": "https://www.google.com/search?q=" - } - } - }, - "log": { - "libclang_comment": "Outputs diagnostics for new C/C++ buffers", - "libclang": false, - "language_server": false + R"RAW( + "window_toggle_directories": "", + "window_toggle_terminal": "", + "window_toggle_menu": "", + "window_toggle_tabs": "", + "window_toggle_zen_mode": "", + "window_clear_terminal": "" + }, + "documentation_searches": { + "clang": { + "separator": "::", + "queries": { + "@empty": "https://www.google.com/search?q=c%2B%2B+", + "std": "https://www.google.com/search?q=site:http://www.cplusplus.com/reference/+", + "boost": "https://www.google.com/search?q=site:http://www.boost.org/doc/libs/1_59_0/+", + "Gtk": "https://www.google.com/search?q=site:https://developer.gnome.org/gtkmm/stable/+", + "@any": "https://www.google.com/search?q=" + } } + }, + "log": { + "libclang_comment": "Outputs diagnostics for new C/C++ buffers", + "libclang": false, + "language_server": false + } } )RAW"; diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 76acacf..a1a75aa 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -9,7 +9,7 @@ //Only use on small files std::string filesystem::read(const std::string &path) { std::string str; - std::ifstream input(path, std::ofstream::binary); + std::ifstream input(path, std::ios::binary); if(input) { input.seekg(0, std::ios::end); auto size = input.tellg(); @@ -23,7 +23,7 @@ std::string filesystem::read(const std::string &path) { //Only use on small files bool filesystem::write(const std::string &path, const std::string &new_content) { - std::ofstream output(path, std::ofstream::binary); + std::ofstream output(path, std::ios::binary); if(output) output << new_content; else diff --git a/src/json.cpp b/src/json.cpp new file mode 100644 index 0000000..42a9bbc --- /dev/null +++ b/src/json.cpp @@ -0,0 +1,101 @@ +#include "json.hpp" +#include +#include +#include +#include + +void JSON::write_json_internal(std::ostream &stream, const boost::property_tree::ptree &pt, bool pretty, const std::vector ¬_string_keys, size_t column, const std::string &key) { + // Based on boost::property_tree::json_parser::write_json_helper() + + if(pt.empty()) { // Value + auto value = pt.get_value(); + if(std::any_of(not_string_keys.begin(), not_string_keys.end(), [&key](const std::string ¬_string_key) { + return key == not_string_key; + })) + stream << value; + else + stream << '"' << escape_string(value) << '"'; + } + else if(pt.count(std::string{}) == pt.size()) { // Array + stream << '['; + if(pretty) + stream << '\n'; + + for(auto it = pt.begin(); it != pt.end(); ++it) { + if(pretty) + stream << std::string((column + 1) * 2, ' '); + + write_json_internal(stream, it->second, pretty, not_string_keys, column + 1, key); + + if(std::next(it) != pt.end()) + stream << ','; + if(pretty) + stream << '\n'; + } + + if(pretty) + stream << std::string(column * 2, ' '); + stream << ']'; + } + else { // Object + stream << '{'; + if(pretty) + stream << '\n'; + + for(auto it = pt.begin(); it != pt.end(); ++it) { + if(pretty) + stream << std::string((column + 1) * 2, ' '); + + stream << '"' << escape_string(it->first) << "\":"; + if(pretty) + stream << ' '; + write_json_internal(stream, it->second, pretty, not_string_keys, column + 1, it->first); + + if(std::next(it) != pt.end()) + stream << ','; + if(pretty) + stream << '\n'; + } + + if(pretty) + stream << std::string(column * 2, ' '); + stream << '}'; + } +} +void JSON::write(std::ostream &stream, const boost::property_tree::ptree &pt, bool pretty, const std::vector ¬_string_keys) { + write_json_internal(stream, pt, pretty, not_string_keys, 0, ""); +} + +std::string JSON::escape_string(std::string string) { + for(size_t c = 0; c < string.size(); ++c) { + if(string[c] == '\b') { + string.replace(c, 1, "\\b"); + ++c; + } + else if(string[c] == '\f') { + string.replace(c, 1, "\\f"); + ++c; + } + else if(string[c] == '\n') { + string.replace(c, 1, "\\n"); + ++c; + } + else if(string[c] == '\r') { + string.replace(c, 1, "\\r"); + ++c; + } + else if(string[c] == '\t') { + string.replace(c, 1, "\\t"); + ++c; + } + else if(string[c] == '"') { + string.replace(c, 1, "\\\""); + ++c; + } + else if(string[c] == '\\') { + string.replace(c, 1, "\\\\"); + ++c; + } + } + return string; +} diff --git a/src/json.hpp b/src/json.hpp new file mode 100644 index 0000000..41b6d6b --- /dev/null +++ b/src/json.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +class JSON { + static void write_json_internal(std::ostream &stream, const boost::property_tree::ptree &pt, bool pretty, const std::vector ¬_string_keys, size_t column, const std::string &key); + +public: + /// A replacement of boost::property_tree::write_json() as it does not conform to the JSON standard (https://svn.boost.org/trac10/ticket/9721). + /// Some JSON parses expects, for instance, number types instead of numbers in strings. + /// Note that boost::property_tree::write_json() will always produce string values. + /// Use not_string_keys to specify which keys that should not have string values. + /// TODO: replace boost::property_tree with another JSON library. + static void write(std::ostream &stream, const boost::property_tree::ptree &pt, bool pretty = true, const std::vector ¬_string_keys = {}); + /// A replacement of boost::property_tree::escape_text() as it does not conform to the JSON standard. + static std::string escape_string(std::string string); +}; diff --git a/src/meson.cpp b/src/meson.cpp index 22de0cb..926db34 100644 --- a/src/meson.cpp +++ b/src/meson.cpp @@ -12,7 +12,7 @@ Meson::Meson(const boost::filesystem::path &path) { const auto find_project = [](const boost::filesystem::path &file_path) { - std::ifstream input(file_path.string(), std::ofstream::binary); + std::ifstream input(file_path.string(), std::ios::binary); if(input) { std::string line; while(std::getline(input, line)) { diff --git a/src/project.cpp b/src/project.cpp index b4db0b5..27d8200 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -487,7 +487,7 @@ void Project::LLDB::debug_start(const std::string &command, const boost::filesys auto sysroot = filesystem::get_rust_sysroot_path().string(); if(!sysroot.empty()) { std::string line; - std::ifstream input(sysroot + "/lib/rustlib/etc/lldb_commands", std::ofstream::binary); + std::ifstream input(sysroot + "/lib/rustlib/etc/lldb_commands", std::ios::binary); if(input) { startup_commands.emplace_back("command script import \"" + sysroot + "/lib/rustlib/etc/lldb_lookup.py\""); while(std::getline(input, line)) diff --git a/src/project_build.cpp b/src/project_build.cpp index 6bc8f1d..2c74e63 100644 --- a/src/project_build.cpp +++ b/src/project_build.cpp @@ -152,7 +152,7 @@ bool Project::CMakeBuild::is_valid() { auto default_path = get_default_path(); if(default_path.empty()) return true; - std::ifstream input((default_path / "CMakeCache.txt").string(), std::ofstream::binary); + std::ifstream input((default_path / "CMakeCache.txt").string(), std::ios::binary); if(!input) return true; std::string line; diff --git a/src/source_base.cpp b/src/source_base.cpp index 948513d..7ef914c 100644 --- a/src/source_base.cpp +++ b/src/source_base.cpp @@ -409,7 +409,7 @@ bool Source::BaseView::load(bool not_undoable_action) { } } catch(const Glib::Error &error) { - Terminal::get().print("\e[31mError\e[m: Could not read file " + filesystem::get_short_path(file_path).string() + ": " + error.what() + '\n', true); + Terminal::get().print("\e[31mError\e[m: could not read file " + filesystem::get_short_path(file_path).string() + ": " + error.what() + '\n', true); return false; } } @@ -479,7 +479,7 @@ void Source::BaseView::replace_text(const std::string &new_text) { } } catch(...) { - Terminal::get().print("\e[31mError\e[m: Could not replace text in buffer\n", true); + Terminal::get().print("\e[31mError\e[m: could not replace text in buffer\n", true); } get_buffer()->end_user_action(); diff --git a/src/source_language_protocol.cpp b/src/source_language_protocol.cpp index b77adfa..e8a99f0 100644 --- a/src/source_language_protocol.cpp +++ b/src/source_language_protocol.cpp @@ -9,6 +9,7 @@ #include "debug_lldb.hpp" #endif #include "config.hpp" +#include "json.hpp" #include "menu.hpp" #include "utility.hpp" #include @@ -16,6 +17,7 @@ #include #include +const std::vector not_string_keys = {"line", "character", "severity", "tags", "isPreferred", "deprecated", "preselect", "insertTextFormat", "insertTextMode", "version"}; const std::string type_coverage_message = "Un-type checked code. Consider adding type annotations."; LanguageProtocol::Offset::Offset(const boost::property_tree::ptree &pt) { @@ -39,9 +41,21 @@ LanguageProtocol::Location::Location(const boost::property_tree::ptree &pt, std: file = std::move(file_); } +LanguageProtocol::Documentation::Documentation(const boost::property_tree::ptree &pt) { + value = pt.get("documentation", ""); + if(value.empty()) { + if(auto documentation_pt = pt.get_child_optional("documentation")) { + value = documentation_pt->get("value", ""); + kind = documentation_pt->get("kind", ""); + } + } + if(value == "null") // Python erroneously returns "null" when a parameter is not documented + value.clear(); +} + LanguageProtocol::Diagnostic::RelatedInformation::RelatedInformation(const boost::property_tree::ptree &pt) : message(pt.get("message")), location(pt.get_child("location")) {} -LanguageProtocol::Diagnostic::Diagnostic(const boost::property_tree::ptree &pt) : message(pt.get("message")), range(pt.get_child("range")), severity(pt.get("severity", 0)), code(pt.get("code", "")) { +LanguageProtocol::Diagnostic::Diagnostic(const boost::property_tree::ptree &pt) : message(pt.get("message")), range(pt.get_child("range")), severity(pt.get("severity", 0)), code(pt.get("code", "")), ptree(pt) { auto related_information_it = pt.get_child("relatedInformation", boost::property_tree::ptree()); for(auto it = related_information_it.begin(); it != related_information_it.end(); ++it) related_informations.emplace_back(it->second); @@ -89,32 +103,6 @@ LanguageProtocol::WorkspaceEdit::WorkspaceEdit(const boost::property_tree::ptree } } -std::string LanguageProtocol::escape_text(std::string text) { - for(size_t c = 0; c < text.size(); ++c) { - if(text[c] == '\n') { - text.replace(c, 1, "\\n"); - ++c; - } - else if(text[c] == '\r') { - text.replace(c, 1, "\\r"); - ++c; - } - else if(text[c] == '\t') { - text.replace(c, 1, "\\t"); - ++c; - } - else if(text[c] == '"') { - text.replace(c, 1, "\\\""); - ++c; - } - else if(text[c] == '\\') { - text.replace(c, 1, "\\\\"); - ++c; - } - } - return text; -} - LanguageProtocol::Client::Client(boost::filesystem::path root_path_, std::string language_id_, const std::string &language_server) : root_path(std::move(root_path_)), language_id(std::move(language_id_)) { process = std::make_unique( filesystem::escape_argument(language_server), root_path.string(), @@ -218,7 +206,7 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang process_id = process->get_id(); } write_request( - nullptr, "initialize", "\"processId\":" + std::to_string(process_id) + ",\"rootUri\":\"" + LanguageProtocol::escape_text(filesystem::get_uri_from_path(root_path)) + R"(","capabilities": { + nullptr, "initialize", "\"processId\":" + std::to_string(process_id) + ",\"rootUri\":\"" + JSON::escape_string(filesystem::get_uri_from_path(root_path)) + R"(","capabilities": { "workspace": { "symbol": { "dynamicRegistration": false } }, @@ -286,6 +274,7 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang } capabilities.hover = capabilities_pt->get("hoverProvider", false); capabilities.completion = static_cast(capabilities_pt->get_child_optional("completionProvider")); + capabilities.completion_resolve = capabilities_pt->get("completionProvider.resolveProvider", false); 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); @@ -302,6 +291,7 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang capabilities.code_action = capabilities_pt->get("codeActionProvider", false); if(!capabilities.code_action) capabilities.code_action = static_cast(capabilities_pt->get_child_optional("codeActionProvider.codeActionKinds")); + capabilities.code_action_resolve = capabilities_pt->get("codeActionProvider.resolveProvider", false); capabilities.execute_command = static_cast(capabilities_pt->get_child_optional("executeCommandProvider")); capabilities.type_coverage = capabilities_pt->get("typeCoverageProvider", false); } @@ -383,7 +373,8 @@ void LanguageProtocol::Client::parse_server_message() { if(Config::get().log.language_server) { std::cout << "language server: "; - boost::property_tree::write_json(std::cout, pt); + JSON::write(std::cout, pt); + std::cout << '\n'; } auto message_id = pt.get_optional("id"); @@ -402,8 +393,10 @@ void LanguageProtocol::Client::parse_server_message() { } } else if(auto error = pt.get_child_optional("error")) { - if(!Config::get().log.language_server) - boost::property_tree::write_json(std::cerr, pt); + if(!Config::get().log.language_server) { + JSON::write(std::cerr, pt); + std::cerr << '\n'; + } if(message_id) { auto id_it = handlers.find(*message_id); if(id_it != handlers.end()) { @@ -472,7 +465,7 @@ void LanguageProtocol::Client::write_request(Source::LanguageProtocolView *view, } }); } - std::string content("{\"jsonrpc\":\"2.0\",\"id\":" + std::to_string(message_id++) + ",\"method\":\"" + method + "\",\"params\":{" + params + "}}"); + std::string content("{\"jsonrpc\":\"2.0\",\"id\":" + std::to_string(message_id++) + ",\"method\":\"" + method + "\"" + (params.empty() ? "" : ",\"params\":{" + params + '}') + '}'); if(Config::get().log.language_server) std::cout << "Language client: " << content << std::endl; if(!process->write("Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content)) { @@ -498,7 +491,7 @@ void LanguageProtocol::Client::write_response(size_t id, const std::string &resu void LanguageProtocol::Client::write_notification(const std::string &method, const std::string ¶ms) { LockGuard lock(read_write_mutex); - std::string content("{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"params\":{" + params + "}}"); + std::string content("{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\"" + (params.empty() ? "" : ",\"params\":{" + params + '}') + '}'); if(Config::get().log.language_server) std::cout << "Language client: " << content << std::endl; process->write("Content-Length: " + std::to_string(content.size()) + "\r\n\r\n" + content); @@ -531,7 +524,8 @@ void LanguageProtocol::Client::handle_server_notification(const std::string &met void LanguageProtocol::Client::handle_server_request(size_t id, const std::string &method, const boost::property_tree::ptree ¶ms) { if(method == "workspace/applyEdit") { std::promise result_processed; - dispatcher->post([&result_processed, params] { + bool applied = true; + dispatcher->post([&result_processed, &applied, params] { ScopeGuard guard({[&result_processed] { result_processed.set_value(); }}); @@ -541,6 +535,7 @@ void LanguageProtocol::Client::handle_server_request(size_t id, const std::strin workspace_edit = LanguageProtocol::WorkspaceEdit(params.get_child("edit"), current_view->file_path); } catch(...) { + applied = false; return; } @@ -559,8 +554,10 @@ void LanguageProtocol::Client::handle_server_request(size_t id, const std::strin } } if(!view) { - if(!Notebook::get().open(document_edit.file)) + if(!Notebook::get().open(document_edit.file)) { + applied = false; return; + } view = Notebook::get().get_current_view(); document_edits_and_views.emplace_back(DocumentEditAndView{&document_edit, view}); } @@ -598,18 +595,22 @@ void LanguageProtocol::Client::handle_server_request(size_t id, const std::strin } buffer->end_user_action(); - if(!view->save()) + if(!view->save()) { + applied = false; return; + } } } }); result_processed.get_future().get(); + write_response(id, std::string("\"applied\":") + (applied ? "true" : "false")); } - write_response(id, ""); + else + write_response(id, ""); // TODO: write error instead on unsupported methods } Source::LanguageProtocolView::LanguageProtocolView(const boost::filesystem::path &file_path, const Glib::RefPtr &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(LanguageProtocol::escape_text(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::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)) { initialize(); } @@ -746,7 +747,7 @@ void Source::LanguageProtocolView::write_notification(const std::string &method) } 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()) + "\"}"); + client->write_notification("textDocument/didOpen", "\"textDocument\":{\"uri\":\"" + uri_escaped + "\",\"version\":" + std::to_string(document_version++) + ",\"languageId\":\"" + language_id + "\",\"text\":\"" + JSON::escape_string(get_buffer()->get_text().raw()) + "\"}"); } void Source::LanguageProtocolView::write_did_change_notification(const std::vector> ¶ms) { @@ -759,7 +760,7 @@ void Source::LanguageProtocolView::rename(const boost::filesystem::path &path) { dispatcher.reset(); Source::DiffView::rename(path); uri = filesystem::get_uri_from_path(path); - uri_escaped = LanguageProtocol::escape_text(uri); + uri_escaped = JSON::escape_string(uri); client = LanguageProtocol::Client::get(file_path, language_id, language_server); initialize(); } @@ -1277,111 +1278,105 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() { return; auto edit_pt = results[index].second.get_child_optional("edit"); - if(!edit_pt) { - - if(capabilities.execute_command) { - auto command_pt = results[index].second.get_child_optional("command"); - if(command_pt) { - std::stringstream ss; - boost::property_tree::write_json(ss, *command_pt, false); - // ss.str() is enclosed in {}, and has ending newline - auto str = ss.str(); - if(str.size() <= 3) - return; - write_request("workspace/executeCommand", str.substr(1, str.size() - 3), [](const boost::property_tree::ptree &result, bool error) { - }); - return; - } + if(capabilities.code_action_resolve && !edit_pt) { + std::promise result_processed; + std::stringstream ss; + for(auto it = results[index].second.begin(); it != results[index].second.end(); ++it) { + ss << (it != results[index].second.begin() ? ",\"" : "\"") << JSON::escape_string(it->first) << "\":"; + JSON::write(ss, it->second, false, not_string_keys); } - - // TODO: disabled since rust-analyzer does not yet support numbers inside "" (boost::property_tree::write_json outputs all values with "") - // std::promise result_processed; - // std::stringstream ss; - // boost::property_tree::write_json(ss, results[index].second, false); - // // ss.str() is enclosed in {}, and has ending newline - // auto str = ss.str(); - // if(str.size() <= 3) - // return; - // write_request("codeAction/resolve", str.substr(1, str.size() - 3), [&result_processed, &edit_pt](const boost::property_tree::ptree &result, bool error) { - // if(!error) { - // auto child = result.get_child_optional("edit"); - // if(child) - // edit_pt = *child; // Make copy, since result will be destroyed at end of callback - // } - // result_processed.set_value(); - // }); - // result_processed.get_future().get(); - - if(!edit_pt) - return; + write_request("codeAction/resolve", ss.str(), [&result_processed, &edit_pt](const boost::property_tree::ptree &result, bool error) { + if(!error) { + auto child = result.get_child_optional("edit"); + if(child) + edit_pt = *child; // Make copy, since result will be destroyed at end of callback + } + result_processed.set_value(); + }); + result_processed.get_future().get(); } - LanguageProtocol::WorkspaceEdit workspace_edit; - try { - workspace_edit = LanguageProtocol::WorkspaceEdit(*edit_pt, file_path); - } - catch(...) { - return; - } + if(edit_pt) { + LanguageProtocol::WorkspaceEdit workspace_edit; + try { + workspace_edit = LanguageProtocol::WorkspaceEdit(*edit_pt, file_path); + } + catch(...) { + return; + } - auto current_view = Notebook::get().get_current_view(); + auto current_view = Notebook::get().get_current_view(); - struct DocumentEditAndView { - LanguageProtocol::TextDocumentEdit *document_edit; - Source::View *view; - }; - std::vector document_edits_and_views; + struct DocumentEditAndView { + LanguageProtocol::TextDocumentEdit *document_edit; + Source::View *view; + }; + std::vector document_edits_and_views; - for(auto &document_edit : workspace_edit.document_edits) { - Source::View *view = nullptr; - for(auto it = views.begin(); it != views.end(); ++it) { - if((*it)->file_path == document_edit.file) { - view = *it; - break; + for(auto &document_edit : workspace_edit.document_edits) { + Source::View *view = nullptr; + for(auto it = views.begin(); it != views.end(); ++it) { + if((*it)->file_path == document_edit.file) { + view = *it; + break; + } } + if(!view) { + if(!Notebook::get().open(document_edit.file)) + return; + view = Notebook::get().get_current_view(); + document_edits_and_views.emplace_back(DocumentEditAndView{&document_edit, view}); + } + else + document_edits_and_views.emplace_back(DocumentEditAndView{&document_edit, view}); } - if(!view) { - if(!Notebook::get().open(document_edit.file)) - return; - view = Notebook::get().get_current_view(); - document_edits_and_views.emplace_back(DocumentEditAndView{&document_edit, view}); - } - else - document_edits_and_views.emplace_back(DocumentEditAndView{&document_edit, view}); - } - if(current_view) - Notebook::get().open(current_view); + if(current_view) + Notebook::get().open(current_view); - for(auto &document_edit_and_view : document_edits_and_views) { - auto document_edit = document_edit_and_view.document_edit; - auto view = document_edit_and_view.view; - auto buffer = view->get_buffer(); - buffer->begin_user_action(); + for(auto &document_edit_and_view : document_edits_and_views) { + auto document_edit = document_edit_and_view.document_edit; + auto view = document_edit_and_view.view; + auto buffer = view->get_buffer(); + buffer->begin_user_action(); - auto end_iter = buffer->end(); - // If entire buffer is replaced - if(document_edit->text_edits.size() == 1 && - document_edit->text_edits[0].range.start.line == 0 && document_edit->text_edits[0].range.start.character == 0 && - (document_edit->text_edits[0].range.end.line > end_iter.get_line() || - (document_edit->text_edits[0].range.end.line == end_iter.get_line() && document_edit->text_edits[0].range.end.character >= get_line_pos(end_iter)))) { - view->replace_text(document_edit->text_edits[0].new_text); - } - else { - for(auto text_edit_it = document_edit->text_edits.rbegin(); text_edit_it != document_edit->text_edits.rend(); ++text_edit_it) { - auto start_iter = view->get_iter_at_line_pos(text_edit_it->range.start.line, text_edit_it->range.start.character); - auto end_iter = view->get_iter_at_line_pos(text_edit_it->range.end.line, text_edit_it->range.end.character); - if(view != current_view) - view->get_buffer()->place_cursor(start_iter); - buffer->erase(start_iter, end_iter); - start_iter = view->get_iter_at_line_pos(text_edit_it->range.start.line, text_edit_it->range.start.character); - buffer->insert(start_iter, text_edit_it->new_text); + auto end_iter = buffer->end(); + // If entire buffer is replaced + if(document_edit->text_edits.size() == 1 && + document_edit->text_edits[0].range.start.line == 0 && document_edit->text_edits[0].range.start.character == 0 && + (document_edit->text_edits[0].range.end.line > end_iter.get_line() || + (document_edit->text_edits[0].range.end.line == end_iter.get_line() && document_edit->text_edits[0].range.end.character >= get_line_pos(end_iter)))) { + view->replace_text(document_edit->text_edits[0].new_text); + } + else { + for(auto text_edit_it = document_edit->text_edits.rbegin(); text_edit_it != document_edit->text_edits.rend(); ++text_edit_it) { + auto start_iter = view->get_iter_at_line_pos(text_edit_it->range.start.line, text_edit_it->range.start.character); + auto end_iter = view->get_iter_at_line_pos(text_edit_it->range.end.line, text_edit_it->range.end.character); + if(view != current_view) + view->get_buffer()->place_cursor(start_iter); + buffer->erase(start_iter, end_iter); + start_iter = view->get_iter_at_line_pos(text_edit_it->range.start.line, text_edit_it->range.start.character); + buffer->insert(start_iter, text_edit_it->new_text); + } } + + buffer->end_user_action(); + if(!view->save()) + return; } + } - buffer->end_user_action(); - if(!view->save()) - return; + if(capabilities.execute_command) { + auto command_pt = results[index].second.get_child_optional("command"); + if(command_pt) { + std::stringstream ss; + for(auto it = command_pt->begin(); it != command_pt->end(); ++it) { + ss << (it != command_pt->begin() ? ",\"" : "\"") << JSON::escape_string(it->first) << "\":"; + JSON::write(ss, it->second, false, not_string_keys); + } + write_request("workspace/executeCommand", ss.str(), [](const boost::property_tree::ptree &result, bool error) { + }); + } } }; hide_tooltips(); @@ -1396,7 +1391,7 @@ void Source::LanguageProtocolView::setup_signals() { 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()) + '"'}}) + "}]"}}); + write_did_change_notification({{"contentChanges", "[{" + to_string({make_range(location, location), {"text", '"' + JSON::escape_string(text.raw()) + '"'}}) + "}]"}}); }, false); @@ -1408,7 +1403,7 @@ void Source::LanguageProtocolView::setup_signals() { } 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()) + '"'}) + "}]"}}); + write_did_change_notification({{"contentChanges", "[{" + to_string({"text", '"' + JSON::escape_string(get_buffer()->get_text().raw()) + '"'}) + "}]"}}); }); } } @@ -1652,20 +1647,9 @@ void Source::LanguageProtocolView::setup_autocomplete() { 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), {}, {}}); + autocomplete_rows.emplace_back(AutocompleteRow{std::move(insert), {}, LanguageProtocol::Documentation(parameter_it->second), {}, {}}); } } parameter_position++; @@ -1710,17 +1694,10 @@ void Source::LanguageProtocolView::setup_autocomplete() { 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", ""); - } - } + LanguageProtocol::Documentation documentation(it->second); boost::property_tree::ptree ptree; - if(detail.empty() && documentation.empty() && (is_incomplete || is_js)) // Workaround for typescript-language-server (is_js) + if(detail.empty() && documentation.value.empty() && (is_incomplete || is_js)) // Workaround for typescript-language-server (is_js) ptree = it->second; std::vector additional_text_edits; @@ -1745,7 +1722,7 @@ void Source::LanguageProtocolView::setup_autocomplete() { 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), std::move(ptree), std::move(additional_text_edits)}); + autocomplete_rows.emplace_back(AutocompleteRow{std::move(insert), std::move(detail), std::move(documentation), std::move(ptree), std::move(additional_text_edits)}); } } } @@ -1756,7 +1733,7 @@ void Source::LanguageProtocolView::setup_autocomplete() { 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, {}, {}, {}}); + autocomplete_rows.emplace_back(AutocompleteRow{snippet.body, {}, LanguageProtocol::Documentation(snippet.description), {}, {}}); } } } @@ -1843,53 +1820,42 @@ void Source::LanguageProtocolView::setup_autocomplete() { autocomplete->set_tooltip_buffer = [this](unsigned int index) -> std::function { auto autocomplete = autocomplete_rows[index]; - if(autocomplete.detail.empty() && autocomplete.documentation.empty() && !autocomplete.ptree.empty()) { - try { - std::stringstream ss; - boost::property_tree::write_json(ss, autocomplete.ptree, false); - // ss.str() is enclosed in {}, and has ending newline - auto str = ss.str(); - if(str.size() <= 3) - return nullptr; - std::promise result_processed; - write_request("completionItem/resolve", str.substr(1, str.size() - 3), [&result_processed, &autocomplete](const boost::property_tree::ptree &result, bool error) { - if(!error) { - autocomplete.detail = result.get("detail", ""); - autocomplete.documentation = result.get("documentation", ""); - if(autocomplete.documentation.empty()) { - if(auto documentation = result.get_child_optional("documentation")) { - autocomplete.kind = documentation->get("kind", ""); - autocomplete.documentation = documentation->get("value", ""); - } - } - } - result_processed.set_value(); - }); - result_processed.get_future().get(); - } - catch(...) { + if(capabilities.completion_resolve && autocomplete.detail.empty() && autocomplete.documentation.value.empty() && !autocomplete.ptree.empty()) { + std::stringstream ss; + for(auto it = autocomplete.ptree.begin(); it != autocomplete.ptree.end(); ++it) { + ss << (it != autocomplete.ptree.begin() ? ",\"" : "\"") << JSON::escape_string(it->first) << "\":"; + JSON::write(ss, it->second, false, not_string_keys); } + std::promise result_processed; + write_request("completionItem/resolve", ss.str(), [&result_processed, &autocomplete](const boost::property_tree::ptree &result, bool error) { + if(!error) { + autocomplete.detail = result.get("detail", ""); + autocomplete.documentation = LanguageProtocol::Documentation(result); + } + result_processed.set_value(); + }); + result_processed.get_future().get(); } - if(autocomplete.detail.empty() && autocomplete.documentation.empty()) + if(autocomplete.detail.empty() && autocomplete.documentation.value.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); + tooltip.insert_docstring(autocomplete.documentation.value); else { if(!autocomplete.detail.empty()) { tooltip.insert_code(autocomplete.detail, language); tooltip.remove_trailing_newlines(); } - if(!autocomplete.documentation.empty()) { + if(!autocomplete.documentation.value.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); + if(autocomplete.documentation.kind == "plaintext" || autocomplete.documentation.kind.empty()) + tooltip.insert_with_links_tagged(autocomplete.documentation.value); + else if(autocomplete.documentation.kind == "markdown") + tooltip.insert_markdown(autocomplete.documentation.value); else - tooltip.insert_code(autocomplete.documentation, autocomplete.kind); + tooltip.insert_code(autocomplete.documentation.value, autocomplete.documentation.kind); } } }; @@ -1903,24 +1869,21 @@ void Source::LanguageProtocolView::update_diagnostics_async(std::vector 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) + '}'; + std::stringstream diagnostics_ss; + for(auto it = diagnostics.begin(); it != diagnostics.end(); ++it) { + if(it != diagnostics.begin()) + diagnostics_ss << ","; + JSON::write(diagnostics_ss, it->ptree, false, not_string_keys); } - if(diagnostics.size() != 1) { // Use diagnostic range if only one diagnostic, otherwise use whole buffer + 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}); + else { 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\"]"}}) + '}'}}; + std::vector> params = {range, {"context", '{' + to_string({{"diagnostics", '[' + diagnostics_ss.str() + ']'}, {"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; diff --git a/src/source_language_protocol.hpp b/src/source_language_protocol.hpp index 722af3b..135190b 100644 --- a/src/source_language_protocol.hpp +++ b/src/source_language_protocol.hpp @@ -57,6 +57,14 @@ namespace LanguageProtocol { } }; + class Documentation { + public: + Documentation(const boost::property_tree::ptree &pt); + Documentation(std::string value) : value(std::move(value)) {} + std::string value; + std::string kind; + }; + class Diagnostic { public: class RelatedInformation { @@ -74,6 +82,7 @@ namespace LanguageProtocol { std::string code; std::vector related_informations; std::map> quickfixes; + boost::property_tree::ptree ptree; }; class TextEdit { @@ -107,6 +116,7 @@ namespace LanguageProtocol { TextDocumentSync text_document_sync = TextDocumentSync::none; bool hover = false; bool completion = false; + bool completion_resolve = false; bool signature_help = false; bool definition = false; bool type_definition = false; @@ -119,13 +129,12 @@ namespace LanguageProtocol { bool document_range_formatting = false; bool rename = false; bool code_action = false; + bool code_action_resolve = false; bool execute_command = false; bool type_coverage = false; bool use_line_index = false; }; - std::string escape_text(std::string text); - class Client { Client(boost::filesystem::path root_path, std::string language_id, const std::string &language_server); boost::filesystem::path root_path; @@ -253,8 +262,7 @@ namespace Source { struct AutocompleteRow { std::string insert; std::string detail; - std::string documentation; - std::string kind; + LanguageProtocol::Documentation documentation; /// CompletionItem for completionItem/resolve boost::property_tree::ptree ptree; std::vector additional_text_edits; diff --git a/src/usages_clang.cpp b/src/usages_clang.cpp index 11150d3..c48e2e4 100644 --- a/src/usages_clang.cpp +++ b/src/usages_clang.cpp @@ -729,7 +729,7 @@ void Usages::Clang::write_cache(const boost::filesystem::path &path, const Clang return; tmp_file /= ("jucipp" + std::to_string(get_current_process_id()) + path_str); - std::ofstream stream(tmp_file.string()); + std::ofstream stream(tmp_file.string(), std::ios::binary); if(stream) { try { boost::archive::text_oarchive text_oarchive(stream); diff --git a/src/window.cpp b/src/window.cpp index 562e0be..03b64b1 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -11,6 +11,7 @@ #include "filesystem.hpp" #include "grep.hpp" #include "info.hpp" +#include "json.hpp" #include "menu.hpp" #include "notebook.hpp" #include "project.hpp" @@ -253,7 +254,7 @@ void Window::configure() { if(scheme) style = scheme->get_style("def:note"); else { - Terminal::get().print("\e[31mError\e[m: Could not find gtksourceview style: " + Config::get().source.style + '\n', true); + Terminal::get().print("\e[31mError\e[m: could not find gtksourceview style: " + Config::get().source.style + '\n', true); } } auto foreground_value = style && style->property_foreground_set() ? style->property_foreground().get_value() : get_style_context()->get_color().to_string(); @@ -385,7 +386,7 @@ void Window::set_menu_actions() { Terminal::get().print(" \e[32mcreated\e[m\n"); } else - Terminal::get().print("\e[31mError\e[m: Could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); + Terminal::get().print("\e[31mError\e[m: could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); } }); menu.add_action("file_new_project_cpp", []() { @@ -442,7 +443,7 @@ void Window::set_menu_actions() { Terminal::get().print(" \e[32mcreated\e[m\n"); } else - Terminal::get().print("\e[31mError\e[m: Could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); + Terminal::get().print("\e[31mError\e[m: could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); } }); menu.add_action("file_new_project_rust", []() { @@ -471,7 +472,7 @@ void Window::set_menu_actions() { Terminal::get().print(" \e[32mcreated\e[m\n"); } else - Terminal::get().print("\e[31mError\e[m: Could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); + Terminal::get().print("\e[31mError\e[m: could not create project " + filesystem::get_short_path(project_path).string() + "\n", true); } }); @@ -527,7 +528,7 @@ void Window::set_menu_actions() { if(auto view = Notebook::get().get_current_view()) { auto path = Dialog::save_file_as(view->file_path); if(!path.empty()) { - std::ofstream file(path, std::ofstream::binary); + std::ofstream file(path, std::ios::binary); if(file) { file << view->get_buffer()->get_text().raw(); file.close(); @@ -2318,7 +2319,14 @@ void Window::save_session() { window_pt.put("height", height); root_pt.add_child("window", window_pt); - boost::property_tree::write_json((Config::get().home_juci_path / "last_session.json").string(), root_pt); + auto path = Config::get().home_juci_path / "last_session.json"; + std::ofstream output(path.string(), std::ios::binary); + if(output) { + JSON::write(output, root_pt); + output << '\n'; + } + else + Terminal::get().print("\e[31mError\e[m: could not write session file: " + filesystem::get_short_path(path).string() + "\n", true); } catch(...) { } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 59a3867..08fcbf8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -90,7 +90,11 @@ if(BUILD_TESTING) add_test(language_protocol_client_test language_protocol_client_test) add_executable(language_protocol_server_test language_protocol_server_test.cpp) - target_link_libraries(language_protocol_server_test Boost::filesystem) + target_link_libraries(language_protocol_server_test juci_shared) + + add_executable(json_test json_test.cpp $) + target_link_libraries(json_test juci_shared) + add_test(json_test json_test) endif() if(BUILD_FUZZING) diff --git a/tests/json_test.cpp b/tests/json_test.cpp new file mode 100644 index 0000000..92057ef --- /dev/null +++ b/tests/json_test.cpp @@ -0,0 +1,58 @@ +#include "json.hpp" +#include +#include +#include + +int main() { + std::string json = R"({ + "integer": 3, + "integer_as_string": "3", + "string": "some\ntext", + "string2": "1test", + "array": [ + 1, + 3, + 3.14 + ], + "array_with_strings": [ + "a", + "b", + "c" + ], + "object": { + "integer": 3, + "string": "some\ntext", + "array": [ + 1, + 3, + 3.14 + ] + } +})"; + { + std::istringstream istream(json); + + boost::property_tree::ptree pt; + boost::property_tree::read_json(istream, pt); + + std::ostringstream ostream; + JSON::write(ostream, pt, true, {"integer", "array"}); + g_assert(ostream.str() == json); + } + { + std::istringstream istream(json); + + boost::property_tree::ptree pt; + boost::property_tree::read_json(istream, pt); + + std::ostringstream ostream; + JSON::write(ostream, pt, false, {"integer", "array"}); + + std::string non_pretty; + for(auto &chr : json) { + if(chr != ' ' && chr != '\n') + non_pretty += chr; + } + g_assert(ostream.str() == non_pretty); + } +} diff --git a/tests/language_protocol_server_test.cpp b/tests/language_protocol_server_test.cpp index b3535d2..ef3172d 100644 --- a/tests/language_protocol_server_test.cpp +++ b/tests/language_protocol_server_test.cpp @@ -1,5 +1,5 @@ +#include "json.hpp" #include -#include #include #ifdef _WIN32 @@ -7,32 +7,6 @@ #include #endif -std::string escape_text(std::string text) { - for(size_t c = 0; c < text.size(); ++c) { - if(text[c] == '\n') { - text.replace(c, 1, "\\n"); - ++c; - } - else if(text[c] == '\r') { - text.replace(c, 1, "\\r"); - ++c; - } - else if(text[c] == '\t') { - text.replace(c, 1, "\\t"); - ++c; - } - else if(text[c] == '"') { - text.replace(c, 1, "\\\""); - ++c; - } - else if(text[c] == '\\') { - text.replace(c, 1, "\\\\"); - ++c; - } - } - return text; -} - int main() { #ifdef _WIN32 _setmode(_fileno(stdout), _O_BINARY); @@ -472,7 +446,7 @@ int main() { "id": "5", "result": [ { - "uri": "file://)" + escape_text(file_path.string()) + + "uri": "file://)" + JSON::escape_string(file_path.string()) + R"(", "range": { "start": { @@ -486,7 +460,7 @@ int main() { } }, { - "uri": "file://)" + escape_text(file_path.string()) + + "uri": "file://)" + JSON::escape_string(file_path.string()) + R"(", "range": { "start": {