mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 10:22:45 +00:00 
			
		
		
		
	LibGUI+HackStudio: Add an opt-in autocompletion interface to TextEditor
...and use that to implement autocomplete in HackStudio. Now everyone can have autocomplete :^)
This commit is contained in:
		
							parent
							
								
									7e457b98c3
								
							
						
					
					
						commit
						20b74e4ede
					
				
					 19 changed files with 211 additions and 162 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/16x16/completion/cpp-identifier.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/16x16/completion/cpp-identifier.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 337 B | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/16x16/completion/unspecified-identifier.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/16x16/completion/unspecified-identifier.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/16x16/completion/unspecified-unspecified.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/16x16/completion/unspecified-unspecified.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.6 KiB | 
|  | @ -28,45 +28,36 @@ | |||
| 
 | ||||
| #include <AK/String.h> | ||||
| #include <AK/Types.h> | ||||
| #include <LibGUI/AutocompleteProvider.h> | ||||
| #include <LibIPC/Decoder.h> | ||||
| #include <LibIPC/Encoder.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| 
 | ||||
| enum class CompletionKind { | ||||
|     Identifier, | ||||
| }; | ||||
| 
 | ||||
| struct AutoCompleteResponse { | ||||
|     String completion; | ||||
|     size_t partial_input_length { 0 }; | ||||
|     CompletionKind kind { CompletionKind::Identifier }; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| namespace IPC { | ||||
| 
 | ||||
| template<> | ||||
| inline bool encode(IPC::Encoder& encoder, const HackStudio::AutoCompleteResponse& response) | ||||
| inline bool encode(IPC::Encoder& encoder, const GUI::AutocompleteProvider::Entry& response) | ||||
| { | ||||
|     encoder << response.completion; | ||||
|     encoder << (u64)response.partial_input_length; | ||||
|     encoder << (u32)response.kind; | ||||
|     encoder << (u32)response.language; | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| template<> | ||||
| inline bool decode(IPC::Decoder& decoder, HackStudio::AutoCompleteResponse& response) | ||||
| inline bool decode(IPC::Decoder& decoder, GUI::AutocompleteProvider::Entry& response) | ||||
| { | ||||
|     u32 kind = 0; | ||||
|     u32 language = 0; | ||||
|     u64 partial_input_length = 0; | ||||
|     bool ok = decoder.decode(response.completion) | ||||
|         && decoder.decode(partial_input_length) | ||||
|         && decoder.decode(kind); | ||||
|         && decoder.decode(kind) | ||||
|         && decoder.decode(language); | ||||
| 
 | ||||
|     if (ok) { | ||||
|         response.kind = static_cast<HackStudio::CompletionKind>(kind); | ||||
|         response.kind = static_cast<GUI::AutocompleteProvider::CompletionKind>(kind); | ||||
|         response.language = static_cast<GUI::AutocompleteProvider::Language>(language); | ||||
|         response.partial_input_length = partial_input_length; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ add_subdirectory(LanguageServers) | |||
| add_subdirectory(LanguageClients) | ||||
| 
 | ||||
| set(SOURCES | ||||
|     AutoCompleteBox.cpp | ||||
|     CodeDocument.cpp | ||||
|     CursorTool.cpp | ||||
|     Debugger/BacktraceModel.cpp | ||||
|  |  | |||
|  | @ -62,8 +62,6 @@ Editor::Editor() | |||
|     m_documentation_tooltip_window->set_rect(0, 0, 500, 400); | ||||
|     m_documentation_tooltip_window->set_window_type(GUI::WindowType::Tooltip); | ||||
|     m_documentation_page_view = m_documentation_tooltip_window->set_main_widget<Web::OutOfProcessWebView>(); | ||||
| 
 | ||||
|     m_autocomplete_box = make<AutoCompleteBox>(*this); | ||||
| } | ||||
| 
 | ||||
| Editor::~Editor() | ||||
|  | @ -315,50 +313,6 @@ void Editor::mousedown_event(GUI::MouseEvent& event) | |||
|     GUI::TextEditor::mousedown_event(event); | ||||
| } | ||||
| 
 | ||||
| void Editor::keydown_event(GUI::KeyEvent& event) | ||||
| { | ||||
|     if (m_autocomplete_in_focus) { | ||||
|         if (event.key() == Key_Escape) { | ||||
|             m_autocomplete_in_focus = false; | ||||
|             m_autocomplete_box->close(); | ||||
|             return; | ||||
|         } | ||||
|         if (event.key() == Key_Down) { | ||||
|             m_autocomplete_box->next_suggestion(); | ||||
|             return; | ||||
|         } | ||||
|         if (event.key() == Key_Up) { | ||||
|             m_autocomplete_box->previous_suggestion(); | ||||
|             return; | ||||
|         } | ||||
|         if (event.key() == Key_Return || event.key() == Key_Tab) { | ||||
|             m_autocomplete_box->apply_suggestion(); | ||||
|             close_autocomplete(); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     auto autocomplete_action = [this]() { | ||||
|         auto data = get_autocomplete_request_data(); | ||||
|         if (data.has_value()) { | ||||
|             update_autocomplete(data.value()); | ||||
|             if (m_autocomplete_in_focus) | ||||
|                 show_autocomplete(data.value()); | ||||
|         } else { | ||||
|             close_autocomplete(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     if (event.ctrl() && event.key() == Key_Space) { | ||||
|         autocomplete_action(); | ||||
|     } | ||||
|     GUI::TextEditor::keydown_event(event); | ||||
| 
 | ||||
|     if (m_autocomplete_in_focus) { | ||||
|         autocomplete_action(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Editor::enter_event(Core::Event& event) | ||||
| { | ||||
|     m_hovering_editor = true; | ||||
|  | @ -486,6 +440,7 @@ void Editor::set_document(GUI::TextDocument& doc) | |||
|     } | ||||
| 
 | ||||
|     if (m_language_client) { | ||||
|         set_autocomplete_provider(make<LanguageServerAidedAutocompleteProvider>(*m_language_client)); | ||||
|         dbgln("Opening {}", code_document.file_path()); | ||||
|         int fd = open(code_document.file_path().characters(), O_RDONLY | O_NOCTTY); | ||||
|         if (fd < 0) { | ||||
|  | @ -505,39 +460,21 @@ Optional<Editor::AutoCompleteRequestData> Editor::get_autocomplete_request_data( | |||
|     return Editor::AutoCompleteRequestData { cursor() }; | ||||
| } | ||||
| 
 | ||||
| void Editor::update_autocomplete(const AutoCompleteRequestData& data) | ||||
| void Editor::LanguageServerAidedAutocompleteProvider::provide_completions(Function<void(Vector<Entry>)> callback) | ||||
| { | ||||
|     if (!m_language_client) | ||||
|         return; | ||||
|     auto& editor = static_cast<Editor&>(*m_editor).wrapper().editor(); | ||||
|     auto data = editor.get_autocomplete_request_data(); | ||||
|     if (!data.has_value()) | ||||
|         callback({}); | ||||
| 
 | ||||
|     m_language_client->on_autocomplete_suggestions = [=, this](auto suggestions) { | ||||
|         if (suggestions.is_empty()) { | ||||
|             close_autocomplete(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         show_autocomplete(data); | ||||
| 
 | ||||
|         m_autocomplete_box->update_suggestions(move(suggestions)); | ||||
|         m_autocomplete_in_focus = true; | ||||
|     m_language_client.on_autocomplete_suggestions = [callback = move(callback)](auto suggestions) { | ||||
|         callback(suggestions); | ||||
|     }; | ||||
| 
 | ||||
|     m_language_client->request_autocomplete( | ||||
|         code_document().file_path(), | ||||
|         data.position.line(), | ||||
|         data.position.column()); | ||||
| } | ||||
| 
 | ||||
| void Editor::show_autocomplete(const AutoCompleteRequestData& data) | ||||
| { | ||||
|     auto suggestion_box_location = content_rect_for_position(data.position).bottom_right().translated(screen_relative_rect().top_left().translated(ruler_width(), 0).translated(10, 5)); | ||||
|     m_autocomplete_box->show(suggestion_box_location); | ||||
| } | ||||
| 
 | ||||
| void Editor::close_autocomplete() | ||||
| { | ||||
|     m_autocomplete_box->close(); | ||||
|     m_autocomplete_in_focus = false; | ||||
|     m_language_client.request_autocomplete( | ||||
|         editor.code_document().file_path(), | ||||
|         data.value().position.line(), | ||||
|         data.value().position.column()); | ||||
| } | ||||
| 
 | ||||
| void Editor::on_edit_action(const GUI::Command& command) | ||||
|  |  | |||
|  | @ -26,7 +26,6 @@ | |||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "AutoCompleteBox.h" | ||||
| #include "CodeDocument.h" | ||||
| #include "Debugger/BreakpointCallback.h" | ||||
| #include "LanguageClient.h" | ||||
|  | @ -72,7 +71,6 @@ private: | |||
|     virtual void paint_event(GUI::PaintEvent&) override; | ||||
|     virtual void mousemove_event(GUI::MouseEvent&) override; | ||||
|     virtual void mousedown_event(GUI::MouseEvent&) override; | ||||
|     virtual void keydown_event(GUI::KeyEvent&) override; | ||||
|     virtual void enter_event(Core::Event&) override; | ||||
|     virtual void leave_event(Core::Event&) override; | ||||
| 
 | ||||
|  | @ -87,18 +85,26 @@ private: | |||
|         GUI::TextPosition position; | ||||
|     }; | ||||
| 
 | ||||
|     Optional<AutoCompleteRequestData> get_autocomplete_request_data(); | ||||
|     class LanguageServerAidedAutocompleteProvider final : virtual public GUI::AutocompleteProvider { | ||||
|     public: | ||||
|         LanguageServerAidedAutocompleteProvider(LanguageClient& client) | ||||
|             : m_language_client(client) | ||||
|         { | ||||
|         } | ||||
|         virtual ~LanguageServerAidedAutocompleteProvider() override { } | ||||
| 
 | ||||
|     void update_autocomplete(const AutoCompleteRequestData&); | ||||
|     void show_autocomplete(const AutoCompleteRequestData&); | ||||
|     void close_autocomplete(); | ||||
|     private: | ||||
|         virtual void provide_completions(Function<void(Vector<Entry>)> callback) override; | ||||
|         LanguageClient& m_language_client; | ||||
|     }; | ||||
| 
 | ||||
|     Optional<AutoCompleteRequestData> get_autocomplete_request_data(); | ||||
| 
 | ||||
|     void flush_file_content_to_langauge_server(); | ||||
| 
 | ||||
|     explicit Editor(); | ||||
| 
 | ||||
|     RefPtr<GUI::Window> m_documentation_tooltip_window; | ||||
|     OwnPtr<AutoCompleteBox> m_autocomplete_box; | ||||
|     RefPtr<Web::OutOfProcessWebView> m_documentation_page_view; | ||||
|     String m_last_parsed_token; | ||||
|     GUI::TextPosition m_previous_text_position { 0, 0 }; | ||||
|  |  | |||
|  | @ -61,7 +61,7 @@ void LanguageClient::request_autocomplete(const String& path, size_t cursor_line | |||
|     m_connection.post_message(Messages::LanguageServer::AutoCompleteSuggestions(path, cursor_line, cursor_column)); | ||||
| } | ||||
| 
 | ||||
| void LanguageClient::provide_autocomplete_suggestions(const Vector<AutoCompleteResponse>& suggestions) | ||||
| void LanguageClient::provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>& suggestions) | ||||
| { | ||||
|     if (on_autocomplete_suggestions) | ||||
|         on_autocomplete_suggestions(suggestions); | ||||
|  |  | |||
|  | @ -104,9 +104,9 @@ public: | |||
|     virtual void remove_text(const String& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column); | ||||
|     virtual void request_autocomplete(const String& path, size_t cursor_line, size_t cursor_column); | ||||
| 
 | ||||
|     void provide_autocomplete_suggestions(const Vector<AutoCompleteResponse>&); | ||||
|     void provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>&); | ||||
| 
 | ||||
|     Function<void(Vector<AutoCompleteResponse>)> on_autocomplete_suggestions; | ||||
|     Function<void(Vector<GUI::AutocompleteProvider::Entry>)> on_autocomplete_suggestions; | ||||
| 
 | ||||
| private: | ||||
|     ServerConnection& m_connection; | ||||
|  |  | |||
|  | @ -32,10 +32,10 @@ | |||
| 
 | ||||
| namespace LanguageServers::Cpp { | ||||
| 
 | ||||
| Vector<AutoCompleteResponse> AutoComplete::get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position) | ||||
| Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position) | ||||
| { | ||||
|     auto lines = code.split('\n', true); | ||||
|     Lexer lexer(code); | ||||
|     Cpp::Lexer lexer(code); | ||||
|     auto tokens = lexer.lex(); | ||||
| 
 | ||||
|     auto index_of_target_token = token_in_position(tokens, autocomplete_position); | ||||
|  | @ -75,10 +75,10 @@ Optional<size_t> AutoComplete::token_in_position(const Vector<Cpp::Token>& token | |||
|     return {}; | ||||
| } | ||||
| 
 | ||||
| Vector<AutoCompleteResponse> AutoComplete::identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>& tokens, size_t target_token_index) | ||||
| Vector<GUI::AutocompleteProvider::Entry> AutoComplete::identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>& tokens, size_t target_token_index) | ||||
| { | ||||
|     auto partial_input = text_of_token(lines, tokens[target_token_index]); | ||||
|     Vector<AutoCompleteResponse> suggestions; | ||||
|     Vector<GUI::AutocompleteProvider::Entry> suggestions; | ||||
| 
 | ||||
|     HashTable<String> suggestions_lookup; // To avoid duplicate results
 | ||||
| 
 | ||||
|  | @ -88,7 +88,7 @@ Vector<AutoCompleteResponse> AutoComplete::identifier_prefixes(const Vector<Stri | |||
|             continue; | ||||
|         auto text = text_of_token(lines, token); | ||||
|         if (text.starts_with(partial_input) && suggestions_lookup.set(text) == AK::HashSetResult::InsertedNewEntry) { | ||||
|             suggestions.append({ text, partial_input.length(), HackStudio::CompletionKind::Identifier }); | ||||
|             suggestions.append({ text, partial_input.length(), GUI::AutocompleteProvider::CompletionKind::Identifier }); | ||||
|         } | ||||
|     } | ||||
|     return suggestions; | ||||
|  |  | |||
|  | @ -35,18 +35,17 @@ | |||
| namespace LanguageServers::Cpp { | ||||
| 
 | ||||
| using namespace ::Cpp; | ||||
| using ::HackStudio::AutoCompleteResponse; | ||||
| 
 | ||||
| class AutoComplete { | ||||
| public: | ||||
|     AutoComplete() = delete; | ||||
| 
 | ||||
|     static Vector<AutoCompleteResponse> get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position); | ||||
|     static Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position); | ||||
| 
 | ||||
| private: | ||||
|     static Optional<size_t> token_in_position(const Vector<Cpp::Token>&, const GUI::TextPosition&); | ||||
|     static StringView text_of_token(const Vector<String>& lines, const Cpp::Token&); | ||||
|     static Vector<AutoCompleteResponse> identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>&, size_t target_token_index); | ||||
|     static Vector<GUI::AutocompleteProvider::Entry> identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>&, size_t target_token_index); | ||||
| }; | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| endpoint LanguageClient = 8002 | ||||
| { | ||||
|     AutoCompleteSuggestions(Vector<HackStudio::AutoCompleteResponse> suggestions) =| | ||||
|     AutoCompleteSuggestions(Vector<GUI::AutocompleteProvider::Entry> suggestions) =| | ||||
| } | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ | |||
| 
 | ||||
| namespace LanguageServers::Shell { | ||||
| 
 | ||||
| Vector<AutoCompleteResponse> AutoComplete::get_suggestions(const String& code, size_t offset) | ||||
| Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, size_t offset) | ||||
| { | ||||
|     // FIXME: No need to reparse this every time!
 | ||||
|     auto ast = ::Shell::Parser { code }.parse(); | ||||
|  | @ -49,7 +49,7 @@ Vector<AutoCompleteResponse> AutoComplete::get_suggestions(const String& code, s | |||
| #endif | ||||
| 
 | ||||
|     auto result = ast->complete_for_editor(m_shell, offset); | ||||
|     Vector<AutoCompleteResponse> completions; | ||||
|     Vector<GUI::AutocompleteProvider::Entry> completions; | ||||
|     for (auto& entry : result) { | ||||
| #ifdef DEBUG_AUTOCOMPLETE | ||||
|         dbgln("Suggestion: '{}' starting at {}", entry.text_string, entry.input_offset); | ||||
|  |  | |||
|  | @ -34,8 +34,6 @@ | |||
| 
 | ||||
| namespace LanguageServers::Shell { | ||||
| 
 | ||||
| using namespace ::HackStudio; | ||||
| 
 | ||||
| class AutoComplete { | ||||
| public: | ||||
|     AutoComplete() | ||||
|  | @ -43,7 +41,7 @@ public: | |||
|     { | ||||
|     } | ||||
| 
 | ||||
|     Vector<AutoCompleteResponse> get_suggestions(const String& code, size_t autocomplete_position); | ||||
|     Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, size_t autocomplete_position); | ||||
| 
 | ||||
| private: | ||||
|     NonnullRefPtr<::Shell::Shell> m_shell; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> | ||||
|  * Copyright (c) 2020, the SerenityOS developers. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  | @ -24,20 +24,21 @@ | |||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #include "AutoCompleteBox.h" | ||||
| #include "Editor.h" | ||||
| #include <LibGUI/AutocompleteProvider.h> | ||||
| #include <LibGUI/Model.h> | ||||
| #include <LibGUI/TableView.h> | ||||
| #include <LibGUI/TextEditor.h> | ||||
| #include <LibGUI/Window.h> | ||||
| #include <LibGfx/Bitmap.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| static RefPtr<Gfx::Bitmap> s_cpp_identifier_icon; | ||||
| static RefPtr<Gfx::Bitmap> s_unspecified_identifier_icon; | ||||
| 
 | ||||
| static RefPtr<Gfx::Bitmap> s_cplusplus_icon; | ||||
| namespace GUI { | ||||
| 
 | ||||
| class AutoCompleteSuggestionModel final : public GUI::Model { | ||||
| class AutocompleteSuggestionModel final : public GUI::Model { | ||||
| public: | ||||
|     explicit AutoCompleteSuggestionModel(Vector<AutoCompleteResponse>&& suggestions) | ||||
|     explicit AutocompleteSuggestionModel(Vector<AutocompleteProvider::Entry>&& suggestions) | ||||
|         : m_suggestions(move(suggestions)) | ||||
|     { | ||||
|     } | ||||
|  | @ -64,11 +65,20 @@ public: | |||
|                 return suggestion.completion; | ||||
|             } | ||||
|             if (index.column() == Column::Icon) { | ||||
|                 // TODO: Have separate icons for fields, functions, methods etc
 | ||||
|                 // FIXME: Probably should have different icons for the different kinds, rather than for "c++".
 | ||||
|                 if (suggestion.kind == CompletionKind::Identifier) | ||||
|                     return *s_cplusplus_icon; | ||||
|                 return *s_cplusplus_icon; | ||||
|                 // TODO
 | ||||
|                 if (suggestion.language == GUI::AutocompleteProvider::Language::Cpp) { | ||||
|                     if (!s_cpp_identifier_icon) { | ||||
|                         s_cpp_identifier_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/completion/cpp-identifier.png"); | ||||
|                     } | ||||
|                     return *s_cpp_identifier_icon; | ||||
|                 } | ||||
|                 if (suggestion.language == GUI::AutocompleteProvider::Language::Unspecified) { | ||||
|                     if (!s_unspecified_identifier_icon) { | ||||
|                         s_unspecified_identifier_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/completion/unspecified-identifier.png"); | ||||
|                     } | ||||
|                     return *s_unspecified_identifier_icon; | ||||
|                 } | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -83,18 +93,14 @@ public: | |||
|     virtual void update() override {}; | ||||
| 
 | ||||
| private: | ||||
|     Vector<AutoCompleteResponse> m_suggestions; | ||||
|     Vector<AutocompleteProvider::Entry> m_suggestions; | ||||
| }; | ||||
| 
 | ||||
| AutoCompleteBox::~AutoCompleteBox() { } | ||||
| AutocompleteBox::~AutocompleteBox() { } | ||||
| 
 | ||||
| AutoCompleteBox::AutoCompleteBox(WeakPtr<Editor> editor) | ||||
|     : m_editor(move(editor)) | ||||
| AutocompleteBox::AutocompleteBox(TextEditor& editor) | ||||
|     : m_editor(editor) | ||||
| { | ||||
|     if (!s_cplusplus_icon) { | ||||
|         s_cplusplus_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-cplusplus.png"); | ||||
|     } | ||||
| 
 | ||||
|     m_popup_window = GUI::Window::construct(); | ||||
|     m_popup_window->set_window_type(GUI::WindowType::Tooltip); | ||||
|     m_popup_window->set_rect(0, 0, 200, 100); | ||||
|  | @ -103,13 +109,13 @@ AutoCompleteBox::AutoCompleteBox(WeakPtr<Editor> editor) | |||
|     m_suggestion_view->set_column_headers_visible(false); | ||||
| } | ||||
| 
 | ||||
| void AutoCompleteBox::update_suggestions(Vector<AutoCompleteResponse>&& suggestions) | ||||
| void AutocompleteBox::update_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions) | ||||
| { | ||||
|     if (suggestions.is_empty()) | ||||
|         return; | ||||
| 
 | ||||
|     bool has_suggestions = !suggestions.is_empty(); | ||||
|     m_suggestion_view->set_model(adopt(*new AutoCompleteSuggestionModel(move(suggestions)))); | ||||
|     m_suggestion_view->set_model(adopt(*new AutocompleteSuggestionModel(move(suggestions)))); | ||||
| 
 | ||||
|     if (!has_suggestions) | ||||
|         m_suggestion_view->selection().clear(); | ||||
|  | @ -117,18 +123,23 @@ void AutoCompleteBox::update_suggestions(Vector<AutoCompleteResponse>&& suggesti | |||
|         m_suggestion_view->selection().set(m_suggestion_view->model()->index(0)); | ||||
| } | ||||
| 
 | ||||
| void AutoCompleteBox::show(Gfx::IntPoint suggstion_box_location) | ||||
| bool AutocompleteBox::is_visible() const | ||||
| { | ||||
|     return m_popup_window->is_visible(); | ||||
| } | ||||
| 
 | ||||
| void AutocompleteBox::show(Gfx::IntPoint suggstion_box_location) | ||||
| { | ||||
|     m_popup_window->move_to(suggstion_box_location); | ||||
|     m_popup_window->show(); | ||||
| } | ||||
| 
 | ||||
| void AutoCompleteBox::close() | ||||
| void AutocompleteBox::close() | ||||
| { | ||||
|     m_popup_window->hide(); | ||||
| } | ||||
| 
 | ||||
| void AutoCompleteBox::next_suggestion() | ||||
| void AutocompleteBox::next_suggestion() | ||||
| { | ||||
|     GUI::ModelIndex new_index = m_suggestion_view->selection().first(); | ||||
|     if (new_index.is_valid()) | ||||
|  | @ -142,7 +153,7 @@ void AutoCompleteBox::next_suggestion() | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void AutoCompleteBox::previous_suggestion() | ||||
| void AutocompleteBox::previous_suggestion() | ||||
| { | ||||
|     GUI::ModelIndex new_index = m_suggestion_view->selection().first(); | ||||
|     if (new_index.is_valid()) | ||||
|  | @ -156,22 +167,25 @@ void AutoCompleteBox::previous_suggestion() | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void AutoCompleteBox::apply_suggestion() | ||||
| void AutocompleteBox::apply_suggestion() | ||||
| { | ||||
|     if (m_editor.is_null()) | ||||
|         return; | ||||
| 
 | ||||
|     if (!m_editor->is_editable()) | ||||
|         return; | ||||
| 
 | ||||
|     auto selected_index = m_suggestion_view->selection().first(); | ||||
|     if (!selected_index.is_valid()) | ||||
|         return; | ||||
| 
 | ||||
|     auto suggestion_index = m_suggestion_view->model()->index(selected_index.row(), AutoCompleteSuggestionModel::Column::Name); | ||||
|     auto suggestion_index = m_suggestion_view->model()->index(selected_index.row(), AutocompleteSuggestionModel::Column::Name); | ||||
|     auto suggestion = suggestion_index.data().to_string(); | ||||
|     size_t partial_length = suggestion_index.data((GUI::ModelRole)AutoCompleteSuggestionModel::InternalRole::PartialInputLength).to_i64(); | ||||
|     size_t partial_length = suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::PartialInputLength).to_i64(); | ||||
| 
 | ||||
|     ASSERT(suggestion.length() >= partial_length); | ||||
|     auto completion = suggestion.substring_view(partial_length, suggestion.length() - partial_length); | ||||
|     m_editor->insert_at_cursor_or_replace_selection(completion); | ||||
| } | ||||
| 
 | ||||
| }; | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com> | ||||
|  * Copyright (c) 2020, the SerenityOS developers. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  | @ -26,20 +26,57 @@ | |||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include "AutoCompleteResponse.h" | ||||
| #include <AK/WeakPtr.h> | ||||
| #include <LibGUI/Widget.h> | ||||
| #include <LibGUI/Forward.h> | ||||
| #include <LibGUI/TextEditor.h> | ||||
| #include <LibGUI/Window.h> | ||||
| 
 | ||||
| namespace HackStudio { | ||||
| namespace GUI { | ||||
| 
 | ||||
| class Editor; | ||||
| class AutocompleteProvider { | ||||
|     AK_MAKE_NONCOPYABLE(AutocompleteProvider); | ||||
|     AK_MAKE_NONMOVABLE(AutocompleteProvider); | ||||
| 
 | ||||
| class AutoCompleteBox final { | ||||
| public: | ||||
|     explicit AutoCompleteBox(WeakPtr<Editor>); | ||||
|     ~AutoCompleteBox(); | ||||
|     virtual ~AutocompleteProvider() { } | ||||
| 
 | ||||
|     void update_suggestions(Vector<AutoCompleteResponse>&& suggestions); | ||||
|     enum class CompletionKind { | ||||
|         Identifier, | ||||
|     }; | ||||
| 
 | ||||
|     enum class Language { | ||||
|         Unspecified, | ||||
|         Cpp, | ||||
|     }; | ||||
| 
 | ||||
|     struct Entry { | ||||
|         String completion; | ||||
|         size_t partial_input_length { 0 }; | ||||
|         CompletionKind kind { CompletionKind::Identifier }; | ||||
|         Language language { Language::Unspecified }; | ||||
|     }; | ||||
| 
 | ||||
|     virtual void provide_completions(Function<void(Vector<Entry>)>) = 0; | ||||
| 
 | ||||
|     void attach(TextEditor& editor) | ||||
|     { | ||||
|         ASSERT(!m_editor); | ||||
|         m_editor = editor; | ||||
|     } | ||||
|     void detach() { m_editor.clear(); } | ||||
| 
 | ||||
| protected: | ||||
|     AutocompleteProvider() { } | ||||
| 
 | ||||
|     WeakPtr<TextEditor> m_editor; | ||||
| }; | ||||
| 
 | ||||
| class AutocompleteBox final { | ||||
| public: | ||||
|     explicit AutocompleteBox(TextEditor&); | ||||
|     ~AutocompleteBox(); | ||||
| 
 | ||||
|     void update_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions); | ||||
|     bool is_visible() const; | ||||
|     void show(Gfx::IntPoint suggstion_box_location); | ||||
|     void close(); | ||||
| 
 | ||||
|  | @ -48,10 +85,9 @@ public: | |||
|     void apply_suggestion(); | ||||
| 
 | ||||
| private: | ||||
|     void complete_suggestion(const GUI::ModelIndex&); | ||||
| 
 | ||||
|     WeakPtr<Editor> m_editor; | ||||
|     WeakPtr<TextEditor> m_editor; | ||||
|     RefPtr<GUI::Window> m_popup_window; | ||||
|     RefPtr<GUI::TableView> m_suggestion_view; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
|  | @ -6,9 +6,10 @@ set(SOURCES | |||
|     Action.cpp | ||||
|     ActionGroup.cpp | ||||
|     Application.cpp | ||||
|     AutocompleteProvider.cpp | ||||
|     BoxLayout.cpp | ||||
|     Button.cpp | ||||
|     BreadcrumbBar.cpp | ||||
|     Button.cpp | ||||
|     Calendar.cpp | ||||
|     CheckBox.cpp | ||||
|     Clipboard.cpp | ||||
|  |  | |||
|  | @ -25,10 +25,12 @@ | |||
|  */ | ||||
| 
 | ||||
| #include <AK/QuickSort.h> | ||||
| #include <AK/ScopeGuard.h> | ||||
| #include <AK/StringBuilder.h> | ||||
| #include <AK/TemporaryChange.h> | ||||
| #include <LibCore/Timer.h> | ||||
| #include <LibGUI/Action.h> | ||||
| #include <LibGUI/AutocompleteProvider.h> | ||||
| #include <LibGUI/Clipboard.h> | ||||
| #include <LibGUI/InputBox.h> | ||||
| #include <LibGUI/Menu.h> | ||||
|  | @ -711,6 +713,27 @@ void TextEditor::sort_selected_lines() | |||
| 
 | ||||
| void TextEditor::keydown_event(KeyEvent& event) | ||||
| { | ||||
|     if (m_autocomplete_box && m_autocomplete_box->is_visible() && (event.key() == KeyCode::Key_Return || event.key() == KeyCode::Key_Tab)) { | ||||
|         m_autocomplete_box->apply_suggestion(); | ||||
|         m_autocomplete_box->close(); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (m_autocomplete_box && m_autocomplete_box->is_visible() && event.key() == KeyCode::Key_Escape) { | ||||
|         m_autocomplete_box->close(); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (m_autocomplete_box && m_autocomplete_box->is_visible() && event.key() == KeyCode::Key_Up) { | ||||
|         m_autocomplete_box->previous_suggestion(); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (m_autocomplete_box && m_autocomplete_box->is_visible() && event.key() == KeyCode::Key_Down) { | ||||
|         m_autocomplete_box->next_suggestion(); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (is_single_line() && event.key() == KeyCode::Key_Tab) | ||||
|         return ScrollableWidget::keydown_event(event); | ||||
| 
 | ||||
|  | @ -720,6 +743,14 @@ void TextEditor::keydown_event(KeyEvent& event) | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     ArmedScopeGuard update_autocomplete { [&] { | ||||
|         if (m_autocomplete_box && m_autocomplete_box->is_visible()) { | ||||
|             m_autocomplete_provider->provide_completions([&](auto completions) { | ||||
|                 m_autocomplete_box->update_suggestions(move(completions)); | ||||
|             }); | ||||
|         } | ||||
|     } }; | ||||
| 
 | ||||
|     if (event.key() == KeyCode::Key_Escape) { | ||||
|         if (on_escape_pressed) | ||||
|             on_escape_pressed(); | ||||
|  | @ -997,6 +1028,18 @@ void TextEditor::keydown_event(KeyEvent& event) | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (!event.shift() && !event.alt() && event.ctrl() && event.key() == KeyCode::Key_Space) { | ||||
|         if (m_autocomplete_provider) { | ||||
|             m_autocomplete_provider->provide_completions([&](auto completions) { | ||||
|                 m_autocomplete_box->update_suggestions(move(completions)); | ||||
|                 auto position = content_rect_for_position(cursor()).bottom_right().translated(screen_relative_rect().top_left().translated(ruler_width(), 0).translated(10, 5)); | ||||
|                 m_autocomplete_box->show(position); | ||||
|             }); | ||||
|             update_autocomplete.disarm(); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (is_editable() && !event.ctrl() && !event.alt() && event.code_point() != 0) { | ||||
|         StringBuilder sb; | ||||
|         sb.append_code_point(event.code_point()); | ||||
|  | @ -1759,6 +1802,25 @@ void TextEditor::set_syntax_highlighter(OwnPtr<SyntaxHighlighter> highlighter) | |||
|         document().set_spans({}); | ||||
| } | ||||
| 
 | ||||
| const AutocompleteProvider* TextEditor::autocomplete_provider() const | ||||
| { | ||||
|     return m_autocomplete_provider.ptr(); | ||||
| } | ||||
| 
 | ||||
| void TextEditor::set_autocomplete_provider(OwnPtr<AutocompleteProvider>&& provider) | ||||
| { | ||||
|     if (m_autocomplete_provider) | ||||
|         m_autocomplete_provider->detach(); | ||||
|     m_autocomplete_provider = move(provider); | ||||
|     if (m_autocomplete_provider) { | ||||
|         m_autocomplete_provider->attach(*this); | ||||
|         if (!m_autocomplete_box) | ||||
|             m_autocomplete_box = make<AutocompleteBox>(*this); | ||||
|     } | ||||
|     if (m_autocomplete_box) | ||||
|         m_autocomplete_box->close(); | ||||
| } | ||||
| 
 | ||||
| int TextEditor::line_height() const | ||||
| { | ||||
|     return font().glyph_height() + m_line_spacing; | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ | |||
| #include <AK/NonnullOwnPtrVector.h> | ||||
| #include <AK/NonnullRefPtrVector.h> | ||||
| #include <LibCore/ElapsedTimer.h> | ||||
| #include <LibGUI/Forward.h> | ||||
| #include <LibGUI/ScrollableWidget.h> | ||||
| #include <LibGUI/TextDocument.h> | ||||
| #include <LibGUI/TextRange.h> | ||||
|  | @ -159,6 +160,9 @@ public: | |||
|     const SyntaxHighlighter* syntax_highlighter() const; | ||||
|     void set_syntax_highlighter(OwnPtr<SyntaxHighlighter>); | ||||
| 
 | ||||
|     const AutocompleteProvider* autocomplete_provider() const; | ||||
|     void set_autocomplete_provider(OwnPtr<AutocompleteProvider>&&); | ||||
| 
 | ||||
|     bool is_in_drag_select() const { return m_in_drag_select; } | ||||
| 
 | ||||
| protected: | ||||
|  | @ -317,6 +321,8 @@ private: | |||
|     NonnullOwnPtrVector<LineVisualData> m_line_visual_data; | ||||
| 
 | ||||
|     OwnPtr<SyntaxHighlighter> m_highlighter; | ||||
|     OwnPtr<AutocompleteProvider> m_autocomplete_provider; | ||||
|     OwnPtr<AutocompleteBox> m_autocomplete_box; | ||||
| 
 | ||||
|     RefPtr<Core::Timer> m_automatic_selection_scroll_timer; | ||||
|     Gfx::IntPoint m_last_mousemove_position; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 AnotherTest
						AnotherTest