mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 12:32:43 +00:00 
			
		
		
		
	LibSyntax+LibGUI+LibJS: Move JS syntax highlighter to LibJS
This is a little bit messy but the basic idea is: Syntax::Highlighter now has a Syntax::HighlighterClient to talk to the outside world. It mostly communicates in LibGUI primitives that are available in headers, so inlineable. GUI::TextEditor inherits from Syntax::HighlighterClient. This let us to move GUI::JSSyntaxHighlighter to JS::SyntaxHighlighter and remove LibGUI's dependency on LibJS.
This commit is contained in:
		
							parent
							
								
									22baa5e64f
								
							
						
					
					
						commit
						ddbf20ecf6
					
				
					 23 changed files with 139 additions and 80 deletions
				
			
		|  | @ -28,13 +28,10 @@ | ||||||
| #include "ConsoleWidget.h" | #include "ConsoleWidget.h" | ||||||
| #include <AK/StringBuilder.h> | #include <AK/StringBuilder.h> | ||||||
| #include <LibGUI/BoxLayout.h> | #include <LibGUI/BoxLayout.h> | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> |  | ||||||
| #include <LibGUI/TextBox.h> | #include <LibGUI/TextBox.h> | ||||||
| #include <LibWeb/DOM/DocumentType.h> | #include <LibWeb/DOM/DocumentType.h> | ||||||
| #include <LibWeb/DOM/ElementFactory.h> |  | ||||||
| #include <LibWeb/DOM/Text.h> | #include <LibWeb/DOM/Text.h> | ||||||
| #include <LibWeb/DOMTreeModel.h> | #include <LibWeb/DOMTreeModel.h> | ||||||
| #include <LibWeb/HTML/HTMLBodyElement.h> |  | ||||||
| 
 | 
 | ||||||
