Browse Source

Added JSON::write_json, and some various cleanup

pipelines/353213535
eidheim 5 years ago
parent
commit
c7340b709d
  1. 1
      src/CMakeLists.txt
  2. 2
      src/cmake.cpp
  3. 13
      src/config.cpp
  4. 5
      src/files.hpp
  5. 4
      src/filesystem.cpp
  6. 101
      src/json.cpp
  7. 19
      src/json.hpp
  8. 2
      src/meson.cpp
  9. 2
      src/project.cpp
  10. 2
      src/project_build.cpp
  11. 4
      src/source_base.cpp
  12. 239
      src/source_language_protocol.cpp
  13. 16
      src/source_language_protocol.hpp
  14. 2
      src/usages_clang.cpp
  15. 20
      src/window.cpp
  16. 6
      tests/CMakeLists.txt
  17. 58
      tests/json_test.cpp
  18. 32
      tests/language_protocol_server_test.cpp

1
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

2
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)) {

13
src/config.cpp

@ -1,6 +1,7 @@
#include "config.hpp"
#include "files.hpp"
#include "filesystem.hpp"
#include "json.hpp"
#include "terminal.hpp"
#include <algorithm>
#include <exception>
@ -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) {

5
src/files.hpp

@ -4,8 +4,7 @@
/// 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({
const std::string default_config_file = R"RAW({
"version": ")RAW" +
std::string(JUCI_VERSION) +
R"RAW(",
@ -47,7 +46,7 @@ const std::string default_config_file =
"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",
"map_font_size": 1,
"show_git_diff": true,
"show_background_pattern": true,
"show_right_margin": false,

4
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

101
src/json.cpp

@ -0,0 +1,101 @@
#include "json.hpp"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>
void JSON::write_json_internal(std::ostream &stream, const boost::property_tree::ptree &pt, bool pretty, const std::vector<std::string> &not_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<std::string>();
if(std::any_of(not_string_keys.begin(), not_string_keys.end(), [&key](const std::string &not_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<std::string> &not_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;
}

19
src/json.hpp

@ -0,0 +1,19 @@
#pragma once
#include <boost/filesystem.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <ostream>
class JSON {
static void write_json_internal(std::ostream &stream, const boost::property_tree::ptree &pt, bool pretty, const std::vector<std::string> &not_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<std::string> &not_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);
};

2
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)) {

2
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))

2
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;

4
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();

239
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 <future>
@ -16,6 +17,7 @@
#include <regex>
#include <unordered_map>
const std::vector<std::string> 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<std::string>("documentation", "");
if(value.empty()) {
if(auto documentation_pt = pt.get_child_optional("documentation")) {
value = documentation_pt->get<std::string>("value", "");
kind = documentation_pt->get<std::string>("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<std::string>("message")), location(pt.get_child("location")) {}
LanguageProtocol::Diagnostic::Diagnostic(const boost::property_tree::ptree &pt) : message(pt.get<std::string>("message")), range(pt.get_child("range")), severity(pt.get<int>("severity", 0)), code(pt.get<std::string>("code", "")) {
LanguageProtocol::Diagnostic::Diagnostic(const boost::property_tree::ptree &pt) : message(pt.get<std::string>("message")), range(pt.get_child("range")), severity(pt.get<int>("severity", 0)), code(pt.get<std::string>("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<TinyProcessLib::Process>(
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<bool>("hoverProvider", false);
capabilities.completion = static_cast<bool>(capabilities_pt->get_child_optional("completionProvider"));
capabilities.completion_resolve = capabilities_pt->get<bool>("completionProvider.resolveProvider", false);
capabilities.signature_help = static_cast<bool>(capabilities_pt->get_child_optional("signatureHelpProvider"));
capabilities.definition = capabilities_pt->get<bool>("definitionProvider", false);
capabilities.type_definition = capabilities_pt->get<bool>("typeDefinitionProvider", false);
@ -302,6 +291,7 @@ LanguageProtocol::Capabilities LanguageProtocol::Client::initialize(Source::Lang
capabilities.code_action = capabilities_pt->get<bool>("codeActionProvider", false);
if(!capabilities.code_action)
capabilities.code_action = static_cast<bool>(capabilities_pt->get_child_optional("codeActionProvider.codeActionKinds"));
capabilities.code_action_resolve = capabilities_pt->get<bool>("codeActionProvider.resolveProvider", false);
capabilities.execute_command = static_cast<bool>(capabilities_pt->get_child_optional("executeCommandProvider"));
capabilities.type_coverage = capabilities_pt->get<bool>("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<size_t>("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 &params) {
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 &params) {
if(method == "workspace/applyEdit") {
std::promise<void> 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<Gsv::Language> &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<std::pair<std::string, std::string>> &params) {
@ -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,45 +1278,25 @@ 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) {
if(capabilities.code_action_resolve && !edit_pt) {
std::promise<void> result_processed;
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;
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);
}
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
}
// TODO: disabled since rust-analyzer does not yet support numbers inside "" (boost::property_tree::write_json outputs all values with "")
// std::promise<void> 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;
result_processed.set_value();
});
result_processed.get_future().get();
}
if(edit_pt) {
LanguageProtocol::WorkspaceEdit workspace_edit;
try {
workspace_edit = LanguageProtocol::WorkspaceEdit(*edit_pt, file_path);
@ -1383,6 +1364,20 @@ void Source::LanguageProtocolView::setup_navigation_and_refactoring() {
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();
SelectionDialog::get()->show();
@ -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<int, int> 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<std::string>("label", "");
auto insert = label;
auto documentation = parameter_it->second.get<std::string>("documentation", "");
std::string kind;
if(documentation.empty()) {
auto documentation_pt = parameter_it->second.get_child_optional("documentation");
if(documentation_pt) {
documentation = documentation_pt->get<std::string>("value", "");
kind = documentation_pt->get<std::string>("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<std::string>("label", "");
if(starts_with(label, prefix)) {
auto detail = it->second.get<std::string>("detail", "");
auto documentation = it->second.get<std::string>("documentation", "");
std::string documentation_kind;
if(documentation.empty()) {
if(auto documentation_pt = it->second.get_child_optional("documentation")) {
documentation = documentation_pt->get<std::string>("value", "");
documentation_kind = documentation_pt->get<std::string>("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<LanguageProtocol::TextEdit> 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<void(Tooltip & tooltip)> {
auto autocomplete = autocomplete_rows[index];
if(autocomplete.detail.empty() && autocomplete.documentation.empty() && !autocomplete.ptree.empty()) {
try {
if(capabilities.completion_resolve && autocomplete.detail.empty() && autocomplete.documentation.value.empty() && !autocomplete.ptree.empty()) {
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;
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<void> result_processed;
write_request("completionItem/resolve", str.substr(1, str.size() - 3), [&result_processed, &autocomplete](const boost::property_tree::ptree &result, bool error) {
write_request("completionItem/resolve", ss.str(), [&result_processed, &autocomplete](const boost::property_tree::ptree &result, bool error) {
if(!error) {
autocomplete.detail = result.get<std::string>("detail", "");
autocomplete.documentation = result.get<std::string>("documentation", "");
if(autocomplete.documentation.empty()) {
if(auto documentation = result.get_child_optional("documentation")) {
autocomplete.kind = documentation->get<std::string>("kind", "");
autocomplete.documentation = documentation->get<std::string>("value", "");
}
}
autocomplete.documentation = LanguageProtocol::Documentation(result);
}
result_processed.set_value();
});
result_processed.get_future().get();
}
catch(...) {
}
}
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<Language
dispatcher.post([this, diagnostics = std::move(diagnostics), last_count]() mutable {
if(last_count != update_diagnostics_async_count)
return;
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);
}
std::pair<std::string, std::string> 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<std::pair<std::string, std::string>> diagnostic_params = {range};
diagnostic_params.emplace_back("message", '"' + LanguageProtocol::escape_text(diagnostic.message) + '"');
if(diagnostic.severity != 0)
diagnostic_params.emplace_back("severity", std::to_string(diagnostic.severity));
if(!diagnostic.code.empty())
diagnostic_params.emplace_back("code", '"' + diagnostic.code + '"');
diagnostics_string += (diagnostics_string.empty() ? "{" : ",{") + to_string(diagnostic_params) + '}';
}
if(diagnostics.size() != 1) { // Use diagnostic range if only one diagnostic, otherwise use whole buffer
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<std::pair<std::string, std::string>> params = {range, {"context", '{' + to_string({{"diagnostics", '[' + diagnostics_string + ']'}, {"only", "[\"quickfix\"]"}}) + '}'}};
std::vector<std::pair<std::string, std::string>> 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;

16
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<RelatedInformation> related_informations;
std::map<std::string, std::set<Source::FixIt>> 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<LanguageProtocol::TextEdit> additional_text_edits;

2
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);

20
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(...) {
}

6
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_OBJECTS:test_stubs>)
target_link_libraries(json_test juci_shared)
add_test(json_test json_test)
endif()
if(BUILD_FUZZING)

58
tests/json_test.cpp

@ -0,0 +1,58 @@
#include "json.hpp"
#include <glib.h>
#include <iostream>
#include <sstream>
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);
}
}

32
tests/language_protocol_server_test.cpp

@ -1,5 +1,5 @@
#include "json.hpp"
#include <boost/filesystem.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <iostream>
#ifdef _WIN32
@ -7,32 +7,6 @@
#include <io.h>
#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": {

Loading…
Cancel
Save