mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 12:38:12 +00:00
HackStudio/LanguageServers: Move some components out of Cpp
This makes them available for use by other language servers. Also as a bonus, update the Shell language server to discover some symbols and add go-to-definition functionality :^)
This commit is contained in:
parent
0d17cf121c
commit
e59a631511
20 changed files with 400 additions and 318 deletions
|
@ -25,37 +25,232 @@
|
|||
*/
|
||||
|
||||
#include "AutoComplete.h"
|
||||
#include <AK/Assertions.h>
|
||||
#include <AK/HashTable.h>
|
||||
#include <LibLine/SuggestionManager.h>
|
||||
#include <Shell/AST.h>
|
||||
#include <Shell/Parser.h>
|
||||
#include <Shell/Shell.h>
|
||||
#include <LibRegex/Regex.h>
|
||||
#include <Userland/DevTools/HackStudio/LanguageServers/ClientConnection.h>
|
||||
|
||||
namespace LanguageServers::Shell {
|
||||
|
||||
Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, size_t offset)
|
||||
RefPtr<::Shell::Shell> AutoComplete::s_shell {};
|
||||
|
||||
AutoComplete::AutoComplete(ClientConnection& connection, const FileDB& filedb)
|
||||
: AutoCompleteEngine(connection, filedb, true)
|
||||
{
|
||||
// FIXME: No need to reparse this every time!
|
||||
auto ast = ::Shell::Parser { code }.parse();
|
||||
if (!ast)
|
||||
}
|
||||
|
||||
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::DocumentData> AutoComplete::create_document_data_for(const String& file)
|
||||
{
|
||||
auto document = filedb().get(file);
|
||||
if (!document)
|
||||
return {};
|
||||
auto content = document->text();
|
||||
auto document_data = make<DocumentData>(document->text(), file);
|
||||
for (auto& path : document_data->sourced_paths())
|
||||
get_or_create_document_data(path);
|
||||
|
||||
#if AUTOCOMPLETE_DEBUG
|
||||
dbgln("Complete '{}'", code);
|
||||
ast->dump(1);
|
||||
dbgln("At offset {}", offset);
|
||||
#endif
|
||||
update_declared_symbols(*document_data);
|
||||
return move(document_data);
|
||||
}
|
||||
|
||||
auto result = ast->complete_for_editor(m_shell, offset);
|
||||
Vector<GUI::AutocompleteProvider::Entry> completions;
|
||||
for (auto& entry : result) {
|
||||
#if AUTOCOMPLETE_DEBUG
|
||||
dbgln("Suggestion: '{}' starting at {}", entry.text_string, entry.input_offset);
|
||||
#endif
|
||||
completions.append({ entry.text_string, entry.input_offset });
|
||||
void AutoComplete::set_document_data(const String& file, OwnPtr<DocumentData>&& 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<String>& 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<const ::Shell::AST::ListConcatenate*>(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<String> sourced_files;
|
||||
} visitor;
|
||||
|
||||
node->visit(visitor);
|
||||
|
||||
Vector<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
return completions;
|
||||
offset += position.column() + 1;
|
||||
return offset;
|
||||
}
|
||||
|
||||
Vector<GUI::AutocompleteProvider::Entry> 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<GUI::AutocompleteProvider::Entry> 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<GUI::AutocompleteProvider::ProjectLocation> AutoComplete::find_declaration_of(const String& file_name, const GUI::TextPosition& identifier_position)
|
||||
{
|
||||
dbgln_if(SH_LANGUAGE_SERVER_DEBUG, "find_declaration_of({}, {}:{})", file_name, identifier_position.line(), identifier_position.column());
|
||||
const auto& document = get_or_create_document_data(file_name);
|
||||
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<GUI::AutocompleteProvider::Declaration> declarations;
|
||||
} visitor { document.filename };
|
||||
|
||||
document.node->visit(visitor);
|
||||
|
||||
set_declarations_of_document(document.filename, move(visitor.declarations));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue