Serenity Operating System
at hosted 340 lines 11 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include "Editor.h" 28#include "EditorWrapper.h" 29#include <AK/ByteBuffer.h> 30#include <AK/FileSystemPath.h> 31#include <LibCore/DirIterator.h> 32#include <LibCore/File.h> 33#include <LibGUI/Application.h> 34#include <LibGUI/Painter.h> 35#include <LibGUI/ScrollBar.h> 36#include <LibGUI/SyntaxHighlighter.h> 37#include <LibGUI/Window.h> 38#include <LibMarkdown/MDDocument.h> 39#include <LibWeb/DOM/ElementFactory.h> 40#include <LibWeb/DOM/HTMLHeadElement.h> 41#include <LibWeb/DOM/Text.h> 42#include <LibWeb/HtmlView.h> 43#include <LibWeb/Parser/HTMLParser.h> 44 45// #define EDITOR_DEBUG 46 47Editor::Editor() 48{ 49 m_documentation_tooltip_window = GUI::Window::construct(); 50 m_documentation_tooltip_window->set_rect(0, 0, 500, 400); 51 m_documentation_tooltip_window->set_window_type(GUI::WindowType::Tooltip); 52 m_documentation_html_view = m_documentation_tooltip_window->set_main_widget<Web::HtmlView>(); 53} 54 55Editor::~Editor() 56{ 57} 58 59EditorWrapper& Editor::wrapper() 60{ 61 return static_cast<EditorWrapper&>(*parent()); 62} 63const EditorWrapper& Editor::wrapper() const 64{ 65 return static_cast<const EditorWrapper&>(*parent()); 66} 67 68void Editor::focusin_event(Core::Event& event) 69{ 70 wrapper().set_editor_has_focus({}, true); 71 if (on_focus) 72 on_focus(); 73 GUI::TextEditor::focusin_event(event); 74} 75 76void Editor::focusout_event(Core::Event& event) 77{ 78 wrapper().set_editor_has_focus({}, false); 79 GUI::TextEditor::focusout_event(event); 80} 81 82void Editor::paint_event(GUI::PaintEvent& event) 83{ 84 GUI::TextEditor::paint_event(event); 85 86 if (is_focused()) { 87 GUI::Painter painter(*this); 88 painter.add_clip_rect(event.rect()); 89 90 auto rect = frame_inner_rect(); 91 if (vertical_scrollbar().is_visible()) 92 rect.set_width(rect.width() - vertical_scrollbar().width()); 93 if (horizontal_scrollbar().is_visible()) 94 rect.set_height(rect.height() - horizontal_scrollbar().height()); 95 painter.draw_rect(rect, palette().selection()); 96 } 97 98 if (m_hovering_editor) 99 window()->set_override_cursor(m_hovering_link && m_holding_ctrl ? GUI::StandardCursor::Hand : GUI::StandardCursor::IBeam); 100} 101 102static HashMap<String, String>& man_paths() 103{ 104 static HashMap<String, String> paths; 105 if (paths.is_empty()) { 106 // FIXME: This should also search man3, possibly other places.. 107 Core::DirIterator it("/usr/share/man/man2", Core::DirIterator::Flags::SkipDots); 108 while (it.has_next()) { 109 auto path = String::format("/usr/share/man/man2/%s", it.next_path().characters()); 110 auto title = FileSystemPath(path).title(); 111 paths.set(title, path); 112 } 113 } 114 115 return paths; 116} 117 118void Editor::show_documentation_tooltip_if_available(const String& hovered_token, const Gfx::Point& screen_location) 119{ 120 auto it = man_paths().find(hovered_token); 121 if (it == man_paths().end()) { 122#ifdef EDITOR_DEBUG 123 dbg() << "no man path for " << hovered_token; 124#endif 125 m_documentation_tooltip_window->hide(); 126 return; 127 } 128 129 if (m_documentation_tooltip_window->is_visible() && hovered_token == m_last_parsed_token) { 130 return; 131 } 132 133#ifdef EDITOR_DEBUG 134 dbg() << "opening " << it->value; 135#endif 136 auto file = Core::File::construct(it->value); 137 if (!file->open(Core::File::ReadOnly)) { 138 dbg() << "failed to open " << it->value << " " << file->error_string(); 139 return; 140 } 141 142 MDDocument man_document; 143 bool success = man_document.parse(file->read_all()); 144 145 if (!success) { 146 dbg() << "failed to parse markdown"; 147 return; 148 } 149 150 auto html_text = man_document.render_to_html(); 151 152 auto html_document = Web::parse_html_document(html_text); 153 if (!html_document) { 154 dbg() << "failed to parse HTML"; 155 return; 156 } 157 158 // FIXME: LibWeb needs a friendlier DOM manipulation API. Something like innerHTML :^) 159 auto style_element = create_element(*html_document, "style"); 160 style_element->append_child(adopt(*new Web::Text(*html_document, "body { background-color: #dac7b5; }"))); 161 162 // FIXME: This const_cast should not be necessary. 163 auto* head_element = const_cast<Web::HTMLHeadElement*>(html_document->head()); 164 ASSERT(head_element); 165 head_element->append_child(style_element); 166 167 m_documentation_html_view->set_document(html_document); 168 m_documentation_tooltip_window->move_to(screen_location.translated(4, 4)); 169 m_documentation_tooltip_window->show(); 170 171 m_last_parsed_token = hovered_token; 172} 173 174void Editor::mousemove_event(GUI::MouseEvent& event) 175{ 176 GUI::TextEditor::mousemove_event(event); 177 178 if (document().spans().is_empty()) 179 return; 180 181 auto text_position = text_position_at(event.position()); 182 if (!text_position.is_valid()) { 183 m_documentation_tooltip_window->hide(); 184 return; 185 } 186 187 auto highlighter = wrapper().editor().syntax_highlighter(); 188 if (!highlighter) 189 return; 190 191 bool hide_tooltip = true; 192 bool is_over_link = false; 193 194 for (auto& span : document().spans()) { 195 if (span.range.contains(m_previous_text_position) && !span.range.contains(text_position)) { 196 if (highlighter->is_navigatable(span.data) && span.is_underlined) { 197 span.is_underlined = false; 198 wrapper().editor().update(); 199 } 200 } 201 202 if (span.range.contains(text_position)) { 203 auto adjusted_range = span.range; 204 adjusted_range.end().set_column(adjusted_range.end().column() + 1); 205 auto hovered_span_text = document().text_in_range(adjusted_range); 206#ifdef EDITOR_DEBUG 207 dbg() << "Hovering: " << adjusted_range << " \"" << hovered_span_text << "\""; 208#endif 209 210 if (highlighter->is_navigatable(span.data)) { 211 is_over_link = true; 212 bool was_underlined = span.is_underlined; 213 span.is_underlined = event.modifiers() & Mod_Ctrl; 214 if (span.is_underlined != was_underlined) { 215 wrapper().editor().update(); 216 } 217 } 218 if (highlighter->is_identifier(span.data)) { 219 show_documentation_tooltip_if_available(hovered_span_text, event.position().translated(screen_relative_rect().location())); 220 hide_tooltip = false; 221 } 222 } 223 } 224 225 m_previous_text_position = text_position; 226 if (hide_tooltip) 227 m_documentation_tooltip_window->hide(); 228 229 m_hovering_link = is_over_link && (event.modifiers() & Mod_Ctrl); 230} 231 232void Editor::mousedown_event(GUI::MouseEvent& event) 233{ 234 auto highlighter = wrapper().editor().syntax_highlighter(); 235 if (!highlighter) { 236 GUI::TextEditor::mousedown_event(event); 237 return; 238 } 239 240 if (!(event.modifiers() & Mod_Ctrl)) { 241 GUI::TextEditor::mousedown_event(event); 242 return; 243 } 244 245 auto text_position = text_position_at(event.position()); 246 if (!text_position.is_valid()) { 247 GUI::TextEditor::mousedown_event(event); 248 return; 249 } 250 251 for (auto& span : document().spans()) { 252 if (span.range.contains(text_position)) { 253 if (!highlighter->is_navigatable(span.data)) { 254 GUI::TextEditor::mousedown_event(event); 255 return; 256 } 257 258 auto adjusted_range = span.range; 259 adjusted_range.end().set_column(adjusted_range.end().column() + 1); 260 auto span_text = document().text_in_range(adjusted_range); 261 auto header_path = span_text.substring(1, span_text.length() - 2); 262#ifdef EDITOR_DEBUG 263 dbg() << "Ctrl+click: " << adjusted_range << " \"" << header_path << "\""; 264#endif 265 navigate_to_include_if_available(header_path); 266 return; 267 } 268 } 269 270 GUI::TextEditor::mousedown_event(event); 271} 272 273void Editor::keydown_event(GUI::KeyEvent& event) 274{ 275 if (event.key() == Key_Control) 276 m_holding_ctrl = true; 277 GUI::TextEditor::keydown_event(event); 278} 279 280void Editor::keyup_event(GUI::KeyEvent& event) 281{ 282 if (event.key() == Key_Control) 283 m_holding_ctrl = false; 284 GUI::TextEditor::keyup_event(event); 285} 286 287void Editor::enter_event(Core::Event& event) 288{ 289 m_hovering_editor = true; 290 GUI::TextEditor::enter_event(event); 291} 292 293void Editor::leave_event(Core::Event& event) 294{ 295 m_hovering_editor = false; 296 GUI::TextEditor::leave_event(event); 297} 298 299static HashMap<String, String>& include_paths() 300{ 301 static HashMap<String, String> paths; 302 303 auto add_directory = [](String base, Optional<String> recursive, auto handle_directory) -> void { 304 Core::DirIterator it(recursive.value_or(base), Core::DirIterator::Flags::SkipDots); 305 while (it.has_next()) { 306 auto path = it.next_full_path(); 307 if (!Core::File::is_directory(path)) { 308 auto key = path.substring(base.length() + 1, path.length() - base.length() - 1); 309#ifdef EDITOR_DEBUG 310 dbg() << "Adding header \"" << key << "\" in path \"" << path << "\""; 311#endif 312 paths.set(key, path); 313 } else { 314 handle_directory(base, path, handle_directory); 315 } 316 } 317 }; 318 319 if (paths.is_empty()) { 320 add_directory(".", {}, add_directory); 321 add_directory("/usr/local/include", {}, add_directory); 322 add_directory("/usr/local/include/c++/9.2.0", {}, add_directory); 323 add_directory("/usr/include", {}, add_directory); 324 } 325 326 return paths; 327} 328 329void Editor::navigate_to_include_if_available(String path) 330{ 331 auto it = include_paths().find(path); 332 if (it == include_paths().end()) { 333#ifdef EDITOR_DEBUG 334 dbg() << "no header " << path << " found."; 335#endif 336 return; 337 } 338 339 on_open(it->value); 340}