| namespace Browser { | namespace Browser { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,13 +28,13 @@ | ||||||
| #include <AK/StringBuilder.h> | #include <AK/StringBuilder.h> | ||||||
| #include <LibGUI/BoxLayout.h> | #include <LibGUI/BoxLayout.h> | ||||||
| #include <LibGUI/Button.h> | #include <LibGUI/Button.h> | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> |  | ||||||
| #include <LibGUI/TextBox.h> | #include <LibGUI/TextBox.h> | ||||||
| #include <LibGfx/FontDatabase.h> | #include <LibGfx/FontDatabase.h> | ||||||
| #include <LibJS/Interpreter.h> | #include <LibJS/Interpreter.h> | ||||||
| #include <LibJS/MarkupGenerator.h> | #include <LibJS/MarkupGenerator.h> | ||||||
| #include <LibJS/Parser.h> | #include <LibJS/Parser.h> | ||||||
| #include <LibJS/Runtime/Error.h> | #include <LibJS/Runtime/Error.h> | ||||||
|  | #include <LibJS/SyntaxHighlighter.h> | ||||||
| #include <LibWeb/DOM/DocumentType.h> | #include <LibWeb/DOM/DocumentType.h> | ||||||
| #include <LibWeb/DOM/ElementFactory.h> | #include <LibWeb/DOM/ElementFactory.h> | ||||||
| #include <LibWeb/DOM/Text.h> | #include <LibWeb/DOM/Text.h> | ||||||
|  | @ -66,7 +66,7 @@ ConsoleWidget::ConsoleWidget() | ||||||
|     bottom_container.set_fixed_height(22); |     bottom_container.set_fixed_height(22); | ||||||
| 
 | 
 | ||||||
|     m_input = bottom_container.add<GUI::TextBox>(); |     m_input = bottom_container.add<GUI::TextBox>(); | ||||||
|     m_input->set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>()); |     m_input->set_syntax_highlighter(make<JS::SyntaxHighlighter>()); | ||||||
|     // FIXME: Syntax Highlighting breaks the cursor's position on non fixed-width fonts.
 |     // FIXME: Syntax Highlighting breaks the cursor's position on non fixed-width fonts.
 | ||||||
|     m_input->set_font(Gfx::FontDatabase::default_fixed_width_font()); |     m_input->set_font(Gfx::FontDatabase::default_fixed_width_font()); | ||||||
|     m_input->set_history_enabled(true); |     m_input->set_history_enabled(true); | ||||||
|  |  | ||||||
|  | @ -25,7 +25,6 @@ | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| #include "CellSyntaxHighlighter.h" | #include "CellSyntaxHighlighter.h" | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> |  | ||||||
| #include <LibGUI/TextEditor.h> | #include <LibGUI/TextEditor.h> | ||||||
| #include <LibGfx/Palette.h> | #include <LibGfx/Palette.h> | ||||||
| #include <LibJS/Lexer.h> | #include <LibJS/Lexer.h> | ||||||
|  | @ -34,18 +33,17 @@ namespace Spreadsheet { | ||||||
| 
 | 
 | ||||||
| void CellSyntaxHighlighter::rehighlight(Gfx::Palette palette) | void CellSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto text = m_client->get_text(); | ||||||
|     auto text = m_editor->text(); |     m_client->spans().clear(); | ||||||
|     m_editor->document().spans().clear(); |  | ||||||
|     if (!text.starts_with('=')) { |     if (!text.starts_with('=')) { | ||||||
|         m_editor->update(); |         m_client->do_update(); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     JSSyntaxHighlighter::rehighlight(palette); |     JS::SyntaxHighlighter::rehighlight(palette); | ||||||
| 
 | 
 | ||||||
|     // Highlight the '='
 |     // Highlight the '='
 | ||||||
|     m_editor->document().spans().empend( |     m_client->spans().empend( | ||||||
|         GUI::TextRange { { 0, 0 }, { 0, 1 } }, |         GUI::TextRange { { 0, 0 }, { 0, 1 } }, | ||||||
|         Gfx::TextAttributes { |         Gfx::TextAttributes { | ||||||
|             palette.syntax_keyword(), |             palette.syntax_keyword(), | ||||||
|  | @ -59,7 +57,7 @@ void CellSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
|     if (m_cell && m_cell->exception()) { |     if (m_cell && m_cell->exception()) { | ||||||
|         auto range = m_cell->exception()->source_ranges().first(); |         auto range = m_cell->exception()->source_ranges().first(); | ||||||
|         GUI::TextRange text_range { { range.start.line - 1, range.start.column }, { range.end.line - 1, range.end.column - 1 } }; |         GUI::TextRange text_range { { range.start.line - 1, range.start.column }, { range.end.line - 1, range.end.column - 1 } }; | ||||||
|         m_editor->document().spans().prepend( |         m_client->spans().prepend( | ||||||
|             GUI::TextDocumentSpan { |             GUI::TextDocumentSpan { | ||||||
|                 text_range, |                 text_range, | ||||||
|                 Gfx::TextAttributes { |                 Gfx::TextAttributes { | ||||||
|  | @ -71,7 +69,7 @@ void CellSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
|                 nullptr, |                 nullptr, | ||||||
|                 false }); |                 false }); | ||||||
|     } |     } | ||||||
|     m_editor->update(); |     m_client->do_update(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| CellSyntaxHighlighter::~CellSyntaxHighlighter() | CellSyntaxHighlighter::~CellSyntaxHighlighter() | ||||||
|  |  | ||||||
|  | @ -27,11 +27,11 @@ | ||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
| #include "Cell.h" | #include "Cell.h" | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> | #include <LibJS/SyntaxHighlighter.h> | ||||||
| 
 | 
 | ||||||
| namespace Spreadsheet { | namespace Spreadsheet { | ||||||
| 
 | 
 | ||||||
| class CellSyntaxHighlighter final : public GUI::JSSyntaxHighlighter { | class CellSyntaxHighlighter final : public JS::SyntaxHighlighter { | ||||||
| public: | public: | ||||||
|     CellSyntaxHighlighter() { } |     CellSyntaxHighlighter() { } | ||||||
|     virtual ~CellSyntaxHighlighter() override; |     virtual ~CellSyntaxHighlighter() override; | ||||||
|  |  | ||||||
|  | @ -36,7 +36,6 @@ | ||||||
| #include <LibGUI/ColorInput.h> | #include <LibGUI/ColorInput.h> | ||||||
| #include <LibGUI/ComboBox.h> | #include <LibGUI/ComboBox.h> | ||||||
| #include <LibGUI/ItemListModel.h> | #include <LibGUI/ItemListModel.h> | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> |  | ||||||
| #include <LibGUI/Label.h> | #include <LibGUI/Label.h> | ||||||
| #include <LibGUI/ListView.h> | #include <LibGUI/ListView.h> | ||||||
| #include <LibGUI/SpinBox.h> | #include <LibGUI/SpinBox.h> | ||||||
|  | @ -44,6 +43,7 @@ | ||||||
| #include <LibGUI/TextEditor.h> | #include <LibGUI/TextEditor.h> | ||||||
| #include <LibGUI/Widget.h> | #include <LibGUI/Widget.h> | ||||||
| #include <LibGfx/FontDatabase.h> | #include <LibGfx/FontDatabase.h> | ||||||
|  | #include <LibJS/SyntaxHighlighter.h> | ||||||
| 
 | 
 | ||||||
| REGISTER_WIDGET(Spreadsheet, ConditionsView); | REGISTER_WIDGET(Spreadsheet, ConditionsView); | ||||||
| 
 | 
 | ||||||
|  | @ -425,7 +425,7 @@ ConditionView::ConditionView(ConditionalFormat& fmt) | ||||||
|         m_format.background_color = bg_input.color(); |         m_format.background_color = bg_input.color(); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     formula_editor.set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>()); |     formula_editor.set_syntax_highlighter(make<JS::SyntaxHighlighter>()); | ||||||
|     formula_editor.set_should_hide_unnecessary_scrollbars(true); |     formula_editor.set_should_hide_unnecessary_scrollbars(true); | ||||||
|     formula_editor.set_font(&Gfx::FontDatabase::default_fixed_width_font()); |     formula_editor.set_font(&Gfx::FontDatabase::default_fixed_width_font()); | ||||||
|     formula_editor.on_change = [&] { |     formula_editor.on_change = [&] { | ||||||
|  |  | ||||||
|  | @ -7,4 +7,4 @@ set(SOURCES | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| serenity_app(TextEditor ICON app-text-editor) | serenity_app(TextEditor ICON app-text-editor) | ||||||
| target_link_libraries(TextEditor LibWeb LibMarkdown LibGUI LibShell LibRegex LibDesktop LibCpp) | target_link_libraries(TextEditor LibWeb LibMarkdown LibGUI LibShell LibRegex LibDesktop LibCpp LibJS) | ||||||
|  |  | ||||||
|  | @ -43,7 +43,6 @@ | ||||||
| #include <LibGUI/FontPicker.h> | #include <LibGUI/FontPicker.h> | ||||||
| #include <LibGUI/GMLSyntaxHighlighter.h> | #include <LibGUI/GMLSyntaxHighlighter.h> | ||||||
| #include <LibGUI/INISyntaxHighlighter.h> | #include <LibGUI/INISyntaxHighlighter.h> | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> |  | ||||||
| #include <LibGUI/Menu.h> | #include <LibGUI/Menu.h> | ||||||
| #include <LibGUI/MenuBar.h> | #include <LibGUI/MenuBar.h> | ||||||
| #include <LibGUI/MessageBox.h> | #include <LibGUI/MessageBox.h> | ||||||
|  | @ -57,6 +56,7 @@ | ||||||
| #include <LibGUI/ToolBarContainer.h> | #include <LibGUI/ToolBarContainer.h> | ||||||
| #include <LibGUI/VimEditingEngine.h> | #include <LibGUI/VimEditingEngine.h> | ||||||
| #include <LibGfx/Font.h> | #include <LibGfx/Font.h> | ||||||
|  | #include <LibJS/SyntaxHighlighter.h> | ||||||
| #include <LibMarkdown/Document.h> | #include <LibMarkdown/Document.h> | ||||||
| #include <LibWeb/OutOfProcessWebView.h> | #include <LibWeb/OutOfProcessWebView.h> | ||||||
| #include <string.h> | #include <string.h> | ||||||
|  | @ -476,7 +476,7 @@ TextEditorWidget::TextEditorWidget() | ||||||
|     syntax_menu.add_action(*m_cpp_highlight); |     syntax_menu.add_action(*m_cpp_highlight); | ||||||
| 
 | 
 | ||||||
|     m_js_highlight = GUI::Action::create_checkable("JavaScript", [&](auto&) { |     m_js_highlight = GUI::Action::create_checkable("JavaScript", [&](auto&) { | ||||||
|         m_editor->set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>()); |         m_editor->set_syntax_highlighter(make<JS::SyntaxHighlighter>()); | ||||||
|         m_editor->update(); |         m_editor->update(); | ||||||
|     }); |     }); | ||||||
|     syntax_actions.add_action(*m_js_highlight); |     syntax_actions.add_action(*m_js_highlight); | ||||||
|  |  | ||||||
|  | @ -38,12 +38,12 @@ | ||||||
| #include <LibGUI/Application.h> | #include <LibGUI/Application.h> | ||||||
| #include <LibGUI/GMLSyntaxHighlighter.h> | #include <LibGUI/GMLSyntaxHighlighter.h> | ||||||
| #include <LibGUI/INISyntaxHighlighter.h> | #include <LibGUI/INISyntaxHighlighter.h> | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> |  | ||||||
| #include <LibGUI/Label.h> | #include <LibGUI/Label.h> | ||||||
| #include <LibGUI/Painter.h> | #include <LibGUI/Painter.h> | ||||||
| #include <LibGUI/ScrollBar.h> | #include <LibGUI/ScrollBar.h> | ||||||
| #include <LibGUI/ShellSyntaxHighlighter.h> | #include <LibGUI/ShellSyntaxHighlighter.h> | ||||||
| #include <LibGUI/Window.h> | #include <LibGUI/Window.h> | ||||||
|  | #include <LibJS/SyntaxHighlighter.h> | ||||||
| #include <LibMarkdown/Document.h> | #include <LibMarkdown/Document.h> | ||||||
| #include <LibWeb/DOM/ElementFactory.h> | #include <LibWeb/DOM/ElementFactory.h> | ||||||
| #include <LibWeb/DOM/Text.h> | #include <LibWeb/DOM/Text.h> | ||||||
|  | @ -424,7 +424,7 @@ void Editor::set_document(GUI::TextDocument& doc) | ||||||
|         set_syntax_highlighter(make<GUI::GMLSyntaxHighlighter>()); |         set_syntax_highlighter(make<GUI::GMLSyntaxHighlighter>()); | ||||||
|         break; |         break; | ||||||
|     case Language::JavaScript: |     case Language::JavaScript: | ||||||
|         set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>()); |         set_syntax_highlighter(make<JS::SyntaxHighlighter>()); | ||||||
|         break; |         break; | ||||||
|     case Language::Ini: |     case Language::Ini: | ||||||
|         set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>()); |         set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>()); | ||||||
|  |  | ||||||
|  | @ -77,8 +77,7 @@ bool SyntaxHighlighter::is_navigatable(void* token) const | ||||||
| 
 | 
 | ||||||
| void SyntaxHighlighter::rehighlight(Gfx::Palette palette) | void SyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto text = m_client->get_text(); | ||||||
|     auto text = m_editor->text(); |  | ||||||
|     Cpp::Lexer lexer(text); |     Cpp::Lexer lexer(text); | ||||||
|     auto tokens = lexer.lex(); |     auto tokens = lexer.lex(); | ||||||
| 
 | 
 | ||||||
|  | @ -95,12 +94,12 @@ void SyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
|         span.data = reinterpret_cast<void*>(token.m_type); |         span.data = reinterpret_cast<void*>(token.m_type); | ||||||
|         spans.append(span); |         spans.append(span); | ||||||
|     } |     } | ||||||
|     m_editor->document().set_spans(spans); |     m_client->do_set_spans(move(spans)); | ||||||
| 
 | 
 | ||||||
|     m_has_brace_buddies = false; |     m_has_brace_buddies = false; | ||||||
|     highlight_matching_token_pair(); |     highlight_matching_token_pair(); | ||||||
| 
 | 
 | ||||||
|     m_editor->update(); |     m_client->do_update(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Vector<SyntaxHighlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs() const | Vector<SyntaxHighlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs() const | ||||||
|  |  | ||||||
|  | @ -48,7 +48,6 @@ set(SOURCES | ||||||
|     IconView.cpp |     IconView.cpp | ||||||
|     ImageWidget.cpp |     ImageWidget.cpp | ||||||
|     InputBox.cpp |     InputBox.cpp | ||||||
|     JSSyntaxHighlighter.cpp |  | ||||||
|     JsonArrayModel.cpp |     JsonArrayModel.cpp | ||||||
|     Label.cpp |     Label.cpp | ||||||
|     Layout.cpp |     Layout.cpp | ||||||
|  | @ -109,4 +108,4 @@ set(GENERATED_SOURCES | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| serenity_lib(LibGUI gui) | serenity_lib(LibGUI gui) | ||||||
| target_link_libraries(LibGUI LibCore LibGfx LibIPC LibThread LibShell LibRegex LibJS LibSyntax) | target_link_libraries(LibGUI LibCore LibGfx LibIPC LibThread LibShell LibRegex LibSyntax) | ||||||
|  |  | ||||||
|  | @ -82,6 +82,7 @@ class TableView; | ||||||
| class TextBox; | class TextBox; | ||||||
| class TextDocument; | class TextDocument; | ||||||
| class TextDocumentLine; | class TextDocumentLine; | ||||||
|  | struct TextDocumentSpan; | ||||||
| class TextDocumentUndoCommand; | class TextDocumentUndoCommand; | ||||||
| class TextEditor; | class TextEditor; | ||||||
| class ThemeChangeEvent; | class ThemeChangeEvent; | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
| 
 | 
 | ||||||
| namespace GUI { | namespace GUI { | ||||||
| 
 | 
 | ||||||
| static Syntax::TextStyle style_for_token_type(Gfx::Palette palette, GMLToken::Type type) | static Syntax::TextStyle style_for_token_type(const Gfx::Palette& palette, GMLToken::Type type) | ||||||
| { | { | ||||||
|     switch (type) { |     switch (type) { | ||||||
|     case GMLToken::Type::LeftCurly: |     case GMLToken::Type::LeftCurly: | ||||||
|  | @ -61,8 +61,7 @@ bool GMLSyntaxHighlighter::is_identifier(void* token) const | ||||||
| 
 | 
 | ||||||
| void GMLSyntaxHighlighter::rehighlight(Gfx::Palette palette) | void GMLSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto text = m_client->get_text(); | ||||||
|     auto text = m_editor->text(); |  | ||||||
|     GMLLexer lexer(text); |     GMLLexer lexer(text); | ||||||
|     auto tokens = lexer.lex(); |     auto tokens = lexer.lex(); | ||||||
| 
 | 
 | ||||||
|  | @ -78,12 +77,12 @@ void GMLSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
|         span.data = reinterpret_cast<void*>(token.m_type); |         span.data = reinterpret_cast<void*>(token.m_type); | ||||||
|         spans.append(span); |         spans.append(span); | ||||||
|     } |     } | ||||||
|     m_editor->document().set_spans(spans); |     m_client->do_set_spans(move(spans)); | ||||||
| 
 | 
 | ||||||
|     m_has_brace_buddies = false; |     m_has_brace_buddies = false; | ||||||
|     highlight_matching_token_pair(); |     highlight_matching_token_pair(); | ||||||
| 
 | 
 | ||||||
|     m_editor->update(); |     m_client->do_update(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Vector<GMLSyntaxHighlighter::MatchingTokenPair> GMLSyntaxHighlighter::matching_token_pairs() const | Vector<GMLSyntaxHighlighter::MatchingTokenPair> GMLSyntaxHighlighter::matching_token_pairs() const | ||||||
|  |  | ||||||
|  | @ -60,8 +60,7 @@ bool IniSyntaxHighlighter::is_identifier(void* token) const | ||||||
| 
 | 
 | ||||||
| void IniSyntaxHighlighter::rehighlight(Gfx::Palette palette) | void IniSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto text = m_client->get_text(); | ||||||
|     auto text = m_editor->text(); |  | ||||||
|     IniLexer lexer(text); |     IniLexer lexer(text); | ||||||
|     auto tokens = lexer.lex(); |     auto tokens = lexer.lex(); | ||||||
| 
 | 
 | ||||||
|  | @ -77,12 +76,12 @@ void IniSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
|         span.data = reinterpret_cast<void*>(token.m_type); |         span.data = reinterpret_cast<void*>(token.m_type); | ||||||
|         spans.append(span); |         spans.append(span); | ||||||
|     } |     } | ||||||
|     m_editor->document().set_spans(spans); |     m_client->do_set_spans(move(spans)); | ||||||
| 
 | 
 | ||||||
|     m_has_brace_buddies = false; |     m_has_brace_buddies = false; | ||||||
|     highlight_matching_token_pair(); |     highlight_matching_token_pair(); | ||||||
| 
 | 
 | ||||||
|     m_editor->update(); |     m_client->do_update(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Vector<IniSyntaxHighlighter::MatchingTokenPair> IniSyntaxHighlighter::matching_token_pairs() const | Vector<IniSyntaxHighlighter::MatchingTokenPair> IniSyntaxHighlighter::matching_token_pairs() const | ||||||
|  |  | ||||||
|  | @ -502,25 +502,24 @@ bool ShellSyntaxHighlighter::is_navigatable(void* token) const | ||||||
| 
 | 
 | ||||||
| void ShellSyntaxHighlighter::rehighlight(Gfx::Palette palette) | void ShellSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto text = m_client->get_text(); | ||||||
|     auto text = m_editor->text(); |  | ||||||
| 
 | 
 | ||||||
|     Parser parser(text); |     Parser parser(text); | ||||||
|     auto ast = parser.parse(); |     auto ast = parser.parse(); | ||||||
| 
 | 
 | ||||||
|     Vector<GUI::TextDocumentSpan> spans; |     Vector<GUI::TextDocumentSpan> spans; | ||||||
|     GUI::TextPosition position { 0, 0 }; |     GUI::TextPosition position { 0, 0 }; | ||||||
|     HighlightVisitor visitor { spans, palette, m_editor->document() }; |     HighlightVisitor visitor { spans, palette, m_client->get_document() }; | ||||||
| 
 | 
 | ||||||
|     if (ast) |     if (ast) | ||||||
|         ast->visit(visitor); |         ast->visit(visitor); | ||||||
| 
 | 
 | ||||||
|     quick_sort(spans, [](auto& a, auto& b) { return a.range.start() < b.range.start(); }); |     quick_sort(spans, [](auto& a, auto& b) { return a.range.start() < b.range.start(); }); | ||||||
| 
 | 
 | ||||||
|     m_editor->document().set_spans(spans); |     m_client->do_set_spans(move(spans)); | ||||||
|     m_has_brace_buddies = false; |     m_has_brace_buddies = false; | ||||||
|     highlight_matching_token_pair(); |     highlight_matching_token_pair(); | ||||||
|     m_editor->update(); |     m_client->do_update(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Vector<Syntax::Highlighter::MatchingTokenPair> ShellSyntaxHighlighter::matching_token_pairs() const | Vector<Syntax::Highlighter::MatchingTokenPair> ShellSyntaxHighlighter::matching_token_pairs() const | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /*
 | /*
 | ||||||
|  * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> |  * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> | ||||||
|  * All rights reserved. |  * All rights reserved. | ||||||
|  * |  * | ||||||
|  * Redistribution and use in source and binary forms, with or without |  * Redistribution and use in source and binary forms, with or without | ||||||
|  | @ -79,7 +79,7 @@ public: | ||||||
|     const TextDocumentLine& line(size_t line_index) const { return m_lines[line_index]; } |     const TextDocumentLine& line(size_t line_index) const { return m_lines[line_index]; } | ||||||
|     TextDocumentLine& line(size_t line_index) { return m_lines[line_index]; } |     TextDocumentLine& line(size_t line_index) { return m_lines[line_index]; } | ||||||
| 
 | 
 | ||||||
|     void set_spans(const Vector<TextDocumentSpan>& spans) { m_spans = spans; } |     void set_spans(Vector<TextDocumentSpan> spans) { m_spans = move(spans); } | ||||||
| 
 | 
 | ||||||
|     void set_text(const StringView&); |     void set_text(const StringView&); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -37,13 +37,15 @@ | ||||||
| #include <LibGUI/TextRange.h> | #include <LibGUI/TextRange.h> | ||||||
| #include <LibGfx/TextAlignment.h> | #include <LibGfx/TextAlignment.h> | ||||||
| #include <LibSyntax/Forward.h> | #include <LibSyntax/Forward.h> | ||||||
|  | #include <LibSyntax/HighlighterClient.h> | ||||||
| 
 | 
 | ||||||
| namespace GUI { | namespace GUI { | ||||||
| 
 | 
 | ||||||
| class TextEditor | class TextEditor | ||||||
|     : public ScrollableWidget |     : public ScrollableWidget | ||||||
|     , public TextDocument::Client { |     , public TextDocument::Client | ||||||
|     C_OBJECT(TextEditor) |     , public Syntax::HighlighterClient { | ||||||
|  |     C_OBJECT(TextEditor); | ||||||
| 
 | 
 | ||||||
| public: | public: | ||||||
|     enum Type { |     enum Type { | ||||||
|  | @ -241,6 +243,16 @@ private: | ||||||
|     virtual void document_did_set_text() override; |     virtual void document_did_set_text() override; | ||||||
|     virtual void document_did_set_cursor(const TextPosition&) override; |     virtual void document_did_set_cursor(const TextPosition&) override; | ||||||
| 
 | 
 | ||||||
|  |     // ^Syntax::HighlighterClient
 | ||||||
|  |     virtual Vector<TextDocumentSpan>& spans() final { return document().spans(); } | ||||||
|  |     virtual const Vector<TextDocumentSpan>& spans() const final { return document().spans(); } | ||||||
|  |     virtual void highlighter_did_set_spans(Vector<TextDocumentSpan> spans) { document().set_spans(move(spans)); } | ||||||
|  |     virtual void set_span_at_index(size_t index, TextDocumentSpan span) final { document().set_span_at_index(index, move(span)); } | ||||||
|  |     virtual void highlighter_did_request_update() final { update(); } | ||||||
|  |     virtual String highlighter_did_request_text() const final { return text(); } | ||||||
|  |     virtual GUI::TextDocument& highlighter_did_request_document() final { return document(); } | ||||||
|  |     virtual GUI::TextPosition highlighter_did_request_cursor() const final { return m_cursor; } | ||||||
|  | 
 | ||||||
|     void create_actions(); |     void create_actions(); | ||||||
|     void paint_ruler(Painter&); |     void paint_ruler(Painter&); | ||||||
|     void update_content_size(); |     void update_content_size(); | ||||||
|  |  | ||||||
|  | @ -81,8 +81,9 @@ set(SOURCES | ||||||
|     Runtime/VM.cpp |     Runtime/VM.cpp | ||||||
|     Runtime/Value.cpp |     Runtime/Value.cpp | ||||||
|     Runtime/WithScope.cpp |     Runtime/WithScope.cpp | ||||||
|  |     SyntaxHighlighter.cpp | ||||||
|     Token.cpp |     Token.cpp | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| serenity_lib(LibJS js) | serenity_lib(LibJS js) | ||||||
| target_link_libraries(LibJS LibM LibCore LibCrypto LibRegex) | target_link_libraries(LibJS LibM LibCore LibCrypto LibRegex LibSyntax) | ||||||
|  |  | ||||||
|  | @ -25,16 +25,16 @@ | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| #include <AK/Debug.h> | #include <AK/Debug.h> | ||||||
| #include <LibGUI/JSSyntaxHighlighter.h> |  | ||||||
| #include <LibGUI/TextEditor.h> | #include <LibGUI/TextEditor.h> | ||||||
| #include <LibGfx/Font.h> | #include <LibGfx/Font.h> | ||||||
| #include <LibGfx/Palette.h> | #include <LibGfx/Palette.h> | ||||||
| #include <LibJS/Lexer.h> | #include <LibJS/Lexer.h> | ||||||
|  | #include <LibJS/SyntaxHighlighter.h> | ||||||
| #include <LibJS/Token.h> | #include <LibJS/Token.h> | ||||||
| 
 | 
 | ||||||
| namespace GUI { | namespace JS { | ||||||
| 
 | 
 | ||||||
| static Syntax::TextStyle style_for_token_type(Gfx::Palette palette, JS::TokenType type) | static Syntax::TextStyle style_for_token_type(const Gfx::Palette& palette, JS::TokenType type) | ||||||
| { | { | ||||||
|     switch (JS::Token::category(type)) { |     switch (JS::Token::category(type)) { | ||||||
|     case JS::TokenCategory::Invalid: |     case JS::TokenCategory::Invalid: | ||||||
|  | @ -58,21 +58,20 @@ static Syntax::TextStyle style_for_token_type(Gfx::Palette palette, JS::TokenTyp | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool JSSyntaxHighlighter::is_identifier(void* token) const | bool SyntaxHighlighter::is_identifier(void* token) const | ||||||
| { | { | ||||||
|     auto js_token = static_cast<JS::TokenType>(reinterpret_cast<size_t>(token)); |     auto js_token = static_cast<JS::TokenType>(reinterpret_cast<size_t>(token)); | ||||||
|     return js_token == JS::TokenType::Identifier; |     return js_token == JS::TokenType::Identifier; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool JSSyntaxHighlighter::is_navigatable([[maybe_unused]] void* token) const | bool SyntaxHighlighter::is_navigatable([[maybe_unused]] void* token) const | ||||||
| { | { | ||||||
|     return false; |     return false; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void JSSyntaxHighlighter::rehighlight(Gfx::Palette palette) | void SyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto text = m_client->get_text(); | ||||||
|     auto text = m_editor->text(); |  | ||||||
| 
 | 
 | ||||||
|     JS::Lexer lexer(text); |     JS::Lexer lexer(text); | ||||||
| 
 | 
 | ||||||
|  | @ -125,15 +124,15 @@ void JSSyntaxHighlighter::rehighlight(Gfx::Palette palette) | ||||||
|             was_eof = true; |             was_eof = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     m_editor->document().set_spans(spans); |     m_client->do_set_spans(move(spans)); | ||||||
| 
 | 
 | ||||||
|     m_has_brace_buddies = false; |     m_has_brace_buddies = false; | ||||||
|     highlight_matching_token_pair(); |     highlight_matching_token_pair(); | ||||||
| 
 | 
 | ||||||
|     m_editor->update(); |     m_client->do_update(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Vector<Syntax::Highlighter::MatchingTokenPair> JSSyntaxHighlighter::matching_token_pairs() const | Vector<Syntax::Highlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs() const | ||||||
| { | { | ||||||
|     static Vector<Syntax::Highlighter::MatchingTokenPair> pairs; |     static Vector<Syntax::Highlighter::MatchingTokenPair> pairs; | ||||||
|     if (pairs.is_empty()) { |     if (pairs.is_empty()) { | ||||||
|  | @ -144,12 +143,12 @@ Vector<Syntax::Highlighter::MatchingTokenPair> JSSyntaxHighlighter::matching_tok | ||||||
|     return pairs; |     return pairs; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool JSSyntaxHighlighter::token_types_equal(void* token1, void* token2) const | bool SyntaxHighlighter::token_types_equal(void* token1, void* token2) const | ||||||
| { | { | ||||||
|     return static_cast<JS::TokenType>(reinterpret_cast<size_t>(token1)) == static_cast<JS::TokenType>(reinterpret_cast<size_t>(token2)); |     return static_cast<JS::TokenType>(reinterpret_cast<size_t>(token1)) == static_cast<JS::TokenType>(reinterpret_cast<size_t>(token2)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| JSSyntaxHighlighter::~JSSyntaxHighlighter() | SyntaxHighlighter::~SyntaxHighlighter() | ||||||
| { | { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,12 +28,12 @@ | ||||||
| 
 | 
 | ||||||
| #include <LibSyntax/Highlighter.h> | #include <LibSyntax/Highlighter.h> | ||||||
| 
 | 
 | ||||||
| namespace GUI { | namespace JS { | ||||||
| 
 | 
 | ||||||
| class JSSyntaxHighlighter : public Syntax::Highlighter { | class SyntaxHighlighter : public Syntax::Highlighter { | ||||||
| public: | public: | ||||||
|     JSSyntaxHighlighter() { } |     SyntaxHighlighter() { } | ||||||
|     virtual ~JSSyntaxHighlighter() override; |     virtual ~SyntaxHighlighter() override; | ||||||
| 
 | 
 | ||||||
|     virtual bool is_identifier(void*) const override; |     virtual bool is_identifier(void*) const override; | ||||||
|     virtual bool is_navigatable(void*) const override; |     virtual bool is_navigatable(void*) const override; | ||||||
|  |  | ||||||
|  | @ -29,5 +29,6 @@ | ||||||
| namespace Syntax { | namespace Syntax { | ||||||
| 
 | 
 | ||||||
| class Highlighter; | class Highlighter; | ||||||
|  | class HighlighterClient; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| #include <LibGUI/TextEditor.h> | #include <LibGUI/TextEditor.h> | ||||||
|  | #include <LibGfx/Color.h> | ||||||
| #include <LibSyntax/Highlighter.h> | #include <LibSyntax/Highlighter.h> | ||||||
| 
 | 
 | ||||||
| namespace Syntax { | namespace Syntax { | ||||||
|  | @ -35,8 +36,7 @@ Highlighter::~Highlighter() | ||||||
| 
 | 
 | ||||||
| void Highlighter::highlight_matching_token_pair() | void Highlighter::highlight_matching_token_pair() | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto& document = m_client->get_document(); | ||||||
|     auto& document = m_editor->document(); |  | ||||||
| 
 | 
 | ||||||
|     enum class Direction { |     enum class Direction { | ||||||
|         Forward, |         Forward, | ||||||
|  | @ -93,7 +93,7 @@ void Highlighter::highlight_matching_token_pair() | ||||||
|         buddy1.attributes.background_color = Color::DarkCyan; |         buddy1.attributes.background_color = Color::DarkCyan; | ||||||
|         buddy0.attributes.color = Color::White; |         buddy0.attributes.color = Color::White; | ||||||
|         buddy1.attributes.color = Color::White; |         buddy1.attributes.color = Color::White; | ||||||
|         m_editor->update(); |         m_client->do_update(); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     auto pairs = matching_token_pairs(); |     auto pairs = matching_token_pairs(); | ||||||
|  | @ -103,7 +103,7 @@ void Highlighter::highlight_matching_token_pair() | ||||||
|         auto token_type = span.data; |         auto token_type = span.data; | ||||||
| 
 | 
 | ||||||
|         for (auto& pair : pairs) { |         for (auto& pair : pairs) { | ||||||
|             if (token_types_equal(token_type, pair.open) && span.range.start() == m_editor->cursor()) { |             if (token_types_equal(token_type, pair.open) && span.range.start() == m_client->get_cursor()) { | ||||||
|                 auto buddy = find_span_of_type(i, pair.close, pair.open, Direction::Forward); |                 auto buddy = find_span_of_type(i, pair.close, pair.open, Direction::Forward); | ||||||
|                 if (buddy.has_value()) |                 if (buddy.has_value()) | ||||||
|                     make_buddies(i, buddy.value()); |                     make_buddies(i, buddy.value()); | ||||||
|  | @ -115,7 +115,7 @@ void Highlighter::highlight_matching_token_pair() | ||||||
|         right_of_end.set_column(right_of_end.column() + 1); |         right_of_end.set_column(right_of_end.column() + 1); | ||||||
| 
 | 
 | ||||||
|         for (auto& pair : pairs) { |         for (auto& pair : pairs) { | ||||||
|             if (token_types_equal(token_type, pair.close) && right_of_end == m_editor->cursor()) { |             if (token_types_equal(token_type, pair.close) && right_of_end == m_client->get_cursor()) { | ||||||
|                 auto buddy = find_span_of_type(i, pair.open, pair.close, Direction::Backward); |                 auto buddy = find_span_of_type(i, pair.open, pair.close, Direction::Backward); | ||||||
|                 if (buddy.has_value()) |                 if (buddy.has_value()) | ||||||
|                     make_buddies(i, buddy.value()); |                     make_buddies(i, buddy.value()); | ||||||
|  | @ -125,29 +125,27 @@ void Highlighter::highlight_matching_token_pair() | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Highlighter::attach(GUI::TextEditor& editor) | void Highlighter::attach(HighlighterClient& client) | ||||||
| { | { | ||||||
|     ASSERT(!m_editor); |     ASSERT(!m_client); | ||||||
|     m_editor = editor; |     m_client = &client; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Highlighter::detach() | void Highlighter::detach() | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     m_client = nullptr; | ||||||
|     m_editor = nullptr; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Highlighter::cursor_did_change() | void Highlighter::cursor_did_change() | ||||||
| { | { | ||||||
|     ASSERT(m_editor); |     auto& document = m_client->get_document(); | ||||||
|     auto& document = m_editor->document(); |  | ||||||
|     if (m_has_brace_buddies) { |     if (m_has_brace_buddies) { | ||||||
|         if (m_brace_buddies[0].index >= 0 && m_brace_buddies[0].index < static_cast<int>(document.spans().size())) |         if (m_brace_buddies[0].index >= 0 && m_brace_buddies[0].index < static_cast<int>(document.spans().size())) | ||||||
|             document.set_span_at_index(m_brace_buddies[0].index, m_brace_buddies[0].span_backup); |             document.set_span_at_index(m_brace_buddies[0].index, m_brace_buddies[0].span_backup); | ||||||
|         if (m_brace_buddies[1].index >= 0 && m_brace_buddies[1].index < static_cast<int>(document.spans().size())) |         if (m_brace_buddies[1].index >= 0 && m_brace_buddies[1].index < static_cast<int>(document.spans().size())) | ||||||
|             document.set_span_at_index(m_brace_buddies[1].index, m_brace_buddies[1].span_backup); |             document.set_span_at_index(m_brace_buddies[1].index, m_brace_buddies[1].span_backup); | ||||||
|         m_has_brace_buddies = false; |         m_has_brace_buddies = false; | ||||||
|         m_editor->update(); |         m_client->do_update(); | ||||||
|     } |     } | ||||||
|     highlight_matching_token_pair(); |     highlight_matching_token_pair(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ | ||||||
| #include <AK/WeakPtr.h> | #include <AK/WeakPtr.h> | ||||||
| #include <LibGUI/TextDocument.h> | #include <LibGUI/TextDocument.h> | ||||||
| #include <LibGfx/Palette.h> | #include <LibGfx/Palette.h> | ||||||
|  | #include <LibSyntax/HighlighterClient.h> | ||||||
| 
 | 
 | ||||||
| namespace Syntax { | namespace Syntax { | ||||||
| 
 | 
 | ||||||
|  | @ -61,14 +62,15 @@ public: | ||||||
|     virtual bool is_identifier(void*) const { return false; }; |     virtual bool is_identifier(void*) const { return false; }; | ||||||
|     virtual bool is_navigatable(void*) const { return false; }; |     virtual bool is_navigatable(void*) const { return false; }; | ||||||
| 
 | 
 | ||||||
|     void attach(GUI::TextEditor& editor); |     void attach(HighlighterClient&); | ||||||
|     void detach(); |     void detach(); | ||||||
|     void cursor_did_change(); |     void cursor_did_change(); | ||||||
| 
 | 
 | ||||||
| protected: | protected: | ||||||
|     Highlighter() { } |     Highlighter() { } | ||||||
| 
 | 
 | ||||||
|     WeakPtr<GUI::TextEditor> m_editor; |     // FIXME: This should be WeakPtr somehow
 | ||||||
|  |     HighlighterClient* m_client { nullptr }; | ||||||
| 
 | 
 | ||||||
|     struct MatchingTokenPair { |     struct MatchingTokenPair { | ||||||
|         void* open; |         void* open; | ||||||
|  |  | ||||||
							
								
								
									
										55
									
								
								Userland/Libraries/LibSyntax/HighlighterClient.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								Userland/Libraries/LibSyntax/HighlighterClient.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | ||||||
|  | /*
 | ||||||
|  |  * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> | ||||||
|  |  * 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 <LibGUI/Forward.h> | ||||||
|  | 
 | ||||||
|  | namespace Syntax { | ||||||
|  | 
 | ||||||
|  | class HighlighterClient { | ||||||
|  | public: | ||||||
|  |     virtual ~HighlighterClient() = default; | ||||||
|  | 
 | ||||||
|  |     virtual Vector<GUI::TextDocumentSpan>& spans() = 0; | ||||||
|  |     virtual const Vector<GUI::TextDocumentSpan>& spans() const = 0; | ||||||
|  |     virtual void set_span_at_index(size_t index, GUI::TextDocumentSpan span) = 0; | ||||||
|  | 
 | ||||||
|  |     virtual String highlighter_did_request_text() const = 0; | ||||||
|  |     virtual void highlighter_did_request_update() = 0; | ||||||
|  |     virtual GUI::TextDocument& highlighter_did_request_document() = 0; | ||||||
|  |     virtual GUI::TextPosition highlighter_did_request_cursor() const = 0; | ||||||
|  |     virtual void highlighter_did_set_spans(Vector<GUI::TextDocumentSpan>) = 0; | ||||||
|  | 
 | ||||||
|  |     void do_set_spans(Vector<GUI::TextDocumentSpan> spans) { highlighter_did_set_spans(move(spans)); } | ||||||
|  |     void do_update() { highlighter_did_request_update(); } | ||||||
|  | 
 | ||||||
|  |     String get_text() const { return highlighter_did_request_text(); } | ||||||
|  |     GUI::TextDocument& get_document() { return highlighter_did_request_document(); } | ||||||
|  |     GUI::TextPosition get_cursor() const { return highlighter_did_request_cursor(); } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Andreas Kling
						Andreas Kling