/src/serenity/Userland/Libraries/LibLine/Editor.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2021, the SerenityOS developers. |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include "Editor.h" |
9 | | #include <AK/CharacterTypes.h> |
10 | | #include <AK/Debug.h> |
11 | | #include <AK/GenericLexer.h> |
12 | | #include <AK/JsonObject.h> |
13 | | #include <AK/MemoryStream.h> |
14 | | #include <AK/RedBlackTree.h> |
15 | | #include <AK/ScopeGuard.h> |
16 | | #include <AK/ScopedValueRollback.h> |
17 | | #include <AK/StringBuilder.h> |
18 | | #include <AK/Utf32View.h> |
19 | | #include <AK/Utf8View.h> |
20 | | #include <LibCore/ConfigFile.h> |
21 | | #include <LibCore/Event.h> |
22 | | #include <LibCore/EventLoop.h> |
23 | | #include <LibCore/Notifier.h> |
24 | | #include <LibFileSystem/FileSystem.h> |
25 | | #include <LibFileSystem/TempFile.h> |
26 | | #include <LibUnicode/Segmentation.h> |
27 | | #include <errno.h> |
28 | | #include <fcntl.h> |
29 | | #include <signal.h> |
30 | | #include <stdio.h> |
31 | | #include <sys/ioctl.h> |
32 | | #include <sys/select.h> |
33 | | #include <sys/time.h> |
34 | | #include <unistd.h> |
35 | | |
36 | | namespace Line { |
37 | | |
38 | | Configuration Configuration::from_config(StringView libname) |
39 | 0 | { |
40 | 0 | Configuration configuration; |
41 | 0 | auto config_file = Core::ConfigFile::open_for_lib(libname).release_value_but_fixme_should_propagate_errors(); |
42 | | |
43 | | // Read behavior options. |
44 | 0 | auto refresh = config_file->read_entry("behavior", "refresh", "lazy"); |
45 | 0 | auto operation = config_file->read_entry("behavior", "operation_mode"); |
46 | 0 | auto bracketed_paste = config_file->read_bool_entry("behavior", "bracketed_paste", true); |
47 | 0 | auto default_text_editor = config_file->read_entry("behavior", "default_text_editor"); |
48 | |
|
49 | 0 | Configuration::Flags flags { Configuration::Flags::None }; |
50 | 0 | if (bracketed_paste) |
51 | 0 | flags = static_cast<Flags>(flags | Configuration::Flags::BracketedPaste); |
52 | |
|
53 | 0 | configuration.set(flags); |
54 | |
|
55 | 0 | if (refresh.equals_ignoring_ascii_case("lazy"sv)) |
56 | 0 | configuration.set(Configuration::Lazy); |
57 | 0 | else if (refresh.equals_ignoring_ascii_case("eager"sv)) |
58 | 0 | configuration.set(Configuration::Eager); |
59 | |
|
60 | 0 | if (operation.equals_ignoring_ascii_case("full"sv)) |
61 | 0 | configuration.set(Configuration::OperationMode::Full); |
62 | 0 | else if (operation.equals_ignoring_ascii_case("noescapesequences"sv)) |
63 | 0 | configuration.set(Configuration::OperationMode::NoEscapeSequences); |
64 | 0 | else if (operation.equals_ignoring_ascii_case("noninteractive"sv)) |
65 | 0 | configuration.set(Configuration::OperationMode::NonInteractive); |
66 | 0 | else |
67 | 0 | configuration.set(Configuration::OperationMode::Unset); |
68 | |
|
69 | 0 | if (!default_text_editor.is_empty()) |
70 | 0 | configuration.set(DefaultTextEditor { move(default_text_editor) }); |
71 | 0 | else |
72 | 0 | configuration.set(DefaultTextEditor { "/bin/TextEditor" }); |
73 | | |
74 | | // Read keybinds. |
75 | |
|
76 | 0 | for (auto& binding_key : config_file->keys("keybinds")) { |
77 | 0 | GenericLexer key_lexer(binding_key); |
78 | 0 | auto has_ctrl = false; |
79 | 0 | auto alt = false; |
80 | 0 | auto escape = false; |
81 | 0 | Vector<Key> keys; |
82 | |
|
83 | 0 | while (!key_lexer.is_eof()) { |
84 | 0 | unsigned key; |
85 | 0 | if (escape) { |
86 | 0 | key = key_lexer.consume_escaped_character(); |
87 | 0 | escape = false; |
88 | 0 | } else { |
89 | 0 | if (key_lexer.next_is("alt+")) { |
90 | 0 | alt = key_lexer.consume_specific("alt+"sv); |
91 | 0 | continue; |
92 | 0 | } |
93 | 0 | if (key_lexer.next_is("^[")) { |
94 | 0 | alt = key_lexer.consume_specific("^["sv); |
95 | 0 | continue; |
96 | 0 | } |
97 | 0 | if (key_lexer.next_is("^")) { |
98 | 0 | has_ctrl = key_lexer.consume_specific("^"sv); |
99 | 0 | continue; |
100 | 0 | } |
101 | 0 | if (key_lexer.next_is("ctrl+")) { |
102 | 0 | has_ctrl = key_lexer.consume_specific("ctrl+"sv); |
103 | 0 | continue; |
104 | 0 | } |
105 | 0 | if (key_lexer.next_is("\\")) { |
106 | 0 | escape = true; |
107 | 0 | continue; |
108 | 0 | } |
109 | | // FIXME: Support utf? |
110 | 0 | key = key_lexer.consume(); |
111 | 0 | } |
112 | 0 | if (has_ctrl) |
113 | 0 | key = ctrl(key); |
114 | |
|
115 | 0 | keys.append(Key { key, alt ? Key::Alt : Key::None }); |
116 | 0 | alt = false; |
117 | 0 | has_ctrl = false; |
118 | 0 | } |
119 | |
|
120 | 0 | GenericLexer value_lexer { config_file->read_entry("keybinds", binding_key) }; |
121 | 0 | StringBuilder value_builder; |
122 | 0 | while (!value_lexer.is_eof()) |
123 | 0 | value_builder.append(value_lexer.consume_escaped_character()); |
124 | 0 | auto value = value_builder.string_view(); |
125 | 0 | if (value.starts_with("internal:"sv)) { |
126 | 0 | configuration.set(KeyBinding { |
127 | 0 | keys, |
128 | 0 | KeyBinding::Kind::InternalFunction, |
129 | 0 | value.substring_view(9, value.length() - 9) }); |
130 | 0 | } else { |
131 | 0 | configuration.set(KeyBinding { |
132 | 0 | keys, |
133 | 0 | KeyBinding::Kind::Insertion, |
134 | 0 | value }); |
135 | 0 | } |
136 | 0 | } |
137 | |
|
138 | 0 | return configuration; |
139 | 0 | } |
140 | | |
141 | | void Editor::set_default_keybinds() |
142 | 0 | { |
143 | 0 | register_key_input_callback(ctrl('N'), EDITOR_INTERNAL_FUNCTION(search_forwards)); |
144 | 0 | register_key_input_callback(ctrl('P'), EDITOR_INTERNAL_FUNCTION(search_backwards)); |
145 | 0 | register_key_input_callback(ctrl('A'), EDITOR_INTERNAL_FUNCTION(go_home)); |
146 | 0 | register_key_input_callback(ctrl('B'), EDITOR_INTERNAL_FUNCTION(cursor_left_character)); |
147 | 0 | register_key_input_callback(ctrl('D'), EDITOR_INTERNAL_FUNCTION(erase_character_forwards)); |
148 | 0 | register_key_input_callback(ctrl('E'), EDITOR_INTERNAL_FUNCTION(go_end)); |
149 | 0 | register_key_input_callback(ctrl('F'), EDITOR_INTERNAL_FUNCTION(cursor_right_character)); |
150 | | // ^H: ctrl('H') == '\b' |
151 | 0 | register_key_input_callback(ctrl('H'), EDITOR_INTERNAL_FUNCTION(erase_character_backwards)); |
152 | | // DEL - Some terminals send this instead of ^H. |
153 | 0 | register_key_input_callback((char)127, EDITOR_INTERNAL_FUNCTION(erase_character_backwards)); |
154 | 0 | register_key_input_callback(ctrl('K'), EDITOR_INTERNAL_FUNCTION(erase_to_end)); |
155 | 0 | register_key_input_callback(ctrl('L'), EDITOR_INTERNAL_FUNCTION(clear_screen)); |
156 | 0 | register_key_input_callback(ctrl('R'), EDITOR_INTERNAL_FUNCTION(enter_search)); |
157 | 0 | register_key_input_callback(ctrl(']'), EDITOR_INTERNAL_FUNCTION(search_character_forwards)); |
158 | 0 | register_key_input_callback(Key { ctrl(']'), Key::Alt }, EDITOR_INTERNAL_FUNCTION(search_character_backwards)); |
159 | 0 | register_key_input_callback(ctrl('T'), EDITOR_INTERNAL_FUNCTION(transpose_characters)); |
160 | 0 | register_key_input_callback('\n', EDITOR_INTERNAL_FUNCTION(finish)); |
161 | | |
162 | | // ^X^E: Edit in external editor |
163 | 0 | register_key_input_callback(Vector<Key> { ctrl('X'), ctrl('E') }, EDITOR_INTERNAL_FUNCTION(edit_in_external_editor)); |
164 | | |
165 | | // ^[.: alt-.: insert last arg of previous command (similar to `!$`) |
166 | 0 | register_key_input_callback(Key { '.', Key::Alt }, EDITOR_INTERNAL_FUNCTION(insert_last_words)); |
167 | 0 | register_key_input_callback(ctrl('Y'), EDITOR_INTERNAL_FUNCTION(insert_last_erased)); |
168 | 0 | register_key_input_callback(Key { 'b', Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_left_word)); |
169 | 0 | register_key_input_callback(Key { 'f', Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_right_word)); |
170 | 0 | register_key_input_callback(Key { ctrl('B'), Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_left_nonspace_word)); |
171 | 0 | register_key_input_callback(Key { ctrl('F'), Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_right_nonspace_word)); |
172 | | // ^[^H: alt-backspace: backward delete word |
173 | 0 | register_key_input_callback(Key { '\b', Key::Alt }, EDITOR_INTERNAL_FUNCTION(erase_alnum_word_backwards)); |
174 | 0 | register_key_input_callback(Key { 'd', Key::Alt }, EDITOR_INTERNAL_FUNCTION(erase_alnum_word_forwards)); |
175 | 0 | register_key_input_callback(Key { '\\', Key::Alt }, EDITOR_INTERNAL_FUNCTION(erase_spaces)); |
176 | 0 | register_key_input_callback(Key { 'c', Key::Alt }, EDITOR_INTERNAL_FUNCTION(capitalize_word)); |
177 | 0 | register_key_input_callback(Key { 'l', Key::Alt }, EDITOR_INTERNAL_FUNCTION(lowercase_word)); |
178 | 0 | register_key_input_callback(Key { 'u', Key::Alt }, EDITOR_INTERNAL_FUNCTION(uppercase_word)); |
179 | 0 | register_key_input_callback(Key { 't', Key::Alt }, EDITOR_INTERNAL_FUNCTION(transpose_words)); |
180 | | |
181 | | // Register these last to all the user to override the previous key bindings |
182 | | // Normally ^W. `stty werase \^n` can change it to ^N (or something else). |
183 | 0 | register_key_input_callback(m_termios.c_cc[VWERASE], EDITOR_INTERNAL_FUNCTION(erase_word_backwards)); |
184 | | // Normally ^U. `stty kill \^n` can change it to ^N (or something else). |
185 | 0 | register_key_input_callback(m_termios.c_cc[VKILL], EDITOR_INTERNAL_FUNCTION(kill_line)); |
186 | 0 | register_key_input_callback(m_termios.c_cc[VERASE], EDITOR_INTERNAL_FUNCTION(erase_character_backwards)); |
187 | 0 | } |
188 | | |
189 | | Editor::Editor(Configuration configuration) |
190 | 0 | : m_configuration(move(configuration)) |
191 | 0 | { |
192 | 0 | m_always_refresh = m_configuration.refresh_behavior == Configuration::RefreshBehavior::Eager; |
193 | 0 | m_pending_chars = {}; |
194 | 0 | get_terminal_size(); |
195 | 0 | m_suggestion_display = make<XtermSuggestionDisplay>(m_num_lines, m_num_columns); |
196 | 0 | } |
197 | | |
198 | | Editor::~Editor() |
199 | 0 | { |
200 | 0 | if (m_initialized) |
201 | 0 | restore(); |
202 | 0 | } |
203 | | |
204 | | void Editor::ensure_free_lines_from_origin(size_t count) |
205 | 0 | { |
206 | 0 | if (count > m_num_lines) { |
207 | | // FIXME: Implement paging |
208 | 0 | } |
209 | |
|
210 | 0 | if (m_origin_row + count <= m_num_lines) |
211 | 0 | return; |
212 | | |
213 | 0 | auto diff = m_origin_row + count - m_num_lines - 1; |
214 | 0 | out(stderr, "\x1b[{}S", diff); |
215 | 0 | fflush(stderr); |
216 | 0 | m_origin_row -= diff; |
217 | 0 | m_refresh_needed = false; |
218 | 0 | m_chars_touched_in_the_middle = 0; |
219 | 0 | } |
220 | | |
221 | | void Editor::get_terminal_size() |
222 | 0 | { |
223 | 0 | struct winsize ws; |
224 | 0 | ioctl(STDERR_FILENO, TIOCGWINSZ, &ws); |
225 | 0 | if (ws.ws_col == 0 || ws.ws_row == 0) { |
226 | | // LLDB uses ttys which "work" and then gives us a zero sized |
227 | | // terminal which is far from useful |
228 | 0 | if (int fd = open("/dev/tty", O_RDONLY); fd != -1) { |
229 | 0 | ioctl(fd, TIOCGWINSZ, &ws); |
230 | 0 | close(fd); |
231 | 0 | } |
232 | 0 | } |
233 | 0 | m_num_columns = ws.ws_col; |
234 | 0 | m_num_lines = ws.ws_row; |
235 | 0 | } |
236 | | |
237 | | void Editor::add_to_history(ByteString const& line) |
238 | 0 | { |
239 | 0 | if (line.is_empty()) |
240 | 0 | return; |
241 | 0 | ByteString histcontrol = getenv("HISTCONTROL"); |
242 | 0 | auto ignoredups = histcontrol == "ignoredups" || histcontrol == "ignoreboth"; |
243 | 0 | auto ignorespace = histcontrol == "ignorespace" || histcontrol == "ignoreboth"; |
244 | 0 | if (ignoredups && !m_history.is_empty() && line == m_history.last().entry) |
245 | 0 | return; |
246 | 0 | if (ignorespace && line.starts_with(' ')) |
247 | 0 | return; |
248 | 0 | if ((m_history.size() + 1) > m_history_capacity) |
249 | 0 | m_history.take_first(); |
250 | 0 | struct timeval tv; |
251 | 0 | gettimeofday(&tv, nullptr); |
252 | 0 | m_history.append({ line, tv.tv_sec }); |
253 | 0 | m_history_dirty = true; |
254 | 0 | } |
255 | | |
256 | | ErrorOr<Vector<Editor::HistoryEntry>> Editor::try_load_history(StringView path) |
257 | 0 | { |
258 | 0 | auto history_file_or_error = Core::File::open(path, Core::File::OpenMode::Read); |
259 | | |
260 | | // We ignore "No such file or directory" errors, as that is just equivalent to an empty history. |
261 | 0 | if (history_file_or_error.is_error() && history_file_or_error.error().is_errno() && history_file_or_error.error().code() == ENOENT) |
262 | 0 | return Vector<Editor::HistoryEntry> {}; |
263 | | |
264 | 0 | auto history_file = history_file_or_error.release_value(); |
265 | 0 | auto data = TRY(history_file->read_until_eof()); |
266 | 0 | auto hist = StringView { data }; |
267 | 0 | Vector<HistoryEntry> history; |
268 | 0 | for (auto& str : hist.split_view("\n\n"sv)) { |
269 | 0 | auto it = str.find("::"sv).value_or(0); |
270 | 0 | auto time = str.substring_view(0, it).to_number<time_t>().value_or(0); |
271 | 0 | auto string = str.substring_view(it == 0 ? it : it + 2); |
272 | 0 | history.append({ string, time }); |
273 | 0 | } |
274 | 0 | return history; |
275 | 0 | } |
276 | | |
277 | | bool Editor::load_history(ByteString const& path) |
278 | 0 | { |
279 | 0 | auto history_or_error = try_load_history(path); |
280 | 0 | if (history_or_error.is_error()) |
281 | 0 | return false; |
282 | 0 | auto maybe_error = m_history.try_extend(history_or_error.release_value()); |
283 | 0 | auto okay = !maybe_error.is_error(); |
284 | 0 | return okay; |
285 | 0 | } |
286 | | |
287 | | template<typename It0, typename It1, typename OutputT, typename LessThan> |
288 | | static void merge(It0&& begin0, It0 const& end0, It1&& begin1, It1 const& end1, OutputT& output, LessThan less_than) |
289 | 0 | { |
290 | 0 | for (;;) { |
291 | 0 | if (begin0 == end0 && begin1 == end1) |
292 | 0 | return; |
293 | | |
294 | 0 | if (begin0 == end0) { |
295 | 0 | auto&& right = *begin1; |
296 | 0 | if (output.last().entry != right.entry) |
297 | 0 | output.append(right); |
298 | 0 | ++begin1; |
299 | 0 | continue; |
300 | 0 | } |
301 | | |
302 | 0 | auto&& left = *begin0; |
303 | 0 | if (left.entry.is_whitespace()) { |
304 | 0 | ++begin0; |
305 | 0 | continue; |
306 | 0 | } |
307 | 0 | if (begin1 == end1) { |
308 | 0 | if (output.last().entry != left.entry) |
309 | 0 | output.append(left); |
310 | 0 | ++begin0; |
311 | 0 | continue; |
312 | 0 | } |
313 | | |
314 | 0 | auto&& right = *begin1; |
315 | 0 | if (less_than(left, right)) { |
316 | 0 | if (output.last().entry != left.entry) |
317 | 0 | output.append(left); |
318 | 0 | ++begin0; |
319 | 0 | } else { |
320 | 0 | if (output.last().entry != right.entry) |
321 | 0 | output.append(right); |
322 | 0 | ++begin1; |
323 | 0 | if (right.entry == left.entry) |
324 | 0 | ++begin0; |
325 | 0 | } |
326 | 0 | } |
327 | 0 | } |
328 | | |
329 | | bool Editor::save_history(ByteString const& path) |
330 | 0 | { |
331 | | // Note: Use a dummy entry to simplify merging. |
332 | 0 | Vector<HistoryEntry> final_history { { "", 0 } }; |
333 | 0 | { |
334 | 0 | auto history_or_error = try_load_history(path); |
335 | 0 | if (history_or_error.is_error()) |
336 | 0 | return false; |
337 | 0 | Vector<HistoryEntry> old_history = history_or_error.release_value(); |
338 | 0 | merge( |
339 | 0 | old_history.begin(), old_history.end(), |
340 | 0 | m_history.begin(), m_history.end(), |
341 | 0 | final_history, |
342 | 0 | [](HistoryEntry const& left, HistoryEntry const& right) { return left.timestamp < right.timestamp; }); |
343 | 0 | } |
344 | | |
345 | 0 | auto temp_or_error = FileSystem::TempFile::create_temp_file(); |
346 | 0 | if (temp_or_error.is_error()) |
347 | 0 | return false; |
348 | | |
349 | 0 | auto temp_file = temp_or_error.release_value(); |
350 | |
|
351 | 0 | { |
352 | 0 | auto file_or_error = Core::File::open(temp_file->path(), Core::File::OpenMode::Write, 0600); |
353 | 0 | if (file_or_error.is_error()) |
354 | 0 | return false; |
355 | 0 | auto file = file_or_error.release_value(); |
356 | | // Skip the dummy entry: |
357 | 0 | for (auto iter = final_history.begin() + 1; iter != final_history.end(); ++iter) { |
358 | 0 | auto const& entry = *iter; |
359 | 0 | auto buffer = ByteString::formatted("{}::{}\n\n", entry.timestamp, entry.entry); |
360 | 0 | auto maybe_error = file->write_until_depleted(buffer.bytes()); |
361 | 0 | if (maybe_error.is_error()) |
362 | 0 | return false; |
363 | 0 | } |
364 | 0 | } |
365 | | |
366 | 0 | auto result = FileSystem::copy_file_or_directory(path, temp_file->path(), FileSystem::RecursionMode::Disallowed, FileSystem::LinkMode::Disallowed, FileSystem::AddDuplicateFileMarker::No); |
367 | 0 | if (result.is_error()) |
368 | 0 | return false; |
369 | | |
370 | 0 | m_history_dirty = false; |
371 | 0 | return true; |
372 | 0 | } |
373 | | |
374 | | void Editor::clear_line() |
375 | 0 | { |
376 | 0 | for (size_t i = 0; i < m_cursor; ++i) |
377 | 0 | fputc(0x8, stderr); |
378 | 0 | fputs("\033[K", stderr); |
379 | 0 | fflush(stderr); |
380 | 0 | m_chars_touched_in_the_middle = buffer().size(); |
381 | 0 | m_buffer.clear(); |
382 | 0 | m_cursor = 0; |
383 | 0 | m_inline_search_cursor = m_cursor; |
384 | 0 | } |
385 | | |
386 | | void Editor::insert(Utf32View const& string) |
387 | 0 | { |
388 | 0 | for (size_t i = 0; i < string.length(); ++i) |
389 | 0 | insert(string.code_points()[i]); |
390 | 0 | } |
391 | | |
392 | | void Editor::insert(ByteString const& string) |
393 | 0 | { |
394 | 0 | for (auto ch : Utf8View { string }) |
395 | 0 | insert(ch); |
396 | 0 | } |
397 | | |
398 | | void Editor::insert(StringView string_view) |
399 | 0 | { |
400 | 0 | auto view = Utf8View { string_view }; |
401 | 0 | insert(view); |
402 | 0 | } |
403 | | |
404 | | void Editor::insert(Utf8View& view) |
405 | 0 | { |
406 | 0 | for (auto ch : view) |
407 | 0 | insert(ch); |
408 | 0 | } |
409 | | |
410 | | void Editor::insert(u32 const cp) |
411 | 0 | { |
412 | 0 | StringBuilder builder; |
413 | 0 | builder.append(Utf32View(&cp, 1)); |
414 | 0 | auto str = builder.to_byte_string(); |
415 | 0 | if (m_pending_chars.try_append(str.characters(), str.length()).is_error()) |
416 | 0 | return; |
417 | | |
418 | 0 | readjust_anchored_styles(m_cursor, ModificationKind::Insertion); |
419 | |
|
420 | 0 | if (m_cursor == m_buffer.size()) { |
421 | 0 | m_buffer.append(cp); |
422 | 0 | m_cursor = m_buffer.size(); |
423 | 0 | m_inline_search_cursor = m_cursor; |
424 | 0 | return; |
425 | 0 | } |
426 | | |
427 | 0 | m_buffer.insert(m_cursor, cp); |
428 | 0 | ++m_chars_touched_in_the_middle; |
429 | 0 | ++m_cursor; |
430 | 0 | m_inline_search_cursor = m_cursor; |
431 | 0 | } |
432 | | |
433 | | void Editor::register_key_input_callback(KeyBinding const& binding) |
434 | 0 | { |
435 | 0 | if (binding.kind == KeyBinding::Kind::InternalFunction) { |
436 | 0 | auto internal_function = find_internal_function(binding.binding); |
437 | 0 | if (!internal_function) { |
438 | 0 | dbgln("LibLine: Unknown internal function '{}'", binding.binding); |
439 | 0 | return; |
440 | 0 | } |
441 | 0 | return register_key_input_callback(binding.keys, move(internal_function)); |
442 | 0 | } |
443 | | |
444 | 0 | return register_key_input_callback(binding.keys, [binding = ByteString(binding.binding)](auto& editor) { |
445 | 0 | editor.insert(binding); |
446 | 0 | return false; |
447 | 0 | }); |
448 | 0 | } |
449 | | |
450 | | static size_t code_point_length_in_utf8(u32 code_point) |
451 | 0 | { |
452 | 0 | if (code_point <= 0x7f) |
453 | 0 | return 1; |
454 | 0 | if (code_point <= 0x07ff) |
455 | 0 | return 2; |
456 | 0 | if (code_point <= 0xffff) |
457 | 0 | return 3; |
458 | 0 | if (code_point <= 0x10ffff) |
459 | 0 | return 4; |
460 | 0 | return 3; |
461 | 0 | } |
462 | | |
463 | | // buffer [ 0 1 2 3 . . . A . . . B . . . M . . . N ] |
464 | | // ^ ^ ^ ^ |
465 | | // | | | +- end of buffer |
466 | | // | | +- scan offset = M |
467 | | // | +- range end = M - B |
468 | | // +- range start = M - A |
469 | | // This method converts a byte range defined by [start_byte_offset, end_byte_offset] to a code_point range [M - A, M - B] as shown in the diagram above. |
470 | | // If `reverse' is true, A and B are before M, if not, A and B are after M. |
471 | | Editor::CodepointRange Editor::byte_offset_range_to_code_point_offset_range(size_t start_byte_offset, size_t end_byte_offset, size_t scan_code_point_offset, bool reverse) const |
472 | 0 | { |
473 | 0 | size_t byte_offset = 0; |
474 | 0 | size_t code_point_offset = scan_code_point_offset + (reverse ? 1 : 0); |
475 | 0 | CodepointRange range; |
476 | |
|
477 | 0 | for (;;) { |
478 | 0 | if (!reverse) { |
479 | 0 | if (code_point_offset >= m_buffer.size()) |
480 | 0 | break; |
481 | 0 | } else { |
482 | 0 | if (code_point_offset == 0) |
483 | 0 | break; |
484 | 0 | } |
485 | | |
486 | 0 | if (byte_offset > end_byte_offset) |
487 | 0 | break; |
488 | | |
489 | 0 | if (byte_offset < start_byte_offset) |
490 | 0 | ++range.start; |
491 | |
|
492 | 0 | if (byte_offset < end_byte_offset) |
493 | 0 | ++range.end; |
494 | |
|
495 | 0 | byte_offset += code_point_length_in_utf8(m_buffer[reverse ? --code_point_offset : code_point_offset++]); |
496 | 0 | } |
497 | |
|
498 | 0 | return range; |
499 | 0 | } |
500 | | |
501 | | void Editor::stylize(Span const& span, Style const& style) |
502 | 0 | { |
503 | 0 | if (!span.is_empty()) |
504 | 0 | return; |
505 | 0 | if (style.is_empty()) |
506 | 0 | return; |
507 | | |
508 | 0 | auto start = span.beginning(); |
509 | 0 | auto end = span.end(); |
510 | |
|
511 | 0 | if (span.mode() == Span::ByteOriented) { |
512 | 0 | auto offsets = byte_offset_range_to_code_point_offset_range(start, end, 0); |
513 | |
|
514 | 0 | start = offsets.start; |
515 | 0 | end = offsets.end; |
516 | 0 | } |
517 | |
|
518 | 0 | if (auto maybe_mask = style.mask(); maybe_mask.has_value()) { |
519 | 0 | auto it = m_current_masks.find_smallest_not_below_iterator(span.beginning()); |
520 | 0 | Optional<Style::Mask> last_encountered_entry; |
521 | 0 | if (!it.is_end()) { |
522 | | // Delete all overlapping old masks. |
523 | 0 | while (true) { |
524 | 0 | auto next_it = m_current_masks.find_largest_not_above_iterator(span.end()); |
525 | 0 | if (next_it.is_end()) |
526 | 0 | break; |
527 | 0 | if (it->has_value()) |
528 | 0 | last_encountered_entry = *it; |
529 | 0 | m_current_masks.remove(next_it.key()); |
530 | 0 | } |
531 | 0 | } |
532 | 0 | m_current_masks.insert(span.beginning(), move(maybe_mask)); |
533 | 0 | m_current_masks.insert(span.end(), {}); |
534 | 0 | if (last_encountered_entry.has_value()) |
535 | 0 | m_current_masks.insert(span.end() + 1, move(last_encountered_entry)); |
536 | 0 | style.unset_mask(); |
537 | 0 | } |
538 | |
|
539 | 0 | auto& spans_starting = style.is_anchored() ? m_current_spans.m_anchored_spans_starting : m_current_spans.m_spans_starting; |
540 | 0 | auto& spans_ending = style.is_anchored() ? m_current_spans.m_anchored_spans_ending : m_current_spans.m_spans_ending; |
541 | |
|
542 | 0 | auto& starting_map = spans_starting.ensure(start); |
543 | 0 | if (!starting_map.contains(end)) |
544 | 0 | m_refresh_needed = true; |
545 | 0 | starting_map.set(end, style); |
546 | |
|
547 | 0 | auto& ending_map = spans_ending.ensure(end); |
548 | 0 | if (!ending_map.contains(start)) |
549 | 0 | m_refresh_needed = true; |
550 | 0 | ending_map.set(start, style); |
551 | 0 | } |
552 | | |
553 | | void Editor::transform_suggestion_offsets(size_t& invariant_offset, size_t& static_offset, Span::Mode offset_mode) const |
554 | 0 | { |
555 | 0 | auto internal_static_offset = static_offset; |
556 | 0 | auto internal_invariant_offset = invariant_offset; |
557 | 0 | if (offset_mode == Span::Mode::ByteOriented) { |
558 | | // FIXME: We're assuming that invariant_offset points to the end of the available data |
559 | | // this is not necessarily true, but is true in most cases. |
560 | 0 | auto offsets = byte_offset_range_to_code_point_offset_range(internal_static_offset, internal_invariant_offset + internal_static_offset, m_cursor - 1, true); |
561 | |
|
562 | 0 | internal_static_offset = offsets.start; |
563 | 0 | internal_invariant_offset = offsets.end - offsets.start; |
564 | 0 | } |
565 | 0 | invariant_offset = internal_invariant_offset; |
566 | 0 | static_offset = internal_static_offset; |
567 | 0 | } |
568 | | |
569 | | void Editor::initialize() |
570 | 0 | { |
571 | 0 | if (m_initialized) |
572 | 0 | return; |
573 | | |
574 | 0 | struct termios termios; |
575 | 0 | tcgetattr(0, &termios); |
576 | 0 | m_default_termios = termios; // grab a copy to restore |
577 | |
|
578 | 0 | get_terminal_size(); |
579 | |
|
580 | 0 | if (m_configuration.operation_mode == Configuration::Unset) { |
581 | 0 | auto istty = isatty(STDIN_FILENO) && isatty(STDERR_FILENO); |
582 | 0 | if (!istty) { |
583 | 0 | m_configuration.set(Configuration::NonInteractive); |
584 | 0 | } else { |
585 | 0 | auto* term = getenv("TERM"); |
586 | 0 | if ((term != NULL) && StringView { term, strlen(term) }.starts_with("xterm"sv)) |
587 | 0 | m_configuration.set(Configuration::Full); |
588 | 0 | else |
589 | 0 | m_configuration.set(Configuration::NoEscapeSequences); |
590 | 0 | } |
591 | 0 | } |
592 | | |
593 | | // Because we use our own line discipline which includes echoing, |
594 | | // we disable ICANON and ECHO. |
595 | 0 | if (m_configuration.operation_mode == Configuration::Full) { |
596 | 0 | termios.c_lflag &= ~(ECHO | ICANON); |
597 | 0 | tcsetattr(0, TCSANOW, &termios); |
598 | 0 | } |
599 | |
|
600 | 0 | m_termios = termios; |
601 | |
|
602 | 0 | set_default_keybinds(); |
603 | 0 | for (auto& keybind : m_configuration.keybindings) |
604 | 0 | register_key_input_callback(keybind); |
605 | |
|
606 | 0 | if (m_configuration.m_signal_mode == Configuration::WithSignalHandlers) { |
607 | 0 | m_signal_handlers.append(Core::EventLoop::register_signal(SIGINT, [this](int) { |
608 | 0 | Core::EventLoop::current().deferred_invoke([this] { interrupted().release_value_but_fixme_should_propagate_errors(); }); |
609 | 0 | })); |
610 | |
|
611 | 0 | m_signal_handlers.append(Core::EventLoop::register_signal(SIGWINCH, [this](int) { |
612 | 0 | Core::EventLoop::current().deferred_invoke([this] { resized().release_value_but_fixme_should_propagate_errors(); }); |
613 | 0 | })); |
614 | 0 | } |
615 | |
|
616 | 0 | m_initialized = true; |
617 | 0 | } |
618 | | |
619 | | void Editor::refetch_default_termios() |
620 | 0 | { |
621 | 0 | struct termios termios; |
622 | 0 | tcgetattr(0, &termios); |
623 | 0 | m_default_termios = termios; |
624 | 0 | if (m_configuration.operation_mode == Configuration::Full) |
625 | 0 | termios.c_lflag &= ~(ECHO | ICANON); |
626 | 0 | m_termios = termios; |
627 | 0 | } |
628 | | |
629 | | ErrorOr<void> Editor::interrupted() |
630 | 0 | { |
631 | 0 | if (m_is_searching) |
632 | 0 | return m_search_editor->interrupted(); |
633 | | |
634 | 0 | if (!m_is_editing) |
635 | 0 | return {}; |
636 | | |
637 | 0 | m_was_interrupted = true; |
638 | 0 | handle_interrupt_event(); |
639 | 0 | if (!m_finish || !m_previous_interrupt_was_handled_as_interrupt) |
640 | 0 | return {}; |
641 | | |
642 | 0 | m_finish = false; |
643 | 0 | { |
644 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
645 | 0 | TRY(reposition_cursor(*stderr_stream, true)); |
646 | 0 | if (TRY(m_suggestion_display->cleanup())) { |
647 | 0 | TRY(reposition_cursor(*stderr_stream, true)); |
648 | 0 | TRY(cleanup_suggestions()); |
649 | 0 | } |
650 | 0 | TRY(stderr_stream->write_until_depleted("\r"sv.bytes())); |
651 | 0 | } |
652 | 0 | m_buffer.clear(); |
653 | 0 | m_chars_touched_in_the_middle = buffer().size(); |
654 | 0 | m_is_editing = false; |
655 | 0 | restore(); |
656 | 0 | m_notifier->set_enabled(false); |
657 | 0 | m_notifier = nullptr; |
658 | 0 | Core::EventLoop::current().quit(Retry); |
659 | 0 | return {}; |
660 | 0 | } |
661 | | |
662 | | ErrorOr<void> Editor::resized() |
663 | 0 | { |
664 | 0 | m_was_resized = true; |
665 | 0 | m_previous_num_columns = m_num_columns; |
666 | 0 | auto old_origin_row = m_origin_row; |
667 | 0 | auto old_origin_column = m_origin_column; |
668 | |
|
669 | 0 | get_terminal_size(); |
670 | |
|
671 | 0 | if (!m_has_origin_reset_scheduled) { |
672 | | // Reset the origin, but make sure it doesn't blow up if we can't read it |
673 | 0 | if (set_origin(false)) { |
674 | | // The origin we have right now actually points to where the cursor should be (in the middle of the buffer somewhere) |
675 | | // Find the "true" origin. |
676 | 0 | auto current_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); |
677 | 0 | auto lines = m_cached_prompt_metrics.lines_with_addition(current_buffer_metrics, m_num_columns); |
678 | 0 | auto offset = m_cached_prompt_metrics.offset_with_addition(current_buffer_metrics, m_num_columns); |
679 | 0 | if (lines > m_origin_row) |
680 | 0 | m_origin_row = 1; |
681 | 0 | else |
682 | 0 | m_origin_row -= lines - 1; // the prompt and the origin share a line. |
683 | |
|
684 | 0 | if (offset > m_origin_column) |
685 | 0 | m_origin_column = 1; |
686 | 0 | else |
687 | 0 | m_origin_column -= offset; |
688 | |
|
689 | 0 | set_origin(m_origin_row, m_origin_column); |
690 | |
|
691 | 0 | TRY(handle_resize_event(false)); |
692 | 0 | if (old_origin_column != m_origin_column || old_origin_row != m_origin_row) { |
693 | 0 | m_expected_origin_changed = true; |
694 | 0 | deferred_invoke([this] { |
695 | 0 | (void)refresh_display(); |
696 | 0 | }); |
697 | 0 | } |
698 | 0 | } else { |
699 | 0 | deferred_invoke([this] { handle_resize_event(true).release_value_but_fixme_should_propagate_errors(); }); |
700 | 0 | m_has_origin_reset_scheduled = true; |
701 | 0 | } |
702 | 0 | } |
703 | |
|
704 | 0 | return {}; |
705 | 0 | } |
706 | | |
707 | | ErrorOr<void> Editor::handle_resize_event(bool reset_origin) |
708 | 0 | { |
709 | 0 | if (!m_initialized || !m_is_editing) |
710 | 0 | return {}; |
711 | | |
712 | 0 | m_has_origin_reset_scheduled = false; |
713 | 0 | if (reset_origin && !set_origin(false)) { |
714 | 0 | m_has_origin_reset_scheduled = true; |
715 | 0 | deferred_invoke([this] { handle_resize_event(true).release_value_but_fixme_should_propagate_errors(); }); |
716 | 0 | return {}; |
717 | 0 | } |
718 | | |
719 | 0 | set_origin(m_origin_row, 1); |
720 | |
|
721 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
722 | |
|
723 | 0 | TRY(reposition_cursor(*stderr_stream, true)); |
724 | 0 | TRY(m_suggestion_display->redisplay(m_suggestion_manager, m_num_lines, m_num_columns)); |
725 | 0 | m_origin_row = m_suggestion_display->origin_row(); |
726 | 0 | TRY(reposition_cursor(*stderr_stream)); |
727 | |
|
728 | 0 | if (m_is_searching) |
729 | 0 | TRY(m_search_editor->resized()); |
730 | |
|
731 | 0 | return {}; |
732 | 0 | } |
733 | | |
734 | | ErrorOr<void> Editor::really_quit_event_loop() |
735 | 0 | { |
736 | 0 | m_finish = false; |
737 | 0 | { |
738 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
739 | 0 | TRY(reposition_cursor(*stderr_stream, true)); |
740 | 0 | TRY(stderr_stream->write_until_depleted("\n"sv.bytes())); |
741 | 0 | } |
742 | 0 | auto string = line(); |
743 | 0 | m_buffer.clear(); |
744 | 0 | m_chars_touched_in_the_middle = buffer().size(); |
745 | 0 | m_is_editing = false; |
746 | |
|
747 | 0 | if (m_initialized) |
748 | 0 | restore(); |
749 | |
|
750 | 0 | m_returned_line = string; |
751 | 0 | m_notifier->set_enabled(false); |
752 | 0 | m_notifier = nullptr; |
753 | 0 | Core::EventLoop::current().quit(Exit); |
754 | 0 | return {}; |
755 | 0 | } |
756 | | |
757 | | auto Editor::get_line(ByteString const& prompt) -> Result<ByteString, Editor::Error> |
758 | 0 | { |
759 | 0 | initialize(); |
760 | 0 | m_is_editing = true; |
761 | |
|
762 | 0 | if (m_configuration.operation_mode == Configuration::NoEscapeSequences || m_configuration.operation_mode == Configuration::NonInteractive) { |
763 | | // Do not use escape sequences, instead, use LibC's getline. |
764 | 0 | size_t size = 0; |
765 | 0 | char* line = nullptr; |
766 | | // Show the prompt only on interactive mode (NoEscapeSequences in this case). |
767 | 0 | if (m_configuration.operation_mode != Configuration::NonInteractive) |
768 | 0 | fputs(prompt.characters(), stderr); |
769 | 0 | auto line_length = getline(&line, &size, stdin); |
770 | | // getline() returns -1 and sets errno=0 on EOF. |
771 | 0 | if (line_length == -1) { |
772 | 0 | if (line) |
773 | 0 | free(line); |
774 | 0 | if (errno == 0) |
775 | 0 | return Error::Eof; |
776 | | |
777 | 0 | return Error::ReadFailure; |
778 | 0 | } |
779 | 0 | restore(); |
780 | 0 | if (line) { |
781 | 0 | ByteString result { line, (size_t)line_length, Chomp }; |
782 | 0 | free(line); |
783 | 0 | return result; |
784 | 0 | } |
785 | | |
786 | 0 | return Error::ReadFailure; |
787 | 0 | } |
788 | | |
789 | 0 | auto old_cols = m_num_columns; |
790 | 0 | auto old_lines = m_num_lines; |
791 | 0 | get_terminal_size(); |
792 | |
|
793 | 0 | if (m_configuration.enable_bracketed_paste) |
794 | 0 | fprintf(stderr, "\x1b[?2004h"); |
795 | |
|
796 | 0 | if (m_num_columns != old_cols || m_num_lines != old_lines) |
797 | 0 | m_refresh_needed = true; |
798 | |
|
799 | 0 | set_prompt(prompt); |
800 | 0 | reset(); |
801 | 0 | strip_styles(true); |
802 | |
|
803 | 0 | { |
804 | 0 | auto stderr_stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors(); |
805 | 0 | auto prompt_lines = max(current_prompt_metrics().line_metrics.size(), 1ul) - 1; |
806 | 0 | for (size_t i = 0; i < prompt_lines; ++i) |
807 | 0 | stderr_stream->write_until_depleted("\n"sv.bytes()).release_value_but_fixme_should_propagate_errors(); |
808 | |
|
809 | 0 | VT::move_relative(-static_cast<int>(prompt_lines), 0, *stderr_stream).release_value_but_fixme_should_propagate_errors(); |
810 | 0 | } |
811 | |
|
812 | 0 | set_origin(); |
813 | |
|
814 | 0 | m_history_cursor = m_history.size(); |
815 | |
|
816 | 0 | if (auto refresh_result = refresh_display(); refresh_result.is_error()) |
817 | 0 | m_input_error = Error::ReadFailure; |
818 | |
|
819 | 0 | Core::EventLoop loop; |
820 | |
|
821 | 0 | m_notifier = Core::Notifier::construct(STDIN_FILENO, Core::Notifier::Type::Read); |
822 | |
|
823 | 0 | if (m_input_error.has_value()) |
824 | 0 | loop.quit(Exit); |
825 | |
|
826 | 0 | m_notifier->on_activation = [&] { |
827 | 0 | if (try_update_once().is_error()) |
828 | 0 | loop.quit(Exit); |
829 | 0 | }; |
830 | |
|
831 | 0 | if (!m_incomplete_data.is_empty()) { |
832 | 0 | deferred_invoke([&] { |
833 | 0 | if (try_update_once().is_error()) |
834 | 0 | loop.quit(Exit); |
835 | 0 | }); |
836 | 0 | } |
837 | |
|
838 | 0 | if (loop.exec() == Retry) |
839 | 0 | return get_line(prompt); |
840 | | |
841 | 0 | return m_input_error.has_value() ? Result<ByteString, Editor::Error> { m_input_error.value() } : Result<ByteString, Editor::Error> { m_returned_line }; |
842 | 0 | } |
843 | | |
844 | | ErrorOr<void> Editor::try_update_once() |
845 | 0 | { |
846 | 0 | if (m_was_interrupted) { |
847 | 0 | handle_interrupt_event(); |
848 | 0 | } |
849 | |
|
850 | 0 | TRY(handle_read_event()); |
851 | |
|
852 | 0 | if (m_always_refresh) |
853 | 0 | m_refresh_needed = true; |
854 | |
|
855 | 0 | TRY(refresh_display()); |
856 | |
|
857 | 0 | if (m_finish) |
858 | 0 | TRY(really_quit_event_loop()); |
859 | |
|
860 | 0 | return {}; |
861 | 0 | } |
862 | | |
863 | | void Editor::handle_interrupt_event() |
864 | 0 | { |
865 | 0 | if (!m_initialized || !m_is_editing) |
866 | 0 | return; |
867 | | |
868 | 0 | m_was_interrupted = false; |
869 | 0 | m_previous_interrupt_was_handled_as_interrupt = false; |
870 | |
|
871 | 0 | m_callback_machine.interrupted(*this); |
872 | 0 | if (!m_callback_machine.should_process_last_pressed_key()) |
873 | 0 | return; |
874 | | |
875 | 0 | m_previous_interrupt_was_handled_as_interrupt = true; |
876 | |
|
877 | 0 | fprintf(stderr, "^C\n"); |
878 | 0 | fflush(stderr); |
879 | |
|
880 | 0 | if (on_interrupt_handled) |
881 | 0 | on_interrupt_handled(); |
882 | |
|
883 | 0 | m_buffer.clear(); |
884 | 0 | m_chars_touched_in_the_middle = buffer().size(); |
885 | 0 | m_cursor = 0; |
886 | 0 | set_origin(false); |
887 | |
|
888 | 0 | finish(); |
889 | 0 | } |
890 | | |
891 | | ErrorOr<void> Editor::handle_read_event() |
892 | 0 | { |
893 | 0 | if (m_prohibit_input_processing) { |
894 | 0 | m_have_unprocessed_read_event = true; |
895 | 0 | return {}; |
896 | 0 | } |
897 | | |
898 | 0 | auto prohibit_scope = prohibit_input(); |
899 | |
|
900 | 0 | char keybuf[1024]; |
901 | 0 | ssize_t nread = 0; |
902 | |
|
903 | 0 | if (!m_incomplete_data.size()) |
904 | 0 | nread = read(0, keybuf, sizeof(keybuf)); |
905 | |
|
906 | 0 | if (nread < 0) { |
907 | 0 | if (errno == EINTR) { |
908 | 0 | if (!m_was_interrupted) { |
909 | 0 | if (m_was_resized) |
910 | 0 | return {}; |
911 | | |
912 | 0 | finish(); |
913 | 0 | return {}; |
914 | 0 | } |
915 | | |
916 | 0 | handle_interrupt_event(); |
917 | 0 | return {}; |
918 | 0 | } |
919 | | |
920 | 0 | ScopedValueRollback errno_restorer(errno); |
921 | 0 | perror("read failed"); |
922 | |
|
923 | 0 | m_input_error = Error::ReadFailure; |
924 | 0 | finish(); |
925 | 0 | return {}; |
926 | 0 | } |
927 | | |
928 | 0 | m_incomplete_data.append(keybuf, nread); |
929 | 0 | auto available_bytes = m_incomplete_data.size(); |
930 | |
|
931 | 0 | if (available_bytes == 0) { |
932 | 0 | m_input_error = Error::Empty; |
933 | 0 | finish(); |
934 | 0 | return {}; |
935 | 0 | } |
936 | | |
937 | 0 | auto reverse_tab = false; |
938 | | |
939 | | // Discard starting bytes until they make sense as utf-8. |
940 | 0 | size_t valid_bytes = 0; |
941 | 0 | while (available_bytes > 0) { |
942 | 0 | Utf8View { StringView { m_incomplete_data.data(), available_bytes } }.validate(valid_bytes); |
943 | 0 | if (valid_bytes != 0) |
944 | 0 | break; |
945 | 0 | m_incomplete_data.take_first(); |
946 | 0 | --available_bytes; |
947 | 0 | } |
948 | |
|
949 | 0 | Utf8View input_view { StringView { m_incomplete_data.data(), valid_bytes } }; |
950 | 0 | size_t consumed_code_points = 0; |
951 | |
|
952 | 0 | static Vector<u8, 4> csi_parameter_bytes; |
953 | 0 | static Vector<u8> csi_intermediate_bytes; |
954 | 0 | Vector<unsigned, 4> csi_parameters; |
955 | 0 | u8 csi_final; |
956 | 0 | enum CSIMod { |
957 | 0 | Shift = 1, |
958 | 0 | Alt = 2, |
959 | 0 | Ctrl = 4, |
960 | 0 | }; |
961 | |
|
962 | 0 | for (auto code_point : input_view) { |
963 | 0 | if (m_finish) |
964 | 0 | break; |
965 | | |
966 | 0 | ++consumed_code_points; |
967 | |
|
968 | 0 | if (code_point == 0) |
969 | 0 | continue; |
970 | | |
971 | 0 | switch (m_state) { |
972 | 0 | case InputState::GotEscape: |
973 | 0 | switch (code_point) { |
974 | 0 | case '[': |
975 | 0 | m_state = InputState::CSIExpectParameter; |
976 | 0 | continue; |
977 | 0 | default: { |
978 | 0 | m_callback_machine.key_pressed(*this, { code_point, Key::Alt }); |
979 | 0 | m_state = InputState::Free; |
980 | 0 | TRY(cleanup_suggestions()); |
981 | 0 | continue; |
982 | 0 | } |
983 | 0 | } |
984 | 0 | case InputState::CSIExpectParameter: |
985 | 0 | if (code_point >= 0x30 && code_point <= 0x3f) { // '0123456789:;<=>?' |
986 | 0 | csi_parameter_bytes.append(code_point); |
987 | 0 | continue; |
988 | 0 | } |
989 | 0 | m_state = InputState::CSIExpectIntermediate; |
990 | 0 | [[fallthrough]]; |
991 | 0 | case InputState::CSIExpectIntermediate: |
992 | 0 | if (code_point >= 0x20 && code_point <= 0x2f) { // ' !"#$%&\'()*+,-./' |
993 | 0 | csi_intermediate_bytes.append(code_point); |
994 | 0 | continue; |
995 | 0 | } |
996 | 0 | m_state = InputState::CSIExpectFinal; |
997 | 0 | [[fallthrough]]; |
998 | 0 | case InputState::CSIExpectFinal: { |
999 | 0 | m_state = m_previous_free_state; |
1000 | 0 | auto is_in_paste = m_state == InputState::Paste; |
1001 | 0 | for (auto& parameter : ByteString::copy(csi_parameter_bytes).split(';')) { |
1002 | 0 | if (auto value = parameter.to_number<unsigned>(); value.has_value()) |
1003 | 0 | csi_parameters.append(value.value()); |
1004 | 0 | else |
1005 | 0 | csi_parameters.append(0); |
1006 | 0 | } |
1007 | 0 | unsigned param1 = 0, param2 = 0; |
1008 | 0 | if (csi_parameters.size() >= 1) |
1009 | 0 | param1 = csi_parameters[0]; |
1010 | 0 | if (csi_parameters.size() >= 2) |
1011 | 0 | param2 = csi_parameters[1]; |
1012 | 0 | unsigned modifiers = param2 ? param2 - 1 : 0; |
1013 | |
|
1014 | 0 | if (is_in_paste && code_point != '~' && param1 != 201) { |
1015 | | // The only valid escape to process in paste mode is the stop-paste sequence. |
1016 | | // so treat everything else as part of the pasted data. |
1017 | 0 | insert('\x1b'); |
1018 | 0 | insert('['); |
1019 | 0 | insert(StringView { csi_parameter_bytes.data(), csi_parameter_bytes.size() }); |
1020 | 0 | insert(StringView { csi_intermediate_bytes.data(), csi_intermediate_bytes.size() }); |
1021 | 0 | insert(code_point); |
1022 | 0 | continue; |
1023 | 0 | } |
1024 | 0 | if (!(code_point >= 0x40 && code_point <= 0x7f)) { |
1025 | 0 | dbgln("LibLine: Invalid CSI: {:02x} ({:c})", code_point, code_point); |
1026 | 0 | continue; |
1027 | 0 | } |
1028 | 0 | csi_final = code_point; |
1029 | 0 | csi_parameters.clear(); |
1030 | 0 | csi_parameter_bytes.clear(); |
1031 | 0 | csi_intermediate_bytes.clear(); |
1032 | |
|
1033 | 0 | if (csi_final == 'Z') { |
1034 | | // 'reverse tab' |
1035 | 0 | reverse_tab = true; |
1036 | 0 | break; |
1037 | 0 | } |
1038 | 0 | TRY(cleanup_suggestions()); |
1039 | |
|
1040 | 0 | switch (csi_final) { |
1041 | 0 | case 'A': // ^[[A: arrow up |
1042 | 0 | search_backwards(); |
1043 | 0 | continue; |
1044 | 0 | case 'B': // ^[[B: arrow down |
1045 | 0 | search_forwards(); |
1046 | 0 | continue; |
1047 | 0 | case 'D': // ^[[D: arrow left |
1048 | 0 | if (modifiers == CSIMod::Alt || modifiers == CSIMod::Ctrl) |
1049 | 0 | cursor_left_word(); |
1050 | 0 | else |
1051 | 0 | cursor_left_character(); |
1052 | 0 | continue; |
1053 | 0 | case 'C': // ^[[C: arrow right |
1054 | 0 | if (modifiers == CSIMod::Alt || modifiers == CSIMod::Ctrl) |
1055 | 0 | cursor_right_word(); |
1056 | 0 | else |
1057 | 0 | cursor_right_character(); |
1058 | 0 | continue; |
1059 | 0 | case 'H': // ^[[H: home |
1060 | 0 | go_home(); |
1061 | 0 | continue; |
1062 | 0 | case 'F': // ^[[F: end |
1063 | 0 | go_end(); |
1064 | 0 | continue; |
1065 | 0 | case 127: |
1066 | 0 | if (modifiers == CSIMod::Ctrl) |
1067 | 0 | erase_alnum_word_backwards(); |
1068 | 0 | else |
1069 | 0 | erase_character_backwards(); |
1070 | 0 | continue; |
1071 | 0 | case '~': |
1072 | 0 | if (param1 == 3) { // ^[[3~: delete |
1073 | 0 | if (modifiers == CSIMod::Ctrl) |
1074 | 0 | erase_alnum_word_forwards(); |
1075 | 0 | else |
1076 | 0 | erase_character_forwards(); |
1077 | 0 | m_search_offset = 0; |
1078 | 0 | continue; |
1079 | 0 | } |
1080 | 0 | if (m_configuration.enable_bracketed_paste) { |
1081 | | // ^[[200~: start bracketed paste |
1082 | | // ^[[201~: end bracketed paste |
1083 | 0 | if (!is_in_paste && param1 == 200) { |
1084 | 0 | m_state = InputState::Paste; |
1085 | 0 | continue; |
1086 | 0 | } |
1087 | 0 | if (is_in_paste && param1 == 201) { |
1088 | 0 | m_state = InputState::Free; |
1089 | 0 | if (on_paste) { |
1090 | 0 | on_paste(Utf32View { m_paste_buffer.data(), m_paste_buffer.size() }, *this); |
1091 | 0 | m_paste_buffer.clear_with_capacity(); |
1092 | 0 | } |
1093 | 0 | if (!m_paste_buffer.is_empty()) |
1094 | 0 | insert(Utf32View { m_paste_buffer.data(), m_paste_buffer.size() }); |
1095 | 0 | continue; |
1096 | 0 | } |
1097 | 0 | } |
1098 | | // ^[[5~: page up |
1099 | | // ^[[6~: page down |
1100 | 0 | dbgln("LibLine: Unhandled '~': {}", param1); |
1101 | 0 | continue; |
1102 | 0 | default: |
1103 | 0 | dbgln("LibLine: Unhandled final: {:02x} ({:c})", code_point, code_point); |
1104 | 0 | continue; |
1105 | 0 | } |
1106 | 0 | VERIFY_NOT_REACHED(); |
1107 | 0 | } |
1108 | 0 | case InputState::Verbatim: |
1109 | 0 | m_state = InputState::Free; |
1110 | | // Verbatim mode will bypass all mechanisms and just insert the code point. |
1111 | 0 | insert(code_point); |
1112 | 0 | continue; |
1113 | 0 | case InputState::Paste: |
1114 | 0 | if (code_point == 27) { |
1115 | 0 | m_previous_free_state = InputState::Paste; |
1116 | 0 | m_state = InputState::GotEscape; |
1117 | 0 | continue; |
1118 | 0 | } |
1119 | 0 | if (on_paste) |
1120 | 0 | m_paste_buffer.append(code_point); |
1121 | 0 | else |
1122 | 0 | insert(code_point); |
1123 | 0 | continue; |
1124 | 0 | case InputState::Free: |
1125 | 0 | m_previous_free_state = InputState::Free; |
1126 | 0 | if (code_point == 27) { |
1127 | 0 | m_callback_machine.key_pressed(*this, code_point); |
1128 | | // Note that this should also deal with explicitly registered keys |
1129 | | // that would otherwise be interpreted as escapes. |
1130 | 0 | if (m_callback_machine.should_process_last_pressed_key()) |
1131 | 0 | m_state = InputState::GotEscape; |
1132 | 0 | continue; |
1133 | 0 | } |
1134 | 0 | if (code_point == 22) { // ^v |
1135 | 0 | m_callback_machine.key_pressed(*this, code_point); |
1136 | 0 | if (m_callback_machine.should_process_last_pressed_key()) |
1137 | 0 | m_state = InputState::Verbatim; |
1138 | 0 | continue; |
1139 | 0 | } |
1140 | 0 | break; |
1141 | 0 | } |
1142 | | |
1143 | | // There are no sequences past this point, so short of 'tab', we will want to cleanup the suggestions. |
1144 | 0 | ArmedScopeGuard suggestion_cleanup { [this] { cleanup_suggestions().release_value_but_fixme_should_propagate_errors(); } }; |
1145 | | |
1146 | | // Normally ^D. `stty eof \^n` can change it to ^N (or something else), but Serenity doesn't have `stty` yet. |
1147 | | // Process this here since the keybinds might override its behavior. |
1148 | | // This only applies when the buffer is empty. at any other time, the behavior should be configurable. |
1149 | 0 | if (code_point == m_termios.c_cc[VEOF] && m_buffer.size() == 0) { |
1150 | 0 | finish_edit(); |
1151 | 0 | continue; |
1152 | 0 | } |
1153 | | |
1154 | 0 | m_callback_machine.key_pressed(*this, code_point); |
1155 | 0 | if (!m_callback_machine.should_process_last_pressed_key()) |
1156 | 0 | continue; |
1157 | | |
1158 | 0 | m_search_offset = 0; // reset search offset on any key |
1159 | |
|
1160 | 0 | if (code_point == '\t' || reverse_tab) { |
1161 | 0 | suggestion_cleanup.disarm(); |
1162 | |
|
1163 | 0 | if (!on_tab_complete) |
1164 | 0 | continue; |
1165 | | |
1166 | | // Reverse tab can count as regular tab here. |
1167 | 0 | m_times_tab_pressed++; |
1168 | |
|
1169 | 0 | int token_start = m_cursor; |
1170 | | |
1171 | | // Ask for completions only on the first tab |
1172 | | // and scan for the largest common prefix to display, |
1173 | | // further tabs simply show the cached completions. |
1174 | 0 | if (m_times_tab_pressed == 1) { |
1175 | 0 | m_suggestion_manager.set_suggestions(on_tab_complete(*this)); |
1176 | 0 | m_suggestion_manager.set_start_index(0); |
1177 | 0 | m_prompt_lines_at_suggestion_initiation = num_lines(); |
1178 | 0 | if (m_suggestion_manager.count() == 0) { |
1179 | | // There are no suggestions, beep. |
1180 | 0 | fputc('\a', stderr); |
1181 | 0 | fflush(stderr); |
1182 | 0 | } |
1183 | 0 | } |
1184 | | |
1185 | | // Adjust already incremented / decremented index when switching tab direction. |
1186 | 0 | if (reverse_tab && m_tab_direction != TabDirection::Backward) { |
1187 | 0 | m_suggestion_manager.previous(); |
1188 | 0 | m_suggestion_manager.previous(); |
1189 | 0 | m_tab_direction = TabDirection::Backward; |
1190 | 0 | } |
1191 | 0 | if (!reverse_tab && m_tab_direction != TabDirection::Forward) { |
1192 | 0 | m_suggestion_manager.next(); |
1193 | 0 | m_suggestion_manager.next(); |
1194 | 0 | m_tab_direction = TabDirection::Forward; |
1195 | 0 | } |
1196 | 0 | reverse_tab = false; |
1197 | |
|
1198 | 0 | SuggestionManager::CompletionMode completion_mode; |
1199 | 0 | switch (m_times_tab_pressed) { |
1200 | 0 | case 1: |
1201 | 0 | completion_mode = SuggestionManager::CompletePrefix; |
1202 | 0 | break; |
1203 | 0 | case 2: |
1204 | 0 | completion_mode = SuggestionManager::ShowSuggestions; |
1205 | 0 | break; |
1206 | 0 | default: |
1207 | 0 | completion_mode = SuggestionManager::CycleSuggestions; |
1208 | 0 | break; |
1209 | 0 | } |
1210 | | |
1211 | 0 | insert(Utf32View { m_remembered_suggestion_static_data.data(), m_remembered_suggestion_static_data.size() }); |
1212 | 0 | m_remembered_suggestion_static_data.clear_with_capacity(); |
1213 | |
|
1214 | 0 | auto completion_result = m_suggestion_manager.attempt_completion(completion_mode, token_start); |
1215 | |
|
1216 | 0 | auto new_cursor = m_cursor; |
1217 | |
|
1218 | 0 | new_cursor += completion_result.new_cursor_offset; |
1219 | 0 | for (size_t i = completion_result.offset_region_to_remove.start; i < completion_result.offset_region_to_remove.end; ++i) |
1220 | 0 | remove_at_index(new_cursor); |
1221 | |
|
1222 | 0 | new_cursor -= completion_result.static_offset_from_cursor; |
1223 | 0 | for (size_t i = 0; i < completion_result.static_offset_from_cursor; ++i) { |
1224 | 0 | m_remembered_suggestion_static_data.append(m_buffer[new_cursor]); |
1225 | 0 | remove_at_index(new_cursor); |
1226 | 0 | } |
1227 | |
|
1228 | 0 | m_cursor = new_cursor; |
1229 | 0 | m_inline_search_cursor = new_cursor; |
1230 | 0 | m_refresh_needed = true; |
1231 | 0 | m_chars_touched_in_the_middle++; |
1232 | |
|
1233 | 0 | for (auto& view : completion_result.insert) |
1234 | 0 | insert(view); |
1235 | |
|
1236 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
1237 | 0 | TRY(reposition_cursor(*stderr_stream)); |
1238 | |
|
1239 | 0 | if (completion_result.style_to_apply.has_value()) { |
1240 | | // Apply the style of the last suggestion. |
1241 | 0 | readjust_anchored_styles(m_suggestion_manager.current_suggestion().start_index, ModificationKind::ForcedOverlapRemoval); |
1242 | 0 | stylize({ m_suggestion_manager.current_suggestion().start_index, m_cursor, Span::Mode::CodepointOriented }, completion_result.style_to_apply.value()); |
1243 | 0 | } |
1244 | |
|
1245 | 0 | switch (completion_result.new_completion_mode) { |
1246 | 0 | case SuggestionManager::DontComplete: |
1247 | 0 | m_times_tab_pressed = 0; |
1248 | 0 | m_remembered_suggestion_static_data.clear_with_capacity(); |
1249 | 0 | break; |
1250 | 0 | case SuggestionManager::CompletePrefix: |
1251 | 0 | break; |
1252 | 0 | default: |
1253 | 0 | ++m_times_tab_pressed; |
1254 | 0 | break; |
1255 | 0 | } |
1256 | | |
1257 | 0 | if (m_times_tab_pressed > 1 && m_suggestion_manager.count() > 0) { |
1258 | 0 | if (TRY(m_suggestion_display->cleanup())) |
1259 | 0 | TRY(reposition_cursor(*stderr_stream)); |
1260 | |
|
1261 | 0 | m_suggestion_display->set_initial_prompt_lines(m_prompt_lines_at_suggestion_initiation); |
1262 | |
|
1263 | 0 | TRY(m_suggestion_display->display(m_suggestion_manager)); |
1264 | |
|
1265 | 0 | m_origin_row = m_suggestion_display->origin_row(); |
1266 | 0 | } |
1267 | |
|
1268 | 0 | if (m_times_tab_pressed > 2) { |
1269 | 0 | if (m_tab_direction == TabDirection::Forward) |
1270 | 0 | m_suggestion_manager.next(); |
1271 | 0 | else |
1272 | 0 | m_suggestion_manager.previous(); |
1273 | 0 | } |
1274 | |
|
1275 | 0 | if (m_suggestion_manager.count() < 2 && !completion_result.avoid_committing_to_single_suggestion) { |
1276 | | // We have none, or just one suggestion, |
1277 | | // we should just commit that and continue |
1278 | | // after it, as if it were auto-completed. |
1279 | 0 | TRY(reposition_cursor(*stderr_stream, true)); |
1280 | 0 | TRY(cleanup_suggestions()); |
1281 | 0 | m_remembered_suggestion_static_data.clear_with_capacity(); |
1282 | 0 | } |
1283 | 0 | continue; |
1284 | 0 | } |
1285 | | |
1286 | | // If we got here, manually cleanup the suggestions and then insert the new code point. |
1287 | 0 | m_remembered_suggestion_static_data.clear_with_capacity(); |
1288 | 0 | suggestion_cleanup.disarm(); |
1289 | 0 | TRY(cleanup_suggestions()); |
1290 | 0 | insert(code_point); |
1291 | 0 | } |
1292 | | |
1293 | 0 | if (consumed_code_points == valid_bytes) { |
1294 | 0 | m_incomplete_data.clear(); |
1295 | 0 | } else { |
1296 | 0 | auto bytes_to_drop = input_view.byte_offset_of(consumed_code_points + 1); |
1297 | 0 | for (size_t i = 0; i < bytes_to_drop; ++i) |
1298 | 0 | m_incomplete_data.take_first(); |
1299 | 0 | } |
1300 | |
|
1301 | 0 | if (!m_incomplete_data.is_empty() && !m_finish) |
1302 | 0 | deferred_invoke([&] { try_update_once().release_value_but_fixme_should_propagate_errors(); }); |
1303 | |
|
1304 | 0 | return {}; |
1305 | 0 | } |
1306 | | |
1307 | | ErrorOr<void> Editor::cleanup_suggestions() |
1308 | 0 | { |
1309 | 0 | if (m_times_tab_pressed != 0) { |
1310 | | // Apply the style of the last suggestion. |
1311 | 0 | readjust_anchored_styles(m_suggestion_manager.current_suggestion().start_index, ModificationKind::ForcedOverlapRemoval); |
1312 | 0 | stylize({ m_suggestion_manager.current_suggestion().start_index, m_cursor, Span::Mode::CodepointOriented }, m_suggestion_manager.current_suggestion().style); |
1313 | | // We probably have some suggestions drawn, |
1314 | | // let's clean them up. |
1315 | 0 | if (TRY(m_suggestion_display->cleanup())) { |
1316 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
1317 | 0 | TRY(reposition_cursor(*stderr_stream)); |
1318 | 0 | m_refresh_needed = true; |
1319 | 0 | } |
1320 | 0 | m_suggestion_manager.reset(); |
1321 | 0 | m_suggestion_display->finish(); |
1322 | 0 | } |
1323 | 0 | m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB |
1324 | 0 | return {}; |
1325 | 0 | } |
1326 | | |
1327 | | bool Editor::search(StringView phrase, bool allow_empty, bool from_beginning) |
1328 | 0 | { |
1329 | 0 | int last_matching_offset = -1; |
1330 | 0 | bool found = false; |
1331 | | |
1332 | | // Do not search for empty strings. |
1333 | 0 | if (allow_empty || phrase.length() > 0) { |
1334 | 0 | size_t search_offset = m_search_offset; |
1335 | 0 | for (size_t i = m_history_cursor; i > 0; --i) { |
1336 | 0 | auto& entry = m_history[i - 1]; |
1337 | 0 | auto contains = from_beginning ? entry.entry.starts_with(phrase) : entry.entry.contains(phrase); |
1338 | 0 | if (contains) { |
1339 | 0 | last_matching_offset = i - 1; |
1340 | 0 | if (search_offset == 0) { |
1341 | 0 | found = true; |
1342 | 0 | break; |
1343 | 0 | } |
1344 | 0 | --search_offset; |
1345 | 0 | } |
1346 | 0 | } |
1347 | |
|
1348 | 0 | if (!found) { |
1349 | 0 | fputc('\a', stderr); |
1350 | 0 | fflush(stderr); |
1351 | 0 | } |
1352 | 0 | } |
1353 | |
|
1354 | 0 | if (found) { |
1355 | | // We plan to clear the buffer, so mark the entire thing touched. |
1356 | 0 | m_chars_touched_in_the_middle = m_buffer.size(); |
1357 | 0 | m_buffer.clear(); |
1358 | 0 | m_cursor = 0; |
1359 | 0 | insert(m_history[last_matching_offset].entry); |
1360 | | // Always needed, as we have cleared the buffer above. |
1361 | 0 | m_refresh_needed = true; |
1362 | 0 | } |
1363 | |
|
1364 | 0 | return found; |
1365 | 0 | } |
1366 | | |
1367 | | void Editor::recalculate_origin() |
1368 | 0 | { |
1369 | | // Changing the columns can affect our origin if |
1370 | | // the new size is smaller than our prompt, which would |
1371 | | // cause said prompt to take up more space, so we should |
1372 | | // compensate for that. |
1373 | 0 | if (m_cached_prompt_metrics.max_line_length >= m_num_columns) { |
1374 | 0 | auto added_lines = (m_cached_prompt_metrics.max_line_length + 1) / m_num_columns - 1; |
1375 | 0 | m_origin_row += added_lines; |
1376 | 0 | } |
1377 | | |
1378 | | // We also need to recalculate our cursor position, |
1379 | | // but that will be calculated and applied at the next |
1380 | | // refresh cycle. |
1381 | 0 | } |
1382 | | |
1383 | | ErrorOr<void> Editor::cleanup() |
1384 | 0 | { |
1385 | 0 | auto current_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); |
1386 | 0 | auto new_lines = current_prompt_metrics().lines_with_addition(current_buffer_metrics, m_num_columns); |
1387 | 0 | if (new_lines < m_shown_lines) |
1388 | 0 | m_extra_forward_lines = max(m_shown_lines - new_lines, m_extra_forward_lines); |
1389 | |
|
1390 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
1391 | 0 | TRY(reposition_cursor(*stderr_stream, true)); |
1392 | 0 | auto current_line = num_lines() - 1; |
1393 | 0 | TRY(VT::clear_lines(current_line, m_extra_forward_lines, *stderr_stream)); |
1394 | 0 | m_extra_forward_lines = 0; |
1395 | 0 | TRY(reposition_cursor(*stderr_stream)); |
1396 | 0 | return {}; |
1397 | 0 | } |
1398 | | |
1399 | | ErrorOr<void> Editor::refresh_display() |
1400 | 0 | { |
1401 | 0 | AllocatingMemoryStream output_stream; |
1402 | 0 | ScopeGuard flush_stream { |
1403 | 0 | [&] { |
1404 | 0 | m_shown_lines = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns); |
1405 | |
|
1406 | 0 | if (output_stream.used_buffer_size() == 0) |
1407 | 0 | return; |
1408 | | |
1409 | 0 | auto buffer = output_stream.read_until_eof().release_value_but_fixme_should_propagate_errors(); |
1410 | 0 | fwrite(buffer.data(), sizeof(char), buffer.size(), stderr); |
1411 | 0 | } |
1412 | 0 | }; |
1413 | |
|
1414 | 0 | auto has_cleaned_up = false; |
1415 | | // Someone changed the window size, figure it out |
1416 | | // and react to it, we might need to redraw. |
1417 | 0 | if (m_was_resized) { |
1418 | 0 | if (m_expected_origin_changed || m_previous_num_columns != m_num_columns) { |
1419 | | // We need to cleanup and redo everything. |
1420 | 0 | m_expected_origin_changed = false; |
1421 | 0 | m_cached_prompt_valid = false; |
1422 | 0 | m_refresh_needed = true; |
1423 | 0 | swap(m_previous_num_columns, m_num_columns); |
1424 | 0 | recalculate_origin(); |
1425 | 0 | TRY(cleanup()); |
1426 | 0 | swap(m_previous_num_columns, m_num_columns); |
1427 | 0 | has_cleaned_up = true; |
1428 | 0 | } |
1429 | 0 | m_was_resized = false; |
1430 | 0 | } |
1431 | | // We might be at the last line, and have more than one line; |
1432 | | // Refreshing the display will cause the terminal to scroll, |
1433 | | // so note that fact and bring origin up, making sure to |
1434 | | // reserve the space for however many lines we move it up. |
1435 | 0 | auto current_num_lines = num_lines(); |
1436 | 0 | if (m_origin_row + current_num_lines > m_num_lines) { |
1437 | 0 | if (current_num_lines > m_num_lines) { |
1438 | 0 | for (size_t i = 0; i < m_num_lines; ++i) |
1439 | 0 | TRY(output_stream.write_until_depleted("\n"sv.bytes())); |
1440 | 0 | m_origin_row = 0; |
1441 | 0 | } else { |
1442 | 0 | auto old_origin_row = m_origin_row; |
1443 | 0 | m_origin_row = m_num_lines - current_num_lines + 1; |
1444 | 0 | for (size_t i = 0; i < old_origin_row - m_origin_row; ++i) |
1445 | 0 | TRY(output_stream.write_until_depleted("\n"sv.bytes())); |
1446 | 0 | } |
1447 | 0 | } |
1448 | | // Do not call hook on pure cursor movement. |
1449 | 0 | if (m_cached_prompt_valid && !m_refresh_needed && m_pending_chars.size() == 0) { |
1450 | | // Probably just moving around. |
1451 | 0 | TRY(reposition_cursor(output_stream)); |
1452 | 0 | m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); |
1453 | 0 | m_drawn_end_of_line_offset = m_buffer.size(); |
1454 | 0 | return {}; |
1455 | 0 | } |
1456 | | |
1457 | 0 | if (on_display_refresh) |
1458 | 0 | on_display_refresh(*this); |
1459 | |
|
1460 | 0 | if (m_cached_prompt_valid) { |
1461 | 0 | if (!m_refresh_needed && m_cursor == m_buffer.size()) { |
1462 | | // Just write the characters out and continue, |
1463 | | // no need to refresh the entire line. |
1464 | 0 | TRY(output_stream.write_until_depleted(m_pending_chars)); |
1465 | 0 | m_pending_chars.clear(); |
1466 | 0 | m_drawn_cursor = m_cursor; |
1467 | 0 | m_drawn_end_of_line_offset = m_buffer.size(); |
1468 | 0 | m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); |
1469 | 0 | m_drawn_spans = m_current_spans; |
1470 | 0 | return {}; |
1471 | 0 | } |
1472 | 0 | } |
1473 | | |
1474 | 0 | auto apply_styles = [&, empty_styles = HashMap<u32, Style> {}](size_t i) -> ErrorOr<void> { |
1475 | 0 | auto& ends = m_current_spans.m_spans_ending.get(i).value_or<>(empty_styles); |
1476 | 0 | auto& starts = m_current_spans.m_spans_starting.get(i).value_or<>(empty_styles); |
1477 | |
|
1478 | 0 | auto& anchored_ends = m_current_spans.m_anchored_spans_ending.get(i).value_or<>(empty_styles); |
1479 | 0 | auto& anchored_starts = m_current_spans.m_anchored_spans_starting.get(i).value_or<>(empty_styles); |
1480 | |
|
1481 | 0 | if (ends.size() || anchored_ends.size()) { |
1482 | 0 | Style style; |
1483 | |
|
1484 | 0 | for (auto& applicable_style : ends) |
1485 | 0 | style.unify_with(applicable_style.value); |
1486 | |
|
1487 | 0 | for (auto& applicable_style : anchored_ends) |
1488 | 0 | style.unify_with(applicable_style.value); |
1489 | | |
1490 | | // Disable any style that should be turned off. |
1491 | 0 | TRY(VT::apply_style(style, output_stream, false)); |
1492 | | |
1493 | | // Reapply styles for overlapping spans that include this one. |
1494 | 0 | style = find_applicable_style(i); |
1495 | 0 | TRY(VT::apply_style(style, output_stream, true)); |
1496 | 0 | } |
1497 | 0 | if (starts.size() || anchored_starts.size()) { |
1498 | 0 | Style style; |
1499 | |
|
1500 | 0 | for (auto& applicable_style : starts) |
1501 | 0 | style.unify_with(applicable_style.value); |
1502 | |
|
1503 | 0 | for (auto& applicable_style : anchored_starts) |
1504 | 0 | style.unify_with(applicable_style.value); |
1505 | | |
1506 | | // Set new styles. |
1507 | 0 | TRY(VT::apply_style(style, output_stream, true)); |
1508 | 0 | } |
1509 | |
|
1510 | 0 | return {}; |
1511 | 0 | }; |
1512 | |
|
1513 | 0 | auto print_character_at = [&](size_t i) { |
1514 | 0 | Variant<u32, Utf8View> c { Utf8View {} }; |
1515 | 0 | if (auto it = m_current_masks.find_largest_not_above_iterator(i); !it.is_end() && it->has_value()) { |
1516 | 0 | auto offset = i - it.key(); |
1517 | 0 | if (it->value().mode == Style::Mask::Mode::ReplaceEntireSelection) { |
1518 | 0 | auto& mask = it->value().replacement_view; |
1519 | 0 | auto replacement = mask.begin().peek(offset); |
1520 | 0 | if (!replacement.has_value()) |
1521 | 0 | return; |
1522 | 0 | c = replacement.value(); |
1523 | 0 | ++it; |
1524 | 0 | u32 next_offset = it.is_end() ? m_drawn_end_of_line_offset : it.key(); |
1525 | 0 | if (i + 1 == next_offset) |
1526 | 0 | c = mask.unicode_substring_view(offset, mask.length() - offset); |
1527 | 0 | } else { |
1528 | 0 | c = it->value().replacement_view; |
1529 | 0 | } |
1530 | 0 | } else { |
1531 | 0 | c = m_buffer[i]; |
1532 | 0 | } |
1533 | 0 | auto print_single_character = [&](auto c) -> ErrorOr<void> { |
1534 | 0 | StringBuilder builder; |
1535 | 0 | bool should_print_masked = is_ascii_control(c) && c != '\n'; |
1536 | 0 | bool should_print_caret = c < 64 && should_print_masked; |
1537 | 0 | if (should_print_caret) |
1538 | 0 | builder.appendff("^{:c}", c + 64); |
1539 | 0 | else if (should_print_masked) |
1540 | 0 | builder.appendff("\\x{:0>2x}", c); |
1541 | 0 | else |
1542 | 0 | builder.append(Utf32View { &c, 1 }); |
1543 | |
|
1544 | 0 | if (should_print_masked) |
1545 | 0 | TRY(output_stream.write_until_depleted("\033[7m"sv.bytes())); |
1546 | |
|
1547 | 0 | TRY(output_stream.write_until_depleted(builder.string_view().bytes())); |
1548 | |
|
1549 | 0 | if (should_print_masked) |
1550 | 0 | TRY(output_stream.write_until_depleted("\033[27m"sv.bytes())); |
1551 | |
|
1552 | 0 | return {}; |
1553 | 0 | }; |
1554 | 0 | c.visit( |
1555 | 0 | [&](u32 c) { print_single_character(c).release_value_but_fixme_should_propagate_errors(); }, |
1556 | 0 | [&](auto& view) { for (auto c : view) print_single_character(c).release_value_but_fixme_should_propagate_errors(); }); |
1557 | 0 | }; |
1558 | | |
1559 | | // If there have been no changes to previous sections of the line (style or text) |
1560 | | // just append the new text with the appropriate styles. |
1561 | 0 | if (!m_always_refresh && m_cached_prompt_valid && m_chars_touched_in_the_middle == 0 && m_drawn_spans.contains_up_to_offset(m_current_spans, m_drawn_cursor)) { |
1562 | 0 | auto initial_style = find_applicable_style(m_drawn_end_of_line_offset); |
1563 | 0 | TRY(VT::apply_style(initial_style, output_stream)); |
1564 | |
|
1565 | 0 | for (size_t i = m_drawn_end_of_line_offset; i < m_buffer.size(); ++i) { |
1566 | 0 | TRY(apply_styles(i)); |
1567 | 0 | print_character_at(i); |
1568 | 0 | } |
1569 | |
|
1570 | 0 | TRY(VT::apply_style(Style::reset_style(), output_stream)); |
1571 | 0 | m_pending_chars.clear(); |
1572 | 0 | m_refresh_needed = false; |
1573 | 0 | m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); |
1574 | 0 | m_chars_touched_in_the_middle = 0; |
1575 | 0 | m_drawn_cursor = m_cursor; |
1576 | 0 | m_drawn_end_of_line_offset = m_buffer.size(); |
1577 | | |
1578 | | // No need to reposition the cursor, the cursor is already where it needs to be. |
1579 | 0 | return {}; |
1580 | 0 | } |
1581 | | |
1582 | | if constexpr (LINE_EDITOR_DEBUG) { |
1583 | | if (m_cached_prompt_valid && m_chars_touched_in_the_middle == 0) { |
1584 | | auto x = m_drawn_spans.contains_up_to_offset(m_current_spans, m_drawn_cursor); |
1585 | | dbgln("Contains: {} At offset: {}", x, m_drawn_cursor); |
1586 | | dbgln("Drawn Spans:"); |
1587 | | for (auto& sentry : m_drawn_spans.m_spans_starting) { |
1588 | | for (auto& entry : sentry.value) { |
1589 | | dbgln("{}-{}: {}", sentry.key, entry.key, entry.value.to_byte_string()); |
1590 | | } |
1591 | | } |
1592 | | dbgln("=========================================================================="); |
1593 | | dbgln("Current Spans:"); |
1594 | | for (auto& sentry : m_current_spans.m_spans_starting) { |
1595 | | for (auto& entry : sentry.value) { |
1596 | | dbgln("{}-{}: {}", sentry.key, entry.key, entry.value.to_byte_string()); |
1597 | | } |
1598 | | } |
1599 | | } |
1600 | | } |
1601 | | |
1602 | | // Ouch, reflow entire line. |
1603 | 0 | if (!has_cleaned_up) { |
1604 | 0 | TRY(cleanup()); |
1605 | 0 | } |
1606 | 0 | TRY(VT::move_absolute(m_origin_row, m_origin_column, output_stream)); |
1607 | |
|
1608 | 0 | TRY(output_stream.write_until_depleted(m_new_prompt.bytes())); |
1609 | |
|
1610 | 0 | TRY(VT::clear_to_end_of_line(output_stream)); |
1611 | 0 | StringBuilder builder; |
1612 | 0 | for (size_t i = 0; i < m_buffer.size(); ++i) { |
1613 | 0 | TRY(apply_styles(i)); |
1614 | 0 | print_character_at(i); |
1615 | 0 | } |
1616 | |
|
1617 | 0 | TRY(VT::apply_style(Style::reset_style(), output_stream)); // don't bleed to EOL |
1618 | |
|
1619 | 0 | m_pending_chars.clear(); |
1620 | 0 | m_refresh_needed = false; |
1621 | 0 | m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); |
1622 | 0 | m_chars_touched_in_the_middle = 0; |
1623 | 0 | m_drawn_spans = m_current_spans; |
1624 | 0 | m_drawn_end_of_line_offset = m_buffer.size(); |
1625 | 0 | m_cached_prompt_valid = true; |
1626 | |
|
1627 | 0 | TRY(reposition_cursor(output_stream)); |
1628 | 0 | return {}; |
1629 | 0 | } |
1630 | | |
1631 | | void Editor::strip_styles(bool strip_anchored) |
1632 | 0 | { |
1633 | 0 | m_current_spans.m_spans_starting.clear(); |
1634 | 0 | m_current_spans.m_spans_ending.clear(); |
1635 | 0 | m_current_masks.clear(); |
1636 | 0 | m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), {}); |
1637 | |
|
1638 | 0 | if (strip_anchored) { |
1639 | 0 | m_current_spans.m_anchored_spans_starting.clear(); |
1640 | 0 | m_current_spans.m_anchored_spans_ending.clear(); |
1641 | 0 | } |
1642 | |
|
1643 | 0 | m_refresh_needed = true; |
1644 | 0 | } |
1645 | | |
1646 | | ErrorOr<void> Editor::reposition_cursor(Stream& stream, bool to_end) |
1647 | 0 | { |
1648 | 0 | auto cursor = m_cursor; |
1649 | 0 | auto saved_cursor = m_cursor; |
1650 | 0 | if (to_end) |
1651 | 0 | cursor = m_buffer.size(); |
1652 | |
|
1653 | 0 | m_cursor = cursor; |
1654 | 0 | m_drawn_cursor = cursor; |
1655 | |
|
1656 | 0 | auto line = cursor_line() - 1; |
1657 | 0 | auto column = offset_in_line(); |
1658 | |
|
1659 | 0 | ensure_free_lines_from_origin(line); |
1660 | |
|
1661 | 0 | VERIFY(column + m_origin_column <= m_num_columns); |
1662 | 0 | TRY(VT::move_absolute(line + m_origin_row, column + m_origin_column, stream)); |
1663 | |
|
1664 | 0 | m_cursor = saved_cursor; |
1665 | 0 | return {}; |
1666 | 0 | } |
1667 | | |
1668 | | ErrorOr<void> VT::move_absolute(u32 row, u32 col, Stream& stream) |
1669 | 0 | { |
1670 | 0 | return stream.write_until_depleted(ByteString::formatted("\033[{};{}H", row, col)); |
1671 | 0 | } |
1672 | | |
1673 | | ErrorOr<void> VT::move_relative(int row, int col, Stream& stream) |
1674 | 0 | { |
1675 | 0 | char x_op = 'A', y_op = 'D'; |
1676 | |
|
1677 | 0 | if (row > 0) |
1678 | 0 | x_op = 'B'; |
1679 | 0 | else |
1680 | 0 | row = -row; |
1681 | 0 | if (col > 0) |
1682 | 0 | y_op = 'C'; |
1683 | 0 | else |
1684 | 0 | col = -col; |
1685 | |
|
1686 | 0 | if (row > 0) |
1687 | 0 | TRY(stream.write_until_depleted(ByteString::formatted("\033[{}{}", row, x_op))); |
1688 | 0 | if (col > 0) |
1689 | 0 | TRY(stream.write_until_depleted(ByteString::formatted("\033[{}{}", col, y_op))); |
1690 | |
|
1691 | 0 | return {}; |
1692 | 0 | } |
1693 | | |
1694 | | Style Editor::find_applicable_style(size_t offset) const |
1695 | 0 | { |
1696 | | // Walk through our styles and merge all that fit in the offset. |
1697 | 0 | auto style = Style::reset_style(); |
1698 | 0 | auto unify = [&](auto& entry) { |
1699 | 0 | if (entry.key >= offset) |
1700 | 0 | return; |
1701 | 0 | for (auto& style_value : entry.value) { |
1702 | 0 | if (style_value.key <= offset) |
1703 | 0 | return; |
1704 | 0 | style.unify_with(style_value.value, true); |
1705 | 0 | } |
1706 | 0 | }; |
1707 | |
|
1708 | 0 | for (auto& entry : m_current_spans.m_spans_starting) { |
1709 | 0 | unify(entry); |
1710 | 0 | } |
1711 | |
|
1712 | 0 | for (auto& entry : m_current_spans.m_anchored_spans_starting) { |
1713 | 0 | unify(entry); |
1714 | 0 | } |
1715 | |
|
1716 | 0 | return style; |
1717 | 0 | } |
1718 | | |
1719 | | ByteString Style::Background::to_vt_escape() const |
1720 | 0 | { |
1721 | 0 | if (is_default()) |
1722 | 0 | return ""; |
1723 | | |
1724 | 0 | if (m_is_rgb) { |
1725 | 0 | return ByteString::formatted("\e[48;2;{};{};{}m", m_rgb_color[0], m_rgb_color[1], m_rgb_color[2]); |
1726 | 0 | } else { |
1727 | 0 | return ByteString::formatted("\e[{}m", (u8)m_xterm_color + 40); |
1728 | 0 | } |
1729 | 0 | } |
1730 | | |
1731 | | ByteString Style::Foreground::to_vt_escape() const |
1732 | 0 | { |
1733 | 0 | if (is_default()) |
1734 | 0 | return ""; |
1735 | | |
1736 | 0 | if (m_is_rgb) { |
1737 | 0 | return ByteString::formatted("\e[38;2;{};{};{}m", m_rgb_color[0], m_rgb_color[1], m_rgb_color[2]); |
1738 | 0 | } else { |
1739 | 0 | return ByteString::formatted("\e[{}m", (u8)m_xterm_color + 30); |
1740 | 0 | } |
1741 | 0 | } |
1742 | | |
1743 | | ByteString Style::Hyperlink::to_vt_escape(bool starting) const |
1744 | 0 | { |
1745 | 0 | if (is_empty()) |
1746 | 0 | return ""; |
1747 | | |
1748 | 0 | return ByteString::formatted("\e]8;;{}\e\\", starting ? m_link : ByteString::empty()); |
1749 | 0 | } |
1750 | | |
1751 | | void Style::unify_with(Style const& other, bool prefer_other) |
1752 | 0 | { |
1753 | | // Unify colors. |
1754 | 0 | if (prefer_other || m_background.is_default()) |
1755 | 0 | m_background = other.background(); |
1756 | |
|
1757 | 0 | if (prefer_other || m_foreground.is_default()) |
1758 | 0 | m_foreground = other.foreground(); |
1759 | | |
1760 | | // Unify graphic renditions. |
1761 | 0 | if (other.bold()) |
1762 | 0 | set(Bold); |
1763 | |
|
1764 | 0 | if (other.italic()) |
1765 | 0 | set(Italic); |
1766 | |
|
1767 | 0 | if (other.underline()) |
1768 | 0 | set(Underline); |
1769 | | |
1770 | | // Unify links. |
1771 | 0 | if (prefer_other || m_hyperlink.is_empty()) |
1772 | 0 | m_hyperlink = other.hyperlink(); |
1773 | |
|
1774 | 0 | m_is_empty &= other.m_is_empty; |
1775 | 0 | } |
1776 | | |
1777 | | ByteString Style::to_byte_string() const |
1778 | 0 | { |
1779 | 0 | StringBuilder builder; |
1780 | 0 | builder.append("Style { "sv); |
1781 | |
|
1782 | 0 | if (!m_foreground.is_default()) { |
1783 | 0 | builder.append("Foreground("sv); |
1784 | 0 | if (m_foreground.m_is_rgb) { |
1785 | 0 | builder.join(", "sv, m_foreground.m_rgb_color); |
1786 | 0 | } else { |
1787 | 0 | builder.appendff("(XtermColor) {}", (int)m_foreground.m_xterm_color); |
1788 | 0 | } |
1789 | 0 | builder.append("), "sv); |
1790 | 0 | } |
1791 | |
|
1792 | 0 | if (!m_background.is_default()) { |
1793 | 0 | builder.append("Background("sv); |
1794 | 0 | if (m_background.m_is_rgb) { |
1795 | 0 | builder.join(' ', m_background.m_rgb_color); |
1796 | 0 | } else { |
1797 | 0 | builder.appendff("(XtermColor) {}", (int)m_background.m_xterm_color); |
1798 | 0 | } |
1799 | 0 | builder.append("), "sv); |
1800 | 0 | } |
1801 | |
|
1802 | 0 | if (bold()) |
1803 | 0 | builder.append("Bold, "sv); |
1804 | |
|
1805 | 0 | if (underline()) |
1806 | 0 | builder.append("Underline, "sv); |
1807 | |
|
1808 | 0 | if (italic()) |
1809 | 0 | builder.append("Italic, "sv); |
1810 | |
|
1811 | 0 | if (!m_hyperlink.is_empty()) |
1812 | 0 | builder.appendff("Hyperlink(\"{}\"), ", m_hyperlink.m_link); |
1813 | |
|
1814 | 0 | if (!m_mask.has_value()) { |
1815 | 0 | builder.appendff("Mask(\"{}\", {}), ", |
1816 | 0 | m_mask->replacement, |
1817 | 0 | m_mask->mode == Mask::Mode::ReplaceEntireSelection |
1818 | 0 | ? "ReplaceEntireSelection" |
1819 | 0 | : "ReplaceEachCodePointInSelection"); |
1820 | 0 | } |
1821 | |
|
1822 | 0 | builder.append('}'); |
1823 | |
|
1824 | 0 | return builder.to_byte_string(); |
1825 | 0 | } |
1826 | | |
1827 | | ErrorOr<void> VT::apply_style(Style const& style, Stream& stream, bool is_starting) |
1828 | 0 | { |
1829 | 0 | if (is_starting) { |
1830 | 0 | TRY(stream.write_until_depleted(ByteString::formatted("\033[{};{};{}m{}{}{}", |
1831 | 0 | style.bold() ? 1 : 22, |
1832 | 0 | style.underline() ? 4 : 24, |
1833 | 0 | style.italic() ? 3 : 23, |
1834 | 0 | style.background().to_vt_escape(), |
1835 | 0 | style.foreground().to_vt_escape(), |
1836 | 0 | style.hyperlink().to_vt_escape(true)))); |
1837 | 0 | } else { |
1838 | 0 | TRY(stream.write_until_depleted(style.hyperlink().to_vt_escape(false))); |
1839 | 0 | } |
1840 | |
|
1841 | 0 | return {}; |
1842 | 0 | } |
1843 | | |
1844 | | ErrorOr<void> VT::clear_lines(size_t count_above, size_t count_below, Stream& stream) |
1845 | 0 | { |
1846 | 0 | if (count_below + count_above == 0) { |
1847 | 0 | TRY(stream.write_until_depleted("\033[2K"sv)); |
1848 | 0 | } else { |
1849 | | // Go down count_below lines. |
1850 | 0 | if (count_below > 0) |
1851 | 0 | TRY(stream.write_until_depleted(ByteString::formatted("\033[{}B", count_below))); |
1852 | | // Then clear lines going upwards. |
1853 | 0 | for (size_t i = count_below + count_above; i > 0; --i) { |
1854 | 0 | TRY(stream.write_until_depleted("\033[2K"sv)); |
1855 | 0 | if (i != 1) |
1856 | 0 | TRY(stream.write_until_depleted("\033[A"sv)); |
1857 | 0 | } |
1858 | 0 | } |
1859 | |
|
1860 | 0 | return {}; |
1861 | 0 | } |
1862 | | |
1863 | | ErrorOr<void> VT::save_cursor(Stream& stream) |
1864 | 0 | { |
1865 | 0 | return stream.write_until_depleted("\033[s"sv); |
1866 | 0 | } |
1867 | | |
1868 | | ErrorOr<void> VT::restore_cursor(Stream& stream) |
1869 | 0 | { |
1870 | 0 | return stream.write_until_depleted("\033[u"sv); |
1871 | 0 | } |
1872 | | |
1873 | | ErrorOr<void> VT::clear_to_end_of_line(Stream& stream) |
1874 | 0 | { |
1875 | 0 | return stream.write_until_depleted("\033[K"sv); |
1876 | 0 | } |
1877 | | |
1878 | | enum VTState { |
1879 | | Free = 1, |
1880 | | Escape = 3, |
1881 | | Bracket = 5, |
1882 | | BracketArgsSemi = 7, |
1883 | | Title = 9, |
1884 | | URL = 11, |
1885 | | }; |
1886 | | static VTState actual_rendered_string_length_step(StringMetrics& metrics, size_t index, StringMetrics::LineMetrics& current_line, u32 c, u32 next_c, VTState state, Optional<Style::Mask> const& mask, Optional<size_t> const& maximum_line_width = {}, Optional<size_t&> last_return = {}); |
1887 | | |
1888 | | enum class MaskedSelectionDecision { |
1889 | | Skip, |
1890 | | Continue, |
1891 | | }; |
1892 | | static MaskedSelectionDecision resolve_masked_selection(Optional<Style::Mask>& mask, size_t& i, auto& mask_it, auto& view, auto& state, auto& metrics, auto& current_line) |
1893 | 0 | { |
1894 | 0 | if (mask.has_value() && mask->mode == Style::Mask::Mode::ReplaceEntireSelection) { |
1895 | 0 | ++mask_it; |
1896 | 0 | auto actual_end_offset = mask_it.is_end() ? view.length() : mask_it.key(); |
1897 | 0 | auto end_offset = min(actual_end_offset, view.length()); |
1898 | 0 | size_t j = 0; |
1899 | 0 | for (auto it = mask->replacement_view.begin(); it != mask->replacement_view.end(); ++it) { |
1900 | 0 | auto it_copy = it; |
1901 | 0 | ++it_copy; |
1902 | 0 | auto next_c = it_copy == mask->replacement_view.end() ? 0 : *it_copy; |
1903 | 0 | state = actual_rendered_string_length_step(metrics, j, current_line, *it, next_c, state, {}); |
1904 | 0 | ++j; |
1905 | 0 | if (j <= actual_end_offset - i && j + i >= view.length()) |
1906 | 0 | break; |
1907 | 0 | } |
1908 | 0 | current_line.masked_chars.empend(i, end_offset - i, j); |
1909 | 0 | i = end_offset; |
1910 | |
|
1911 | 0 | if (mask_it.is_end()) |
1912 | 0 | mask = {}; |
1913 | 0 | else |
1914 | 0 | mask = *mask_it; |
1915 | 0 | return MaskedSelectionDecision::Skip; |
1916 | 0 | } |
1917 | 0 | return MaskedSelectionDecision::Continue; |
1918 | 0 | } |
1919 | | |
1920 | | StringMetrics Editor::actual_rendered_string_metrics(StringView string, RedBlackTree<u32, Optional<Style::Mask>> const& masks, Optional<size_t> maximum_line_width) |
1921 | 0 | { |
1922 | 0 | Vector<u32> utf32_buffer; |
1923 | 0 | utf32_buffer.ensure_capacity(string.length()); |
1924 | 0 | for (auto c : Utf8View { string }) |
1925 | 0 | utf32_buffer.append(c); |
1926 | |
|
1927 | 0 | return actual_rendered_string_metrics(Utf32View { utf32_buffer.data(), utf32_buffer.size() }, masks, maximum_line_width); |
1928 | 0 | } |
1929 | | |
1930 | | StringMetrics Editor::actual_rendered_string_metrics(Utf32View const& view, RedBlackTree<u32, Optional<Style::Mask>> const& masks, Optional<size_t> maximum_line_width) |
1931 | 0 | { |
1932 | 0 | StringMetrics metrics; |
1933 | 0 | StringMetrics::LineMetrics current_line; |
1934 | 0 | VTState state { Free }; |
1935 | 0 | Optional<Style::Mask> mask; |
1936 | 0 | size_t last_return { 0 }; |
1937 | |
|
1938 | 0 | auto mask_it = masks.begin(); |
1939 | |
|
1940 | 0 | Vector<size_t> grapheme_breaks; |
1941 | 0 | Unicode::for_each_grapheme_segmentation_boundary(view, [&](size_t offset) -> IterationDecision { |
1942 | 0 | if (offset >= view.length()) |
1943 | 0 | return IterationDecision::Break; |
1944 | | |
1945 | 0 | grapheme_breaks.append(offset); |
1946 | 0 | return IterationDecision::Continue; |
1947 | 0 | }); |
1948 | | |
1949 | | // In case Unicode data isn't available, default to using code points as grapheme boundaries. |
1950 | 0 | if (grapheme_breaks.is_empty()) { |
1951 | 0 | for (size_t i = 0; i < view.length(); ++i) |
1952 | 0 | grapheme_breaks.append(i); |
1953 | 0 | } |
1954 | |
|
1955 | 0 | for (size_t break_index = 0; break_index < grapheme_breaks.size(); ++break_index) { |
1956 | 0 | auto i = grapheme_breaks[break_index]; |
1957 | 0 | if (!mask_it.is_end() && mask_it.key() <= i) |
1958 | 0 | mask = *mask_it; |
1959 | |
|
1960 | 0 | if (resolve_masked_selection(mask, i, mask_it, view, state, metrics, current_line) == MaskedSelectionDecision::Skip) { |
1961 | 0 | --i; |
1962 | 0 | binary_search(grapheme_breaks, i, &break_index); |
1963 | 0 | continue; |
1964 | 0 | } |
1965 | | |
1966 | 0 | auto next_grapheme_start = break_index + 1 < grapheme_breaks.size() ? grapheme_breaks[break_index + 1] : view.length(); |
1967 | 0 | auto next_c = break_index + 1 < grapheme_breaks.size() ? view.code_points()[grapheme_breaks[break_index + 1]] : 0; |
1968 | 0 | auto c = view[i]; |
1969 | 0 | state = actual_rendered_string_length_step(metrics, i, current_line, c, next_c, state, mask, maximum_line_width, last_return); |
1970 | |
|
1971 | 0 | for (size_t j = i + 1; j < next_grapheme_start; ++j) { |
1972 | | // Consume the rest of the code points in this grapheme cluster without updating the state; this is just to account for their length properly. |
1973 | 0 | current_line.length++; |
1974 | 0 | current_line.visible_length++; |
1975 | 0 | metrics.total_length++; |
1976 | 0 | if (current_line.bit_length.has_value()) |
1977 | 0 | current_line.bit_length.value() += code_point_length_in_utf8(view[j]); |
1978 | 0 | } |
1979 | |
|
1980 | 0 | if (!mask_it.is_end() && mask_it.key() <= i) { |
1981 | 0 | auto mask_it_peek = mask_it; |
1982 | 0 | ++mask_it_peek; |
1983 | 0 | if (!mask_it_peek.is_end() && mask_it_peek.key() > i) |
1984 | 0 | mask_it = mask_it_peek; |
1985 | 0 | } |
1986 | 0 | } |
1987 | |
|
1988 | 0 | metrics.line_metrics.append(current_line); |
1989 | |
|
1990 | 0 | for (auto& line : metrics.line_metrics) |
1991 | 0 | metrics.max_line_length = max(line.total_length(), metrics.max_line_length); |
1992 | |
|
1993 | 0 | metrics.grapheme_breaks = move(grapheme_breaks); |
1994 | |
|
1995 | 0 | return metrics; |
1996 | 0 | } |
1997 | | |
1998 | | VTState actual_rendered_string_length_step(StringMetrics& metrics, size_t index, StringMetrics::LineMetrics& current_line, u32 c, u32 next_c, VTState state, Optional<Style::Mask> const& mask, Optional<size_t> const& maximum_line_width, Optional<size_t&> last_return) |
1999 | 0 | { |
2000 | 0 | auto const save_line = [&metrics, ¤t_line, &last_return, &index]() { |
2001 | 0 | if (last_return.has_value()) { |
2002 | 0 | auto const last_index = index - 1; |
2003 | 0 | current_line.bit_length = last_index - *last_return + 1; |
2004 | 0 | last_return.value() = last_index + 1; |
2005 | 0 | } |
2006 | 0 | metrics.line_metrics.append(current_line); |
2007 | |
|
2008 | 0 | current_line.masked_chars = {}; |
2009 | 0 | current_line.length = 0; |
2010 | 0 | current_line.visible_length = 0; |
2011 | 0 | current_line.bit_length = {}; |
2012 | 0 | }; |
2013 | | |
2014 | | // FIXME: current_line.visible_length can go above maximum_line_width when using masks |
2015 | 0 | if (maximum_line_width.has_value() && current_line.visible_length >= maximum_line_width.value()) |
2016 | 0 | save_line(); |
2017 | |
|
2018 | 0 | ScopeGuard bit_length_update { [&last_return, ¤t_line, &index]() { |
2019 | 0 | if (last_return.has_value()) |
2020 | 0 | current_line.bit_length = index - *last_return + 1; |
2021 | 0 | } }; |
2022 | |
|
2023 | 0 | switch (state) { |
2024 | 0 | case Free: { |
2025 | 0 | if (c == '\x1b') { // escape |
2026 | 0 | return Escape; |
2027 | 0 | } |
2028 | 0 | if (c == '\r') { // carriage return |
2029 | 0 | current_line.masked_chars = {}; |
2030 | 0 | current_line.length = 0; |
2031 | 0 | current_line.visible_length = 0; |
2032 | 0 | if (!metrics.line_metrics.is_empty()) |
2033 | 0 | metrics.line_metrics.last() = { {}, 0 }; |
2034 | 0 | return state; |
2035 | 0 | } |
2036 | 0 | if (c == '\n') { // return |
2037 | 0 | save_line(); |
2038 | 0 | return state; |
2039 | 0 | } |
2040 | 0 | if (c == '\t') { |
2041 | | // Tabs are a special case, because their width is variable. |
2042 | 0 | ++current_line.length; |
2043 | 0 | current_line.visible_length += (8 - (current_line.visible_length % 8)); |
2044 | 0 | return state; |
2045 | 0 | } |
2046 | 0 | auto is_control = is_ascii_control(c); |
2047 | 0 | if (is_control) { |
2048 | 0 | if (mask.has_value()) |
2049 | 0 | current_line.masked_chars.append({ index, 1, mask->replacement_view.length() }); |
2050 | 0 | else |
2051 | 0 | current_line.masked_chars.append({ index, 1, c < 64 ? 2u : 4u }); // if the character cannot be represented as ^c, represent it as \xbb. |
2052 | 0 | } |
2053 | | // FIXME: This will not support anything sophisticated |
2054 | 0 | if (mask.has_value()) { |
2055 | 0 | current_line.length += mask->replacement_view.length(); |
2056 | 0 | current_line.visible_length += mask->replacement_view.length(); |
2057 | 0 | metrics.total_length += mask->replacement_view.length(); |
2058 | 0 | } else if (is_control) { |
2059 | 0 | current_line.length += current_line.masked_chars.last().masked_length; |
2060 | 0 | current_line.visible_length += current_line.masked_chars.last().masked_length; |
2061 | 0 | metrics.total_length += current_line.masked_chars.last().masked_length; |
2062 | 0 | } else { |
2063 | 0 | ++current_line.length; |
2064 | 0 | ++current_line.visible_length; |
2065 | 0 | ++metrics.total_length; |
2066 | 0 | } |
2067 | 0 | return state; |
2068 | 0 | } |
2069 | 0 | case Escape: |
2070 | 0 | if (c == ']') { |
2071 | 0 | if (next_c == '0') |
2072 | 0 | state = Title; |
2073 | 0 | if (next_c == '8') |
2074 | 0 | state = URL; |
2075 | 0 | return state; |
2076 | 0 | } |
2077 | 0 | if (c == '[') { |
2078 | 0 | return Bracket; |
2079 | 0 | } |
2080 | | // FIXME: This does not support non-VT (aside from set-title) escapes |
2081 | 0 | return state; |
2082 | 0 | case Bracket: |
2083 | 0 | if (is_ascii_digit(c)) { |
2084 | 0 | return BracketArgsSemi; |
2085 | 0 | } |
2086 | 0 | return state; |
2087 | 0 | case BracketArgsSemi: |
2088 | 0 | if (c == ';') { |
2089 | 0 | return Bracket; |
2090 | 0 | } |
2091 | 0 | if (!is_ascii_digit(c)) |
2092 | 0 | state = Free; |
2093 | 0 | return state; |
2094 | 0 | case Title: |
2095 | 0 | if (c == 7) |
2096 | 0 | state = Free; |
2097 | 0 | return state; |
2098 | 0 | case URL: |
2099 | 0 | if (c == '\\') |
2100 | 0 | state = Free; |
2101 | 0 | return state; |
2102 | 0 | } |
2103 | 0 | return state; |
2104 | 0 | } |
2105 | | |
2106 | | Result<Vector<size_t, 2>, Editor::Error> Editor::vt_dsr() |
2107 | 0 | { |
2108 | 0 | char buf[16]; |
2109 | | |
2110 | | // Read whatever junk there is before talking to the terminal |
2111 | | // and insert them later when we're reading user input. |
2112 | 0 | bool more_junk_to_read { false }; |
2113 | 0 | timeval timeout { 0, 0 }; |
2114 | 0 | fd_set readfds; |
2115 | 0 | FD_ZERO(&readfds); |
2116 | 0 | FD_SET(0, &readfds); |
2117 | |
|
2118 | 0 | do { |
2119 | 0 | more_junk_to_read = false; |
2120 | 0 | [[maybe_unused]] auto rc = select(1, &readfds, nullptr, nullptr, &timeout); |
2121 | 0 | if (FD_ISSET(0, &readfds)) { |
2122 | 0 | auto nread = read(0, buf, 16); |
2123 | 0 | if (nread < 0) { |
2124 | 0 | m_input_error = Error::ReadFailure; |
2125 | 0 | finish(); |
2126 | 0 | break; |
2127 | 0 | } |
2128 | | |
2129 | 0 | if (nread == 0) |
2130 | 0 | break; |
2131 | | |
2132 | 0 | m_incomplete_data.append(buf, nread); |
2133 | 0 | more_junk_to_read = true; |
2134 | 0 | } |
2135 | 0 | } while (more_junk_to_read); |
2136 | |
|
2137 | 0 | if (m_input_error.has_value()) |
2138 | 0 | return m_input_error.value(); |
2139 | | |
2140 | 0 | fputs("\033[6n\n", stderr); |
2141 | 0 | fflush(stderr); |
2142 | | |
2143 | | // Parse the DSR response |
2144 | | // it should be of the form .*\e[\d+;\d+R.* |
2145 | | // Anything not part of the response is just added to the incomplete data. |
2146 | 0 | enum { |
2147 | 0 | Free, |
2148 | 0 | SawEsc, |
2149 | 0 | SawBracket, |
2150 | 0 | InFirstCoordinate, |
2151 | 0 | SawSemicolon, |
2152 | 0 | InSecondCoordinate, |
2153 | 0 | SawR, |
2154 | 0 | } state { Free }; |
2155 | 0 | auto has_error = false; |
2156 | 0 | Vector<char, 4> coordinate_buffer; |
2157 | 0 | size_t row { 1 }, col { 1 }; |
2158 | |
|
2159 | 0 | do { |
2160 | 0 | char c; |
2161 | 0 | auto nread = read(0, &c, 1); |
2162 | 0 | if (nread < 0) { |
2163 | 0 | if (errno == 0 || errno == EINTR) { |
2164 | | // ???? |
2165 | 0 | continue; |
2166 | 0 | } |
2167 | 0 | dbgln("Error while reading DSR: {}", strerror(errno)); |
2168 | 0 | return Error::ReadFailure; |
2169 | 0 | } |
2170 | 0 | if (nread == 0) { |
2171 | 0 | dbgln("Terminal DSR issue; received no response"); |
2172 | 0 | return Error::Empty; |
2173 | 0 | } |
2174 | | |
2175 | 0 | switch (state) { |
2176 | 0 | case Free: |
2177 | 0 | if (c == '\x1b') { |
2178 | 0 | state = SawEsc; |
2179 | 0 | continue; |
2180 | 0 | } |
2181 | 0 | m_incomplete_data.append(c); |
2182 | 0 | continue; |
2183 | 0 | case SawEsc: |
2184 | 0 | if (c == '[') { |
2185 | 0 | state = SawBracket; |
2186 | 0 | continue; |
2187 | 0 | } |
2188 | 0 | m_incomplete_data.append(c); |
2189 | 0 | state = Free; |
2190 | 0 | continue; |
2191 | 0 | case SawBracket: |
2192 | 0 | if (is_ascii_digit(c)) { |
2193 | 0 | state = InFirstCoordinate; |
2194 | 0 | coordinate_buffer.clear_with_capacity(); |
2195 | 0 | coordinate_buffer.append(c); |
2196 | 0 | continue; |
2197 | 0 | } |
2198 | 0 | m_incomplete_data.append(c); |
2199 | 0 | state = Free; |
2200 | 0 | continue; |
2201 | 0 | case InFirstCoordinate: |
2202 | 0 | if (is_ascii_digit(c)) { |
2203 | 0 | coordinate_buffer.append(c); |
2204 | 0 | continue; |
2205 | 0 | } |
2206 | 0 | if (c == ';') { |
2207 | 0 | auto maybe_row = StringView { coordinate_buffer.data(), coordinate_buffer.size() }.to_number<unsigned>(); |
2208 | 0 | if (!maybe_row.has_value()) |
2209 | 0 | has_error = true; |
2210 | 0 | row = maybe_row.value_or(1u); |
2211 | 0 | coordinate_buffer.clear_with_capacity(); |
2212 | 0 | state = SawSemicolon; |
2213 | 0 | continue; |
2214 | 0 | } |
2215 | 0 | m_incomplete_data.append(c); |
2216 | 0 | state = Free; |
2217 | 0 | continue; |
2218 | 0 | case SawSemicolon: |
2219 | 0 | if (is_ascii_digit(c)) { |
2220 | 0 | state = InSecondCoordinate; |
2221 | 0 | coordinate_buffer.append(c); |
2222 | 0 | continue; |
2223 | 0 | } |
2224 | 0 | m_incomplete_data.append(c); |
2225 | 0 | state = Free; |
2226 | 0 | continue; |
2227 | 0 | case InSecondCoordinate: |
2228 | 0 | if (is_ascii_digit(c)) { |
2229 | 0 | coordinate_buffer.append(c); |
2230 | 0 | continue; |
2231 | 0 | } |
2232 | 0 | if (c == 'R') { |
2233 | 0 | auto maybe_column = StringView { coordinate_buffer.data(), coordinate_buffer.size() }.to_number<unsigned>(); |
2234 | 0 | if (!maybe_column.has_value()) |
2235 | 0 | has_error = true; |
2236 | 0 | col = maybe_column.value_or(1u); |
2237 | 0 | coordinate_buffer.clear_with_capacity(); |
2238 | 0 | state = SawR; |
2239 | 0 | continue; |
2240 | 0 | } |
2241 | 0 | m_incomplete_data.append(c); |
2242 | 0 | state = Free; |
2243 | 0 | continue; |
2244 | 0 | case SawR: |
2245 | 0 | m_incomplete_data.append(c); |
2246 | 0 | continue; |
2247 | 0 | default: |
2248 | 0 | VERIFY_NOT_REACHED(); |
2249 | 0 | } |
2250 | 0 | } while (state != SawR); |
2251 | | |
2252 | 0 | if (has_error) |
2253 | 0 | dbgln("Terminal DSR issue, couldn't parse DSR response"); |
2254 | 0 | return Vector<size_t, 2> { row, col }; |
2255 | 0 | } |
2256 | | |
2257 | | ByteString Editor::line(size_t up_to_index) const |
2258 | 0 | { |
2259 | 0 | StringBuilder builder; |
2260 | 0 | builder.append(Utf32View { m_buffer.data(), min(m_buffer.size(), up_to_index) }); |
2261 | 0 | return builder.to_byte_string(); |
2262 | 0 | } |
2263 | | |
2264 | | void Editor::remove_at_index(size_t index) |
2265 | 0 | { |
2266 | | // See if we have any anchored styles, and reposition them if needed. |
2267 | 0 | readjust_anchored_styles(index, ModificationKind::Removal); |
2268 | 0 | auto cp = m_buffer[index]; |
2269 | 0 | m_buffer.remove(index); |
2270 | 0 | if (cp == '\n') |
2271 | 0 | ++m_extra_forward_lines; |
2272 | 0 | ++m_chars_touched_in_the_middle; |
2273 | 0 | } |
2274 | | |
2275 | | void Editor::readjust_anchored_styles(size_t hint_index, ModificationKind modification) |
2276 | 0 | { |
2277 | 0 | struct Anchor { |
2278 | 0 | Span old_span; |
2279 | 0 | Span new_span; |
2280 | 0 | Style style; |
2281 | 0 | }; |
2282 | 0 | Vector<Anchor> anchors_to_relocate; |
2283 | 0 | auto index_shift = modification == ModificationKind::Insertion ? 1 : -1; |
2284 | 0 | auto forced_removal = modification == ModificationKind::ForcedOverlapRemoval; |
2285 | |
|
2286 | 0 | for (auto& start_entry : m_current_spans.m_anchored_spans_starting) { |
2287 | 0 | for (auto& end_entry : start_entry.value) { |
2288 | 0 | if (forced_removal) { |
2289 | 0 | if (start_entry.key <= hint_index && end_entry.key > hint_index) { |
2290 | | // Remove any overlapping regions. |
2291 | 0 | continue; |
2292 | 0 | } |
2293 | 0 | } |
2294 | 0 | if (start_entry.key >= hint_index) { |
2295 | 0 | if (start_entry.key == hint_index && end_entry.key == hint_index + 1 && modification == ModificationKind::Removal) { |
2296 | | // Remove the anchor, as all its text was wiped. |
2297 | 0 | continue; |
2298 | 0 | } |
2299 | | // Shift everything. |
2300 | 0 | anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key + index_shift, end_entry.key + index_shift, Span::Mode::CodepointOriented }, end_entry.value }); |
2301 | 0 | continue; |
2302 | 0 | } |
2303 | 0 | if (end_entry.key > hint_index) { |
2304 | | // Shift just the end. |
2305 | 0 | anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key, end_entry.key + index_shift, Span::Mode::CodepointOriented }, end_entry.value }); |
2306 | 0 | continue; |
2307 | 0 | } |
2308 | 0 | anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, end_entry.value }); |
2309 | 0 | } |
2310 | 0 | } |
2311 | |
|
2312 | 0 | m_current_spans.m_anchored_spans_ending.clear(); |
2313 | 0 | m_current_spans.m_anchored_spans_starting.clear(); |
2314 | | // Pass over the relocations and update the stale entries. |
2315 | 0 | for (auto& relocation : anchors_to_relocate) { |
2316 | 0 | stylize(relocation.new_span, relocation.style); |
2317 | 0 | } |
2318 | 0 | } |
2319 | | |
2320 | | size_t StringMetrics::lines_with_addition(StringMetrics const& offset, size_t column_width) const |
2321 | 0 | { |
2322 | 0 | size_t lines = 0; |
2323 | |
|
2324 | 0 | if (!line_metrics.is_empty()) { |
2325 | 0 | for (size_t i = 0; i < line_metrics.size() - 1; ++i) |
2326 | 0 | lines += (line_metrics[i].total_length() + column_width) / column_width; |
2327 | |
|
2328 | 0 | auto last = line_metrics.last().total_length(); |
2329 | 0 | last += offset.line_metrics.first().total_length(); |
2330 | 0 | lines += (last + column_width) / column_width; |
2331 | 0 | } |
2332 | |
|
2333 | 0 | for (size_t i = 1; i < offset.line_metrics.size(); ++i) |
2334 | 0 | lines += (offset.line_metrics[i].total_length() + column_width) / column_width; |
2335 | |
|
2336 | 0 | return lines; |
2337 | 0 | } |
2338 | | |
2339 | | size_t StringMetrics::offset_with_addition(StringMetrics const& offset, size_t column_width) const |
2340 | 0 | { |
2341 | 0 | if (offset.line_metrics.size() > 1) |
2342 | 0 | return offset.line_metrics.last().total_length() % column_width; |
2343 | | |
2344 | 0 | if (!line_metrics.is_empty()) { |
2345 | 0 | auto last = line_metrics.last().total_length(); |
2346 | 0 | last += offset.line_metrics.first().total_length(); |
2347 | 0 | return last % column_width; |
2348 | 0 | } |
2349 | | |
2350 | 0 | if (offset.line_metrics.is_empty()) |
2351 | 0 | return 0; |
2352 | | |
2353 | 0 | return offset.line_metrics.first().total_length() % column_width; |
2354 | 0 | } |
2355 | | |
2356 | | bool Editor::Spans::contains_up_to_offset(Spans const& other, size_t offset) const |
2357 | 0 | { |
2358 | 0 | auto compare = [&]<typename K, typename V>(HashMap<K, HashMap<K, V>> const& left, HashMap<K, HashMap<K, V>> const& right) -> bool { |
2359 | 0 | for (auto& entry : right) { |
2360 | 0 | if (entry.key > offset + 1) |
2361 | 0 | continue; |
2362 | | |
2363 | 0 | auto left_map_it = left.find(entry.key); |
2364 | 0 | if (left_map_it == left.end()) |
2365 | 0 | return false; |
2366 | | |
2367 | 0 | for (auto& left_entry : left_map_it->value) { |
2368 | 0 | auto value_it = entry.value.find(left_entry.key); |
2369 | 0 | if (value_it == entry.value.end()) { |
2370 | | // Might have the same thing with a longer span |
2371 | 0 | bool found = false; |
2372 | 0 | for (auto& possibly_longer_span_entry : entry.value) { |
2373 | 0 | if (possibly_longer_span_entry.key > left_entry.key && possibly_longer_span_entry.key > offset && left_entry.value == possibly_longer_span_entry.value) { |
2374 | 0 | found = true; |
2375 | 0 | break; |
2376 | 0 | } |
2377 | 0 | } |
2378 | 0 | if (found) |
2379 | 0 | continue; |
2380 | | if constexpr (LINE_EDITOR_DEBUG) { |
2381 | | dbgln("Compare for {}-{} failed, no entry", entry.key, left_entry.key); |
2382 | | for (auto& x : entry.value) |
2383 | | dbgln("Have: {}-{} = {}", entry.key, x.key, x.value.to_byte_string()); |
2384 | | } |
2385 | 0 | return false; |
2386 | 0 | } else if (value_it->value != left_entry.value) { |
2387 | 0 | dbgln_if(LINE_EDITOR_DEBUG, "Compare for {}-{} failed, different values: {} != {}", entry.key, left_entry.key, value_it->value.to_byte_string(), left_entry.value.to_byte_string()); |
2388 | 0 | return false; |
2389 | 0 | } |
2390 | 0 | } |
2391 | 0 | } |
2392 | | |
2393 | 0 | return true; |
2394 | 0 | }; |
2395 | |
|
2396 | 0 | return compare(m_spans_starting, other.m_spans_starting) |
2397 | 0 | && compare(m_anchored_spans_starting, other.m_anchored_spans_starting); |
2398 | 0 | } |
2399 | | |
2400 | | } |