From 400d3ddb08560c9c052423540247039b066439ff Mon Sep 17 00:00:00 2001 From: Itamar Date: Sun, 16 May 2021 17:25:24 +0300 Subject: LanguageServers: Rename AutoCompleteEngine => CodeComprehensionEngine This feels like a better name since the "autocomplete engine" can, in addition to providing autocomplete suggestions, also find declarations of symbols and report back the symbols that are defined in a document. Also, Cpp/ParserAutoComplete has been renamed to CppComprehensionEngine and Shell/AutoComplete has been renamed to ShellComprehensionEngine. --- .../LanguageServers/AutoCompleteEngine.cpp | 34 -- .../LanguageServers/AutoCompleteEngine.h | 44 -- .../HackStudio/LanguageServers/CMakeLists.txt | 2 +- .../HackStudio/LanguageServers/ClientConnection.h | 4 +- .../LanguageServers/CodeComprehensionEngine.cpp | 34 ++ .../LanguageServers/CodeComprehensionEngine.h | 44 ++ .../HackStudio/LanguageServers/Cpp/CMakeLists.txt | 2 +- .../LanguageServers/Cpp/ClientConnection.h | 4 +- .../LanguageServers/Cpp/CppComprehensionEngine.cpp | 593 +++++++++++++++++++++ .../LanguageServers/Cpp/CppComprehensionEngine.h | 110 ++++ .../LanguageServers/Cpp/ParserAutoComplete.cpp | 593 --------------------- .../LanguageServers/Cpp/ParserAutoComplete.h | 110 ---- .../HackStudio/LanguageServers/Cpp/Tests.cpp | 14 +- .../LanguageServers/Shell/AutoComplete.cpp | 236 -------- .../LanguageServers/Shell/AutoComplete.h | 58 -- .../LanguageServers/Shell/CMakeLists.txt | 2 +- .../LanguageServers/Shell/ClientConnection.h | 4 +- .../Shell/ShellComprehensionEngine.cpp | 236 ++++++++ .../Shell/ShellComprehensionEngine.h | 58 ++ 19 files changed, 1091 insertions(+), 1091 deletions(-) delete mode 100644 Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.cpp delete mode 100644 Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.h create mode 100644 Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.cpp create mode 100644 Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.h create mode 100644 Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.cpp create mode 100644 Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.h delete mode 100644 Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.cpp delete mode 100644 Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.h delete mode 100644 Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp delete mode 100644 Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h create mode 100644 Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.cpp create mode 100644 Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.h (limited to 'Userland') diff --git a/Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.cpp b/Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.cpp deleted file mode 100644 index 7a1ede92b3..0000000000 --- a/Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2021, Itamar S. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "AutoCompleteEngine.h" - -namespace LanguageServers { - -AutoCompleteEngine::AutoCompleteEngine(const FileDB& filedb, bool should_store_all_declarations) - : m_filedb(filedb) - , m_store_all_declarations(should_store_all_declarations) -{ -} - -AutoCompleteEngine::~AutoCompleteEngine() -{ -} -void AutoCompleteEngine::set_declarations_of_document(const String& filename, Vector&& declarations) -{ - if (!set_declarations_of_document_callback) - return; - - // Optimization - Only notify callback if declarations have changed - if (auto previous_declarations = m_all_declarations.get(filename); previous_declarations.has_value()) { - if (previous_declarations.value() == declarations) - return; - } - if (m_store_all_declarations) - m_all_declarations.set(filename, declarations); - set_declarations_of_document_callback(filename, move(declarations)); -} -} diff --git a/Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.h b/Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.h deleted file mode 100644 index d4274abd09..0000000000 --- a/Userland/DevTools/HackStudio/LanguageServers/AutoCompleteEngine.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2021, Itamar S. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include "../AutoCompleteResponse.h" -#include "FileDB.h" -#include -#include - -namespace LanguageServers { - -class ClientConnection; - -class AutoCompleteEngine { -public: - AutoCompleteEngine(const FileDB& filedb, bool store_all_declarations = false); - virtual ~AutoCompleteEngine(); - - virtual Vector get_suggestions(const String& file, const GUI::TextPosition& autocomplete_position) = 0; - - // TODO: In the future we can pass the range that was edited and only re-parse what we have to. - virtual void on_edit([[maybe_unused]] const String& file) {}; - virtual void file_opened([[maybe_unused]] const String& file) {}; - - virtual Optional find_declaration_of(const String&, const GUI::TextPosition&) { return {}; }; - -public: - Function&&)> set_declarations_of_document_callback; - -protected: - const FileDB& filedb() const { return m_filedb; } - void set_declarations_of_document(const String&, Vector&&); - const HashMap>& all_declarations() const { return m_all_declarations; } - -private: - HashMap> m_all_declarations; - const FileDB& m_filedb; - bool m_store_all_declarations { false }; -}; -} diff --git a/Userland/DevTools/HackStudio/LanguageServers/CMakeLists.txt b/Userland/DevTools/HackStudio/LanguageServers/CMakeLists.txt index 110e7cca21..8e7c722eb7 100644 --- a/Userland/DevTools/HackStudio/LanguageServers/CMakeLists.txt +++ b/Userland/DevTools/HackStudio/LanguageServers/CMakeLists.txt @@ -2,7 +2,7 @@ compile_ipc(LanguageServer.ipc LanguageServerEndpoint.h) compile_ipc(LanguageClient.ipc LanguageClientEndpoint.h) set(SOURCES - AutoCompleteEngine.cpp + CodeComprehensionEngine.cpp ClientConnection.cpp FileDB.cpp) set(GENERATED_SOURCES diff --git a/Userland/DevTools/HackStudio/LanguageServers/ClientConnection.h b/Userland/DevTools/HackStudio/LanguageServers/ClientConnection.h index 2ad2ce92ac..de1f4398fc 100644 --- a/Userland/DevTools/HackStudio/LanguageServers/ClientConnection.h +++ b/Userland/DevTools/HackStudio/LanguageServers/ClientConnection.h @@ -7,7 +7,7 @@ #pragma once #include "../AutoCompleteResponse.h" -#include "AutoCompleteEngine.h" +#include "CodeComprehensionEngine.h" #include "FileDB.h" #include #include @@ -36,7 +36,7 @@ protected: virtual void find_declaration(GUI::AutocompleteProvider::ProjectLocation const&) override; FileDB m_filedb; - OwnPtr m_autocomplete_engine; + OwnPtr m_autocomplete_engine; }; } diff --git a/Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.cpp b/Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.cpp new file mode 100644 index 0000000000..20f2a7a1d6 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, Itamar S. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CodeComprehensionEngine.h" + +namespace LanguageServers { + +CodeComprehensionEngine::CodeComprehensionEngine(const FileDB& filedb, bool should_store_all_declarations) + : m_filedb(filedb) + , m_store_all_declarations(should_store_all_declarations) +{ +} + +CodeComprehensionEngine::~CodeComprehensionEngine() +{ +} +void CodeComprehensionEngine::set_declarations_of_document(const String& filename, Vector&& declarations) +{ + if (!set_declarations_of_document_callback) + return; + + // Optimization - Only notify callback if declarations have changed + if (auto previous_declarations = m_all_declarations.get(filename); previous_declarations.has_value()) { + if (previous_declarations.value() == declarations) + return; + } + if (m_store_all_declarations) + m_all_declarations.set(filename, declarations); + set_declarations_of_document_callback(filename, move(declarations)); +} +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.h b/Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.h new file mode 100644 index 0000000000..3db803d7c2 --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/CodeComprehensionEngine.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021, Itamar S. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "../AutoCompleteResponse.h" +#include "FileDB.h" +#include +#include + +namespace LanguageServers { + +class ClientConnection; + +class CodeComprehensionEngine { +public: + CodeComprehensionEngine(const FileDB& filedb, bool store_all_declarations = false); + virtual ~CodeComprehensionEngine(); + + virtual Vector get_suggestions(const String& file, const GUI::TextPosition& autocomplete_position) = 0; + + // TODO: In the future we can pass the range that was edited and only re-parse what we have to. + virtual void on_edit([[maybe_unused]] const String& file) {}; + virtual void file_opened([[maybe_unused]] const String& file) {}; + + virtual Optional find_declaration_of(const String&, const GUI::TextPosition&) { return {}; }; + +public: + Function&&)> set_declarations_of_document_callback; + +protected: + const FileDB& filedb() const { return m_filedb; } + void set_declarations_of_document(const String&, Vector&&); + const HashMap>& all_declarations() const { return m_all_declarations; } + +private: + HashMap> m_all_declarations; + const FileDB& m_filedb; + bool m_store_all_declarations { false }; +}; +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/CMakeLists.txt b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CMakeLists.txt index 19e5edd513..de5895e414 100644 --- a/Userland/DevTools/HackStudio/LanguageServers/Cpp/CMakeLists.txt +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CMakeLists.txt @@ -1,5 +1,5 @@ set(SOURCES - ParserAutoComplete.cpp + CppComprehensionEngine.cpp Tests.cpp main.cpp ) diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h index 405727ed46..e3eb4c33a0 100644 --- a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h @@ -6,7 +6,7 @@ #pragma once -#include "ParserAutoComplete.h" +#include "CppComprehensionEngine.h" #include namespace LanguageServers::Cpp { @@ -18,7 +18,7 @@ public: ClientConnection(NonnullRefPtr socket, int client_id) : LanguageServers::ClientConnection(move(socket), client_id) { - m_autocomplete_engine = make(m_filedb); + m_autocomplete_engine = make(m_filedb); m_autocomplete_engine->set_declarations_of_document_callback = [this](const String& filename, Vector&& declarations) { async_declarations_in_document(filename, move(declarations)); }; diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.cpp b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.cpp new file mode 100644 index 0000000000..c637b98a6b --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.cpp @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2021, Itamar S. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CppComprehensionEngine.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LanguageServers::Cpp { + +CppComprehensionEngine::CppComprehensionEngine(const FileDB& filedb) + : CodeComprehensionEngine(filedb, true) +{ +} + +const CppComprehensionEngine::DocumentData* CppComprehensionEngine::get_or_create_document_data(const String& file) +{ + auto absolute_path = filedb().to_absolute_path(file); + if (!m_documents.contains(absolute_path)) { + set_document_data(absolute_path, create_document_data_for(absolute_path)); + } + return get_document_data(absolute_path); +} + +const CppComprehensionEngine::DocumentData* CppComprehensionEngine::get_document_data(const String& file) const +{ + auto absolute_path = filedb().to_absolute_path(file); + auto document_data = m_documents.get(absolute_path); + VERIFY(document_data.has_value()); + return document_data.value(); +} + +OwnPtr CppComprehensionEngine::create_document_data_for(const String& file) +{ + auto document = filedb().get_or_create_from_filesystem(file); + if (!document) + return {}; + return create_document_data(document->text(), file); +} + +void CppComprehensionEngine::set_document_data(const String& file, OwnPtr&& data) +{ + m_documents.set(filedb().to_absolute_path(file), move(data)); +} + +Vector CppComprehensionEngine::get_suggestions(const String& file, const GUI::TextPosition& autocomplete_position) +{ + Cpp::Position position { autocomplete_position.line(), autocomplete_position.column() > 0 ? autocomplete_position.column() - 1 : 0 }; + + dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "CppComprehensionEngine position {}:{}", position.line, position.column); + + const auto* document_ptr = get_or_create_document_data(file); + if (!document_ptr) + return {}; + + const auto& document = *document_ptr; + auto containing_token = document.parser().token_at(position); + auto node = document.parser().node_at(position); + if (!node) { + dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", position.line, position.column); + return {}; + } + + if (node->parent() && node->parent()->parent()) + dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "node: {}, parent: {}, grandparent: {}", node->class_name(), node->parent()->class_name(), node->parent()->parent()->class_name()); + + if (!node->parent()) + return {}; + + auto results = autocomplete_property(document, *node, containing_token); + if (results.has_value()) + return results.value(); + + results = autocomplete_name(document, *node, containing_token); + if (results.has_value()) + return results.value(); + return {}; +} + +Optional> CppComprehensionEngine::autocomplete_name(const DocumentData& document, const ASTNode& node, Optional containing_token) const +{ + auto partial_text = String::empty(); + if (containing_token.has_value() && containing_token.value().type() != Token::Type::ColonColon) { + partial_text = containing_token.value().text(); + } + return autocomplete_name(document, node, partial_text); +} + +Optional> CppComprehensionEngine::autocomplete_property(const DocumentData& document, const ASTNode& node, Optional containing_token) const +{ + if (!containing_token.has_value()) + return {}; + + if (!node.parent()->is_member_expression()) + return {}; + + const auto& parent = static_cast(*node.parent()); + + auto partial_text = String::empty(); + if (containing_token.value().type() != Token::Type::Dot) { + if (&node != parent.m_property) + return {}; + partial_text = containing_token.value().text(); + } + + return autocomplete_property(document, parent, partial_text); +} + +NonnullRefPtrVector CppComprehensionEngine::get_available_declarations(const DocumentData& document, const ASTNode& node, RecurseIntoScopes recurse_into_scopes) const +{ + const Cpp::ASTNode* current = &node; + NonnullRefPtrVector available_declarations; + while (current) { + available_declarations.append(current->declarations()); + current = current->parent(); + } + + available_declarations.append(get_global_declarations_including_headers(document, recurse_into_scopes)); + return available_declarations; +} + +Vector CppComprehensionEngine::autocomplete_name(const DocumentData& document, const ASTNode& node, const String& partial_text) const +{ + auto target_scope = scope_of_name_or_identifier(node); + + auto available_declarations = get_available_declarations(document, node, RecurseIntoScopes::No); + + Vector available_names; + auto add_if_valid = [this, &available_names, &target_scope](auto& decl) { + auto name = decl.m_name; + if (name.is_null() || name.is_empty()) + return; + auto scope = scope_of_declaration(decl); + if (scope.is_null()) + scope = String::empty(); + if (scope != target_scope) + return; + if (!available_names.contains_slow(name)) + available_names.append(name); + }; + + for (auto& decl : available_declarations) { + if (decl.filename() == node.filename() && decl.start().line > node.start().line) + continue; + if (decl.is_variable_or_parameter_declaration()) { + add_if_valid(decl); + } + if (decl.is_struct_or_class()) { + add_if_valid(decl); + } + if (decl.is_function()) { + add_if_valid(decl); + } + if (decl.is_namespace()) { + add_if_valid(decl); + } + } + + Vector suggestions; + for (auto& name : available_names) { + if (name.starts_with(partial_text)) { + suggestions.append({ name.to_string(), partial_text.length(), GUI::AutocompleteProvider::CompletionKind::Identifier }); + } + } + + if (target_scope.is_empty()) { + for (auto& preprocessor_name : document.parser().preprocessor_definitions().keys()) { + if (preprocessor_name.starts_with(partial_text)) { + suggestions.append({ preprocessor_name.to_string(), partial_text.length(), GUI::AutocompleteProvider::CompletionKind::PreprocessorDefinition }); + } + } + } + + return suggestions; +} + +String CppComprehensionEngine::scope_of_name_or_identifier(const ASTNode& node) const +{ + const Name* name = nullptr; + if (node.is_name()) { + name = reinterpret_cast(&node); + } else if (node.is_identifier()) { + auto* parent = node.parent(); + if (!(parent && parent->is_name())) + return {}; + name = reinterpret_cast(parent); + } else { + return String::empty(); + } + + VERIFY(name->is_name()); + + Vector scope_parts; + for (auto& scope_part : name->m_scope) { + scope_parts.append(scope_part.m_name); + } + return String::join("::", scope_parts); +} + +Vector CppComprehensionEngine::autocomplete_property(const DocumentData& document, const MemberExpression& parent, const String partial_text) const +{ + auto type = type_of(document, *parent.m_object); + if (type.is_null()) { + dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "Could not infer type of object"); + return {}; + } + + Vector suggestions; + for (auto& prop : properties_of_type(document, type)) { + if (prop.name.starts_with(partial_text)) { + suggestions.append({ prop.name, partial_text.length(), GUI::AutocompleteProvider::CompletionKind::Identifier }); + } + } + return suggestions; +} + +bool CppComprehensionEngine::is_property(const ASTNode& node) const +{ + if (!node.parent()->is_member_expression()) + return false; + + auto& parent = (MemberExpression&)(*node.parent()); + return parent.m_property.ptr() == &node; +} + +bool CppComprehensionEngine::is_empty_property(const DocumentData& document, const ASTNode& node, const Position& autocomplete_position) const +{ + if (node.parent() == nullptr) + return false; + if (!node.parent()->is_member_expression()) + return false; + auto previous_token = document.parser().token_at(autocomplete_position); + if (!previous_token.has_value()) + return false; + return previous_token.value().type() == Token::Type::Dot; +} + +String CppComprehensionEngine::type_of_property(const DocumentData& document, const Identifier& identifier) const +{ + auto& parent = (const MemberExpression&)(*identifier.parent()); + auto properties = properties_of_type(document, type_of(document, *parent.m_object)); + for (auto& prop : properties) { + if (prop.name == identifier.m_name) + return prop.type->m_name->full_name(); + } + return {}; +} + +String CppComprehensionEngine::type_of_variable(const Identifier& identifier) const +{ + const ASTNode* current = &identifier; + while (current) { + for (auto& decl : current->declarations()) { + if (decl.is_variable_or_parameter_declaration()) { + auto& var_or_param = (VariableOrParameterDeclaration&)decl; + if (var_or_param.m_name == identifier.m_name) { + return var_or_param.m_type->m_name->full_name(); + } + } + } + current = current->parent(); + } + return {}; +} + +String CppComprehensionEngine::type_of(const DocumentData& document, const Expression& expression) const +{ + if (expression.is_member_expression()) { + auto& member_expression = (const MemberExpression&)expression; + if (member_expression.m_property->is_identifier()) + return type_of_property(document, static_cast(*member_expression.m_property)); + return {}; + } + + const Identifier* identifier { nullptr }; + if (expression.is_name()) { + identifier = static_cast(expression).m_name.ptr(); + } else if (expression.is_identifier()) { + identifier = &static_cast(expression); + } else { + dbgln("expected identifier or name, got: {}", expression.class_name()); + VERIFY_NOT_REACHED(); // TODO + } + VERIFY(identifier); + if (is_property(*identifier)) + return type_of_property(document, *identifier); + + return type_of_variable(*identifier); +} + +Vector CppComprehensionEngine::properties_of_type(const DocumentData& document, const String& type) const +{ + auto declarations = get_global_declarations_including_headers(document, RecurseIntoScopes::Yes); + Vector properties; + for (auto& decl : declarations) { + if (!decl.is_struct_or_class()) + continue; + auto& struct_or_class = (StructOrClassDeclaration&)decl; + if (struct_or_class.m_name != type) + continue; + for (auto& member : struct_or_class.m_members) { + properties.append({ member.m_name, member.m_type }); + } + } + return properties; +} + +NonnullRefPtrVector CppComprehensionEngine::get_global_declarations_including_headers(const DocumentData& document, RecurseIntoScopes recurse_into_scopes) const +{ + NonnullRefPtrVector declarations; + for (auto& decl : document.m_declarations_from_headers) + declarations.append(*decl); + + declarations.append(get_global_declarations(document, recurse_into_scopes)); + + return declarations; +} + +NonnullRefPtrVector CppComprehensionEngine::get_global_declarations(const DocumentData& document, RecurseIntoScopes recurse_into_scopes) const +{ + if (recurse_into_scopes == RecurseIntoScopes::Yes) + return get_declarations_recursive(*document.parser().root_node()); + return document.parser().root_node()->declarations(); +} + +NonnullRefPtrVector CppComprehensionEngine::get_declarations_recursive(const ASTNode& node) const +{ + NonnullRefPtrVector declarations; + + for (auto& decl : node.declarations()) { + declarations.append(decl); + if (decl.is_namespace()) { + declarations.append(get_declarations_recursive(decl)); + } + if (decl.is_struct_or_class()) { + for (auto& member_decl : static_cast(decl).declarations()) { + declarations.append(member_decl); + } + } + } + + return declarations; +} + +String CppComprehensionEngine::document_path_from_include_path(const StringView& include_path) const +{ + static Regex library_include("<(.+)>"); + static Regex user_defined_include("\"(.+)\""); + + auto document_path_for_library_include = [&](const StringView& include_path) -> String { + RegexResult result; + if (!library_include.search(include_path, result)) + return {}; + + auto path = result.capture_group_matches.at(0).at(0).view.u8view(); + return String::formatted("/usr/include/{}", path); + }; + + auto document_path_for_user_defined_include = [&](const StringView& include_path) -> String { + RegexResult result; + if (!user_defined_include.search(include_path, result)) + return {}; + + return result.capture_group_matches.at(0).at(0).view.u8view(); + }; + + auto result = document_path_for_library_include(include_path); + if (result.is_null()) + result = document_path_for_user_defined_include(include_path); + + return result; +} + +void CppComprehensionEngine::on_edit(const String& file) +{ + set_document_data(file, create_document_data_for(file)); +} + +void CppComprehensionEngine::file_opened([[maybe_unused]] const String& file) +{ + get_or_create_document_data(file); +} + +Optional CppComprehensionEngine::find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) +{ + const auto* document_ptr = get_or_create_document_data(filename); + if (!document_ptr) + return {}; + + const auto& document = *document_ptr; + auto node = document.parser().node_at(Cpp::Position { identifier_position.line(), identifier_position.column() }); + if (!node) { + dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", identifier_position.line(), identifier_position.column()); + return {}; + } + auto decl = find_declaration_of(document, *node); + if (decl) + return GUI::AutocompleteProvider::ProjectLocation { decl->filename(), decl->start().line, decl->start().column }; + + return find_preprocessor_definition(document, identifier_position); +} + +Optional CppComprehensionEngine::find_preprocessor_definition(const DocumentData& document, const GUI::TextPosition& text_position) +{ + Position cpp_position { text_position.line(), text_position.column() }; + + // Search for a replaced preprocessor token that intersects with text_position + for (auto& replaced_token : document.parser().replaced_preprocessor_tokens()) { + if (replaced_token.token.start() > cpp_position) + continue; + if (replaced_token.token.end() < cpp_position) + continue; + + return GUI::AutocompleteProvider::ProjectLocation { replaced_token.preprocessor_value.filename, replaced_token.preprocessor_value.line, replaced_token.preprocessor_value.column }; + } + return {}; +} + +struct TargetDeclaration { + enum Type { + Variable, + Type, + Function, + Property + } type; + String name; +}; + +static Optional get_target_declaration(const ASTNode& node) +{ + if (!node.is_identifier()) { + dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "node is not an identifier"); + return {}; + } + + String name = static_cast(node).m_name; + + if ((node.parent() && node.parent()->is_function_call()) || (node.parent()->is_name() && node.parent()->parent() && node.parent()->parent()->is_function_call())) { + return TargetDeclaration { TargetDeclaration::Type::Function, name }; + } + + if ((node.parent() && node.parent()->is_type()) || (node.parent()->is_name() && node.parent()->parent() && node.parent()->parent()->is_type())) + return TargetDeclaration { TargetDeclaration::Type::Type, name }; + + if ((node.parent() && node.parent()->is_member_expression())) + return TargetDeclaration { TargetDeclaration::Type::Property, name }; + + return TargetDeclaration { TargetDeclaration::Type::Variable, name }; +} + +RefPtr CppComprehensionEngine::find_declaration_of(const DocumentData& document_data, const ASTNode& node) const +{ + dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "find_declaration_of: {} ({})", document_data.parser().text_of_node(node), node.class_name()); + auto target_decl = get_target_declaration(node); + if (!target_decl.has_value()) + return {}; + + auto declarations = get_available_declarations(document_data, node, RecurseIntoScopes::Yes); + for (auto& decl : declarations) { + if (decl.is_function() && target_decl.value().type == TargetDeclaration::Function) { + if (((Cpp::FunctionDeclaration&)decl).m_name == target_decl.value().name) + return decl; + } + if (decl.is_variable_or_parameter_declaration() && target_decl.value().type == TargetDeclaration::Variable) { + if (((Cpp::VariableOrParameterDeclaration&)decl).m_name == target_decl.value().name) + return decl; + } + + if (decl.is_struct_or_class() && target_decl.value().type == TargetDeclaration::Property) { + // TODO: Also check that the type of the struct/class matches (not just the property name) + for (auto& member : ((Cpp::StructOrClassDeclaration&)decl).m_members) { + VERIFY(node.is_identifier()); + if (member.m_name == target_decl.value().name) { + return member; + } + } + } + + if (decl.is_struct_or_class() && target_decl.value().type == TargetDeclaration::Type) { + if (((Cpp::StructOrClassDeclaration&)decl).m_name == target_decl.value().name) + return decl; + } + } + return {}; +} + +void CppComprehensionEngine::update_declared_symbols(DocumentData& document) +{ + for (auto& include : document.preprocessor().included_paths()) { + auto included_document = get_or_create_document_data(document_path_from_include_path(include)); + if (!included_document) + continue; + for (auto&& decl : get_global_declarations_including_headers(*included_document, RecurseIntoScopes::Yes)) + document.m_declarations_from_headers.set(move(decl)); + } + + Vector declarations; + + for (auto& decl : get_declarations_recursive(*document.parser().root_node())) { + declarations.append({ decl.name(), { document.filename(), decl.start().line, decl.start().column }, type_of_declaration(decl), scope_of_declaration(decl) }); + } + + for (auto& definition : document.preprocessor().definitions()) { + declarations.append({ definition.key, { document.filename(), definition.value.line, definition.value.column }, GUI::AutocompleteProvider::DeclarationType::PreprocessorDefinition, {} }); + } + + set_declarations_of_document(document.filename(), move(declarations)); +} + +GUI::AutocompleteProvider::DeclarationType CppComprehensionEngine::type_of_declaration(const Declaration& decl) +{ + if (decl.is_struct()) + return GUI::AutocompleteProvider::DeclarationType::Struct; + if (decl.is_class()) + return GUI::AutocompleteProvider::DeclarationType::Class; + if (decl.is_function()) + return GUI::AutocompleteProvider::DeclarationType::Function; + if (decl.is_variable_declaration()) + return GUI::AutocompleteProvider::DeclarationType::Variable; + if (decl.is_namespace()) + return GUI::AutocompleteProvider::DeclarationType::Namespace; + if (decl.is_member()) + return GUI::AutocompleteProvider::DeclarationType::Member; + return GUI::AutocompleteProvider::DeclarationType::Variable; +} + +OwnPtr CppComprehensionEngine::create_document_data(String&& text, const String& filename) +{ + auto document_data = make(); + document_data->m_filename = move(filename); + document_data->m_text = move(text); + document_data->m_preprocessor = make(document_data->m_filename, document_data->text()); + document_data->preprocessor().set_ignore_unsupported_keywords(true); + document_data->preprocessor().process(); + + Preprocessor::Definitions all_definitions; + for (auto item : document_data->preprocessor().definitions()) + all_definitions.set(move(item.key), move(item.value)); + + for (auto include : document_data->preprocessor().included_paths()) { + auto included_document = get_or_create_document_data(document_path_from_include_path(include)); + if (!included_document) + continue; + for (auto item : included_document->parser().preprocessor_definitions()) + all_definitions.set(move(item.key), move(item.value)); + } + + document_data->m_parser = make(document_data->preprocessor().processed_text(), filename, move(all_definitions)); + + auto root = document_data->parser().parse(); + + if constexpr (CPP_LANGUAGE_SERVER_DEBUG) + root->dump(0); + + update_declared_symbols(*document_data); + + return document_data; +} + +String CppComprehensionEngine::scope_of_declaration(const Declaration& decl) const +{ + auto parent = decl.parent(); + if (!parent) + return {}; + + if (!parent->is_declaration()) + return {}; + + auto& parent_decl = static_cast(*parent); + + auto parent_scope = scope_of_declaration(parent_decl); + String containing_scope; + if (parent_decl.is_namespace()) + containing_scope = static_cast(parent_decl).m_name; + if (parent_decl.is_struct_or_class()) + containing_scope = static_cast(parent_decl).name(); + + if (parent_scope.is_null()) + return containing_scope; + + return String::formatted("{}::{}", parent_scope, containing_scope); +} + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.h b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.h new file mode 100644 index 0000000000..b3f02b970c --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/CppComprehensionEngine.h @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021, Itamar S. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LanguageServers::Cpp { + +using namespace ::Cpp; + +class CppComprehensionEngine : public CodeComprehensionEngine { +public: + CppComprehensionEngine(const FileDB& filedb); + + virtual Vector get_suggestions(const String& file, const GUI::TextPosition& autocomplete_position) override; + virtual void on_edit(const String& file) override; + virtual void file_opened([[maybe_unused]] const String& file) override; + virtual Optional find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) override; + +private: + struct DocumentData { + const String& filename() const { return m_filename; } + const String& text() const { return m_text; } + const Preprocessor& preprocessor() const + { + VERIFY(m_preprocessor); + return *m_preprocessor; + } + Preprocessor& preprocessor() + { + VERIFY(m_preprocessor); + return *m_preprocessor; + } + const Parser& parser() const + { + VERIFY(m_parser); + return *m_parser; + } + Parser& parser() + { + VERIFY(m_parser); + return *m_parser; + } + + String m_filename; + String m_text; + OwnPtr m_preprocessor; + OwnPtr m_parser; + + // FIXME: This HashTable must be re-computed if a declaration from a header file is modified + HashTable> m_declarations_from_headers; + }; + + Vector autocomplete_property(const DocumentData&, const MemberExpression&, const String partial_text) const; + Vector autocomplete_name(const DocumentData&, const ASTNode&, const String& partial_text) const; + String type_of(const DocumentData&, const Expression&) const; + String type_of_property(const DocumentData&, const Identifier&) const; + String type_of_variable(const Identifier&) const; + bool is_property(const ASTNode&) const; + bool is_empty_property(const DocumentData&, const ASTNode&, const Position& autocomplete_position) const; + RefPtr find_declaration_of(const DocumentData&, const ASTNode&) const; + + enum class RecurseIntoScopes { + No, + Yes + }; + NonnullRefPtrVector get_available_declarations(const DocumentData&, const ASTNode&, RecurseIntoScopes) const; + + struct PropertyInfo { + StringView name; + RefPtr type; + }; + Vector properties_of_type(const DocumentData& document, const String& type) const; + NonnullRefPtrVector get_global_declarations_including_headers(const DocumentData&, RecurseIntoScopes) const; + NonnullRefPtrVector get_global_declarations(const DocumentData&, RecurseIntoScopes) const; + NonnullRefPtrVector get_declarations_recursive(const ASTNode&) const; + + const DocumentData* get_document_data(const String& file) const; + const DocumentData* get_or_create_document_data(const String& file); + void set_document_data(const String& file, OwnPtr&& data); + + OwnPtr create_document_data_for(const String& file); + String document_path_from_include_path(const StringView& include_path) const; + void update_declared_symbols(DocumentData&); + GUI::AutocompleteProvider::DeclarationType type_of_declaration(const Declaration&); + String scope_of_declaration(const Declaration&) const; + String scope_of_name_or_identifier(const ASTNode& node) const; + Optional find_preprocessor_definition(const DocumentData&, const GUI::TextPosition&); + + OwnPtr create_document_data(String&& text, const String& filename); + Optional> autocomplete_property(const DocumentData&, const ASTNode&, Optional containing_token) const; + Optional> autocomplete_name(const DocumentData&, const ASTNode&, Optional containing_token) const; + + HashMap> m_documents; +}; + +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.cpp b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.cpp deleted file mode 100644 index ee6e5bef4b..0000000000 --- a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.cpp +++ /dev/null @@ -1,593 +0,0 @@ -/* - * Copyright (c) 2021, Itamar S. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "ParserAutoComplete.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace LanguageServers::Cpp { - -ParserAutoComplete::ParserAutoComplete(const FileDB& filedb) - : AutoCompleteEngine(filedb, true) -{ -} - -const ParserAutoComplete::DocumentData* ParserAutoComplete::get_or_create_document_data(const String& file) -{ - auto absolute_path = filedb().to_absolute_path(file); - if (!m_documents.contains(absolute_path)) { - set_document_data(absolute_path, create_document_data_for(absolute_path)); - } - return get_document_data(absolute_path); -} - -const ParserAutoComplete::DocumentData* ParserAutoComplete::get_document_data(const String& file) const -{ - auto absolute_path = filedb().to_absolute_path(file); - auto document_data = m_documents.get(absolute_path); - VERIFY(document_data.has_value()); - return document_data.value(); -} - -OwnPtr ParserAutoComplete::create_document_data_for(const String& file) -{ - auto document = filedb().get_or_create_from_filesystem(file); - if (!document) - return {}; - return create_document_data(document->text(), file); -} - -void ParserAutoComplete::set_document_data(const String& file, OwnPtr&& data) -{ - m_documents.set(filedb().to_absolute_path(file), move(data)); -} - -Vector ParserAutoComplete::get_suggestions(const String& file, const GUI::TextPosition& autocomplete_position) -{ - Cpp::Position position { autocomplete_position.line(), autocomplete_position.column() > 0 ? autocomplete_position.column() - 1 : 0 }; - - dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "ParserAutoComplete position {}:{}", position.line, position.column); - - const auto* document_ptr = get_or_create_document_data(file); - if (!document_ptr) - return {}; - - const auto& document = *document_ptr; - auto containing_token = document.parser().token_at(position); - auto node = document.parser().node_at(position); - if (!node) { - dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", position.line, position.column); - return {}; - } - - if (node->parent() && node->parent()->parent()) - dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "node: {}, parent: {}, grandparent: {}", node->class_name(), node->parent()->class_name(), node->parent()->parent()->class_name()); - - if (!node->parent()) - return {}; - - auto results = autocomplete_property(document, *node, containing_token); - if (results.has_value()) - return results.value(); - - results = autocomplete_name(document, *node, containing_token); - if (results.has_value()) - return results.value(); - return {}; -} - -Optional> ParserAutoComplete::autocomplete_name(const DocumentData& document, const ASTNode& node, Optional containing_token) const -{ - auto partial_text = String::empty(); - if (containing_token.has_value() && containing_token.value().type() != Token::Type::ColonColon) { - partial_text = containing_token.value().text(); - } - return autocomplete_name(document, node, partial_text); -} - -Optional> ParserAutoComplete::autocomplete_property(const DocumentData& document, const ASTNode& node, Optional containing_token) const -{ - if (!containing_token.has_value()) - return {}; - - if (!node.parent()->is_member_expression()) - return {}; - - const auto& parent = static_cast(*node.parent()); - - auto partial_text = String::empty(); - if (containing_token.value().type() != Token::Type::Dot) { - if (&node != parent.m_property) - return {}; - partial_text = containing_token.value().text(); - } - - return autocomplete_property(document, parent, partial_text); -} - -NonnullRefPtrVector ParserAutoComplete::get_available_declarations(const DocumentData& document, const ASTNode& node, RecurseIntoScopes recurse_into_scopes) const -{ - const Cpp::ASTNode* current = &node; - NonnullRefPtrVector available_declarations; - while (current) { - available_declarations.append(current->declarations()); - current = current->parent(); - } - - available_declarations.append(get_global_declarations_including_headers(document, recurse_into_scopes)); - return available_declarations; -} - -Vector ParserAutoComplete::autocomplete_name(const DocumentData& document, const ASTNode& node, const String& partial_text) const -{ - auto target_scope = scope_of_name_or_identifier(node); - - auto available_declarations = get_available_declarations(document, node, RecurseIntoScopes::No); - - Vector available_names; - auto add_if_valid = [this, &available_names, &target_scope](auto& decl) { - auto name = decl.m_name; - if (name.is_null() || name.is_empty()) - return; - auto scope = scope_of_declaration(decl); - if (scope.is_null()) - scope = String::empty(); - if (scope != target_scope) - return; - if (!available_names.contains_slow(name)) - available_names.append(name); - }; - - for (auto& decl : available_declarations) { - if (decl.filename() == node.filename() && decl.start().line > node.start().line) - continue; - if (decl.is_variable_or_parameter_declaration()) { - add_if_valid(decl); - } - if (decl.is_struct_or_class()) { - add_if_valid(decl); - } - if (decl.is_function()) { - add_if_valid(decl); - } - if (decl.is_namespace()) { - add_if_valid(decl); - } - } - - Vector suggestions; - for (auto& name : available_names) { - if (name.starts_with(partial_text)) { - suggestions.append({ name.to_string(), partial_text.length(), GUI::AutocompleteProvider::CompletionKind::Identifier }); - } - } - - if (target_scope.is_empty()) { - for (auto& preprocessor_name : document.parser().preprocessor_definitions().keys()) { - if (preprocessor_name.starts_with(partial_text)) { - suggestions.append({ preprocessor_name.to_string(), partial_text.length(), GUI::AutocompleteProvider::CompletionKind::PreprocessorDefinition }); - } - } - } - - return suggestions; -} - -String ParserAutoComplete::scope_of_name_or_identifier(const ASTNode& node) const -{ - const Name* name = nullptr; - if (node.is_name()) { - name = reinterpret_cast(&node); - } else if (node.is_identifier()) { - auto* parent = node.parent(); - if (!(parent && parent->is_name())) - return {}; - name = reinterpret_cast(parent); - } else { - return String::empty(); - } - - VERIFY(name->is_name()); - - Vector scope_parts; - for (auto& scope_part : name->m_scope) { - scope_parts.append(scope_part.m_name); - } - return String::join("::", scope_parts); -} - -Vector ParserAutoComplete::autocomplete_property(const DocumentData& document, const MemberExpression& parent, const String partial_text) const -{ - auto type = type_of(document, *parent.m_object); - if (type.is_null()) { - dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "Could not infer type of object"); - return {}; - } - - Vector suggestions; - for (auto& prop : properties_of_type(document, type)) { - if (prop.name.starts_with(partial_text)) { - suggestions.append({ prop.name, partial_text.length(), GUI::AutocompleteProvider::CompletionKind::Identifier }); - } - } - return suggestions; -} - -bool ParserAutoComplete::is_property(const ASTNode& node) const -{ - if (!node.parent()->is_member_expression()) - return false; - - auto& parent = (MemberExpression&)(*node.parent()); - return parent.m_property.ptr() == &node; -} - -bool ParserAutoComplete::is_empty_property(const DocumentData& document, const ASTNode& node, const Position& autocomplete_position) const -{ - if (node.parent() == nullptr) - return false; - if (!node.parent()->is_member_expression()) - return false; - auto previous_token = document.parser().token_at(autocomplete_position); - if (!previous_token.has_value()) - return false; - return previous_token.value().type() == Token::Type::Dot; -} - -String ParserAutoComplete::type_of_property(const DocumentData& document, const Identifier& identifier) const -{ - auto& parent = (const MemberExpression&)(*identifier.parent()); - auto properties = properties_of_type(document, type_of(document, *parent.m_object)); - for (auto& prop : properties) { - if (prop.name == identifier.m_name) - return prop.type->m_name->full_name(); - } - return {}; -} - -String ParserAutoComplete::type_of_variable(const Identifier& identifier) const -{ - const ASTNode* current = &identifier; - while (current) { - for (auto& decl : current->declarations()) { - if (decl.is_variable_or_parameter_declaration()) { - auto& var_or_param = (VariableOrParameterDeclaration&)decl; - if (var_or_param.m_name == identifier.m_name) { - return var_or_param.m_type->m_name->full_name(); - } - } - } - current = current->parent(); - } - return {}; -} - -String ParserAutoComplete::type_of(const DocumentData& document, const Expression& expression) const -{ - if (expression.is_member_expression()) { - auto& member_expression = (const MemberExpression&)expression; - if (member_expression.m_property->is_identifier()) - return type_of_property(document, static_cast(*member_expression.m_property)); - return {}; - } - - const Identifier* identifier { nullptr }; - if (expression.is_name()) { - identifier = static_cast(expression).m_name.ptr(); - } else if (expression.is_identifier()) { - identifier = &static_cast(expression); - } else { - dbgln("expected identifier or name, got: {}", expression.class_name()); - VERIFY_NOT_REACHED(); // TODO - } - VERIFY(identifier); - if (is_property(*identifier)) - return type_of_property(document, *identifier); - - return type_of_variable(*identifier); -} - -Vector ParserAutoComplete::properties_of_type(const DocumentData& document, const String& type) const -{ - auto declarations = get_global_declarations_including_headers(document, RecurseIntoScopes::Yes); - Vector properties; - for (auto& decl : declarations) { - if (!decl.is_struct_or_class()) - continue; - auto& struct_or_class = (StructOrClassDeclaration&)decl; - if (struct_or_class.m_name != type) - continue; - for (auto& member : struct_or_class.m_members) { - properties.append({ member.m_name, member.m_type }); - } - } - return properties; -} - -NonnullRefPtrVector ParserAutoComplete::get_global_declarations_including_headers(const DocumentData& document, RecurseIntoScopes recurse_into_scopes) const -{ - NonnullRefPtrVector declarations; - for (auto& decl : document.m_declarations_from_headers) - declarations.append(*decl); - - declarations.append(get_global_declarations(document, recurse_into_scopes)); - - return declarations; -} - -NonnullRefPtrVector ParserAutoComplete::get_global_declarations(const DocumentData& document, RecurseIntoScopes recurse_into_scopes) const -{ - if (recurse_into_scopes == RecurseIntoScopes::Yes) - return get_declarations_recursive(*document.parser().root_node()); - return document.parser().root_node()->declarations(); -} - -NonnullRefPtrVector ParserAutoComplete::get_declarations_recursive(const ASTNode& node) const -{ - NonnullRefPtrVector declarations; - - for (auto& decl : node.declarations()) { - declarations.append(decl); - if (decl.is_namespace()) { - declarations.append(get_declarations_recursive(decl)); - } - if (decl.is_struct_or_class()) { - for (auto& member_decl : static_cast(decl).declarations()) { - declarations.append(member_decl); - } - } - } - - return declarations; -} - -String ParserAutoComplete::document_path_from_include_path(const StringView& include_path) const -{ - static Regex library_include("<(.+)>"); - static Regex user_defined_include("\"(.+)\""); - - auto document_path_for_library_include = [&](const StringView& include_path) -> String { - RegexResult result; - if (!library_include.search(include_path, result)) - return {}; - - auto path = result.capture_group_matches.at(0).at(0).view.u8view(); - return String::formatted("/usr/include/{}", path); - }; - - auto document_path_for_user_defined_include = [&](const StringView& include_path) -> String { - RegexResult result; - if (!user_defined_include.search(include_path, result)) - return {}; - - return result.capture_group_matches.at(0).at(0).view.u8view(); - }; - - auto result = document_path_for_library_include(include_path); - if (result.is_null()) - result = document_path_for_user_defined_include(include_path); - - return result; -} - -void ParserAutoComplete::on_edit(const String& file) -{ - set_document_data(file, create_document_data_for(file)); -} - -void ParserAutoComplete::file_opened([[maybe_unused]] const String& file) -{ - get_or_create_document_data(file); -} - -Optional ParserAutoComplete::find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) -{ - const auto* document_ptr = get_or_create_document_data(filename); - if (!document_ptr) - return {}; - - const auto& document = *document_ptr; - auto node = document.parser().node_at(Cpp::Position { identifier_position.line(), identifier_position.column() }); - if (!node) { - dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", identifier_position.line(), identifier_position.column()); - return {}; - } - auto decl = find_declaration_of(document, *node); - if (decl) - return GUI::AutocompleteProvider::ProjectLocation { decl->filename(), decl->start().line, decl->start().column }; - - return find_preprocessor_definition(document, identifier_position); -} - -Optional ParserAutoComplete::find_preprocessor_definition(const DocumentData& document, const GUI::TextPosition& text_position) -{ - Position cpp_position { text_position.line(), text_position.column() }; - - // Search for a replaced preprocessor token that intersects with text_position - for (auto& replaced_token : document.parser().replaced_preprocessor_tokens()) { - if (replaced_token.token.start() > cpp_position) - continue; - if (replaced_token.token.end() < cpp_position) - continue; - - return GUI::AutocompleteProvider::ProjectLocation { replaced_token.preprocessor_value.filename, replaced_token.preprocessor_value.line, replaced_token.preprocessor_value.column }; - } - return {}; -} - -struct TargetDeclaration { - enum Type { - Variable, - Type, - Function, - Property - } type; - String name; -}; - -static Optional get_target_declaration(const ASTNode& node) -{ - if (!node.is_identifier()) { - dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "node is not an identifier"); - return {}; - } - - String name = static_cast(node).m_name; - - if ((node.parent() && node.parent()->is_function_call()) || (node.parent()->is_name() && node.parent()->parent() && node.parent()->parent()->is_function_call())) { - return TargetDeclaration { TargetDeclaration::Type::Function, name }; - } - - if ((node.parent() && node.parent()->is_type()) || (node.parent()->is_name() && node.parent()->parent() && node.parent()->parent()->is_type())) - return TargetDeclaration { TargetDeclaration::Type::Type, name }; - - if ((node.parent() && node.parent()->is_member_expression())) - return TargetDeclaration { TargetDeclaration::Type::Property, name }; - - return TargetDeclaration { TargetDeclaration::Type::Variable, name }; -} - -RefPtr ParserAutoComplete::find_declaration_of(const DocumentData& document_data, const ASTNode& node) const -{ - dbgln_if(CPP_LANGUAGE_SERVER_DEBUG, "find_declaration_of: {} ({})", document_data.parser().text_of_node(node), node.class_name()); - auto target_decl = get_target_declaration(node); - if (!target_decl.has_value()) - return {}; - - auto declarations = get_available_declarations(document_data, node, RecurseIntoScopes::Yes); - for (auto& decl : declarations) { - if (decl.is_function() && target_decl.value().type == TargetDeclaration::Function) { - if (((Cpp::FunctionDeclaration&)decl).m_name == target_decl.value().name) - return decl; - } - if (decl.is_variable_or_parameter_declaration() && target_decl.value().type == TargetDeclaration::Variable) { - if (((Cpp::VariableOrParameterDeclaration&)decl).m_name == target_decl.value().name) - return decl; - } - - if (decl.is_struct_or_class() && target_decl.value().type == TargetDeclaration::Property) { - // TODO: Also check that the type of the struct/class matches (not just the property name) - for (auto& member : ((Cpp::StructOrClassDeclaration&)decl).m_members) { - VERIFY(node.is_identifier()); - if (member.m_name == target_decl.value().name) { - return member; - } - } - } - - if (decl.is_struct_or_class() && target_decl.value().type == TargetDeclaration::Type) { - if (((Cpp::StructOrClassDeclaration&)decl).m_name == target_decl.value().name) - return decl; - } - } - return {}; -} - -void ParserAutoComplete::update_declared_symbols(DocumentData& document) -{ - for (auto& include : document.preprocessor().included_paths()) { - auto included_document = get_or_create_document_data(document_path_from_include_path(include)); - if (!included_document) - continue; - for (auto&& decl : get_global_declarations_including_headers(*included_document, RecurseIntoScopes::Yes)) - document.m_declarations_from_headers.set(move(decl)); - } - - Vector declarations; - - for (auto& decl : get_declarations_recursive(*document.parser().root_node())) { - declarations.append({ decl.name(), { document.filename(), decl.start().line, decl.start().column }, type_of_declaration(decl), scope_of_declaration(decl) }); - } - - for (auto& definition : document.preprocessor().definitions()) { - declarations.append({ definition.key, { document.filename(), definition.value.line, definition.value.column }, GUI::AutocompleteProvider::DeclarationType::PreprocessorDefinition, {} }); - } - - set_declarations_of_document(document.filename(), move(declarations)); -} - -GUI::AutocompleteProvider::DeclarationType ParserAutoComplete::type_of_declaration(const Declaration& decl) -{ - if (decl.is_struct()) - return GUI::AutocompleteProvider::DeclarationType::Struct; - if (decl.is_class()) - return GUI::AutocompleteProvider::DeclarationType::Class; - if (decl.is_function()) - return GUI::AutocompleteProvider::DeclarationType::Function; - if (decl.is_variable_declaration()) - return GUI::AutocompleteProvider::DeclarationType::Variable; - if (decl.is_namespace()) - return GUI::AutocompleteProvider::DeclarationType::Namespace; - if (decl.is_member()) - return GUI::AutocompleteProvider::DeclarationType::Member; - return GUI::AutocompleteProvider::DeclarationType::Variable; -} - -OwnPtr ParserAutoComplete::create_document_data(String&& text, const String& filename) -{ - auto document_data = make(); - document_data->m_filename = move(filename); - document_data->m_text = move(text); - document_data->m_preprocessor = make(document_data->m_filename, document_data->text()); - document_data->preprocessor().set_ignore_unsupported_keywords(true); - document_data->preprocessor().process(); - - Preprocessor::Definitions all_definitions; - for (auto item : document_data->preprocessor().definitions()) - all_definitions.set(move(item.key), move(item.value)); - - for (auto include : document_data->preprocessor().included_paths()) { - auto included_document = get_or_create_document_data(document_path_from_include_path(include)); - if (!included_document) - continue; - for (auto item : included_document->parser().preprocessor_definitions()) - all_definitions.set(move(item.key), move(item.value)); - } - - document_data->m_parser = make(document_data->preprocessor().processed_text(), filename, move(all_definitions)); - - auto root = document_data->parser().parse(); - - if constexpr (CPP_LANGUAGE_SERVER_DEBUG) - root->dump(0); - - update_declared_symbols(*document_data); - - return document_data; -} - -String ParserAutoComplete::scope_of_declaration(const Declaration& decl) const -{ - auto parent = decl.parent(); - if (!parent) - return {}; - - if (!parent->is_declaration()) - return {}; - - auto& parent_decl = static_cast(*parent); - - auto parent_scope = scope_of_declaration(parent_decl); - String containing_scope; - if (parent_decl.is_namespace()) - containing_scope = static_cast(parent_decl).m_name; - if (parent_decl.is_struct_or_class()) - containing_scope = static_cast(parent_decl).name(); - - if (parent_scope.is_null()) - return containing_scope; - - return String::formatted("{}::{}", parent_scope, containing_scope); -} - -} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.h b/Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.h deleted file mode 100644 index c3b735d948..0000000000 --- a/Userland/DevTools/HackStudio/LanguageServers/Cpp/ParserAutoComplete.h +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2021, Itamar S. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace LanguageServers::Cpp { - -using namespace ::Cpp; - -class ParserAutoComplete : public AutoCompleteEngine { -public: - ParserAutoComplete(const FileDB& filedb); - - virtual Vector get_suggestions(const String& file, const GUI::TextPosition& autocomplete_position) override; - virtual void on_edit(const String& file) override; - virtual void file_opened([[maybe_unused]] const String& file) override; - virtual Optional find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) override; - -private: - struct DocumentData { - const String& filename() const { return m_filename; } - const String& text() const { return m_text; } - const Preprocessor& preprocessor() const - { - VERIFY(m_preprocessor); - return *m_preprocessor; - } - Preprocessor& preprocessor() - { - VERIFY(m_preprocessor); - return *m_preprocessor; - } - const Parser& parser() const - { - VERIFY(m_parser); - return *m_parser; - } - Parser& parser() - { - VERIFY(m_parser); - return *m_parser; - } - - String m_filename; - String m_text; - OwnPtr m_preprocessor; - OwnPtr m_parser; - - // FIXME: This HashTable must be re-computed if a declaration from a header file is modified - HashTable> m_declarations_from_headers; - }; - - Vector autocomplete_property(const DocumentData&, const MemberExpression&, const String partial_text) const; - Vector autocomplete_name(const DocumentData&, const ASTNode&, const String& partial_text) const; - String type_of(const DocumentData&, const Expression&) const; - String type_of_property(const DocumentData&, const Identifier&) const; - String type_of_variable(const Identifier&) const; - bool is_property(const ASTNode&) const; - bool is_empty_property(const DocumentData&, const ASTNode&, const Position& autocomplete_position) const; - RefPtr find_declaration_of(const DocumentData&, const ASTNode&) const; - - enum class RecurseIntoScopes { - No, - Yes - }; - NonnullRefPtrVector get_available_declarations(const DocumentData&, const ASTNode&, RecurseIntoScopes) const; - - struct PropertyInfo { - StringView name; - RefPtr type; - }; - Vector properties_of_type(const DocumentData& document, const String& type) const; - NonnullRefPtrVector get_global_declarations_including_headers(const DocumentData&, RecurseIntoScopes) const; - NonnullRefPtrVector get_global_declarations(const DocumentData&, RecurseIntoScopes) const; - NonnullRefPtrVector get_declarations_recursive(const ASTNode&) const; - - const DocumentData* get_document_data(const String& file) const; - const DocumentData* get_or_create_document_data(const String& file); - void set_document_data(const String& file, OwnPtr&& data); - - OwnPtr create_document_data_for(const String& file); - String document_path_from_include_path(const StringView& include_path) const; - void update_declared_symbols(DocumentData&); - GUI::AutocompleteProvider::DeclarationType type_of_declaration(const Declaration&); - String scope_of_declaration(const Declaration&) const; - String scope_of_name_or_identifier(const ASTNode& node) const; - Optional find_preprocessor_definition(const DocumentData&, const GUI::TextPosition&); - - OwnPtr create_document_data(String&& text, const String& filename); - Optional> autocomplete_property(const DocumentData&, const ASTNode&, Optional containing_token) const; - Optional> autocomplete_name(const DocumentData&, const ASTNode&, Optional containing_token) const; - - HashMap> m_documents; -}; - -} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Cpp/Tests.cpp b/Userland/DevTools/HackStudio/LanguageServers/Cpp/Tests.cpp index 1116fe937e..7f69238596 100644 --- a/Userland/DevTools/HackStudio/LanguageServers/Cpp/Tests.cpp +++ b/Userland/DevTools/HackStudio/LanguageServers/Cpp/Tests.cpp @@ -6,7 +6,7 @@ #include "Tests.h" #include "../FileDB.h" -#include "ParserAutoComplete.h" +#include "CppComprehensionEngine.h" using namespace LanguageServers; using namespace LanguageServers::Cpp; @@ -57,8 +57,8 @@ ar } )"; filedb.add("a.cpp", content); - ParserAutoComplete autocomplete(filedb); - auto suggestions = autocomplete.get_suggestions("a.cpp", { 2, 2 }); + CppComprehensionEngine engine(filedb); + auto suggestions = engine.get_suggestions("a.cpp", { 2, 2 }); if (suggestions.size() != 2) FAIL(bad size); @@ -79,7 +79,7 @@ myv } )"; filedb.add("a.cpp", content); - ParserAutoComplete autocomplete(filedb); + CppComprehensionEngine autocomplete(filedb); auto suggestions = autocomplete.get_suggestions("a.cpp", { 3, 3 }); if (suggestions.size() != 1) FAIL(bad size); @@ -103,7 +103,7 @@ MyS } )"; filedb.add("a.cpp", content); - ParserAutoComplete autocomplete(filedb); + CppComprehensionEngine autocomplete(filedb); auto suggestions = autocomplete.get_suggestions("a.cpp", { 5, 3 }); if (suggestions.size() != 1) FAIL(bad size); @@ -124,8 +124,8 @@ argv = nullptr; } )"; filedb.add("a.cpp", content); - ParserAutoComplete autocomplete(filedb); - auto position = autocomplete.find_declaration_of("a.cpp", { 2, 1 }); + CppComprehensionEngine engine(filedb); + auto position = engine.find_declaration_of("a.cpp", { 2, 1 }); if (!position.has_value()) FAIL("declaration not found"); diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp b/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp deleted file mode 100644 index 3b2e542799..0000000000 --- a/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "AutoComplete.h" -#include -#include -#include -#include - -namespace LanguageServers::Shell { - -RefPtr<::Shell::Shell> AutoComplete::s_shell {}; - -AutoComplete::AutoComplete(const FileDB& filedb) - : AutoCompleteEngine(filedb, true) -{ -} - -const AutoComplete::DocumentData& AutoComplete::get_or_create_document_data(const String& file) -{ - auto absolute_path = filedb().to_absolute_path(file); - if (!m_documents.contains(absolute_path)) { - set_document_data(absolute_path, create_document_data_for(absolute_path)); - } - return get_document_data(absolute_path); -} - -const AutoComplete::DocumentData& AutoComplete::get_document_data(const String& file) const -{ - auto absolute_path = filedb().to_absolute_path(file); - auto document_data = m_documents.get(absolute_path); - VERIFY(document_data.has_value()); - return *document_data.value(); -} - -OwnPtr AutoComplete::create_document_data_for(const String& file) -{ - auto document = filedb().get(file); - if (!document) - return {}; - auto content = document->text(); - auto document_data = make(document->text(), file); - for (auto& path : document_data->sourced_paths()) - get_or_create_document_data(path); - - update_declared_symbols(*document_data); - return document_data; -} - -void AutoComplete::set_document_data(const String& file, OwnPtr&& data) -{ - m_documents.set(filedb().to_absolute_path(file), move(data)); -} - -AutoComplete::DocumentData::DocumentData(String&& _text, String _filename) - : filename(move(_filename)) - , text(move(_text)) - , node(parse()) -{ -} - -const Vector& AutoComplete::DocumentData::sourced_paths() const -{ - if (all_sourced_paths.has_value()) - return all_sourced_paths.value(); - - struct : public ::Shell::AST::NodeVisitor { - void visit(const ::Shell::AST::CastToCommand* node) override - { - auto& inner = node->inner(); - if (inner->is_list()) { - if (auto* list = dynamic_cast(inner.ptr())) { - auto& entries = list->list(); - if (entries.size() == 2 && entries.first()->is_bareword() && static_ptr_cast<::Shell::AST::BarewordLiteral>(entries.first())->text() == "source") { - auto& filename = entries[1]; - if (filename->would_execute()) - return; - auto name_list = const_cast<::Shell::AST::Node*>(filename.ptr())->run(nullptr)->resolve_as_list(nullptr); - StringBuilder builder; - builder.join(" ", name_list); - sourced_files.set(builder.build()); - } - } - } - ::Shell::AST::NodeVisitor::visit(node); - } - - HashTable sourced_files; - } visitor; - - node->visit(visitor); - - Vector sourced_paths; - for (auto& entry : visitor.sourced_files) - sourced_paths.append(move(entry)); - - all_sourced_paths = move(sourced_paths); - return all_sourced_paths.value(); -} - -NonnullRefPtr<::Shell::AST::Node> AutoComplete::DocumentData::parse() const -{ - ::Shell::Parser parser { text }; - if (auto node = parser.parse()) - return node.release_nonnull(); - - return ::Shell::AST::create<::Shell::AST::SyntaxError>(::Shell::AST::Position {}, "Unable to parse file"); -} - -size_t AutoComplete::resolve(const AutoComplete::DocumentData& document, const GUI::TextPosition& position) -{ - size_t offset = 0; - - if (position.line() > 0) { - auto first = true; - size_t line = 0; - for (auto& line_view : document.text.split_limit('\n', position.line() + 1, true)) { - if (line == position.line()) - break; - if (first) - first = false; - else - ++offset; // For the newline. - offset += line_view.length(); - ++line; - } - } - - offset += position.column() + 1; - return offset; -} - -Vector AutoComplete::get_suggestions(const String& file, const GUI::TextPosition& position) -{ - dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "AutoComplete position {}:{}", position.line(), position.column()); - - const auto& document = get_or_create_document_data(file); - size_t offset_in_file = resolve(document, position); - - ::Shell::AST::HitTestResult hit_test = document.node->hit_test_position(offset_in_file); - if (!hit_test.matching_node) { - dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", position.line(), position.column()); - return {}; - } - - auto completions = const_cast<::Shell::AST::Node*>(document.node.ptr())->complete_for_editor(shell(), offset_in_file, hit_test); - Vector entries; - for (auto& completion : completions) - entries.append({ completion.text_string, completion.input_offset }); - - return entries; -} - -void AutoComplete::on_edit(const String& file) -{ - set_document_data(file, create_document_data_for(file)); -} - -void AutoComplete::file_opened([[maybe_unused]] const String& file) -{ - set_document_data(file, create_document_data_for(file)); -} - -Optional AutoComplete::find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) -{ - dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "find_declaration_of({}, {}:{})", filename, identifier_position.line(), identifier_position.column()); - const auto& document = get_or_create_document_data(filename); - auto position = resolve(document, identifier_position); - auto result = document.node->hit_test_position(position); - if (!result.matching_node) { - dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", identifier_position.line(), identifier_position.column()); - return {}; - } - - if (!result.matching_node->is_bareword()) { - dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "no bareword at position {}:{}", identifier_position.line(), identifier_position.column()); - return {}; - } - - auto name = static_ptr_cast<::Shell::AST::BarewordLiteral>(result.matching_node)->text(); - auto& declarations = all_declarations(); - for (auto& entry : declarations) { - for (auto& declaration : entry.value) { - if (declaration.name == name) - return declaration.position; - } - } - - return {}; -} - -void AutoComplete::update_declared_symbols(const DocumentData& document) -{ - struct Visitor : public ::Shell::AST::NodeVisitor { - explicit Visitor(const String& filename) - : filename(filename) - { - } - - void visit(const ::Shell::AST::VariableDeclarations* node) override - { - for (auto& entry : node->variables()) { - auto literal = entry.name->leftmost_trivial_literal(); - if (!literal) - continue; - - String name; - if (literal->is_bareword()) - name = static_ptr_cast<::Shell::AST::BarewordLiteral>(literal)->text(); - - if (!name.is_empty()) { - dbgln("Found variable {}", name); - declarations.append({ move(name), { filename, entry.name->position().start_line.line_number, entry.name->position().start_line.line_column }, GUI::AutocompleteProvider::DeclarationType::Variable, {} }); - } - } - ::Shell::AST::NodeVisitor::visit(node); - } - - void visit(const ::Shell::AST::FunctionDeclaration* node) override - { - dbgln("Found function {}", node->name().name); - declarations.append({ node->name().name, { filename, node->position().start_line.line_number, node->position().start_line.line_column }, GUI::AutocompleteProvider::DeclarationType::Function, {} }); - } - - const String& filename; - Vector declarations; - } visitor { document.filename }; - - document.node->visit(visitor); - - set_declarations_of_document(document.filename, move(visitor.declarations)); -} -} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h b/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h deleted file mode 100644 index ad2b82e0ef..0000000000 --- a/Userland/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include - -namespace LanguageServers::Shell { - -class AutoComplete : public AutoCompleteEngine { -public: - AutoComplete(const FileDB& filedb); - virtual Vector get_suggestions(const String& file, const GUI::TextPosition& position) override; - virtual void on_edit(const String& file) override; - virtual void file_opened([[maybe_unused]] const String& file) override; - virtual Optional find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) override; - -private: - struct DocumentData { - DocumentData(String&& text, String filename); - String filename; - String text; - NonnullRefPtr<::Shell::AST::Node> node; - - const Vector& sourced_paths() const; - - private: - NonnullRefPtr<::Shell::AST::Node> parse() const; - - mutable Optional> all_sourced_paths {}; - }; - - const DocumentData& get_document_data(const String& file) const; - const DocumentData& get_or_create_document_data(const String& file); - void set_document_data(const String& file, OwnPtr&& data); - - OwnPtr create_document_data_for(const String& file); - String document_path_from_include_path(const StringView& include_path) const; - void update_declared_symbols(const DocumentData&); - - static size_t resolve(const AutoComplete::DocumentData& document, const GUI::TextPosition& position); - - ::Shell::Shell& shell() - { - if (s_shell) - return *s_shell; - s_shell = ::Shell::Shell::construct(); - return *s_shell; - } - - HashMap> m_documents; - static RefPtr<::Shell::Shell> s_shell; -}; -} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt b/Userland/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt index 0b7db53f80..9924d8ebc7 100644 --- a/Userland/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt @@ -1,5 +1,5 @@ set(SOURCES - AutoComplete.cpp + ShellComprehensionEngine.cpp main.cpp ) diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h b/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h index cca280f166..542faccc31 100644 --- a/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h @@ -6,7 +6,7 @@ #pragma once -#include "AutoComplete.h" +#include "ShellComprehensionEngine.h" #include namespace LanguageServers::Shell { @@ -17,7 +17,7 @@ class ClientConnection final : public LanguageServers::ClientConnection { ClientConnection(NonnullRefPtr socket, int client_id) : LanguageServers::ClientConnection(move(socket), client_id) { - m_autocomplete_engine = make(m_filedb); + m_autocomplete_engine = make(m_filedb); m_autocomplete_engine->set_declarations_of_document_callback = [this](const String& filename, Vector&& declarations) { async_declarations_in_document(filename, move(declarations)); }; diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.cpp b/Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.cpp new file mode 100644 index 0000000000..fa7cc39f5a --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.cpp @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ShellComprehensionEngine.h" +#include +#include +#include +#include + +namespace LanguageServers::Shell { + +RefPtr<::Shell::Shell> ShellComprehensionEngine::s_shell {}; + +ShellComprehensionEngine::ShellComprehensionEngine(const FileDB& filedb) + : CodeComprehensionEngine(filedb, true) +{ +} + +const ShellComprehensionEngine::DocumentData& ShellComprehensionEngine::get_or_create_document_data(const String& file) +{ + auto absolute_path = filedb().to_absolute_path(file); + if (!m_documents.contains(absolute_path)) { + set_document_data(absolute_path, create_document_data_for(absolute_path)); + } + return get_document_data(absolute_path); +} + +const ShellComprehensionEngine::DocumentData& ShellComprehensionEngine::get_document_data(const String& file) const +{ + auto absolute_path = filedb().to_absolute_path(file); + auto document_data = m_documents.get(absolute_path); + VERIFY(document_data.has_value()); + return *document_data.value(); +} + +OwnPtr ShellComprehensionEngine::create_document_data_for(const String& file) +{ + auto document = filedb().get(file); + if (!document) + return {}; + auto content = document->text(); + auto document_data = make(document->text(), file); + for (auto& path : document_data->sourced_paths()) + get_or_create_document_data(path); + + update_declared_symbols(*document_data); + return document_data; +} + +void ShellComprehensionEngine::set_document_data(const String& file, OwnPtr&& data) +{ + m_documents.set(filedb().to_absolute_path(file), move(data)); +} + +ShellComprehensionEngine::DocumentData::DocumentData(String&& _text, String _filename) + : filename(move(_filename)) + , text(move(_text)) + , node(parse()) +{ +} + +const Vector& ShellComprehensionEngine::DocumentData::sourced_paths() const +{ + if (all_sourced_paths.has_value()) + return all_sourced_paths.value(); + + struct : public ::Shell::AST::NodeVisitor { + void visit(const ::Shell::AST::CastToCommand* node) override + { + auto& inner = node->inner(); + if (inner->is_list()) { + if (auto* list = dynamic_cast(inner.ptr())) { + auto& entries = list->list(); + if (entries.size() == 2 && entries.first()->is_bareword() && static_ptr_cast<::Shell::AST::BarewordLiteral>(entries.first())->text() == "source") { + auto& filename = entries[1]; + if (filename->would_execute()) + return; + auto name_list = const_cast<::Shell::AST::Node*>(filename.ptr())->run(nullptr)->resolve_as_list(nullptr); + StringBuilder builder; + builder.join(" ", name_list); + sourced_files.set(builder.build()); + } + } + } + ::Shell::AST::NodeVisitor::visit(node); + } + + HashTable sourced_files; + } visitor; + + node->visit(visitor); + + Vector sourced_paths; + for (auto& entry : visitor.sourced_files) + sourced_paths.append(move(entry)); + + all_sourced_paths = move(sourced_paths); + return all_sourced_paths.value(); +} + +NonnullRefPtr<::Shell::AST::Node> ShellComprehensionEngine::DocumentData::parse() const +{ + ::Shell::Parser parser { text }; + if (auto node = parser.parse()) + return node.release_nonnull(); + + return ::Shell::AST::create<::Shell::AST::SyntaxError>(::Shell::AST::Position {}, "Unable to parse file"); +} + +size_t ShellComprehensionEngine::resolve(const ShellComprehensionEngine::DocumentData& document, const GUI::TextPosition& position) +{ + size_t offset = 0; + + if (position.line() > 0) { + auto first = true; + size_t line = 0; + for (auto& line_view : document.text.split_limit('\n', position.line() + 1, true)) { + if (line == position.line()) + break; + if (first) + first = false; + else + ++offset; // For the newline. + offset += line_view.length(); + ++line; + } + } + + offset += position.column() + 1; + return offset; +} + +Vector ShellComprehensionEngine::get_suggestions(const String& file, const GUI::TextPosition& position) +{ + dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "ShellComprehensionEngine position {}:{}", position.line(), position.column()); + + const auto& document = get_or_create_document_data(file); + size_t offset_in_file = resolve(document, position); + + ::Shell::AST::HitTestResult hit_test = document.node->hit_test_position(offset_in_file); + if (!hit_test.matching_node) { + dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", position.line(), position.column()); + return {}; + } + + auto completions = const_cast<::Shell::AST::Node*>(document.node.ptr())->complete_for_editor(shell(), offset_in_file, hit_test); + Vector entries; + for (auto& completion : completions) + entries.append({ completion.text_string, completion.input_offset }); + + return entries; +} + +void ShellComprehensionEngine::on_edit(const String& file) +{ + set_document_data(file, create_document_data_for(file)); +} + +void ShellComprehensionEngine::file_opened([[maybe_unused]] const String& file) +{ + set_document_data(file, create_document_data_for(file)); +} + +Optional ShellComprehensionEngine::find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) +{ + dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "find_declaration_of({}, {}:{})", filename, identifier_position.line(), identifier_position.column()); + const auto& document = get_or_create_document_data(filename); + auto position = resolve(document, identifier_position); + auto result = document.node->hit_test_position(position); + if (!result.matching_node) { + dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "no node at position {}:{}", identifier_position.line(), identifier_position.column()); + return {}; + } + + if (!result.matching_node->is_bareword()) { + dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "no bareword at position {}:{}", identifier_position.line(), identifier_position.column()); + return {}; + } + + auto name = static_ptr_cast<::Shell::AST::BarewordLiteral>(result.matching_node)->text(); + auto& declarations = all_declarations(); + for (auto& entry : declarations) { + for (auto& declaration : entry.value) { + if (declaration.name == name) + return declaration.position; + } + } + + return {}; +} + +void ShellComprehensionEngine::update_declared_symbols(const DocumentData& document) +{ + struct Visitor : public ::Shell::AST::NodeVisitor { + explicit Visitor(const String& filename) + : filename(filename) + { + } + + void visit(const ::Shell::AST::VariableDeclarations* node) override + { + for (auto& entry : node->variables()) { + auto literal = entry.name->leftmost_trivial_literal(); + if (!literal) + continue; + + String name; + if (literal->is_bareword()) + name = static_ptr_cast<::Shell::AST::BarewordLiteral>(literal)->text(); + + if (!name.is_empty()) { + dbgln("Found variable {}", name); + declarations.append({ move(name), { filename, entry.name->position().start_line.line_number, entry.name->position().start_line.line_column }, GUI::AutocompleteProvider::DeclarationType::Variable, {} }); + } + } + ::Shell::AST::NodeVisitor::visit(node); + } + + void visit(const ::Shell::AST::FunctionDeclaration* node) override + { + dbgln("Found function {}", node->name().name); + declarations.append({ node->name().name, { filename, node->position().start_line.line_number, node->position().start_line.line_column }, GUI::AutocompleteProvider::DeclarationType::Function, {} }); + } + + const String& filename; + Vector declarations; + } visitor { document.filename }; + + document.node->visit(visitor); + + set_declarations_of_document(document.filename, move(visitor.declarations)); +} +} diff --git a/Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.h b/Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.h new file mode 100644 index 0000000000..cc25b812cc --- /dev/null +++ b/Userland/DevTools/HackStudio/LanguageServers/Shell/ShellComprehensionEngine.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace LanguageServers::Shell { + +class ShellComprehensionEngine : public CodeComprehensionEngine { +public: + ShellComprehensionEngine(const FileDB& filedb); + virtual Vector get_suggestions(const String& file, const GUI::TextPosition& position) override; + virtual void on_edit(const String& file) override; + virtual void file_opened([[maybe_unused]] const String& file) override; + virtual Optional find_declaration_of(const String& filename, const GUI::TextPosition& identifier_position) override; + +private: + struct DocumentData { + DocumentData(String&& text, String filename); + String filename; + String text; + NonnullRefPtr<::Shell::AST::Node> node; + + const Vector& sourced_paths() const; + + private: + NonnullRefPtr<::Shell::AST::Node> parse() const; + + mutable Optional> all_sourced_paths {}; + }; + + const DocumentData& get_document_data(const String& file) const; + const DocumentData& get_or_create_document_data(const String& file); + void set_document_data(const String& file, OwnPtr&& data); + + OwnPtr create_document_data_for(const String& file); + String document_path_from_include_path(const StringView& include_path) const; + void update_declared_symbols(const DocumentData&); + + static size_t resolve(const ShellComprehensionEngine::DocumentData& document, const GUI::TextPosition& position); + + ::Shell::Shell& shell() + { + if (s_shell) + return *s_shell; + s_shell = ::Shell::Shell::construct(); + return *s_shell; + } + + HashMap> m_documents; + static RefPtr<::Shell::Shell> s_shell; +}; +} -- cgit v1.2.3