diff --git a/Base/etc/SystemServer.ini b/Base/etc/SystemServer.ini index 1d1df557d4d..5f010f9d4cc 100644 --- a/Base/etc/SystemServer.ini +++ b/Base/etc/SystemServer.ini @@ -149,3 +149,11 @@ Lazy=1 User=anon MultiInstance=1 AcceptSocketConnections=1 + +[ShellLanguageServer] +Socket=/tmp/portal/language/shell +SocketPermissions=660 +Lazy=1 +User=anon +MultiInstance=1 +AcceptSocketConnections=1 diff --git a/DevTools/HackStudio/Editor.cpp b/DevTools/HackStudio/Editor.cpp index 13e07f4188a..da060158555 100644 --- a/DevTools/HackStudio/Editor.cpp +++ b/DevTools/HackStudio/Editor.cpp @@ -483,6 +483,7 @@ void Editor::set_document(GUI::TextDocument& doc) break; case Language::Shell: set_syntax_highlighter(make()); + m_language_client = get_language_client(project().root_directory()); break; default: set_syntax_highlighter(nullptr); diff --git a/DevTools/HackStudio/LanguageClients/CMakeLists.txt b/DevTools/HackStudio/LanguageClients/CMakeLists.txt index 8ffd97ebb75..e06c45f98c2 100644 --- a/DevTools/HackStudio/LanguageClients/CMakeLists.txt +++ b/DevTools/HackStudio/LanguageClients/CMakeLists.txt @@ -1,4 +1,4 @@ - set(GENERATED_SOURCES - ../../LanguageServers/LanguageServerEndpoint.h - ../../LanguageServers/LanguageClientEndpoint.h - ) +set(GENERATED_SOURCES + ../../LanguageServers/LanguageServerEndpoint.h + ../../LanguageServers/LanguageClientEndpoint.h +) diff --git a/DevTools/HackStudio/LanguageClients/ServerConnections.h b/DevTools/HackStudio/LanguageClients/ServerConnections.h index cab9404f665..89266e37cab 100644 --- a/DevTools/HackStudio/LanguageClients/ServerConnections.h +++ b/DevTools/HackStudio/LanguageClients/ServerConnections.h @@ -47,6 +47,7 @@ namespace LanguageClients { LANGUAGE_CLIENT(Cpp, cpp) +LANGUAGE_CLIENT(Shell, shell) } diff --git a/DevTools/HackStudio/LanguageServers/CMakeLists.txt b/DevTools/HackStudio/LanguageServers/CMakeLists.txt index 156dbe15c1b..afcc0118d06 100644 --- a/DevTools/HackStudio/LanguageServers/CMakeLists.txt +++ b/DevTools/HackStudio/LanguageServers/CMakeLists.txt @@ -2,3 +2,4 @@ compile_ipc(LanguageServer.ipc LanguageServerEndpoint.h) compile_ipc(LanguageClient.ipc LanguageClientEndpoint.h) add_subdirectory(Cpp) +add_subdirectory(Shell) diff --git a/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp b/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp new file mode 100644 index 00000000000..fb39ca415ab --- /dev/null +++ b/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "AutoComplete.h" +#include +#include +#include +#include +#include + +// #define DEBUG_AUTOCOMPLETE + +namespace LanguageServers::Shell { + +Vector AutoComplete::get_suggestions(const String& code, size_t offset) +{ + // FIXME: No need to reparse this every time! + auto ast = ::Shell::Parser { code }.parse(); + if (!ast) + return {}; + +#ifdef DEBUG_AUTOCOMPLETE + dbg() << "Complete '" << code << "': "; + ast->dump(1); + dbg() << "At offset " << offset; +#endif + + auto result = ast->complete_for_editor(m_shell, offset); + Vector completions; + for (auto& entry : result) { +#ifdef DEBUG_AUTOCOMPLETE + dbg() << "Suggestion: '" << entry.text_string << "' starting at " << entry.input_offset; +#endif + completions.append({ entry.text_string, entry.input_offset }); + } + + return completions; +} + +} diff --git a/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h b/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h new file mode 100644 index 00000000000..924864bcf46 --- /dev/null +++ b/DevTools/HackStudio/LanguageServers/Shell/AutoComplete.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace LanguageServers::Shell { + +using namespace ::HackStudio; + +class AutoComplete { +public: + AutoComplete() + : m_shell(::Shell::Shell::construct()) + { + } + + Vector get_suggestions(const String& code, size_t autocomplete_position); + +private: + NonnullRefPtr<::Shell::Shell> m_shell; +}; + +} diff --git a/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt b/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt new file mode 100644 index 00000000000..144a4526a3e --- /dev/null +++ b/DevTools/HackStudio/LanguageServers/Shell/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES + ClientConnection.cpp + main.cpp + AutoComplete.cpp +) + +set(GENERATED_SOURCES + ../LanguageServerEndpoint.h + ../LanguageClientEndpoint.h) + +serenity_bin(ShellLanguageServer) + +# We link with LibGUI because we use GUI::TextDocument to update +# the content of files according to the edit actions we receive over IPC. +target_link_libraries(ShellLanguageServer LibIPC LibShell LibGUI) diff --git a/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.cpp b/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.cpp new file mode 100644 index 00000000000..5e0aba2b35f --- /dev/null +++ b/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.cpp @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ClientConnection.h" +#include "AutoComplete.h" +#include +#include +#include + +// #define DEBUG_SH_LANGUAGE_SERVER +// #define DEBUG_FILE_CONTENT + +namespace LanguageServers::Shell { + +static HashMap> s_connections; + +ClientConnection::ClientConnection(NonnullRefPtr socket, int client_id) + : IPC::ClientConnection(*this, move(socket), client_id) +{ + s_connections.set(client_id, *this); +} + +ClientConnection::~ClientConnection() +{ +} + +void ClientConnection::die() +{ + s_connections.remove(client_id()); + exit(0); +} + +OwnPtr ClientConnection::handle(const Messages::LanguageServer::Greet& message) +{ + m_project_root = LexicalPath(message.project_root()); +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbg() << "project_root: " << m_project_root.string(); +#endif + return make(client_id()); +} + +class DefaultDocumentClient final : public GUI::TextDocument::Client { +public: + virtual ~DefaultDocumentClient() override = default; + virtual void document_did_append_line() override {}; + virtual void document_did_insert_line(size_t) override {}; + virtual void document_did_remove_line(size_t) override {}; + virtual void document_did_remove_all_lines() override {}; + virtual void document_did_change() override {}; + virtual void document_did_set_text() override {}; + virtual void document_did_set_cursor(const GUI::TextPosition&) override {}; + + virtual bool is_automatic_indentation_enabled() const override { return true; } + virtual int soft_tab_width() const override { return 4; } +}; + +static DefaultDocumentClient s_default_document_client; + +void ClientConnection::handle(const Messages::LanguageServer::FileOpened& message) +{ + LexicalPath file_path(String::format("%s/%s", m_project_root.string().characters(), message.file_name().characters())); +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbg() << "FileOpened: " << file_path.string(); +#endif + + auto file = Core::File::construct(file_path.string()); + if (!file->open(Core::IODevice::ReadOnly)) { + errno = file->error(); + perror("open"); + dbg() << "Failed to open project file: " << file_path.string(); + return; + } + auto content = file->read_all(); + StringView content_view(content); + auto document = GUI::TextDocument::create(&s_default_document_client); + document->set_text(content_view); + m_open_files.set(message.file_name(), document); +#ifdef DEBUG_FILE_CONTENT + dbg() << document->text(); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::FileEditInsertText& message) +{ +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbg() << "InsertText for file: " << message.file_name(); + dbg() << "Text: " << message.text(); + dbg() << "[" << message.start_line() << ":" << message.start_column() << "]"; +#endif + auto document = document_for(message.file_name()); + if (!document) { + dbg() << "file " << message.file_name() << " has not been opened"; + return; + } + GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() }; + document->insert_at(start_position, message.text(), &s_default_document_client); +#ifdef DEBUG_FILE_CONTENT + dbg() << document->text(); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::FileEditRemoveText& message) +{ +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbg() << "RemoveText for file: " << message.file_name(); + dbg() << "[" << message.start_line() << ":" << message.start_column() << " - " << message.end_line() << ":" << message.end_column() << "]"; +#endif + auto document = document_for(message.file_name()); + if (!document) { + dbg() << "file " << message.file_name() << " has not been opened"; + return; + } + GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() }; + GUI::TextRange range { + GUI::TextPosition { (size_t)message.start_line(), + (size_t)message.start_column() }, + GUI::TextPosition { (size_t)message.end_line(), + (size_t)message.end_column() } + }; + + document->remove(range); +#ifdef DEBUG_FILE_CONTENT + dbg() << document->text(); +#endif +} + +void ClientConnection::handle(const Messages::LanguageServer::AutoCompleteSuggestions& message) +{ +#ifdef DEBUG_SH_LANGUAGE_SERVER + dbg() << "AutoCompleteSuggestions for: " << message.file_name() << " " << message.cursor_line() << ":" << message.cursor_column(); +#endif + + auto document = document_for(message.file_name()); + if (!document) { + dbg() << "file " << message.file_name() << " has not been opened"; + return; + } + + auto& lines = document->lines(); + size_t offset = 0; + + if (message.cursor_line() > 0) { + for (auto i = 0; i < message.cursor_line(); ++i) + offset += lines[i].length() + 1; + } + offset += message.cursor_column(); + + auto suggestions = m_autocomplete.get_suggestions(document->text(), offset); + post_message(Messages::LanguageClient::AutoCompleteSuggestions(move(suggestions))); +} + +RefPtr ClientConnection::document_for(const String& file_name) +{ + auto document_optional = m_open_files.get(file_name); + if (!document_optional.has_value()) + return nullptr; + + return document_optional.value(); +} + +void ClientConnection::handle(const Messages::LanguageServer::SetFileContent& message) +{ + auto document = document_for(message.file_name()); + if (!document) { + dbg() << "file " << message.file_name() << " has not been opened"; + return; + } + auto content = message.content(); + document->set_text(content.view()); +} + +} diff --git a/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h b/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h new file mode 100644 index 00000000000..27f53857b38 --- /dev/null +++ b/DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "AutoComplete.h" +#include +#include +#include +#include +#include + +#include +#include + +namespace LanguageServers::Shell { + +class ClientConnection final + : public IPC::ClientConnection + , public LanguageServerEndpoint { + C_OBJECT(ClientConnection); + +public: + explicit ClientConnection(NonnullRefPtr, int client_id); + ~ClientConnection() override; + + virtual void die() override; + +private: + virtual OwnPtr handle(const Messages::LanguageServer::Greet&) override; + virtual void handle(const Messages::LanguageServer::FileOpened&) override; + virtual void handle(const Messages::LanguageServer::FileEditInsertText&) override; + virtual void handle(const Messages::LanguageServer::FileEditRemoveText&) override; + virtual void handle(const Messages::LanguageServer::SetFileContent&) override; + virtual void handle(const Messages::LanguageServer::AutoCompleteSuggestions&) override; + + RefPtr document_for(const String& file_name); + + LexicalPath m_project_root; + HashMap> m_open_files; + + AutoComplete m_autocomplete; +}; + +} diff --git a/DevTools/HackStudio/LanguageServers/Shell/main.cpp b/DevTools/HackStudio/LanguageServers/Shell/main.cpp new file mode 100644 index 00000000000..a5e1417e679 --- /dev/null +++ b/DevTools/HackStudio/LanguageServers/Shell/main.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int, char**) +{ + Core::EventLoop event_loop; + if (pledge("stdio unix rpath", nullptr) < 0) { + perror("pledge"); + return 1; + } + + auto socket = Core::LocalSocket::take_over_accepted_socket_from_system_server(); + IPC::new_client_connection(socket.release_nonnull(), 1); + if (pledge("stdio rpath", nullptr) < 0) { + perror("pledge"); + return 1; + } + return event_loop.exec(); +}