/* * Copyright (c) 2020, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include "ShellComprehensionEngine.h" #include #include #include namespace CodeComprehension::Shell { RefPtr<::Shell::Shell> ShellComprehensionEngine::s_shell {}; ShellComprehensionEngine::ShellComprehensionEngine(FileDB const& filedb) : CodeComprehensionEngine(filedb, true) { } ShellComprehensionEngine::DocumentData const& ShellComprehensionEngine::get_or_create_document_data(DeprecatedString const& 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); } ShellComprehensionEngine::DocumentData const& ShellComprehensionEngine::get_document_data(DeprecatedString const& 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(DeprecatedString const& file) { auto document = filedb().get_or_read_from_filesystem(file); if (!document.has_value()) return {}; auto content = document.value(); auto document_data = make(move(content), 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(DeprecatedString const& file, OwnPtr&& data) { m_documents.set(filedb().to_absolute_path(file), move(data)); } ShellComprehensionEngine::DocumentData::DocumentData(DeprecatedString&& _text, DeprecatedString _filename) : filename(move(_filename)) , text(move(_text)) , node(parse()) { } Vector const& ShellComprehensionEngine::DocumentData::sourced_paths() const { if (all_sourced_paths.has_value()) return all_sourced_paths.value(); struct : public ::Shell::AST::NodeVisitor { void visit(::Shell::AST::CastToCommand const* node) override { auto& inner = node->inner(); if (inner->is_list()) { if (auto* list = dynamic_cast<::Shell::AST::ListConcatenate const*>(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.to_deprecated_string()); } } } ::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::make_ref_counted<::Shell::AST::SyntaxError>(::Shell::AST::Position {}, "Unable to parse file"); } size_t ShellComprehensionEngine::resolve(ShellComprehensionEngine::DocumentData const& 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, SplitBehavior::KeepEmpty)) { 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(DeprecatedString const& file, const GUI::TextPosition& position) { dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "ShellComprehensionEngine position {}:{}", position.line(), position.column()); auto const& 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(DeprecatedString const& file) { set_document_data(file, create_document_data_for(file)); } void ShellComprehensionEngine::file_opened([[maybe_unused]] DeprecatedString const& file) { set_document_data(file, create_document_data_for(file)); } Optional ShellComprehensionEngine::find_declaration_of(DeprecatedString const& filename, const GUI::TextPosition& identifier_position) { dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "find_declaration_of({}, {}:{})", filename, identifier_position.line(), identifier_position.column()); auto const& 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 const>(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(DocumentData const& document) { struct Visitor : public ::Shell::AST::NodeVisitor { explicit Visitor(DeprecatedString const& filename) : filename(filename) { } void visit(::Shell::AST::VariableDeclarations const* node) override { for (auto& entry : node->variables()) { auto literal = entry.name->leftmost_trivial_literal(); if (!literal) continue; DeprecatedString name; if (literal->is_bareword()) name = static_ptr_cast<::Shell::AST::BarewordLiteral const>(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 }, CodeComprehension::DeclarationType::Variable, {} }); } } ::Shell::AST::NodeVisitor::visit(node); } void visit(::Shell::AST::FunctionDeclaration const* 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 }, CodeComprehension::DeclarationType::Function, {} }); } DeprecatedString const& filename; Vector declarations; } visitor { document.filename }; document.node->visit(visitor); set_declarations_of_document(document.filename, move(visitor.declarations)); } }