mirror of
https://github.com/open-goal/jak-project.git
synced 2024-10-20 21:27:52 -04:00
d95ff2d2fb
Some new hover features: docstrings (`@param/@return` syntax is not yet supported, nor is a syntax-highlighted preview, will do later) ![image](https://user-images.githubusercontent.com/13153231/190830490-d0ef774c-e7e5-4bb9-8007-b366be0f491e.png) better MIPs instruction descriptions ![image](https://user-images.githubusercontent.com/13153231/190830507-0bb35c13-7e88-4b74-a63b-b7fb3587b82e.png) numeric hovers convert to different bases / tell you the OpenGOAL method id (i can't tell you the amount of times ive done -16 / 4 manually myself...) ![image](https://user-images.githubusercontent.com/13153231/190830674-f66ed15a-f983-48ff-b251-259374dfbcac.png) Closes https://github.com/open-goal/opengoal-vscode/issues/33 Closes https://github.com/open-goal/opengoal-vscode/issues/31 Related to https://github.com/open-goal/opengoal-vscode/issues/29
325 lines
11 KiB
C++
325 lines
11 KiB
C++
#include "workspace.h"
|
|
|
|
#include <iomanip>
|
|
#include <regex>
|
|
#include <sstream>
|
|
|
|
#include "common/log/log.h"
|
|
|
|
#include "lsp/protocol/common_types.h"
|
|
|
|
std::string url_encode(const std::string& value) {
|
|
std::ostringstream escaped;
|
|
escaped.fill('0');
|
|
escaped << std::hex;
|
|
|
|
for (std::string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
|
|
std::string::value_type c = (*i);
|
|
|
|
// Keep alphanumeric and other accepted characters intact
|
|
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') {
|
|
escaped << c;
|
|
continue;
|
|
}
|
|
|
|
// Any other characters are percent-encoded
|
|
escaped << std::uppercase;
|
|
escaped << '%' << std::setw(2) << int((unsigned char)c);
|
|
escaped << std::nouppercase;
|
|
}
|
|
|
|
return escaped.str();
|
|
}
|
|
|
|
LSPSpec::DocumentUri uri_from_path(fs::path path) {
|
|
std::string path_str = path.string();
|
|
// Replace slash type on windows
|
|
#ifdef _WIN32
|
|
std::replace(path_str.begin(), path_str.end(), '\\', '/');
|
|
#endif
|
|
// vscode works with proper URL encoded URIs for file paths
|
|
// which means we have to roll our own...
|
|
path_str = url_encode(path_str);
|
|
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<WorkspaceIRFile> 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<DefinitionMetadata> Workspace::get_definition_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_metadata_map.count(symbol_name) == 0) {
|
|
return {};
|
|
}
|
|
return dts.symbol_metadata_map.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 - {}", file_uri);
|
|
WorkspaceIRFile file(content);
|
|
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 - {}", file.m_all_types_uri);
|
|
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) {
|
|
lg::debug("potentially updating - {}", file_uri);
|
|
// 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) {
|
|
lg::debug("updating tracked IR file - {}", file_uri);
|
|
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) {
|
|
lg::debug("updating tracked all types file - {}", file_uri);
|
|
// 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) {
|
|
const auto& game_version = matches[1];
|
|
const 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())) {
|
|
m_game_version = game_name_to_version(game_version.str());
|
|
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) {
|
|
const 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)) {
|
|
// 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::regex error_regex(";; ERROR: (.*)");
|
|
std::smatch info_matches;
|
|
std::smatch warn_matches;
|
|
std::smatch error_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) {
|
|
const auto& match = info_matches[1];
|
|
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) {
|
|
const auto& match = warn_matches[1];
|
|
LSPSpec::Diagnostic new_diag;
|
|
new_diag.m_severity = LSPSpec::DiagnosticSeverity::Warning;
|
|
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 error level warnings
|
|
if (std::regex_search(line, error_matches, error_regex)) {
|
|
// NOTE - assumes we can only find 1 function per line
|
|
if (error_matches.size() == 2) {
|
|
const auto& match = error_matches[1];
|
|
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<std::string> 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)) {
|
|
const auto& match = matches[0];
|
|
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<std::string> 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);
|
|
std::smatch matches;
|
|
std::regex regex("[\\w\\.\\-_!<>*]+");
|
|
std::regex_token_iterator<std::string::iterator> rend;
|
|
|
|
std::regex_token_iterator<std::string::iterator> match(line.begin(), line.end(), regex);
|
|
while (match != rend) {
|
|
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) {
|
|
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();
|
|
}
|