diff --git a/.github/workflows/linux-build-clang.yaml b/.github/workflows/linux-build-clang.yaml index c35de0dafb..9a3ec6e577 100644 --- a/.github/workflows/linux-build-clang.yaml +++ b/.github/workflows/linux-build-clang.yaml @@ -63,3 +63,4 @@ jobs: ./build/goalc/goalc ./build/decompiler/extractor ./build/game/gk + ./build/lsp/lsp diff --git a/.github/workflows/release-pipeline.yaml b/.github/workflows/release-pipeline.yaml index 26871ebe27..644af0cfa7 100644 --- a/.github/workflows/release-pipeline.yaml +++ b/.github/workflows/release-pipeline.yaml @@ -46,20 +46,24 @@ jobs: - name: Display structure of downloaded files run: ls -Rl ./ci-artifacts/ - - name: Prepare Linux Release Asset + - name: Prepare Linux Release Assets run: | mkdir -p ./ci-artifacts/linux ./.github/scripts/releases/extract_build_linux.sh ./ci-artifacts/linux ./ci-artifacts/opengoal-linux-static ./ - cd ci-artifacts/linux + pushd ci-artifacts/linux TAG_VAL=$(echo ${{ github.REF }} | awk -F'refs/tags/' '{print $2}') tar czf ../final/opengoal-linux-${TAG_VAL}.tar.gz . + popd + strip ./ci-artifacts/opengoal-linux-static/lsp/lsp + cp ./ci-artifacts/opengoal-linux-static/lsp/lsp ./ci-artifacts/final/opengoal-lsp-linux-${TAG_VAL}.bin - - name: Prepare Windows Build Asset + - name: Prepare Windows Build Assets run: | mkdir -p ./ci-artifacts/windows ./.github/scripts/releases/extract_build_windows.sh ./ci-artifacts/windows ./ci-artifacts/opengoal-windows-static ./ TAG_VAL=$(echo ${{ github.REF }} | awk -F'refs/tags/' '{print $2}') 7z a -tzip ./ci-artifacts/final/opengoal-windows-${TAG_VAL}.zip ./ci-artifacts/windows/* + cp ./ci-artifacts/opengoal-windows-static/lsp.exe ./ci-artifacts/final/opengoal-lsp-windows-${TAG_VAL}.exe - name: Upload Assets env: diff --git a/.vs/launch.vs.json b/.vs/launch.vs.json index b94ab2bb1b..24b20316cc 100644 --- a/.vs/launch.vs.json +++ b/.vs/launch.vs.json @@ -170,6 +170,13 @@ "projectTarget": "extractor.exe (bin\\extractor.exe)", "name": "Tools - Extractor - Full", "args": ["\"E:\\ISOs\\Jak\\Jak 1.iso\""] + }, + { + "type" : "default", + "project" : "CMakeLists.txt", + "projectTarget" : "lsp.exe (bin\\lsp.exe)", + "name" : "Run - LSP", + "args" : [] } ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a54ca9640..e430f3de55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,6 +153,9 @@ add_subdirectory(decompiler) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${THIRDPARTY_IGNORED_WARNINGS} ") add_subdirectory(third-party/cubeb) +# build LSP +add_subdirectory(lsp) + # build glfw library add_subdirectory(third-party/glfw) add_subdirectory(third-party/zstd) diff --git a/common/goos/TextDB.cpp b/common/goos/TextDB.cpp index 03ca3a2e0c..4ac1f2ea6c 100644 --- a/common/goos/TextDB.cpp +++ b/common/goos/TextDB.cpp @@ -137,6 +137,19 @@ std::string TextDb::get_info_for(const Object& o, bool* terminate_compiler_error } } +std::optional TextDb::get_short_info_for(const Object& o) const { + if (o.is_pair()) { + auto kv = m_map.find(o.heap_obj); + if (kv != m_map.end()) { + return get_short_info_for(kv->second.frag, kv->second.offset); + } else { + return {}; + } + } else { + return {}; + } +} + /*! * Given a source text and an offset, print a description of where it is. */ @@ -151,6 +164,17 @@ std::string TextDb::get_info_for(const std::shared_ptr& frag, int of return result + pointer; } +std::optional TextDb::get_short_info_for(const std::shared_ptr& frag, + int offset) const { + int line_idx = frag->get_line_idx(offset); + int offset_in_line = std::max(offset - frag->get_offset_of_line(line_idx), 1) - 1; + ShortInfo info_result; + info_result.filename = frag->get_description(); + info_result.line_idx_to_display = line_idx; + info_result.pos_in_line = offset_in_line; + return std::make_optional(info_result); +} + std::optional TextDb::try_get_short_info(const Object& o) const { if (o.is_pair()) { auto it = m_map.find(o.heap_obj); diff --git a/common/goos/TextDB.h b/common/goos/TextDB.h index 735b80ac61..258b314772 100644 --- a/common/goos/TextDB.h +++ b/common/goos/TextDB.h @@ -107,7 +107,10 @@ class TextDb { void insert(const std::shared_ptr& frag); void link(const Object& o, std::shared_ptr frag, int offset); std::string get_info_for(const Object& o, bool* terminate_compiler_error = nullptr) const; + std::optional get_short_info_for(const Object& o) const; std::string get_info_for(const std::shared_ptr& frag, int offset) const; + std::optional get_short_info_for(const std::shared_ptr& frag, + int offset) const; std::optional try_get_short_info(const Object& o) const; bool has_info(const Object& o) const; void inherit_info(const Object& parent, const Object& child); diff --git a/common/log/log.h b/common/log/log.h index 225ddcbd59..b26d0dd230 100644 --- a/common/log/log.h +++ b/common/log/log.h @@ -22,7 +22,7 @@ struct LogTime { #endif // Logging API -enum class level { trace = 0, debug = 1, info = 2, warn = 3, error = 4, die = 5 }; +enum class level { trace = 0, debug = 1, info = 2, warn = 3, error = 4, die = 5, off = 6 }; namespace internal { // log implementation stuff, not to be called by the user diff --git a/common/type_system/Type.h b/common/type_system/Type.h index f36daefea8..83784736df 100644 --- a/common/type_system/Type.h +++ b/common/type_system/Type.h @@ -123,6 +123,13 @@ class Type { std::string m_runtime_name; bool m_is_boxed = false; // does this have runtime type information? int m_heap_base = 0; + + // definition information + // TODO - LSP - .gc support + /*std::string m_defining_file; + int m_line_number; + int m_line_offset; + void update_definition_meta(const std::string& defining_file, int line_number, int line_offset);*/ }; /*! diff --git a/common/type_system/TypeSpec.cpp b/common/type_system/TypeSpec.cpp index 6932c86f86..c20bda33de 100644 --- a/common/type_system/TypeSpec.cpp +++ b/common/type_system/TypeSpec.cpp @@ -130,4 +130,4 @@ void TypeSpec::add_or_modify_tag(const std::string& tag_name, const std::string& } } m_tags.push_back({tag_name, tag_value}); -} \ No newline at end of file +} diff --git a/common/versions.cpp b/common/versions.cpp index ec3aac7e3a..1608a535ce 100644 --- a/common/versions.cpp +++ b/common/versions.cpp @@ -12,4 +12,8 @@ GameVersion game_name_to_version(const std::string& name) { } else { ASSERT_MSG(false, fmt::format("invalid game name: {}", name)); } -} \ No newline at end of file +} + +bool valid_game_version(const std::string& name) { + return name == "jak1" || name == "jak2"; +} diff --git a/common/versions.h b/common/versions.h index 1345dc43f3..381816d208 100644 --- a/common/versions.h +++ b/common/versions.h @@ -52,4 +52,5 @@ struct PerGameVersion { constexpr PerGameVersion game_version_names = {"jak1", "jak2"}; -GameVersion game_name_to_version(const std::string& name); \ No newline at end of file +GameVersion game_name_to_version(const std::string& name); +bool valid_game_version(const std::string& name); diff --git a/decompiler/Function/Function.cpp b/decompiler/Function/Function.cpp index f2f4f4f5db..86e7a9c14a 100644 --- a/decompiler/Function/Function.cpp +++ b/decompiler/Function/Function.cpp @@ -476,7 +476,8 @@ void Function::find_global_function_defs(LinkedObjectFile& file, DecompilerTypeS auto& func = file.get_function_at_label(label_id); ASSERT(func.guessed_name.empty()); func.guessed_name.set_as_global(name); - dts.add_symbol(name, "function"); + // TODO - get definition info? + dts.add_symbol(name, "function", {}); ; // todo - inform function. } diff --git a/decompiler/ObjectFile/LinkedObjectFileCreation.cpp b/decompiler/ObjectFile/LinkedObjectFileCreation.cpp index 97c25b726e..f95d717482 100644 --- a/decompiler/ObjectFile/LinkedObjectFileCreation.cpp +++ b/decompiler/ObjectFile/LinkedObjectFileCreation.cpp @@ -125,7 +125,7 @@ static uint32_t c_symlink2(LinkedObjectFile& f, word_kind = LinkedWord::EMPTY_PTR; break; case SymbolLinkKind::TYPE: - dts.add_symbol(name, "type"); + dts.add_symbol(name, "type", {}); word_kind = LinkedWord::TYPE_PTR; break; default: @@ -183,7 +183,7 @@ static uint32_t c_symlink3(LinkedObjectFile& f, word_kind = LinkedWord::EMPTY_PTR; break; case SymbolLinkKind::TYPE: - dts.add_symbol(name, "type"); + dts.add_symbol(name, "type", {}); word_kind = LinkedWord::TYPE_PTR; break; default: diff --git a/decompiler/ObjectFile/ObjectFileDB_IR2.cpp b/decompiler/ObjectFile/ObjectFileDB_IR2.cpp index 16a40863aa..bf3ebe8c78 100644 --- a/decompiler/ObjectFile/ObjectFileDB_IR2.cpp +++ b/decompiler/ObjectFile/ObjectFileDB_IR2.cpp @@ -693,8 +693,6 @@ void ObjectFileDB::ir2_write_results(const fs::path& output_dir, const std::vector& imports, ObjectFileData& obj) { if (obj.linked_data.has_any_functions()) { - // todo - auto file_text = ir2_to_file(obj, config); auto file_name = output_dir / (obj.to_unique_name() + "_ir2.asm"); file_util::write_text_file(file_name, file_text); @@ -708,6 +706,11 @@ void ObjectFileDB::ir2_write_results(const fs::path& output_dir, std::string ObjectFileDB::ir2_to_file(ObjectFileData& data, const Config& config) { std::string result; + auto all_types_path = file_util::get_file_path({config.all_types_file}); + auto game_version = game_version_names[config.game_version]; + + result += fmt::format("; ALL_TYPES={}={}\n\n", game_version, all_types_path); + const char* segment_names[] = {"main segment", "debug segment", "top-level segment"}; ASSERT(data.linked_data.segments <= 3); for (int seg = data.linked_data.segments; seg-- > 0;) { diff --git a/decompiler/util/DecompilerTypeSystem.cpp b/decompiler/util/DecompilerTypeSystem.cpp index f1508acf57..38b67513dc 100644 --- a/decompiler/util/DecompilerTypeSystem.cpp +++ b/decompiler/util/DecompilerTypeSystem.cpp @@ -61,16 +61,19 @@ void DecompilerTypeSystem::parse_type_defs(const std::vector& file_ if (!cdr(*rest).is_empty_list()) { throw std::runtime_error("malformed define-extern"); } - add_symbol(sym_name.as_symbol()->name, parse_typespec(&ts, sym_type)); + auto info = m_reader.db.get_short_info_for(o); + add_symbol(sym_name.as_symbol()->name, parse_typespec(&ts, sym_type), info); } else if (car(o).as_symbol()->name == "deftype") { auto dtr = parse_deftype(cdr(o), &ts); + auto info = m_reader.db.get_short_info_for(o); if (dtr.create_runtime_type) { - add_symbol(dtr.type.base_type(), "type"); + add_symbol(dtr.type.base_type(), "type", info); } // declare the type's states globally for (auto& state : dtr.type_info->get_states_declared_for_type()) { - add_symbol(state.first, state.second); + // TODO - get definition info for the state definitions specifically + add_symbol(state.first, state.second, info); } } else if (car(o).as_symbol()->name == "declare-type") { @@ -158,11 +161,17 @@ bool DecompilerTypeSystem::lookup_flags(const std::string& type, u64* dest) cons return false; } -void DecompilerTypeSystem::add_symbol(const std::string& name, const TypeSpec& type_spec) { +void DecompilerTypeSystem::add_symbol( + const std::string& name, + const TypeSpec& type_spec, + const std::optional& definition_info) { add_symbol(name); auto skv = symbol_types.find(name); if (skv == symbol_types.end() || skv->second == type_spec) { symbol_types[name] = type_spec; + if (definition_info) { + symbol_definition_info[name] = definition_info.value(); + } } else { if (ts.tc(type_spec, skv->second)) { } else { diff --git a/decompiler/util/DecompilerTypeSystem.h b/decompiler/util/DecompilerTypeSystem.h index c9da04a0fa..76f02d5034 100644 --- a/decompiler/util/DecompilerTypeSystem.h +++ b/decompiler/util/DecompilerTypeSystem.h @@ -2,6 +2,7 @@ #include "common/goos/Reader.h" #include "common/type_system/TypeSystem.h" +#include #include "decompiler/Disasm/Register.h" @@ -15,6 +16,7 @@ class DecompilerTypeSystem { TypeSystem ts; std::unordered_map symbol_types; std::unordered_set symbols; + std::unordered_map symbol_definition_info; std::vector symbol_add_order; std::unordered_map type_flags; std::unordered_map type_parents; @@ -30,11 +32,15 @@ class DecompilerTypeSystem { } } - void add_symbol(const std::string& name, const std::string& base_type) { - add_symbol(name, TypeSpec(base_type)); + void add_symbol(const std::string& name, + const std::string& base_type, + const std::optional& definition_info) { + add_symbol(name, TypeSpec(base_type), definition_info); } - void add_symbol(const std::string& name, const TypeSpec& type_spec); + void add_symbol(const std::string& name, + const TypeSpec& type_spec, + const std::optional& definition_info); void parse_type_defs(const std::vector& file_path); TypeSpec parse_type_spec(const std::string& str) const; void add_type_flags(const std::string& name, u64 flags); diff --git a/lsp/CMakeLists.txt b/lsp/CMakeLists.txt new file mode 100644 index 0000000000..34b4d9e49f --- /dev/null +++ b/lsp/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(lsp + main.cpp + transport/stdio.cpp + state/workspace.cpp + handlers/lsp_router.cpp + protocol/common_types.cpp + protocol/document_symbols.cpp + protocol/document_synchronization.cpp + protocol/document_diagnostics.cpp + protocol/hover.cpp + state/data/mips_instruction.cpp) + +target_link_libraries(lsp common decomp) + diff --git a/lsp/handlers/initialize.h b/lsp/handlers/initialize.h new file mode 100644 index 0000000000..d48c51f622 --- /dev/null +++ b/lsp/handlers/initialize.h @@ -0,0 +1,12 @@ +#include "common/log/log.h" + +#include "lsp/protocol/initialize_result.h" + +#include "third-party/json.hpp" + +using json = nlohmann::json; + +std::optional initialize_handler(Workspace& workspace, int id, json params) { + InitializeResult result; + return result.to_json(); +} diff --git a/lsp/handlers/lsp_router.cpp b/lsp/handlers/lsp_router.cpp new file mode 100644 index 0000000000..ed645790ee --- /dev/null +++ b/lsp/handlers/lsp_router.cpp @@ -0,0 +1,138 @@ +#include "lsp_router.h" + +#include "lsp/handlers/initialize.h" + +#include "common/log/log.h" + +#include "lsp/protocol/error_codes.h" +#include "text_document/document_symbol.h" +#include "text_document/document_synchronization.h" +#include "text_document/go_to.h" +#include "text_document/hover.h" + +#include "third-party/fmt/core.h" + +LSPRoute::LSPRoute() : m_route_type(LSPRouteType::NOOP) {} + +LSPRoute::LSPRoute(std::function notification_handler) + : m_route_type(LSPRouteType::NOTIFICATION), m_notification_handler(notification_handler) {} + +LSPRoute::LSPRoute(std::function notification_handler, + std::function(Workspace&, json)> post_notification_publish) + : m_route_type(LSPRouteType::NOTIFICATION), + m_notification_handler(notification_handler), + m_post_notification_publish(post_notification_publish) {} + +LSPRoute::LSPRoute(std::function(Workspace&, int, json)> request_handler) + : m_route_type(LSPRouteType::REQUEST_RESPONSE), m_request_handler(request_handler) {} + +void LSPRouter::init_routes() { + m_routes["initialize"] = LSPRoute(initialize_handler); + m_routes["initialize"].m_generic_post_action = [](Workspace& workspace) { + workspace.set_initialized(true); + }; + m_routes["initialized"] = LSPRoute(); + m_routes["textDocument/documentSymbol"] = LSPRoute(document_symbols_handler); + m_routes["textDocument/didOpen"] = LSPRoute(did_open_handler, did_open_push_diagnostics); + m_routes["textDocument/didChange"] = LSPRoute(did_change_handler, did_change_push_diagnostics); + m_routes["textDocument/didClose"] = LSPRoute(did_close_handler); + m_routes["textDocument/hover"] = LSPRoute(hover_handler); + m_routes["textDocument/definition"] = LSPRoute(go_to_definition_handler); +} + +json error_resp(ErrorCodes error_code, const std::string& error_message) { + json error{ + {"code", static_cast(error_code)}, + {"message", error_message}, + }; + return json{{"error", error}}; +} + +std::string LSPRouter::make_response(const json& result) { + json content = result; + content["jsonrpc"] = "2.0"; + + std::string header; + header.append("Content-Length: " + std::to_string(content.dump().size()) + "\r\n"); + header.append("Content-Type: application/vscode-jsonrpc;charset=utf-8\r\n"); + header.append("\r\n"); + return header + content.dump(); +} + +std::optional> LSPRouter::route_message( + const MessageBuffer& message_buffer, + AppState& appstate) { + const json& body = message_buffer.body(); + auto method = body["method"]; + lg::info(method); + + // If the workspace has not yet been initialized but the client sends a + // message that doesn't have method "initialize" then we'll return an error + // as per LSP spec. + if (method != "initialize" && !appstate.workspace.is_initialized()) { + auto error = { + make_response(error_resp(ErrorCodes::ServerNotInitialized, "Server not yet initialized."))}; + return std::make_optional(error); + } + + // Exit early if we can't handle the route + if (m_routes.count(method) == 0) { + lg::warn("Method not supported '{}'", method); + auto error = {make_response( + error_resp(ErrorCodes::MethodNotFound, fmt::format("Method '{}' not supported", method)))}; + return std::make_optional(error); + } + + try { + auto route = m_routes[method]; + std::vector resp_bodies; + // Handle the request/notificiation + switch (route.m_route_type) { + case LSPRouteType::NOOP: + break; + case LSPRouteType::NOTIFICATION: + route.m_notification_handler(appstate.workspace, body["params"]); + break; + case LSPRouteType::REQUEST_RESPONSE: + auto resp_body = route.m_request_handler(appstate.workspace, body["id"], body["params"]); + if (resp_body) { + json resp; + // TODO - this should be the job of the handler, not here! + resp["id"] = body["id"]; + resp["result"] = resp_body.value(); + resp_bodies.push_back(resp); + } + break; + } + + // Run any publish we need to do after the fact + if (route.m_post_notification_publish) { + auto resp = route.m_post_notification_publish.value()(appstate.workspace, body["params"]); + if (resp) { + lg::info("adding publish resp"); + resp_bodies.push_back(resp.value()); + } + } + + // Run any generic post action + if (route.m_generic_post_action) { + route.m_generic_post_action.value()(appstate.workspace); + } + + // Serialize all payloads with headers + std::vector resps; + for (const auto& body : resp_bodies) { + resps.push_back(make_response(body)); + } + + // Return + if (resps.empty()) { + return {}; + } + return resps; + } catch (std::exception& e) { + lg::error("Unexpected exception occurred - {} | {}", e.what(), body.dump()); + // TODO - return an error with the message + return {}; + } +} diff --git a/lsp/handlers/lsp_router.h b/lsp/handlers/lsp_router.h new file mode 100644 index 0000000000..6625a0b962 --- /dev/null +++ b/lsp/handlers/lsp_router.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include "lsp/state/app.h" +#include "lsp/state/workspace.h" +#include "lsp/transport/stdio.h" + +#include "third-party/json.hpp" + +using json = nlohmann::json; + +enum class LSPRouteType { NOOP = 0, NOTIFICATION = 1, REQUEST_RESPONSE = 2 }; + +class LSPRoute { + public: + LSPRoute(); + LSPRoute(std::function notification_handler); + LSPRoute(std::function notification_handler, + std::function(Workspace&, json)> post_notification_publish); + LSPRoute(std::function(Workspace&, int, json)> request_handler); + + LSPRouteType m_route_type; + + /// @brief Handle a notification -- this requires no response to the client + std::function m_notification_handler; + /// @brief Handle a new request from the client that expects a response + std::function(Workspace&, int, json)> m_request_handler; + /// @brief Prepares a notification response body to be published _after_ the main handler is + /// processed + std::optional(Workspace&, json)>> m_post_notification_publish; + /// @brief Generic function to perform some action after processing + std::optional> m_generic_post_action; +}; + +class LSPRouter { + public: + void init_routes(); + std::optional> route_message(const MessageBuffer& message_buffer, + AppState& appstate); + std::string make_response(const json& result); + + private: + std::unordered_map m_routes; + ; +}; diff --git a/lsp/handlers/text_document/document_symbol.h b/lsp/handlers/text_document/document_symbol.h new file mode 100644 index 0000000000..8e0757744d --- /dev/null +++ b/lsp/handlers/text_document/document_symbol.h @@ -0,0 +1,25 @@ +#include + +#include "lsp/protocol/common_types.h" +#include "lsp/state/workspace.h" + +#include "third-party/json.hpp" + +using json = nlohmann::json; + +std::optional document_symbols_handler(Workspace& workspace, int id, json params) { + auto converted_params = params.get(); + auto tracked_file = workspace.get_tracked_ir_file(converted_params.m_textDocument.m_uri); + + if (!tracked_file) { + return {}; + } + + // TODO - convert to type! + + json arr = json::array(); + for (const auto& symbol : tracked_file.value().m_symbols) { + arr.push_back(symbol); + } + return arr; +} diff --git a/lsp/handlers/text_document/document_synchronization.h b/lsp/handlers/text_document/document_synchronization.h new file mode 100644 index 0000000000..b0c7e67569 --- /dev/null +++ b/lsp/handlers/text_document/document_synchronization.h @@ -0,0 +1,67 @@ +#include + +#include "lsp/protocol/document_diagnostics.h" +#include "lsp/protocol/document_synchronization.h" +#include "lsp/state/workspace.h" + +#include "third-party/json.hpp" + +using json = nlohmann::json; + +void did_open_handler(Workspace& workspace, json raw_params) { + auto params = raw_params.get(); + workspace.start_tracking_file(params.m_textDocument.m_uri, params.m_textDocument.m_languageId, + params.m_textDocument.m_text); +} + +void did_change_handler(Workspace& workspace, json raw_params) { + auto params = raw_params.get(); + for (const auto& change : params.m_contentChanges) { + workspace.update_tracked_file(params.m_textDocument.m_uri, change.m_text); + } +} + +void did_close_handler(Workspace& workspace, json raw_params) { + auto params = raw_params.get(); + workspace.stop_tracking_file(params.m_textDocument.m_uri); +} + +std::optional did_open_push_diagnostics(Workspace& workspace, json params) { + auto converted_params = params.get(); + auto tracked_file = workspace.get_tracked_ir_file(converted_params.m_textDocument.m_uri); + + if (!tracked_file) { + return {}; + } + + LSPSpec::PublishDiagnosticParams publish_params; + publish_params.m_uri = converted_params.m_textDocument.m_uri; + publish_params.m_diagnostics = tracked_file.value().m_diagnostics; + publish_params.m_version = converted_params.m_textDocument.m_version; + + json response; + response["method"] = "textDocument/publishDiagnostics"; + response["params"] = publish_params; + + return response; +} + +std::optional did_change_push_diagnostics(Workspace& workspace, json raw_params) { + auto params = raw_params.get(); + auto tracked_file = workspace.get_tracked_ir_file(params.m_textDocument.m_uri); + + if (!tracked_file) { + return {}; + } + + LSPSpec::PublishDiagnosticParams publish_params; + publish_params.m_uri = params.m_textDocument.m_uri; + publish_params.m_diagnostics = tracked_file.value().m_diagnostics; + publish_params.m_version = params.m_textDocument.m_version; + + json response; + response["method"] = "textDocument/publishDiagnostics"; + response["params"] = publish_params; + + return response; +} diff --git a/lsp/handlers/text_document/go_to.h b/lsp/handlers/text_document/go_to.h new file mode 100644 index 0000000000..11eb59fb50 --- /dev/null +++ b/lsp/handlers/text_document/go_to.h @@ -0,0 +1,44 @@ +#include + +#include "lsp/protocol/common_types.h" +#include "lsp/protocol/hover.h" +#include "lsp/state/data/mips_instructions.h" +#include "lsp/state/workspace.h" + +std::optional go_to_definition_handler(Workspace& workspace, int id, json params) { + auto converted_params = params.get(); + auto tracked_file = workspace.get_tracked_ir_file(converted_params.m_textDocument.m_uri); + + if (!tracked_file) { + lg::debug("GOTODEF - no file"); + return {}; + } + + json locations = json::array(); + auto symbol_name = tracked_file->get_symbol_at_position(converted_params.m_position); + + if (!symbol_name) { + lg::debug("GOTODEF - no symbol"); + return locations; + } + + lg::debug("GOTODEF - symbol - {}", symbol_name.value()); + + auto symbol_info = + workspace.get_symbol_info_from_all_types(symbol_name.value(), tracked_file->m_all_types_uri); + + if (!symbol_info) { + lg::debug("GOTODEF - no symbol info"); + return locations; + } + + LSPSpec::Location location; + location.m_uri = tracked_file->m_all_types_uri; + location.m_range.m_start = {(uint32_t)symbol_info->line_idx_to_display, + (uint32_t)symbol_info->pos_in_line}; + location.m_range.m_end = {(uint32_t)symbol_info->line_idx_to_display, + (uint32_t)symbol_info->pos_in_line}; + locations.push_back(location); + + return locations; +} diff --git a/lsp/handlers/text_document/hover.h b/lsp/handlers/text_document/hover.h new file mode 100644 index 0000000000..9f837791b6 --- /dev/null +++ b/lsp/handlers/text_document/hover.h @@ -0,0 +1,44 @@ +#include + +#include "lsp/protocol/common_types.h" +#include "lsp/protocol/hover.h" +#include "lsp/state/data/mips_instructions.h" +#include "lsp/state/workspace.h" + +std::optional hover_handler(Workspace& workspace, int id, json params) { + auto converted_params = params.get(); + auto tracked_file = workspace.get_tracked_ir_file(converted_params.m_textDocument.m_uri); + + if (!tracked_file) { + return {}; + } + + auto token_at_pos = tracked_file->get_mips_instruction_at_position(converted_params.m_position); + + LSPSpec::MarkupContent markup; + markup.m_kind = "markdown"; + if (token_at_pos) { + auto token = token_at_pos.value(); + std::transform(token.begin(), token.end(), token.begin(), + [](unsigned char c) { return std::tolower(c); }); + // Find the instruction, there are some edge-cases here where they could be multiple + // TODO - handle those (print both is an easy fix?) + // TODO - havn't addressed `bc` and such instructions! Those need to be prefixed matched + for (const auto& instr : LSPData::MIPS_INSTRUCTION_LIST) { + auto mnemonic_lower = instr.mnemonic; + std::transform(mnemonic_lower.begin(), mnemonic_lower.end(), mnemonic_lower.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (mnemonic_lower == token) { + markup.m_value = fmt::format("### {}\n{}", instr.mnemonic, instr.description); + break; + } + } + } else { + return {}; + } + LSPSpec::Hover hover_resp; + hover_resp.m_contents = markup; + // TODO - try specifying the range so it highlights everything, ie. `c.lt.s` + + return hover_resp; +} diff --git a/lsp/main.cpp b/lsp/main.cpp new file mode 100644 index 0000000000..13ad889e08 --- /dev/null +++ b/lsp/main.cpp @@ -0,0 +1,108 @@ +// clang-format off + +#ifdef _WIN32 +#include +#include +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#include +#include +#include + +#include "common/log/log.h" + +#include "lsp/handlers/lsp_router.h" +#include "lsp/state/workspace.h" +#include "lsp/transport/stdio.h" +#include "lsp/state/app.h" + +#include "third-party/CLI11.hpp" + +// clang-format on + +// NOTE - if we ever add HTTP support to the LSP +/* + What needs to be understood is that for connection timing issues the server is actually a client + and the client is the server in terms of opening the ports. + + When you specify a socket transport the client is listening on that port for a connection. The + socket port number is passed as --socket=${port} to the server process started. +*/ + +void setup_logging(bool verbose, std::string log_file) { + lg::set_file(log_file); + if (verbose) { + lg::set_file_level(lg::level::debug); + lg::set_flush_level(lg::level::debug); + } else { + lg::set_file_level(lg::level::info); + lg::set_flush_level(lg::level::info); + } + + // We use stdout to communicate with the client, so don't use it at all! + lg::set_stdout_level(lg::level::off); + lg::initialize(); +} + +int main(int argc, char** argv) { + fs::u8arguments u8guard(argc, argv); + if (!u8guard.valid()) { + exit(EXIT_FAILURE); + } + + CLI::App app{"OpenGOAL Language Server"}; + + bool use_stdin = true; + bool verbose = false; + std::string logfile; + app.add_flag("--stdio", use_stdin, + "Don't launch an HTTP server and instead accept input on stdin"); + app.add_flag("-v,--verbose", verbose, "Enable verbose logging"); + app.add_option("-l,--log", logfile, "Log file path"); + app.validate_positionals(); + CLI11_PARSE(app, argc, argv); + + AppState appstate; + LSPRouter lsp_router; + appstate.verbose = verbose; + if (!logfile.empty()) { + setup_logging(appstate.verbose, logfile); + } + lsp_router.init_routes(); + + lg::info("OpenGOAL LSP Initialized, ready for requests"); + +#ifdef _WIN32 + _setmode(_fileno(stdout), _O_BINARY); + _setmode(_fileno(stdin), _O_BINARY); +#endif + + char c; + MessageBuffer message_buffer; + while (std::cin.get(c)) { + message_buffer.handle_char(c); + + if (message_buffer.message_completed()) { + json body = message_buffer.body(); + auto method_name = body["method"].get(); + lg::info(">>> Received message of method '{}'", method_name); + auto responses = lsp_router.route_message(message_buffer, appstate); + if (responses) { + for (const auto& response : responses.value()) { + std::cout << response.c_str() << std::flush; + if (appstate.verbose) { + lg::debug("<<< Sending message: {}", response); + } else { + lg::info("<<< Sending message of method '{}'", method_name); + } + } + } + message_buffer.clear(); + } + } + + return 0; +} diff --git a/lsp/protocol/common_types.cpp b/lsp/protocol/common_types.cpp new file mode 100644 index 0000000000..6f521c056b --- /dev/null +++ b/lsp/protocol/common_types.cpp @@ -0,0 +1,69 @@ +#include "common_types.h" + +void LSPSpec::to_json(json& j, const Position& obj) { + j = json{{"line", obj.m_line}, {"character", obj.m_character}}; +} + +void LSPSpec::from_json(const json& j, Position& obj) { + j.at("line").get_to(obj.m_line); + j.at("character").get_to(obj.m_character); +} + +void LSPSpec::to_json(json& j, const Range& obj) { + // TODO - not sure if this works yet, but nice if it does! + j = json{{"start", obj.m_start}, {"end", obj.m_end}}; +} + +void LSPSpec::from_json(const json& j, Range& obj) { + obj.m_start = j.at("start").get(); + obj.m_end = j.at("end").get(); +} + +void LSPSpec::to_json(json& j, const TextDocumentItem& obj) { + j = json{{"uri", obj.m_uri}, + {"languageId", obj.m_languageId}, + {"version", obj.m_version}, + {"text", obj.m_text}}; +} + +void LSPSpec::from_json(const json& j, TextDocumentItem& obj) { + j.at("uri").get_to(obj.m_uri); + j.at("languageId").get_to(obj.m_languageId); + j.at("version").get_to(obj.m_version); + j.at("text").get_to(obj.m_text); +} + +void LSPSpec::to_json(json& j, const TextDocumentIdentifier& obj) { + j = json{{"uri", obj.m_uri}}; +} + +void LSPSpec::from_json(const json& j, TextDocumentIdentifier& obj) { + j.at("uri").get_to(obj.m_uri); +} + +void LSPSpec::to_json(json& j, const VersionedTextDocumentIdentifier& obj) { + j = json{{"uri", obj.m_uri}, {"version", obj.m_version}}; +} + +void LSPSpec::from_json(const json& j, VersionedTextDocumentIdentifier& obj) { + j.at("uri").get_to(obj.m_uri); + j.at("version").get_to(obj.m_version); +} + +void LSPSpec::to_json(json& j, const Location& obj) { + j = json{{"uri", obj.m_uri}, {"range", obj.m_range}}; +} + +void LSPSpec::from_json(const json& j, Location& obj) { + j.at("uri").get_to(obj.m_uri); + j.at("range").get_to(obj.m_range); +} + +void LSPSpec::to_json(json& j, const TextDocumentPositionParams& obj) { + j = json{{"textDocument", obj.m_textDocument}, {"position", obj.m_position}}; +} + +void LSPSpec::from_json(const json& j, TextDocumentPositionParams& obj) { + j.at("textDocument").get_to(obj.m_textDocument); + j.at("position").get_to(obj.m_position); +} diff --git a/lsp/protocol/common_types.h b/lsp/protocol/common_types.h new file mode 100644 index 0000000000..ed9fa393c9 --- /dev/null +++ b/lsp/protocol/common_types.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include + +#include "third-party/json.hpp" + +using json = nlohmann::json; + +namespace LSPSpec { +// TODO - eventually parse this +typedef std::string URI; +typedef std::string DocumentUri; + +struct Position { + /// @brief Line position in a document (zero-based). + uint32_t m_line; + /// @brief Character offset on a line in a document (zero-based). The meaning of this + /// offset is determined by the negotiated `PositionEncodingKind`. + /// + /// If the character value is greater than the line length it defaults back + /// to the line length. + uint32_t m_character; +}; +void to_json(json& j, const Position& obj); +void from_json(const json& j, Position& obj); + +struct Range { + Position m_start; + Position m_end; +}; +void to_json(json& j, const Range& obj); +void from_json(const json& j, Range& obj); + +struct TextDocumentItem { + DocumentUri m_uri; + std::string m_languageId; // ie. opengoal-ir + int32_t m_version; + std::string m_text; +}; + +void to_json(json& j, const TextDocumentItem& obj); +void from_json(const json& j, TextDocumentItem& obj); + +struct TextDocumentIdentifier { + DocumentUri m_uri; +}; + +void to_json(json& j, const TextDocumentIdentifier& obj); +void from_json(const json& j, TextDocumentIdentifier& obj); + +struct VersionedTextDocumentIdentifier { + DocumentUri m_uri; + /// @brief The version number of this document. + /// The version number of a document will increase after each change, including undo/redo. The + /// number doesn't need to be consecutive. + int32_t m_version; +}; + +void to_json(json& j, const VersionedTextDocumentIdentifier& obj); +void from_json(const json& j, VersionedTextDocumentIdentifier& obj); + +struct Location { + DocumentUri m_uri; + Range m_range; +}; +void to_json(json& j, const Location& obj); +void from_json(const json& j, Location& obj); + +struct TextDocumentPositionParams { + /// @brief The text document. + TextDocumentIdentifier m_textDocument; + /// @brief The position inside the text document. + Position m_position; +}; + +void to_json(json& j, const TextDocumentPositionParams& obj); +void from_json(const json& j, TextDocumentPositionParams& obj); +} // namespace LSPSpec diff --git a/lsp/protocol/document_diagnostics.cpp b/lsp/protocol/document_diagnostics.cpp new file mode 100644 index 0000000000..2bf95d3961 --- /dev/null +++ b/lsp/protocol/document_diagnostics.cpp @@ -0,0 +1,74 @@ +#include "document_diagnostics.h" + +void LSPSpec::to_json(json& j, const CodeDescription& obj) { + j = json{{"href", obj.m_href}}; +} + +void LSPSpec::from_json(const json& j, CodeDescription& obj) { + j.at("href").get_to(obj.m_href); +} + +void LSPSpec::to_json(json& j, const DiangosticRelatedInformation& obj) { + j = json{{"location", obj.m_location}, {"message", obj.m_message}}; +} + +void LSPSpec::from_json(const json& j, DiangosticRelatedInformation& obj) { + j.at("location").get_to(obj.m_location); + j.at("message").get_to(obj.m_message); +} + +void LSPSpec::to_json(json& j, const Diagnostic& obj) { + j = json{{"range", obj.m_range}, {"severity", obj.m_severity}, {"message", obj.m_message}}; + if (obj.m_code) { + j["code"] = obj.m_code.value(); + } + if (obj.m_codeDescription) { + j["codeDescription"] = obj.m_codeDescription.value(); + } + if (obj.m_source) { + j["source"] = obj.m_source.value(); + } + if (obj.m_tags) { + j["tags"] = obj.m_tags.value(); + } + if (obj.m_relatedInformation) { + j["relatedInformation"] = obj.m_relatedInformation.value(); + } +} + +void LSPSpec::from_json(const json& j, Diagnostic& obj) { + j.at("range").get_to(obj.m_range); + j.at("severity").get_to(obj.m_severity); + j.at("message").get_to(obj.m_message); + if (j.contains("code")) { + obj.m_code = std::make_optional(j.at("code").get()); + } + if (j.contains("codeDescription")) { + obj.m_codeDescription = std::make_optional(j.at("codeDescription").get()); + } + if (j.contains("source")) { + obj.m_source = std::make_optional(j.at("source").get()); + } + if (j.contains("tags")) { + obj.m_tags = std::make_optional(j.at("tags").get>()); + } + if (j.contains("relatedInformation")) { + obj.m_relatedInformation = std::make_optional( + j.at("relatedInformation").get>()); + } +} + +void LSPSpec::to_json(json& j, const PublishDiagnosticParams& obj) { + j = json{{"uri", obj.m_uri}, {"diagnostics", obj.m_diagnostics}}; + if (obj.m_version) { + j["version"] = obj.m_version.value(); + } +} + +void LSPSpec::from_json(const json& j, PublishDiagnosticParams& obj) { + j.at("uri").get_to(obj.m_uri); + j.at("diagnostics").get_to(obj.m_diagnostics); + if (j.contains("version")) { + obj.m_version = std::make_optional(j.at("version").get()); + } +} diff --git a/lsp/protocol/document_diagnostics.h b/lsp/protocol/document_diagnostics.h new file mode 100644 index 0000000000..661f96ef0c --- /dev/null +++ b/lsp/protocol/document_diagnostics.h @@ -0,0 +1,86 @@ +#pragma once + +#include "common_types.h" + +namespace LSPSpec { +enum class DiagnosticSeverity { + /// Reports an error. + Error = 1, + /// Reports a warning. + Warning = 2, + /// Reports an information. + Information = 3, + /// Reports a hint. + Hint = 4, +}; + +/// @brief Structure to capture a description for an error code. +struct CodeDescription { + /// @brief An URI to open with more information about the diagnostic error. + URI m_href; +}; + +void to_json(json& j, const CodeDescription& obj); +void from_json(const json& j, CodeDescription& obj); + +enum class DiagnosticTag { + /// @brief Unused or unnecessary code. + /// Clients are allowed to render diagnostics with this tag faded out instead of having an error + /// squiggle. + Unnecessary = 1, + /// @brief Deprecated or obsolete code. + /// Clients are allowed to rendered diagnostics with this tag strike through. + Deprecated = 2 +}; + +struct DiangosticRelatedInformation { + /// @brief The location of this related diagnostic information. + Location m_location; + /// @brief The message of this related diagnostic information. + std::string m_message; +}; + +void to_json(json& j, const DiangosticRelatedInformation& obj); +void from_json(const json& j, DiangosticRelatedInformation& obj); + +struct Diagnostic { + /// @brief The range at which the message applies. + Range m_range; + /// @brief The diagnostic's severity. Can be omitted. If omitted it is up to the client to + /// interpret diagnostics as error, warning, info or hint. + DiagnosticSeverity m_severity; + /// @brief The diagnostic's code, which might appear in the user interface. + std::optional m_code; + /// @brief An optional property to describe the error code. + std::optional m_codeDescription; + /// @brief A human-readable string describing the source of this diagnostic, e.g. 'typescript' or + /// 'super lint'. + std::optional m_source; + /// @brief The diagnostic's message. + std::string m_message; + /// @brief Additional metadata about the diagnostic. + std::optional> m_tags; + /// @brief An array of related diagnostic information, e.g. when symbol-names within a scope + /// collide all definitions can be marked via this property. + std::optional> m_relatedInformation; + // omitting `data` field + /// A data entry field that is preserved between a `textDocument/publishDiagnostics` notification + /// and `textDocument/codeAction` request +}; + +void to_json(json& j, const Diagnostic& obj); +void from_json(const json& j, Diagnostic& obj); + +struct PublishDiagnosticParams { + /// @brief The URI for which diagnostic information is reported + DocumentUri m_uri; + /// @brief Optional the version number of the document the diagnostics are published for + std::optional m_version; + /// @brief An array of diagnostic information items + std::vector m_diagnostics; +}; + +void to_json(json& j, const PublishDiagnosticParams& obj); +void from_json(const json& j, PublishDiagnosticParams& obj); + +} // namespace LSPSpec diff --git a/lsp/protocol/document_symbols.cpp b/lsp/protocol/document_symbols.cpp new file mode 100644 index 0000000000..5427ae82b7 --- /dev/null +++ b/lsp/protocol/document_symbols.cpp @@ -0,0 +1,41 @@ +#include "document_symbols.h" + +void LSPSpec::to_json(json& j, const DocumentSymbol& obj) { + j = json{{"name", obj.m_name}, + {"kind", obj.m_kind}, + {"range", obj.m_range}, + {"selectionRange", obj.m_selectionRange}}; + if (obj.m_detail) { + j["detail"] = obj.m_detail.value(); + } + if (obj.m_tags) { + j["tags"] = obj.m_tags.value(); + } + if (obj.m_children) { + j["children"] = obj.m_children.value(); + } +} + +void LSPSpec::from_json(const json& j, DocumentSymbol& obj) { + j.at("name").get_to(obj.m_name); + j.at("kind").get_to(obj.m_kind); + j.at("range").get_to(obj.m_range); + j.at("selectionRange").get_to(obj.m_selectionRange); + if (j.contains("detail")) { + obj.m_detail = std::make_optional(j.at("detail").get()); + } + if (j.contains("tags")) { + obj.m_tags = std::make_optional(j.at("tags").get>()); + } + if (j.contains("children")) { + obj.m_children = std::make_optional(j.at("children").get>()); + } +} + +void LSPSpec::to_json(json& j, const DocumentSymbolParams& obj) { + j = json{{"textDocument", obj.m_textDocument}}; +} + +void LSPSpec::from_json(const json& j, DocumentSymbolParams& obj) { + j.at("textDocument").get_to(obj.m_textDocument); +} diff --git a/lsp/protocol/document_symbols.h b/lsp/protocol/document_symbols.h new file mode 100644 index 0000000000..b45f88b95c --- /dev/null +++ b/lsp/protocol/document_symbols.h @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include "common_types.h" + +#include "third-party/json.hpp" + +namespace LSPSpec { +enum class SymbolKind { + File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, + Object = 19, + Key = 20, + Null = 21, + EnumMember = 22, + Struct = 23, + Event = 24, + Operator = 25, + TypeParameter = 26 +}; + +/// @brief Symbol tags are extra annotations that tweak the rendering of a symbol. +/// @since 3.16 +enum class SymbolTag { + /// @brief Render a symbol as obsolete, usually using a strike-out. + Deprecated = 1 +}; + +struct DocumentSymbol { + std::string m_name; + std::optional m_detail; + SymbolKind m_kind; + std::optional> m_tags; + /// @brief The range enclosing this symbol not including leading/trailing whitespace + /// but everything else like comments. This information is typically used to determine + /// if the clients cursor is inside the symbol to reveal in the symbol in the UI. + Range m_range; + /// @brief The range that should be selected and revealed when this symbol is being + /// picked, e.g. the name of a function. Must be contained by the `range`. + Range m_selectionRange; + /// @brief Children of this symbol, e.g. properties of a class. + std::optional> m_children; +}; + +void to_json(json& j, const DocumentSymbol& obj); +void from_json(const json& j, DocumentSymbol& obj); + +struct DocumentSymbolParams { + TextDocumentIdentifier m_textDocument; +}; + +void to_json(json& j, const DocumentSymbolParams& obj); +void from_json(const json& j, DocumentSymbolParams& obj); + +} // namespace LSPSpec diff --git a/lsp/protocol/document_synchronization.cpp b/lsp/protocol/document_synchronization.cpp new file mode 100644 index 0000000000..41baf897e6 --- /dev/null +++ b/lsp/protocol/document_synchronization.cpp @@ -0,0 +1,33 @@ +#include "document_synchronization.h" + +void LSPSpec::to_json(json& j, const DidOpenTextDocumentParams& obj) { + j = json{{"textDocument", obj.m_textDocument}}; +} + +void LSPSpec::from_json(const json& j, DidOpenTextDocumentParams& obj) { + j.at("textDocument").get_to(obj.m_textDocument); +} + +void LSPSpec::to_json(json& j, const TextDocumentContentChangeEvent& obj) { + j = json{{"text", obj.m_text}}; +} + +void LSPSpec::from_json(const json& j, TextDocumentContentChangeEvent& obj) { + j.at("text").get_to(obj.m_text); +} + +void LSPSpec::to_json(json& j, const DidChangeTextDocumentParams& obj) { + j = json{{"textDocument", obj.m_textDocument}}; +} + +void LSPSpec::from_json(const json& j, DidChangeTextDocumentParams& obj) { + j.at("textDocument").get_to(obj.m_textDocument); +} + +void LSPSpec::to_json(json& j, const DidCloseTextDocumentParams& obj) { + j = json{{"textDocument", obj.m_textDocument}}; +} + +void LSPSpec::from_json(const json& j, DidCloseTextDocumentParams& obj) { + j.at("textDocument").get_to(obj.m_textDocument); +} diff --git a/lsp/protocol/document_synchronization.h b/lsp/protocol/document_synchronization.h new file mode 100644 index 0000000000..476aa7c1b6 --- /dev/null +++ b/lsp/protocol/document_synchronization.h @@ -0,0 +1,35 @@ +#pragma once + +#include "common_types.h" + +namespace LSPSpec { +struct DidOpenTextDocumentParams { + TextDocumentItem m_textDocument; +}; + +void to_json(json& j, const DidOpenTextDocumentParams& obj); +void from_json(const json& j, DidOpenTextDocumentParams& obj); + +struct TextDocumentContentChangeEvent { + std::string m_text; +}; + +void to_json(json& j, const TextDocumentContentChangeEvent& obj); +void from_json(const json& j, TextDocumentContentChangeEvent& obj); + +struct DidChangeTextDocumentParams { + VersionedTextDocumentIdentifier m_textDocument; + std::vector m_contentChanges; +}; + +void to_json(json& j, const DidChangeTextDocumentParams& obj); +void from_json(const json& j, DidChangeTextDocumentParams& obj); + +struct DidCloseTextDocumentParams { + TextDocumentIdentifier m_textDocument; +}; + +void to_json(json& j, const DidCloseTextDocumentParams& obj); +void from_json(const json& j, DidCloseTextDocumentParams& obj); + +} // namespace LSPSpec diff --git a/lsp/protocol/error_codes.h b/lsp/protocol/error_codes.h new file mode 100644 index 0000000000..2447ce848e --- /dev/null +++ b/lsp/protocol/error_codes.h @@ -0,0 +1,93 @@ +#pragma once + +enum class ErrorCodes { + // Defined by JSON-RPC + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + + /** + * This is the start range of JSON-RPC reserved error codes. + * It doesn't denote a real error code. No LSP error codes should + * be defined between the start and end range. For backwards + * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` + * are left in the range. + * + * @since 3.16.0 + */ + jsonrpcReservedErrorRangeStart = -32099, + /** @deprecated use jsonrpcReservedErrorRangeStart */ + serverErrorStart = jsonrpcReservedErrorRangeStart, + + /** + * Error code indicating that a server received a notification or + * request before the server has received the `initialize` request. + */ + ServerNotInitialized = -32002, + UnknownErrorCode = -32001, + + /** + * This is the end range of JSON-RPC reserved error codes. + * It doesn't denote a real error code. + * + * @since 3.16.0 + */ + jsonrpcReservedErrorRangeEnd = -32000, + /** @deprecated use jsonrpcReservedErrorRangeEnd */ + serverErrorEnd = jsonrpcReservedErrorRangeEnd, + + /** + * This is the start range of LSP reserved error codes. + * It doesn't denote a real error code. + * + * @since 3.16.0 + */ + lspReservedErrorRangeStart = -32899, + + /** + * A request failed but it was syntactically correct, e.g the + * method name was known and the parameters were valid. The error + * message should contain human readable information about why + * the request failed. + * + * @since 3.17.0 + */ + RequestFailed = -32803, + + /** + * The server cancelled the request. This error code should + * only be used for requests that explicitly support being + * server cancellable. + * + * @since 3.17.0 + */ + ServerCancelled = -32802, + + /** + * The server detected that the content of a document got + * modified outside normal conditions. A server should + * NOT send this error code if it detects a content change + * in it unprocessed messages. The result even computed + * on an older state might still be useful for the client. + * + * If a client decides that a result is not of any use anymore + * the client should cancel the request. + */ + ContentModified = -32801, + + /** + * The client has canceled a request and a server as detected + * the cancel. + */ + RequestCancelled = -32800, + + /** + * This is the end range of LSP reserved error codes. + * It doesn't denote a real error code. + * + * @since 3.16.0 + */ + lspReservedErrorRangeEnd = -32800, +}; diff --git a/lsp/protocol/hover.cpp b/lsp/protocol/hover.cpp new file mode 100644 index 0000000000..576f94a3cf --- /dev/null +++ b/lsp/protocol/hover.cpp @@ -0,0 +1,24 @@ +#include "hover.h" + +void LSPSpec::to_json(json& j, const MarkupContent& obj) { + j = json{{"kind", obj.m_kind}, {"value", obj.m_value}}; +} + +void LSPSpec::from_json(const json& j, MarkupContent& obj) { + j.at("kind").get_to(obj.m_kind); + j.at("value").get_to(obj.m_value); +} + +void LSPSpec::to_json(json& j, const Hover& obj) { + j = json{{"contents", obj.m_contents}}; + if (obj.m_range) { + j["range"] = obj.m_range.value(); + } +} + +void LSPSpec::from_json(const json& j, Hover& obj) { + j.at("contents").get_to(obj.m_contents); + if (j.contains("range")) { + obj.m_range = std::make_optional(j.at("range").get()); + } +} diff --git a/lsp/protocol/hover.h b/lsp/protocol/hover.h new file mode 100644 index 0000000000..c10ac45d63 --- /dev/null +++ b/lsp/protocol/hover.h @@ -0,0 +1,51 @@ +#pragma once + +#include "common_types.h" + +namespace LSPSpec { +/** + * A `MarkupContent` literal represents a string value which content is + * interpreted base on its kind flag. Currently the protocol supports + * `plaintext` and `markdown` as markup kinds. + * + * If the kind is `markdown` then the value can contain fenced code blocks like + * in GitHub issues. + * + * Here is an example how such a string can be constructed using + * JavaScript / TypeScript: + * ```typescript + * let markdown: MarkdownContent = { + * kind: MarkupKind.Markdown, + * value: [ + * '# Header', + * 'Some text', + * '```typescript', + * 'someCode();', + * '```' + * ].join('\n') + * }; + * ``` + * + * *Please Note* that clients might sanitize the return markdown. A client could + * decide to remove HTML from the markdown to avoid script execution. + */ +struct MarkupContent { + std::string m_kind; // Actually a MarkupKind which is either 'plaintext' or 'markdown' + std::string m_value; +}; + +void to_json(json& j, const MarkupContent& obj); +void from_json(const json& j, MarkupContent& obj); + +struct Hover { + /// @brief The hover's content + MarkupContent m_contents; + /// @brief An optional range is a range inside a text document that is used to visualize a hover, + /// e.g. by changing the background color. + std::optional m_range; +}; + +void to_json(json& j, const Hover& obj); +void from_json(const json& j, Hover& obj); + +} // namespace LSPSpec diff --git a/lsp/protocol/initialize_result.h b/lsp/protocol/initialize_result.h new file mode 100644 index 0000000000..c468d11237 --- /dev/null +++ b/lsp/protocol/initialize_result.h @@ -0,0 +1,59 @@ +// TODO - convert this a proper class + +#include "third-party/json.hpp" + +using json = nlohmann::json; + +class InitializeResult { + public: + InitializeResult(){}; + json to_json() { return result; } + + private: + json text_document_sync{ + {"openClose", true}, + {"change", 1}, // Full sync + {"willSave", false}, + {"willSaveWaitUntil", false}, + {"save", {{"includeText", false}}}, + }; + + json completion_provider{ + {"resolveProvider", false}, + {"triggerCharacters", {}}, + }; + json signature_help_provider{{"triggerCharacters", ""}}; + json code_lens_provider{{"resolveProvider", false}}; + json document_on_type_formatting_provider{ + {"firstTriggerCharacter", ""}, + {"moreTriggerCharacter", ""}, + }; + json document_link_provider{{"resolveProvider", false}}; + json execute_command_provider{{"commands", {}}}; + + json document_symbol_provder{{"label", "OpenGOAL"}}; + + json result{{"capabilities", + { + {"textDocumentSync", text_document_sync}, + {"hoverProvider", true}, + {"completionProvider", completion_provider}, + {"signatureHelpProvider", signature_help_provider}, + {"definitionProvider", true}, + {"referencesProvider", false}, + {"documentHighlightProvider", false}, + {"documentSymbolProvider", + document_symbol_provder}, // TODO - there is another selectionRangeProvider i + // think i need, or word boundaries need to change! + {"workspaceSymbolProvider", false}, + {"codeActionProvider", false}, + {"codeLensProvider", code_lens_provider}, + {"documentFormattingProvider", false}, + {"documentRangeFormattingProvider", false}, + {"documentOnTypeFormattingProvider", document_on_type_formatting_provider}, + {"renameProvider", false}, + {"documentLinkProvider", document_link_provider}, + {"executeCommandProvider", execute_command_provider}, + {"experimental", {}}, + }}}; +}; diff --git a/lsp/state/app.h b/lsp/state/app.h new file mode 100644 index 0000000000..9d3c317347 --- /dev/null +++ b/lsp/state/app.h @@ -0,0 +1,8 @@ +#pragma once + +#include "workspace.h" + +struct AppState { + Workspace workspace; + bool verbose; +}; diff --git a/lsp/state/data/mips_instruction.cpp b/lsp/state/data/mips_instruction.cpp new file mode 100644 index 0000000000..2d7df7ea8f --- /dev/null +++ b/lsp/state/data/mips_instruction.cpp @@ -0,0 +1,531 @@ +#include "mips_instructions.h" + +namespace LSPData { + +const std::vector MIPS_INSTRUCTION_LIST = { + {"ee", "ADD", "Add Word"}, + {"ee", "ADDI", "Add Immediate Word"}, + {"ee", "ADDIU", "Add Immediate Unsigned Word"}, + {"ee", "ADDU", "Add Unsigned Word"}, + {"ee", "AND", "And"}, + {"ee", "ANDI", "Add Immediate"}, + {"ee", "BEQ", "Branch on Equal"}, + {"ee", "BEQL", "Branch on Equal Likely"}, + {"ee", "BGEZ", "Branch on Greater Than or Equal to Zero"}, + {"ee", "BGEZAL", "Branch on Greater Than or Equal to Zero and Link"}, + {"ee", "BGEZALL", "Branch on Greater Than or Equal to Zero and Link Likely"}, + {"ee", "BGEZL", "Branch on Greater Than or Equal to Zero Likely"}, + {"ee", "BGTZ", "Branch on Greater Than Zero"}, + {"ee", "BGTZL", "Branch on Greater Than Zero Likely"}, + {"ee", "BLEZ", "Branch on Less Than or Equal to Zero"}, + {"ee", "BLEZL", "Branch on Less Than or Equal to Zero Likely"}, + {"ee", "BLTZ", "Branch on Less Than Zero"}, + {"ee", "BLTZAL", "Branch on Less Than Zero and Link"}, + {"ee", "BLTZALL", "Branch on Less Than Zero and Link Likely"}, + {"ee", "BLTZL", "Branch on Less Than Zero Likely"}, + {"ee", "BNE", "Branch on Not Equal"}, + {"ee", "BNEL", "Branch on Not Equal Likely"}, + {"ee", "BREAK", "Breakpoint"}, + {"ee", "DADD", "Doubleword Add"}, + {"ee", "DADDI", "Doubleword Add Immediate"}, + {"ee", "DADDIU", "Doubleword Add Immediate Unsigned"}, + {"ee", "DADDU", "Doubleword Add Unsigned"}, + {"ee", "DIV", "Divide Word"}, + {"ee", "DIVU", "Divide Unsigned Word"}, + {"ee", "DSLL", "Doubleword Shift Left Logical"}, + {"ee", "DSLL32", "Doubleword Shift Left Logical Plus 32"}, + {"ee", "DSLLV", "Doubleword Shift Left Logical Variable"}, + {"ee", "DSRA", "Doubleword Shift Right Arithmetic"}, + {"ee", "DSRA32", "Doubleword Shift Right Arithmetic Plus 32"}, + {"ee", "DSRAV", "Doubleword Shift Right Arithmetic Variable"}, + {"ee", "DSRL", "Doubleword Shift Right Logical"}, + {"ee", "DSRL32", "Doubleword Shift Right Logical Plus 32"}, + {"ee", "DSRLV", "Doubleword Shift Right Logical Variable"}, + {"ee", "DSUB", "Doubleword Subtract"}, + {"ee", "DSUBU", "Doubleword Subtract Unsigned"}, + {"ee", "J", "Jump"}, + {"ee", "JAL", "Jump and Link"}, + {"ee", "JALR", "Jump and Link Register"}, + {"ee", "JR", "Jump Register"}, + {"ee", "LB", "Load Byte"}, + {"ee", "LBU", "Load Byte Unsigned"}, + {"ee", "LD", "Load Doubleword"}, + {"ee", "LDL", "Load Doubleword Left"}, + {"ee", "LDR", "Load Doubleword Right"}, + {"ee", "LH", "Load Halfword"}, + {"ee", "LHU", "Load Halfword Unsigned"}, + {"ee", "LUI", "Load Upper Immediate"}, + {"ee", "LW", "Load Word"}, + {"ee", "LWL", "Load Word Left"}, + {"ee", "LWR", "Load Word Right"}, + {"ee", "LWU", "Load Word Unsigned"}, + {"ee", "MFHI", "Move from HI Register"}, + {"ee", "MFLO", "Move from LO Register"}, + {"ee", "MOVN", "Move Conditional on Not Zero"}, + {"ee", "MOVZ", "Move Conditional on Zero"}, + {"ee", "MTHI", "Move to HI Register"}, + {"ee", "MTLO", "Move to LO Register"}, + {"ee", "MULT", "Multiply Word"}, + {"ee", "MULTU", "Multiply Unsigned Word"}, + {"ee", "NOR", "Not Or"}, + {"ee", "OR", "Or"}, + {"ee", "ORI", "Or immediate"}, + {"ee", "PREF", "Prefetch"}, + {"ee", "SB", "Store Byte"}, + {"ee", "SD", "Store Doubleword"}, + {"ee", "SDL", "Store Doubleword Left"}, + {"ee", "SDR", "Store Doubleword Right"}, + {"ee", "SH", "Store Halfword"}, + {"ee", "SLL", "Shift Word Left Logical"}, + {"ee", "SLLV", "Shift Word Left Logical Variable"}, + {"ee", "SLT", "Set on Less Than"}, + {"ee", "SLTI", "Set on Less Than Immediate"}, + {"ee", "SLTIU", "Set on Less Than Immediate Unsigned"}, + {"ee", "SLTU", "Set on Less Than Unsigned"}, + {"ee", "SRA", "Shift Word Right Arithmetic"}, + {"ee", "SRAV", "Shift Word Right Arithmetic Variable"}, + {"ee", "SRL", "Shift Word Right Logical"}, + {"ee", "SRLV", "Shift Word Right Logical Variable"}, + {"ee", "SUB", "Subtract Word"}, + {"ee", "SUBU", "Subtract Unsigned Word"}, + {"ee", "SW", "Store Word"}, + {"ee", "SWL", "Store Word Left"}, + {"ee", "SWR", "Store Word Right"}, + {"ee", "SYNC", "Synchronize Shared Memory"}, + {"ee", "SYNC.L", "Synchronize Shared Memory"}, + {"ee", "SYNC.P", "Synchronize Shared Memory"}, + {"ee", "SYSCALL", "System Call"}, + {"ee", "TEQ", "Trap if Equal"}, + {"ee", "TEQI", "Trap if Equal Immediate"}, + {"ee", "TGE", "Trap if Greater or Equal"}, + {"ee", "TGEI", "Trap if Greater or Equal Immediate"}, + {"ee", "TGEIU", "Trap if Greater or Equal Immediate Unsigned"}, + {"ee", "TGEU", "Trap if Greater or Equal Unsigned"}, + {"ee", "TLT", "Trap if Less Than"}, + {"ee", "TLTI", "Trap if Less Than Immediate"}, + {"ee", "TLTIU", "Trap if Less Than Immediate Unsigned"}, + {"ee", "TLTU", "Trap if Less Than Unsigned"}, + {"ee", "TNE", "Trap if Not Equal"}, + {"ee", "TNEI", "Trap if Not Equal Immediate"}, + {"ee", "XOR", "Exclusive OR"}, + {"ee", "XORI", "Exclusive OR Immediate"}, + {"ee", "DIV1", "Divide Word Pipeline 1"}, + {"ee", "DIVU1", "Divide Unsigned Word Pipeline 1"}, + {"ee", "LQ", "Load Quadword"}, + {"ee", "MADD", "Multiply-Add word"}, + {"ee", "MADD1", "Multiply-Add word Pipeline"}, + {"ee", "MADDU", "Multiply-Add Unsigned word"}, + {"ee", "MADDU1", "Multiply-Add Unsigned word Pipeline"}, + {"ee", "MFHI1", "Move From HI1 Register"}, + {"ee", "MFLO1", "Move From LO1 Register"}, + {"ee", "MFSA", "Move from Shift Amount Register"}, + {"ee", "MTHI1", "Move To HI1 Register"}, + {"ee", "MTLO1", "Move To LO1 Register"}, + {"ee", "MTSA", "Move to Shift Amount Register"}, + {"ee", "MTSAB", "Move Byte Count to Shift Amount Register"}, + {"ee", "MTSAH", "Move Halfword Count to Shift Amount Register"}, + {"ee", "MULT", "Multiply Word"}, + {"ee", "MULT1", "Multiply Word Pipeline"}, + {"ee", "MULTU", "Multiply Unsigned Word"}, + {"ee", "MULTU1", "Multiply Unsigned Word Pipeline"}, + {"ee", "PABSH", "Parallel Absolute Halfword"}, + {"ee", "PABSW", "Parallel Absolute Word"}, + {"ee", "PADDB", "Parallel Add Byte"}, + {"ee", "PADDH", "Parallel Add Halfword"}, + {"ee", "PADDSB", "Parallel Add with Signed Saturation Byte"}, + {"ee", "PADDSH", "Parallel Add with Signed Saturation Halfword"}, + {"ee", "PADDSW", "Parallel Add with Signed Saturation Word"}, + {"ee", "PADDUB", "Parallel Add with Unsigned Saturation Byte"}, + {"ee", "PADDUH", "Parallel Add with Unsigned Saturation Halfword"}, + {"ee", "PADDUW", "Parallel Add with Unsigned Saturation Word"}, + {"ee", "PADDW", "Parallel Add Word"}, + {"ee", "PADSBH", "Parallel Add/Subtract Halfword"}, + {"ee", "PAND", "Parallel And"}, + {"ee", "PCEQB", "Parallel Compare for Equal Byte"}, + {"ee", "PCEQH", "Parallel Compare for Equal Halfword"}, + {"ee", "PCEQW", "Parallel Compare for Equal Word"}, + {"ee", "PCGTB", "Parallel Compare for Greater Than Byte"}, + {"ee", "PCGTH", "Parallel Compare for Greater Than Halfword"}, + {"ee", "PCGTW", "Parallel Compare for Greater Than Word"}, + {"ee", "PCPYH", "Parallel Copy Halfword"}, + {"ee", "PCPYLD", "Parallel Copy Lower Doubleword"}, + {"ee", "PCPYUD", "Parallel Copy Upper Doubleword"}, + {"ee", "PDIVBW", "Parallel Divide Broadcast Word"}, + {"ee", "PDIVUW", "Parallel Divide Unsigned Word"}, + {"ee", "PDIVW", "Parallel Divide Word"}, + {"ee", "PEXCH", "Parallel Exchange Center Halfword"}, + {"ee", "PEXCW", "Parallel Exchange Center Word"}, + {"ee", "PEXEH", "Parallel Exchange Even Halfword"}, + {"ee", "PEXEW", "Parallel Exchange Even Word"}, + {"ee", "PEXT5", "Parallel Extend from 5 bits"}, + {"ee", "PEXTLB", "Parallel Extend Lower from Byte"}, + {"ee", "PEXTLH", "Parallel Extend Lower from Halfword"}, + {"ee", "PEXTLW", "Parallel Extend Lower from Word"}, + {"ee", "PEXTUB", "Parallel Extend Upper from Byte"}, + {"ee", "PEXTUH", "Parallel Extend Upper from Halfword"}, + {"ee", "PEXTUW", "Parallel Extend Upper from Word"}, + {"ee", "PHMADH", "Parallel Horizontal Multiply-Add Halfword"}, + {"ee", "PHMSBH", "Parallel Horizontal Multiply-Subtract Halfword"}, + {"ee", "PINTEH", "Parallel Interleave Even Halfword"}, + {"ee", "PINTH", "Parallel Interleave Halfword"}, + {"ee", "PLZCW", "Parallel Leading Zero or one Count Word"}, + {"ee", "PMADDH", "Parallel Multiply-Add Halfword"}, + {"ee", "PMADDUW", "Parallel Multiply-Add Unsigned Word"}, + {"ee", "PMADDW", "Parallel Multiply-Add Word"}, + {"ee", "PMAXH", "Parallel Maximize Halfword"}, + {"ee", "PMAXW", "Parallel Maximize Word"}, + {"ee", "PMFHI", "Parallel Move From HI Register"}, + {"ee", "PMFHL", "Parallel Move From HI/LO Register"}, + {"ee", "PMFHL", "Parallel Move From HI/LO Register"}, + {"ee", "PMFHL", "Parallel Move From HI/LO Register"}, + {"ee", "PMFHL", "Parallel Move From HI/LO Register"}, + {"ee", "PMFHL", "Parallel Move From HI/LO Register"}, + {"ee", "PMFLO", "Parallel Move From LO Register"}, + {"ee", "PMINH", "Parallel Minimize Halfword"}, + {"ee", "PMINW", "Parallel Minimize Word"}, + {"ee", "PMSUBH", "Parallel Multiply-Subtract Halfword"}, + {"ee", "PMSUBW", "Parallel Multiply-Subtract Word"}, + {"ee", "PMTHI", "Parallel Move To HI Register"}, + {"ee", "PMTHL", "Parallel Move To HI/LO Register"}, + {"ee", "PMTLO", "Parallel Move To LO Register"}, + {"ee", "PMULTH", "Parallel Multiply Halfword"}, + {"ee", "PMULTUW", "Parallel Multiply Unsigned Word"}, + {"ee", "PMULTW", "Parallel Multiply Word"}, + {"ee", "PNOR", "Parallel Not Or"}, + {"ee", "POR", "Parallel Or"}, + {"ee", "PPAC5", "Parallel Pack to 5 bits"}, + {"ee", "PPACB", "Parallel Pack to Byte"}, + {"ee", "PPACH", "Parallel Pack to Halfword"}, + {"ee", "PPACW", "Parallel Pack to Word"}, + {"ee", "PREVH", "Parallel Reverse Halfword"}, + {"ee", "PROT3W", "Parallel Rotate 3 Words Left"}, + {"ee", "PSLLH", "Parallel Shift Left Logical Halfword"}, + {"ee", "PSLLVW", "Parallel Shift Left Logical Variable Word"}, + {"ee", "PSLLW", "Parallel Shift Left Logical Word"}, + {"ee", "PSRAH", "Parallel Shift Right Arithmetic Halfword"}, + {"ee", "PSRAVW", "Parallel Shift Right Arithmetic Variable Word"}, + {"ee", "PSRAW", "Parallel Shift Right Arithmetic Word"}, + {"ee", "PSRLH", "Parallel Shift Right Logical Halfword"}, + {"ee", "PSRLVW", "Parallel Shift Right Logical Variable Word"}, + {"ee", "PSRLW", "Parallel Shift Right Logical Word"}, + {"ee", "PSUBB", "Parallel Subtract Byte"}, + {"ee", "PSUBH", "Parallel Subtract Halfword"}, + {"ee", "PSUBSB", "Parallel Subtract with Signed saturation Byte"}, + {"ee", "PSUBSH", "Parallel Subtract with Signed Saturation Halfword"}, + {"ee", "PSUBSW", "Parallel Subtract with Signed Saturation Word"}, + {"ee", "PSUBUB", "Parallel Subtract with Unsigned Saturation Byte"}, + {"ee", "PSUBUH", "Parallel Subtract with Unsigned Saturation Halfword"}, + {"ee", "PSUBUW", "Parallel Subtract with Unsigned Saturation Word"}, + {"ee", "PSUBW", "Parallel Subtract Word"}, + {"ee", "PXOR", "Parallel Exclusive OR"}, + {"ee", "QFSRV", "Quadword Funnel Shift Right Variable"}, + {"ee", "SQ", "Store Quadword"}, + {"ee", "BC0F", "Branch on Coprocessor 0 False"}, + {"ee", "BC0FL", "Branch on Coprocessor 0 False Likely"}, + {"ee", "BC0T", "Branch on Coprocessor 0 True"}, + {"ee", "BC0TL", "Branch on Coprocessor 0 True Likely"}, + {"ee", "cache bfh", "Cache Operation (BTAC Flush)"}, + {"ee", "cache bhinbt", "Cache Operation (Hit Invalidate BTAC)"}, + {"ee", "cache bxlbt", "Cache Operation (Index Load BTAC)"}, + {"ee", "cache bxsbt", "Cache Operation (Index Store BTAC)"}, + {"ee", "cache dhin", "Cache Operation (Hit Invalidate)"}, + {"ee", "cache dhwbin", "Cache Operation (Hit Writeback Invalidate)"}, + {"ee", "cache dhwoin", "Cache Operation (Hit Writeback Without Invalidate)"}, + {"ee", "cache dxin", "Cache Operation (Index Invalidate)"}, + {"ee", "cache dxldt", "Cache Operation (Index Load Data)"}, + {"ee", "cache dxltg", "Cache Operation (Index Load Tag)"}, + {"ee", "cache dxsdt", "Cache Operation (Index Store Data)"}, + {"ee", "cache dxstg", "Cache Operation (Index Store Tag)"}, + {"ee", "cache dxwbin", "Cache Operation (Index Writeback Invalidate)"}, + {"ee", "cache ifl", "Cache Operation (Fill)"}, + {"ee", "cache ihin", "Cache Operation (Hit Invalidate)"}, + {"ee", "cache ixin", "Cache Operation (Index Invalidate)"}, + {"ee", "cache ixldt", "Cache Operation (Index Load Data)"}, + {"ee", "cache ixltg", "Cache Operation (Index Load Tag)"}, + {"ee", "cache ixsdt", "Cache Operation (Index Store Data)"}, + {"ee", "cache ixstg", "Cache Operation (Index Store Tag)"}, + {"ee", "DI", "Disable Interrupt"}, + {"ee", "EI", "Enable Interrupt"}, + {"ee", "ERET", "Exception Return"}, + {"ee", "MFBPC", "Move from Breakpoint Control Register"}, + {"ee", "MFC0", "Move from System Control Coprocessor"}, + {"ee", "MFDAB", "Move from Data Address Breakpoint Register"}, + {"ee", "MFDABM", "Move from Data Address Breakpoint Mask Register"}, + {"ee", "MFDVB", "Move from Data value Breakpoint Register"}, + {"ee", "MFDVBM", "Move from Data Value Breakpoint Mask Register"}, + {"ee", "MFIAB", "Move from Instruction Address Breakpoint Register"}, + {"ee", "MFIABM", "Move from Instruction Address Breakpoint Mask Register"}, + {"ee", "MFPC", "Move from Performance Counter"}, + {"ee", "MFPS", "Move from Performance Event Specifier"}, + {"ee", "MTBPC", "Move to Breakpoint Control Register"}, + {"ee", "MTC0", "Move to System Control Coprocessor"}, + {"ee", "MTDAB", "Move to Data Address Breakpoint Register"}, + {"ee", "MTDABM", "Move to Data Address Breakpoint Mask Register"}, + {"ee", "MTDVB", "Move to Data Value Breakpoint Register"}, + {"ee", "MTDVBM", "Move to Data Value Breakpoint Mask Register"}, + {"ee", "MTIAB", "Move to Instruction Address Breakpoint Register"}, + {"ee", "MTIABM", "Move to Instruction Address Breakpoint Mask Register"}, + {"ee", "MTPC", "Move to Performance Counter"}, + {"ee", "MTPS", "Move to Performance Event Specifier"}, + {"ee", "TLBP", "Probe TLB for Matching Entry"}, + {"ee", "TLBR", "Read Indexed TLB Entry"}, + {"ee", "TLBWI", "Write Index TLB Entry"}, + {"ee", "TLBWR", "Write Random TLB Entry"}, + {"ee", "ABS", "Floating Point Absolute Value"}, + {"ee", "ADD", "Floating Point ADD"}, + {"ee", "ADDA", "Floating Point Add to Accumulator"}, + {"ee", "BC1F", "Branch on FP False"}, + {"ee", "BC1FL", "Branch on FP False Likely"}, + {"ee", "BC1T", "Branch on FP True"}, + {"ee", "BC1TL", "Branch on FP True Likely"}, + {"ee", "C.EQ.S", "Floating Point Compare"}, + {"ee", "c.f.s", "Floating Point Compare"}, + {"ee", "c.le.s", "Floating Point Compare"}, + {"ee", "c.lt.s", "Floating Point Compare"}, + {"ee", "CFC1", "Move Control Word from Floating Point"}, + {"ee", "CTC1", "Move Control Word to Floating Point"}, + {"ee", "CVT", "Fixed-point Convert to Single Floating Point"}, + {"ee", "CVT", "Floating Point Convert to Word Fixed-point"}, + {"ee", "DIV", "Floating Point Divide"}, + {"ee", "LWC1", "Load Word to Floating Point"}, + {"ee", "MADD", "Floating Point Multiply-ADD"}, + {"ee", "MADDA", "Floating Point Multiply-Add"}, + {"ee", "MAX", "Floating Point Maximum"}, + {"ee", "MFC1", "Move Word from Floating Point"}, + {"ee", "MIN", "Floating Point Minimum"}, + {"ee", "MOV", "Floating Point Move"}, + {"ee", "MSUB", "Floating Point Multiply and Subtract"}, + {"ee", "MSUBA", "Floating Point Multiply and Subtract from Accumulator"}, + {"ee", "MTC1", "Move Word to Floating Point"}, + {"ee", "MUL", "Floating Point Multiply"}, + {"ee", "MULA", "Floating Point Multiply to Accumulator"}, + {"ee", "NEG", "Floating Point Negate"}, + {"ee", "RSQRT", "Floating Point Reciprocal Square Root"}, + {"ee", "SQRT", "Floating Point Square Root"}, + {"ee", "SUB", "Floating Point Subtract"}, + {"ee", "SUBA", "Floating Point Subtract to Accumulator"}, + {"ee", "SWC1", "Store Word from Floating Point"}, + {"vu", "ABS", "Absolute Value"}, + {"vu", "ADD", "Add"}, + {"vu", "ADDi", "Add to I Register"}, + {"vu", "ADDq", "Add to Q Register"}, + {"vu", "ADDbc", "Broadcast Add"}, + {"vu", "ADDA", "Add to Accumulator"}, + {"vu", "ADDAi", "Add I Register to Accumulator"}, + {"vu", "ADDAq", "Add Q Register to Accumulator"}, + {"vu", "ADDAbc", "Broadcast Add to Accumulator"}, + {"vu", "CLIP", "Clipping Judgment"}, + {"vu", "FTOI0", "Convert to Fixed Point"}, + {"vu", "FTOI4", "Convert to Fixed Point"}, + {"vu", "FTOI12", "Convert to Fixed Point"}, + {"vu", "FTOI15", "Convert to Fixed Point"}, + {"vu", "ITOF0", "Convert to Floating-Point Number"}, + {"vu", "ITOF4", "Convert to Floating-Point Number"}, + {"vu", "ITOF12", "Convert to Floating-Point Number"}, + {"vu", "ITOF15", "Convert to Floating-Point Number"}, + {"vu", "MADD", "Product Sum"}, + {"vu", "MADDi", "Product Sum with I Register"}, + {"vu", "MADDq", "Product Sum by Q Register"}, + {"vu", "MADDbc", "Broadcast Product Sum"}, + {"vu", "MADDA", "Product Sum to Accumulator"}, + {"vu", "MADDAi", "Product Sum by I register, to Accumulator"}, + {"vu", "MADDAq", "Product Sum by Q Register, to Accumulator"}, + {"vu", "MADDAbc", "Broadcast Product Sum to Accumulator"}, + {"vu", "MAX", "Maximum Value"}, + {"vu", "MAXi", "Maximum Value"}, + {"vu", "MAXbc", "Maximum Value"}, + {"vu", "MINI", "Minimum Value"}, + {"vu", "MINIi", "Minimum Value"}, + {"vu", "MINIbc", "Minimum Value"}, + {"vu", "MSUB", "Multiply and Subtract"}, + {"vu", "MSUBi", "Multiply and Subtract with I Register"}, + {"vu", "MSUBq", "Multiply and Subtract by Q Register"}, + {"vu", "MSUBbc", "Broadcast Multiply and Subtract"}, + {"vu", "MSUBA", "Multiply and Subtract to Accumulator"}, + {"vu", "MSUBAi", "Multiply and Subtract with I Register, to Accumulator"}, + {"vu", "MSUBAq", "Multiply and Subtract by Q Register, to Accumulator"}, + {"vu", "MSUBAbc", "Broadcast Multiply and Subtract to Accumulator"}, + {"vu", "MUL", "Multiply"}, + {"vu", "MULi", "Multiply by I Register"}, + {"vu", "MULq", "Multiply by Q Register"}, + {"vu", "MULbc", "Multiply by Broadcast"}, + {"vu", "MULA", "Multiply to Accumulator"}, + {"vu", "MULAi", "Multiply by I Register, to Accumulator"}, + {"vu", "MULAq", "Multiply by Q Register, to Accumulator"}, + {"vu", "MULAbc", "Broadcast Multiply by broadcast, to Accumulator"}, + {"vu", "NOP", "No Operation"}, + {"vu", "OPMULA", "Vector Outer Product"}, + {"vu", "OPMSUB", "Vector Outer Product"}, + {"vu", "SUB", "Subtract"}, + {"vu", "SUBi", "Subtract I Register"}, + {"vu", "SUBq", "Subtract Q Register"}, + {"vu", "SUBbc", "Broadcast Subtract"}, + {"vu", "SUBA", "Substract to Accumulator"}, + {"vu", "SUBAi", "Subtract I Register to Accumulator"}, + {"vu", "SUBAq", "Subtract Q Register to Accumulator"}, + {"vu", "SUBAbc", "Broadcast Subtract to Accumulator"}, + {"vu", "B", "Unconditional Branch"}, + {"vu", "BAL", "Unconditional Branch with Saving Address"}, + {"vu", "DIV", "Divide"}, + {"vu", "EATAN", "Arctangent"}, + {"vu", "EATANxy", "Arctangent"}, + {"vu", "EATANxz", "Arctangent"}, + {"vu", "EEXP", "Exponent"}, + {"vu", "ELENG", "Length"}, + {"vu", "ERCPR", "Reciprocal Number"}, + {"vu", "ERLENG", "Reciprocal Number of Length"}, + {"vu", "ERSADD", "Reciprocal Number"}, + {"vu", "ERSQRT", "Reciprocal Number of Square Root"}, + {"vu", "ESADD", "Sum of Square Numbers"}, + {"vu", "ESIN", "Sine"}, + {"vu", "ESQRT", "Square Root"}, + {"vu", "ESUM", "Sum of Each Field"}, + {"vu", "FCAND", "Test Clipping Flag"}, + {"vu", "FCEQ", "Test Clipping Flag"}, + {"vu", "FCGET", "Get Clipping Flag"}, + {"vu", "FCOR", "Test Clipping Flag"}, + {"vu", "FCSET", "Setting Clipping Flag"}, + {"vu", "FMAND", "Test MAC Flag Check"}, + {"vu", "FMEQ", "Test MAC Flag Check"}, + {"vu", "FMOR", "Test MAC Flag Check"}, + {"vu", "FSAND", "Test Status Flag Check"}, + {"vu", "FSEQ", "Test Status Flag Check"}, + {"vu", "FSOR", "Test Status Flag"}, + {"vu", "FSSET", "Set Sticky Flags"}, + {"vu", "IADD", "ADD Integer"}, + {"vu", "IADDI", "Add Immediate Value Integer"}, + {"vu", "IADDIU", "Add Immediate Integer"}, + {"vu", "IAND", "Logical Product"}, + {"vu", "IBEQ", "Conditional Branch"}, + {"vu", "IBGEZ", "Conditional Branch"}, + {"vu", "IBGTZ", "Conditional Branch"}, + {"vu", "IBLEZ", "Conditional Branch"}, + {"vu", "IBLTZ", "Conditional Branch"}, + {"vu", "IBNE", "Conditional Branch"}, + {"vu", "ILW", "Integer Load with Offset Specification"}, + {"vu", "ILWR", "Integer Load"}, + {"vu", "IOR", "Logical Sum"}, + {"vu", "ISUB", "Integer Subtract"}, + {"vu", "ISUBIU", "Immediate Value Integer Subtract"}, + {"vu", "ISW", "Integer Store with Offset"}, + {"vu", "ISWR", "Integer Store"}, + {"vu", "JALR", "Unconditional Jump with Address Saving"}, + {"vu", "JR", "Unconditional Jump"}, + {"vu", "LQ", "Load Qword"}, + {"vu", "LQD", "Load Qword with Pre-Decrement"}, + {"vu", "LQI", "Load with Post-Increment"}, + {"vu", "MFIR", "Move from Integer Register to Floating-Point Register"}, + {"vu", "MFP", "Move from P Register to Floating-Point Register"}, + {"vu", "MOVE", "Transfer between Floating-Point Registers"}, + {"vu", "MR32", "Move with Rotate"}, + {"vu", "MTIR", "Move from Floating-Point Register to Integer Register"}, + {"vu", "RGET", "Get Random Number"}, + {"vu", "RINIT", "Random Number Intialize"}, + {"vu", "RNEXT", "Next Random Number"}, + {"vu", "RSQRT", "Square Root Division"}, + {"vu", "RXOR", "Random Number Set"}, + {"vu", "SQ", "Store Qword with Offset"}, + {"vu", "SQD", "Store Qword with Pre-Decrement"}, + {"vu", "SQI", "Store with Post-Increment"}, + {"vu", "SQRT", "Square Root"}, + {"vu", "WAITP", "P Register Syncronize"}, + {"vu", "WAITQ", "Q Register Syncronize"}, + {"vu", "XGKICK", "GIF Control"}, + {"vu", "XITOP", "VIF Control"}, + {"vu", "XTOP", "VIF Control"}, + {"vu", "BC2F", "Branch on COP2 Conditional Signal"}, + {"vu", "BC2FL", "Branch on COP2 Conditional Signal"}, + {"vu", "BC2T", "Branch on COP2 Conditional Signal"}, + {"vu", "BC2TL", "Branch on COP2 Conditional signal"}, + {"vu", "CFC2", "Transfer Integer Data from VU to EE Core"}, + {"vu", "CTC2", "Transfer Integer Data from EE Core to VU"}, + {"vu", "LQC2", "Floating-Point Data Transfer from EE Core to VU"}, + {"vu", "QMFC2", "Floating-Point Data Transfer from VU to EE Core"}, + {"vu", "QMTC2", "Floating-Point Data Transfer from EE Core to VU"}, + {"vu", "SQC2", "Floating-Point Data Transfer from VU to EE Core"}, + {"vu", "VABS", "Absolute Value"}, + {"vu", "VADD", "Add"}, + {"vu", "VADDi", "Add to I Register"}, + {"vu", "VADDq", "Add to Q Register"}, + {"vu", "VADDbc", "Broadcast Add"}, + {"vu", "VADDA", "Add to Accumulator"}, + {"vu", "VADDAi", "Add I Register to Accumulator"}, + {"vu", "VADDAq", "Add Q Register to Accumulator"}, + {"vu", "VADDAbc", "Broadcast Add to Accumulator"}, + {"vu", "VCALLMS", "Start Micro Sub-Routine"}, + {"vu", "VCALLMSR", "Start Micro Sub-Routine by Register"}, + {"vu", "VCLIP", "Clipping Judgment"}, + {"vu", "VDIV", "Divide"}, + {"vu", "VFTOI0", "Conversion to Fixed Point"}, + {"vu", "VFTOI4", "Conversion to Fixed Point"}, + {"vu", "VFTOI12", "Conversion to Fixed Point"}, + {"vu", "VFTOI15", "Conversion to Fixed Point"}, + {"vu", "VIADD", "Add Integer"}, + {"vu", "VIADDI", "Add Immediate Value Integer"}, + {"vu", "VIAND", "Logical Product"}, + {"vu", "VILWR", "Integer Load"}, + {"vu", "VIOR", "Logical Sum"}, + {"vu", "VISUB", "Integer Subtract"}, + {"vu", "VISWR", "Integer Store"}, + {"vu", "VITOF0", "Conversion to Floating-Point Number"}, + {"vu", "VITOF4", "Conversion to Floating-Point Number"}, + {"vu", "VITOF12", "Conversion to Floating-Point Number"}, + {"vu", "VITOF15", "Conversion to Floating-Point Number"}, + {"vu", "VLQD", "Load with Pre-Decrement"}, + {"vu", "VLQI", "Load with Post-Increment"}, + {"vu", "VMADD", "Product Sum"}, + {"vu", "VMADDi", "Product Sum with I Register"}, + {"vu", "VMADDq", "Product Sum with Q Register"}, + {"vu", "VMADDbc", "Broadcast Product Sum"}, + {"vu", "VMADDA", "Product Sum to Accumulator"}, + {"vu", "VMADDAi", "Product Sum with I Register, to Accumulator"}, + {"vu", "VMADDAq", "Product Sum with Q Register, to Accumulator"}, + {"vu", "VMADDAbc", "Broadcast Product Sum to Accumulator"}, + {"vu", "VMAX", "Maximum Value"}, + {"vu", "VMAXi", "Maximum Value"}, + {"vu", "VMAXbc", "Maximum Value"}, + {"vu", "VMFIR", "Transfer from Integer Register to Floating-Point Register"}, + {"vu", "VMINI", "Minimum Value"}, + {"vu", "VMINIi", "Minimum Value"}, + {"vu", "VMINIbc", "Minimum Value"}, + {"vu", "VMOVE", "Transfer between Floating-Point Registers"}, + {"vu", "VMR32", "Vector Rotate"}, + {"vu", "VMSUB", "Multiply and Subtract"}, + {"vu", "VMSUBi", "Multiply and Subtract with I Register"}, + {"vu", "VMSUBq", "Multiply and Subtract Q Register"}, + {"vu", "VMSUBbc", "Broadcast Multiply and Subtract"}, + {"vu", "VMSUBA", "Multiply and Subtract to Accumulator"}, + {"vu", "VMSUBAi", "Multiply and Subtract with I Register, to Accumulator"}, + {"vu", "VMSUBAq", "Multiply and Subtract with Q Register, to Accumulator"}, + {"vu", "VMSUBAbc", "Broadcast Multiply and Subtract to Accumulator"}, + {"vu", "VMTIR", "Transfer from Floating-Point Register to Integer Register"}, + {"vu", "VMUL", "Multiply"}, + {"vu", "VMULi", "Multiply by I Register"}, + {"vu", "VMULq", "Multiply by Q Register"}, + {"vu", "VMULbc", "Broadcast Multiply"}, + {"vu", "VMULA", "Multiply to Accumulator"}, + {"vu", "VMULAi", "Multiply by I Register to Accumulator"}, + {"vu", "VMULAq", "Multiply by Q Register to Accumulator"}, + {"vu", "VMULAbc", "Broadcast Multiply to Accumulator"}, + {"vu", "VNOP", "No Operation"}, + {"vu", "VOPMULA", "Vector Outer Product"}, + {"vu", "VOPMSUB", "Vector Outer Product"}, + {"vu", "VRGET", "Get Random Numbers"}, + {"vu", "VRINIT", "Random Number Initial Set"}, + {"vu", "VRNEXT", "New Random Numbers"}, + {"vu", "VRSQRT", "Square Root Division"}, + {"vu", "VRXOR", "Random Number Set"}, + {"vu", "VSQD", "Store with Pre-Decrement"}, + {"vu", "VSQI", "Store with Post-Increment"}, + {"vu", "VSQRT", "Square Root"}, + {"vu", "VSUB", "Subtract"}, + {"vu", "VSUBi", "Subtract I Register"}, + {"vu", "VSUBq", "Subtract Q Register"}, + {"vu", "VSUBbc", "Broadcast Subtract"}, + {"vu", "VSUBA", "Subtract to Accumulator"}, + {"vu", "VSUBAi", "Subtract I Register to Accumulator"}, + {"vu", "VSUBAq", "Subtract Q Register to Accumulator"}, + {"vu", "VSUBAbc", "Broadcast Subtract to Accumulator"}, + {"vu", "VWAITQ", "Q Register Synchronize"}, +}; +} diff --git a/lsp/state/data/mips_instructions.h b/lsp/state/data/mips_instructions.h new file mode 100644 index 0000000000..4ec6626c89 --- /dev/null +++ b/lsp/state/data/mips_instructions.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace LSPData { +struct MIPSInstruction { + std::string type; + std::string mnemonic; + std::string description; +}; + +extern const std::vector MIPS_INSTRUCTION_LIST; +} // namespace LSPData diff --git a/lsp/state/workspace.cpp b/lsp/state/workspace.cpp new file mode 100644 index 0000000000..2f3cb3f9bc --- /dev/null +++ b/lsp/state/workspace.cpp @@ -0,0 +1,289 @@ +#include "workspace.h" + +#include + +#include "common/log/log.h" + +#include "lsp/protocol/common_types.h" + +LSPSpec::DocumentUri uri_from_path(fs::path path) { + // Replace slash type on windows + std::string path_str = path.string(); +#ifdef _WIN32 + std::replace(path_str.begin(), path_str.end(), '\\', '/'); +#endif + return fmt::format("file:///{}", path_str); +} + +Workspace::Workspace(){}; +Workspace::~Workspace(){}; + +bool Workspace::is_initialized() { + return m_initialized; +}; + +void Workspace::set_initialized(bool new_value) { + m_initialized = new_value; +} + +std::optional Workspace::get_tracked_ir_file(const LSPSpec::URI& file_uri) { + if (m_tracked_ir_files.count(file_uri) == 0) { + return {}; + } + return m_tracked_ir_files[file_uri]; +} + +std::optional Workspace::get_symbol_info_from_all_types( + const std::string& symbol_name, + const LSPSpec::DocumentUri& all_types_uri) { + if (m_tracked_all_types_files.count(all_types_uri) == 0) { + return {}; + } + const auto& dts = m_tracked_all_types_files[all_types_uri].m_dts; + if (dts.symbol_definition_info.count(symbol_name) == 0) { + return {}; + } + return dts.symbol_definition_info.at(symbol_name); +} + +void Workspace::start_tracking_file(const LSPSpec::DocumentUri& file_uri, + const std::string& language_id, + const std::string& content) { + if (language_id == "opengoal-ir") { + lg::debug("new ir file"); + WorkspaceIRFile file(content); + lg::debug("parsed!"); + m_tracked_ir_files[file_uri] = file; + if (!file.m_all_types_uri.empty()) { + if (m_tracked_all_types_files.count(file.m_all_types_uri) == 0) { + lg::debug("new all-types file"); + m_tracked_all_types_files[file.m_all_types_uri] = WorkspaceAllTypesFile( + file.m_all_types_uri, file.m_game_version, file.m_all_types_file_path); + m_tracked_all_types_files[file.m_all_types_uri].parse_type_system(); + } + + } + } + // TODO - only supporting IR files currently! +} + +void Workspace::update_tracked_file(const LSPSpec::DocumentUri& file_uri, + const std::string& content) { + // Check if the file is already tracked or not, this is done because change events don't give + // language details it's assumed you are keeping track of that! + if (m_tracked_ir_files.count(file_uri) != 0) { + WorkspaceIRFile file(content); + m_tracked_ir_files[file_uri] = file; + // There is the potential for the all-types to have changed, albeit this is probably never going + // to happen + if (!file.m_all_types_uri.empty() && + m_tracked_all_types_files.count(file.m_all_types_uri) == 0) { + auto& all_types_file = m_tracked_all_types_files[file.m_all_types_uri]; + all_types_file.m_file_path = file.m_all_types_file_path; + all_types_file.m_uri = file.m_all_types_uri; + all_types_file.m_game_version = file.m_game_version; + all_types_file.update_type_system(); + } + } + + if (m_tracked_all_types_files.count(file_uri) != 0) { + // If the all-types file has changed, re-parse it + // NOTE - this assumes its still for the same game version! + m_tracked_all_types_files[file_uri].update_type_system(); + } +}; + +void Workspace::stop_tracking_file(const LSPSpec::DocumentUri& file_uri) { + if (m_tracked_ir_files.count(file_uri) != 0) { + m_tracked_ir_files.erase(file_uri); + } + if (m_tracked_all_types_files.count(file_uri) != 0) { + m_tracked_all_types_files.erase(file_uri); + } +} + +WorkspaceIRFile::WorkspaceIRFile(const std::string& content) { + // Get all lines of file + std::string::size_type pos = 0; + std::string::size_type prev = 0; + + // TODO - i hate this assignment inside a conditional, get rid of it + while ((pos = content.find('\r\n', prev)) != std::string::npos) { + std::string line = content.substr(prev, pos - prev); + m_lines.push_back(line); + // Run any checks on that line + find_all_types_path(line); + find_function_symbol(m_lines.size() - 1, line); + identify_diagnostics(m_lines.size() - 1, line); + prev = pos + 1; + } + std::string line = content.substr(prev); + m_lines.push_back(line); + find_function_symbol(m_lines.size() - 1, line); + identify_diagnostics(m_lines.size() - 1, line); + + lg::info("Added new file. {} lines with {} symbols and {} diagnostics", m_lines.size(), + m_symbols.size(), m_diagnostics.size()); +} + +// This is kind of a hack, but to ensure consistency. The file will reference the all-types.gc +// file it was generated with, this lets us accurately jump to the definition properly! +void WorkspaceIRFile::find_all_types_path(const std::string& line) { + std::regex regex("; ALL_TYPES=(.*)=(.*)"); + std::smatch matches; + + if (std::regex_search(line, matches, regex)) { + if (matches.size() == 3) { + auto game_version = matches[1]; + auto all_types_path = matches[2]; + lg::debug("Found DTS Path - {} : {}", game_version.str(), all_types_path.str()); + auto all_types_uri = uri_from_path(fs::path(all_types_path.str())); + lg::debug("DTS URI - {}", all_types_uri); + + if (valid_game_version(game_version.str())) { + lg::debug("a"); + m_game_version = game_name_to_version(game_version.str()); + lg::debug("b"); + m_all_types_uri = all_types_uri; + m_all_types_file_path = fs::path(all_types_path.str()); + } else { + lg::error("Invalid game version, ignoring - {}", game_version.str()); + } + } + } +} + +void WorkspaceIRFile::find_function_symbol(const uint32_t line_num_zero_based, + const std::string& line) { + std::regex regex("; \\.function (.*)"); + std::smatch matches; + + if (std::regex_search(line, matches, regex)) { + // NOTE - assumes we can only find 1 function per line + if (matches.size() == 2) { + auto match = matches[1]; + lg::info("Adding Symbol - {}", match.str()); + LSPSpec::DocumentSymbol new_symbol; + new_symbol.m_name = match.str(); + // TODO - function doc-string + // new_symbol.m_detail = ... + new_symbol.m_kind = LSPSpec::SymbolKind::Function; + LSPSpec::Range symbol_range; + symbol_range.m_start = {line_num_zero_based, 0}; + symbol_range.m_end = {line_num_zero_based, 0}; // NOTE - set on the next function + new_symbol.m_range = symbol_range; + LSPSpec::Range symbol_selection_range; + symbol_selection_range.m_start = {line_num_zero_based, 0}; + symbol_selection_range.m_end = {line_num_zero_based, (uint32_t)line.length() - 1}; + new_symbol.m_selectionRange = symbol_selection_range; + m_symbols.push_back(new_symbol); + } + } + + std::regex end_function("^;; \\.endfunction\\s*$"); + if (std::regex_match(line, end_function)) { + lg::info("Found end of previous function on line - {}", line); + // Set the previous symbols end-line + if (!m_symbols.empty()) { + m_symbols[m_symbols.size() - 1].m_range.m_end.m_line = line_num_zero_based - 1; + } + } +} + +void WorkspaceIRFile::identify_diagnostics(const uint32_t line_num_zero_based, + const std::string& line) { + std::regex info_regex(";; INFO: (.*)"); + std::regex warn_regex(";; WARN: (.*)"); + std::smatch info_matches; + std::smatch warn_matches; + + LSPSpec::Range diag_range; + diag_range.m_start = {line_num_zero_based, 0}; + diag_range.m_end = {line_num_zero_based, (uint32_t)line.length() - 1}; + + // Check for an info-level warnings + if (std::regex_search(line, info_matches, info_regex)) { + // NOTE - assumes we can only find 1 function per line + if (info_matches.size() == 2) { + auto match = info_matches[1]; + lg::debug("Found info-level diagnostic - {}", match.str()); + LSPSpec::Diagnostic new_diag; + new_diag.m_severity = LSPSpec::DiagnosticSeverity::Information; + new_diag.m_message = match.str(); + new_diag.m_range = diag_range; + new_diag.m_source = "OpenGOAL LSP"; + m_diagnostics.push_back(new_diag); + return; + } + } + // Check for a warn level warnings + if (std::regex_search(line, warn_matches, warn_regex)) { + // NOTE - assumes we can only find 1 function per line + if (warn_matches.size() == 2) { + auto match = warn_matches[1]; + lg::debug("Found warn-level diagnostic - {}", match.str()); + LSPSpec::Diagnostic new_diag; + new_diag.m_severity = LSPSpec::DiagnosticSeverity::Error; + new_diag.m_message = match.str(); + new_diag.m_range = diag_range; + new_diag.m_source = "OpenGOAL LSP"; + m_diagnostics.push_back(new_diag); + return; + } + } +} + +std::optional WorkspaceIRFile::get_mips_instruction_at_position( + const LSPSpec::Position position) { + // Split the line on typical word boundaries + std::string line = m_lines.at(position.m_line); + std::smatch matches; + std::regex regex("[\\w\\.]+"); + + if (std::regex_search(line, matches, regex)) { + auto match = matches[0]; + lg::info("hover first match - {}", match.str()); + auto match_start = matches.position(0); + auto match_end = match_start + match.length(); + if (position.m_character >= match_start && position.m_character <= match_end) { + return match; + } + } + + return {}; +} + +std::optional WorkspaceIRFile::get_symbol_at_position( + const LSPSpec::Position position) { + // Split the line on typical word boundaries + std::string line = m_lines.at(position.m_line); + lg::debug("symbol checking - '{}'", line); + std::smatch matches; + std::regex regex("[\\w\\.\\-_!<>*]+"); + std::regex_token_iterator rend; + + std::regex_token_iterator match(line.begin(), line.end(), regex); + while (match != rend) { + lg::debug("match - '{}'", match->str()); + auto match_start = std::distance(line.begin(), match->first); + auto match_end = match_start + match->length(); + if (position.m_character >= match_start && position.m_character <= match_end) { + lg::debug("returning"); + return match->str(); + } + match++; + } + + return {}; +} + +void WorkspaceAllTypesFile::parse_type_system() { + lg::debug("DTS Loading - '{}'", m_file_path.string()); + m_dts.parse_type_defs({m_file_path.string()}); + lg::debug("DTS Loaded At - '{}'", m_file_path.string()); +} + +void WorkspaceAllTypesFile::update_type_system() { + m_dts = decompiler::DecompilerTypeSystem(m_game_version); + parse_type_system(); +} diff --git a/lsp/state/workspace.h b/lsp/state/workspace.h new file mode 100644 index 0000000000..ae69f854be --- /dev/null +++ b/lsp/state/workspace.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +#include "common/util/FileUtil.h" + +#include "decompiler/util/DecompilerTypeSystem.h" +#include "lsp/protocol/common_types.h" +#include "lsp/protocol/document_diagnostics.h" +#include "lsp/protocol/document_symbols.h" + +class WorkspaceIRFile { + public: + WorkspaceIRFile(){}; + WorkspaceIRFile(const std::string& content); + // TODO - make private + int32_t version; + std::vector m_lines; + std::vector m_symbols; + std::vector m_diagnostics; + GameVersion m_game_version; + LSPSpec::DocumentUri m_all_types_uri = ""; + fs::path m_all_types_file_path; + + std::optional get_mips_instruction_at_position(const LSPSpec::Position position); + std::optional get_symbol_at_position(const LSPSpec::Position position); + + private: + void find_all_types_path(const std::string& line); + void find_function_symbol(const uint32_t line_num_zero_based, const std::string& line); + /// @brief Make any relevant diagnostics on the IR line. + /// It's assumed each line in an IR can have atmost one diagnostic, and they are contained to just + /// that line! + /// @param line_num_zero_based + /// @param line + void identify_diagnostics(const uint32_t line_num_zero_based, const std::string& line); +}; + +class WorkspaceAllTypesFile { + public: + WorkspaceAllTypesFile() : m_dts(GameVersion::Jak1){}; + WorkspaceAllTypesFile(const LSPSpec::DocumentUri& uri, + const GameVersion version, + const fs::path file_path) + : m_uri(uri), m_game_version(version), m_dts(m_game_version), m_file_path(file_path){}; + + GameVersion m_game_version; + LSPSpec::DocumentUri m_uri; + decompiler::DecompilerTypeSystem m_dts; + fs::path m_file_path; + + void parse_type_system(); + void update_type_system(); +}; + +class Workspace { + public: + Workspace(); + virtual ~Workspace(); + + bool is_initialized(); + void set_initialized(bool new_value); + + void start_tracking_file(const LSPSpec::DocumentUri& file_uri, + const std::string& language_id, + const std::string& content); + void update_tracked_file(const LSPSpec::DocumentUri& file_uri, const std::string& content); + void stop_tracking_file(const LSPSpec::DocumentUri& file_uri); + std::optional get_tracked_ir_file(const LSPSpec::URI& file_uri); + std::optional get_symbol_info_from_all_types( + const std::string& symbol_name, + const std::string& all_types_uri); + + private: + bool m_initialized = false; + std::unordered_map m_tracked_ir_files = {}; + std::unordered_map m_tracked_all_types_files = {}; +}; diff --git a/lsp/transport/stdio.cpp b/lsp/transport/stdio.cpp new file mode 100644 index 0000000000..721acb9620 --- /dev/null +++ b/lsp/transport/stdio.cpp @@ -0,0 +1,109 @@ +/* + https://github.com/svenstaro/glsl-language-server/blob/master/LICENSE + + MIT License + + Copyright (c) 2017 Sven-Hendrik Haase + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "stdio.h" + +#include "common/log/log.h" +#include "common/util/FileUtil.h" + +MessageBuffer::MessageBuffer() {} +MessageBuffer::~MessageBuffer() {} + +void MessageBuffer::handle_char(char c) { + m_raw_message += c; + + if (!m_reading_content) { + auto [header_name, header_value] = try_parse_header(m_raw_message); + // Check whether we were actually able to parse a header. + // If so, add it to our known headers. + // We'll also reset our string then. + if (!header_name.empty()) { + lg::debug("found header!"); + m_headers[header_name] = header_value; + m_raw_message.clear(); + } + } + + // A sole \r\n is the separator between the header block and the body block + // but we don't need it. + if (m_raw_message == "\r\n") { + m_raw_message.clear(); + m_is_header_done = true; + m_reading_content = true; + lg::debug("Header complete, content length: {}", m_headers["Content-Length"]); + } + + if (m_is_header_done) { + // Now that we know that we're in the body, we just have to count until + // we reach the length of the body as provided in the Content-Length + // header. + auto content_length = std::stoi(m_headers["Content-Length"]); + if (m_raw_message.length() == content_length) { + m_body = json::parse(m_raw_message); + m_reading_content = false; + } + } +} + +const std::map& MessageBuffer::headers() const { + return m_headers; +} + +const json& MessageBuffer::body() const { + return m_body; +} + +const std::string& MessageBuffer::raw() const { + return m_raw_message; +} + +bool MessageBuffer::message_completed() { + if (m_is_header_done && !m_body.empty()) { + return true; + } + return false; +} + +std::tuple MessageBuffer::try_parse_header(std::string& message) const { + auto eol_pos = message.find("\r\n"); + if (eol_pos != std::string::npos) { + std::string header_string = message.substr(0, eol_pos); + auto delim_pos = header_string.find(":"); + if (delim_pos != std::string::npos) { + std::string header_name = header_string.substr(0, delim_pos); + std::string header_value = header_string.substr(delim_pos + 1); + return std::make_tuple(header_name, header_value); + } + } + return std::make_tuple(std::string{}, std::string{}); +} + +void MessageBuffer::clear() { + m_raw_message.clear(); + m_headers.clear(); + m_body.clear(); + m_is_header_done = false; +} diff --git a/lsp/transport/stdio.h b/lsp/transport/stdio.h new file mode 100644 index 0000000000..c1d54de790 --- /dev/null +++ b/lsp/transport/stdio.h @@ -0,0 +1,58 @@ +/* + https://github.com/svenstaro/glsl-language-server/blob/master/LICENSE + + MIT License + + Copyright (c) 2017 Sven-Hendrik Haase + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#pragma once + +#include +#include + +#include "third-party/json.hpp" + +using json = nlohmann::json; + +class MessageBuffer { + public: + MessageBuffer(); + virtual ~MessageBuffer(); + void handle_char(char c); + const std::map& headers() const; + const json& body() const; + const std::string& raw() const; + bool message_completed(); + void clear(); + + private: + std::tuple try_parse_header(std::string& message) const; + + std::string m_raw_message; + std::map m_headers; + json m_body; + + // This is set once a sole \r\n is encountered because it denotes that the + // header is done. + bool m_is_header_done = false; + bool m_reading_content = false; +};