Serenity Operating System
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#pragma once
9
10#include <AK/BinarySearch.h>
11#include <AK/ByteBuffer.h>
12#include <AK/DeprecatedString.h>
13#include <AK/Function.h>
14#include <AK/HashMap.h>
15#include <AK/OwnPtr.h>
16#include <AK/RedBlackTree.h>
17#include <AK/Result.h>
18#include <AK/Traits.h>
19#include <AK/Utf32View.h>
20#include <AK/Utf8View.h>
21#include <AK/Vector.h>
22#include <LibCore/DirIterator.h>
23#include <LibCore/EventLoop.h>
24#include <LibCore/Notifier.h>
25#include <LibCore/Object.h>
26#include <LibLine/KeyCallbackMachine.h>
27#include <LibLine/Span.h>
28#include <LibLine/StringMetrics.h>
29#include <LibLine/Style.h>
30#include <LibLine/SuggestionDisplay.h>
31#include <LibLine/SuggestionManager.h>
32#include <LibLine/VT.h>
33#include <sys/ioctl.h>
34#include <sys/stat.h>
35#include <termios.h>
36
37namespace Line {
38
39struct KeyBinding {
40 Vector<Key> keys;
41 enum class Kind {
42 InternalFunction,
43 Insertion,
44 } kind { Kind::InternalFunction };
45 DeprecatedString binding;
46};
47
48struct Configuration {
49 enum RefreshBehavior {
50 Lazy,
51 Eager,
52 };
53 enum OperationMode {
54 Unset,
55 Full,
56 NoEscapeSequences,
57 NonInteractive,
58 };
59 enum SignalHandler {
60 WithSignalHandlers,
61 NoSignalHandlers,
62 };
63
64 enum Flags : u32 {
65 None = 0,
66 BracketedPaste = 1,
67 };
68
69 struct DefaultTextEditor {
70 DeprecatedString command;
71 };
72
73 Configuration()
74 {
75 }
76
77 template<typename Arg, typename... Rest>
78 Configuration(Arg arg, Rest... rest)
79 : Configuration(rest...)
80 {
81 set(arg);
82 }
83
84 void set(RefreshBehavior refresh) { refresh_behavior = refresh; }
85 void set(OperationMode mode) { operation_mode = mode; }
86 void set(SignalHandler mode) { m_signal_mode = mode; }
87 void set(KeyBinding const& binding) { keybindings.append(binding); }
88 void set(DefaultTextEditor editor) { m_default_text_editor = move(editor.command); }
89 void set(Flags flags)
90 {
91 enable_bracketed_paste = flags & Flags::BracketedPaste;
92 }
93
94 static Configuration from_config(StringView libname = "line"sv);
95
96 RefreshBehavior refresh_behavior { RefreshBehavior::Lazy };
97 SignalHandler m_signal_mode { SignalHandler::WithSignalHandlers };
98 OperationMode operation_mode { OperationMode::Unset };
99 Vector<KeyBinding> keybindings;
100 DeprecatedString m_default_text_editor {};
101 bool enable_bracketed_paste { false };
102};
103
104#define ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(M) \
105 M(clear_screen) \
106 M(cursor_left_character) \
107 M(cursor_left_word) \
108 M(cursor_right_character) \
109 M(cursor_right_word) \
110 M(enter_search) \
111 M(erase_character_backwards) \
112 M(erase_character_forwards) \
113 M(erase_to_beginning) \
114 M(erase_to_end) \
115 M(erase_word_backwards) \
116 M(finish_edit) \
117 M(go_end) \
118 M(go_home) \
119 M(kill_line) \
120 M(search_backwards) \
121 M(search_forwards) \
122 M(transpose_characters) \
123 M(transpose_words) \
124 M(insert_last_words) \
125 M(erase_alnum_word_backwards) \
126 M(erase_alnum_word_forwards) \
127 M(capitalize_word) \
128 M(lowercase_word) \
129 M(uppercase_word) \
130 M(edit_in_external_editor)
131
132#define EDITOR_INTERNAL_FUNCTION(name) \
133 [](auto& editor) { editor.name(); return false; }
134
135class Editor : public Core::Object {
136 C_OBJECT(Editor);
137
138public:
139 enum class Error {
140 ReadFailure,
141 Empty,
142 Eof,
143 };
144
145 ~Editor();
146
147 Result<DeprecatedString, Error> get_line(DeprecatedString const& prompt);
148
149 void initialize();
150
151 void refetch_default_termios();
152
153 void add_to_history(DeprecatedString const& line);
154 bool load_history(DeprecatedString const& path);
155 bool save_history(DeprecatedString const& path);
156 auto const& history() const { return m_history; }
157 bool is_history_dirty() const { return m_history_dirty; }
158
159 void register_key_input_callback(KeyBinding const&);
160 void register_key_input_callback(Vector<Key> keys, Function<bool(Editor&)> callback) { m_callback_machine.register_key_input_callback(move(keys), move(callback)); }
161 void register_key_input_callback(Key key, Function<bool(Editor&)> callback) { register_key_input_callback(Vector<Key> { key }, move(callback)); }
162
163 static StringMetrics actual_rendered_string_metrics(StringView, RedBlackTree<u32, Optional<Style::Mask>> const& masks = {}, Optional<size_t> maximum_line_width = {});
164 static StringMetrics actual_rendered_string_metrics(Utf32View const&, RedBlackTree<u32, Optional<Style::Mask>> const& masks = {});
165
166 Function<Vector<CompletionSuggestion>(Editor const&)> on_tab_complete;
167 Function<void(Utf32View, Editor&)> on_paste;
168 Function<void()> on_interrupt_handled;
169 Function<void(Editor&)> on_display_refresh;
170
171 static Function<bool(Editor&)> find_internal_function(StringView name);
172 enum class CaseChangeOp {
173 Lowercase,
174 Uppercase,
175 Capital,
176 };
177 void case_change_word(CaseChangeOp);
178#define __ENUMERATE_EDITOR_INTERNAL_FUNCTION(name) \
179 void name();
180
181 ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(__ENUMERATE_EDITOR_INTERNAL_FUNCTION)
182
183#undef __ENUMERATE_EDITOR_INTERNAL_FUNCTION
184
185 ErrorOr<void> interrupted();
186 ErrorOr<void> resized();
187
188 size_t cursor() const { return m_cursor; }
189 void set_cursor(size_t cursor)
190 {
191 if (cursor > m_buffer.size())
192 cursor = m_buffer.size();
193 m_cursor = cursor;
194 }
195 Vector<u32, 1024> const& buffer() const { return m_buffer; }
196 u32 buffer_at(size_t pos) const { return m_buffer.at(pos); }
197 DeprecatedString line() const { return line(m_buffer.size()); }
198 DeprecatedString line(size_t up_to_index) const;
199
200 // Only makes sense inside a character_input callback or on_* callback.
201 void set_prompt(DeprecatedString const& prompt)
202 {
203 if (m_cached_prompt_valid)
204 m_old_prompt_metrics = m_cached_prompt_metrics;
205 m_cached_prompt_valid = false;
206 m_cached_prompt_metrics = actual_rendered_string_metrics(prompt, {});
207 m_new_prompt = prompt;
208 }
209
210 void clear_line();
211 void insert(DeprecatedString const&);
212 void insert(StringView);
213 void insert(Utf32View const&);
214 void insert(const u32);
215 void stylize(Span const&, Style const&);
216 void strip_styles(bool strip_anchored = false);
217
218 // Invariant Offset is an offset into the suggested data, hinting the editor what parts of the suggestion will not change
219 // Static Offset is an offset into the token, signifying where the suggestions start
220 // e.g.
221 // foobar<suggestion initiated>, on_tab_complete returns "barx", "bary", "barz"
222 // ^ ^
223 // +-|- static offset: the suggestions start here
224 // +- invariant offset: the suggestions do not change up to here
225 //
226 void transform_suggestion_offsets(size_t& invariant_offset, size_t& static_offset, Span::Mode offset_mode = Span::ByteOriented) const;
227
228 const struct termios& termios() const { return m_termios; }
229 const struct termios& default_termios() const { return m_default_termios; }
230 struct winsize terminal_size() const
231 {
232 winsize ws { (u16)m_num_lines, (u16)m_num_columns, 0, 0 };
233 return ws;
234 }
235
236 void finish()
237 {
238 m_finish = true;
239 }
240
241 bool is_editing() const { return m_is_editing; }
242
243 const Utf32View buffer_view() const { return { m_buffer.data(), m_buffer.size() }; }
244
245 auto prohibit_input()
246 {
247 auto previous_value = m_prohibit_input_processing;
248 m_prohibit_input_processing = true;
249 m_have_unprocessed_read_event = false;
250 return ScopeGuard {
251 [this, previous_value] {
252 m_prohibit_input_processing = previous_value;
253 if (!m_prohibit_input_processing && m_have_unprocessed_read_event)
254 handle_read_event().release_value_but_fixme_should_propagate_errors();
255 }
256 };
257 }
258
259private:
260 explicit Editor(Configuration configuration = Configuration::from_config());
261
262 void set_default_keybinds();
263
264 enum LoopExitCode {
265 Exit = 0,
266 Retry
267 };
268
269 // FIXME: Port to Core::Property
270 void save_to(JsonObject&);
271
272 ErrorOr<void> try_update_once();
273 void handle_interrupt_event();
274 ErrorOr<void> handle_read_event();
275 ErrorOr<void> handle_resize_event(bool reset_origin);
276
277 void ensure_free_lines_from_origin(size_t count);
278
279 Result<Vector<size_t, 2>, Error> vt_dsr();
280 void remove_at_index(size_t);
281
282 enum class ModificationKind {
283 Insertion,
284 Removal,
285 ForcedOverlapRemoval,
286 };
287 void readjust_anchored_styles(size_t hint_index, ModificationKind);
288
289 Style find_applicable_style(size_t offset) const;
290
291 bool search(StringView, bool allow_empty = false, bool from_beginning = true);
292 inline void end_search()
293 {
294 m_is_searching = false;
295 m_refresh_needed = true;
296 m_search_offset = 0;
297 if (m_reset_buffer_on_search_end) {
298 m_buffer.clear();
299 for (auto ch : m_pre_search_buffer)
300 m_buffer.append(ch);
301 m_cursor = m_pre_search_cursor;
302 }
303 m_reset_buffer_on_search_end = true;
304 m_search_editor = nullptr;
305 }
306
307 void reset()
308 {
309 m_cached_buffer_metrics.reset();
310 m_cached_prompt_valid = false;
311 m_cursor = 0;
312 m_drawn_cursor = 0;
313 m_inline_search_cursor = 0;
314 m_search_offset = 0;
315 m_search_offset_state = SearchOffsetState::Unbiased;
316 m_old_prompt_metrics = m_cached_prompt_metrics;
317 set_origin(0, 0);
318 m_prompt_lines_at_suggestion_initiation = 0;
319 m_refresh_needed = true;
320 m_input_error.clear();
321 m_returned_line = DeprecatedString::empty();
322 m_chars_touched_in_the_middle = 0;
323 m_drawn_end_of_line_offset = 0;
324 m_drawn_spans = {};
325 m_paste_buffer.clear_with_capacity();
326 }
327
328 ErrorOr<void> refresh_display();
329 ErrorOr<void> cleanup();
330 ErrorOr<void> cleanup_suggestions();
331 ErrorOr<void> really_quit_event_loop();
332
333 void restore()
334 {
335 VERIFY(m_initialized);
336 tcsetattr(0, TCSANOW, &m_default_termios);
337 m_initialized = false;
338 if (m_configuration.enable_bracketed_paste)
339 warn("\x1b[?2004l");
340 for (auto id : m_signal_handlers)
341 Core::EventLoop::unregister_signal(id);
342 }
343
344 StringMetrics const& current_prompt_metrics() const
345 {
346 return m_cached_prompt_valid ? m_cached_prompt_metrics : m_old_prompt_metrics;
347 }
348
349 size_t num_lines() const
350 {
351 return current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns);
352 }
353
354 size_t cursor_line() const
355 {
356 auto cursor = m_drawn_cursor;
357 if (cursor > m_cursor)
358 cursor = m_cursor;
359 return current_prompt_metrics().lines_with_addition(
360 actual_rendered_string_metrics(buffer_view().substring_view(0, cursor), m_current_masks),
361 m_num_columns);
362 }
363
364 size_t offset_in_line() const
365 {
366 auto cursor = m_drawn_cursor;
367 if (cursor > m_cursor)
368 cursor = m_cursor;
369 auto buffer_metrics = actual_rendered_string_metrics(buffer_view().substring_view(0, cursor), m_current_masks);
370 return current_prompt_metrics().offset_with_addition(buffer_metrics, m_num_columns);
371 }
372
373 bool set_origin(bool quit_on_error = true)
374 {
375 auto position = vt_dsr();
376 if (!position.is_error()) {
377 set_origin(position.value()[0], position.value()[1]);
378 return true;
379 }
380 if (quit_on_error && position.is_error()) {
381 m_input_error = position.error();
382 finish();
383 }
384 return false;
385 }
386
387 void set_origin(int row, int col)
388 {
389 m_origin_row = row;
390 m_origin_column = col;
391 m_suggestion_display->set_origin(row, col, {});
392 }
393
394 void recalculate_origin();
395 ErrorOr<void> reposition_cursor(Stream&, bool to_end = false);
396
397 struct CodepointRange {
398 size_t start { 0 };
399 size_t end { 0 };
400 };
401 CodepointRange byte_offset_range_to_code_point_offset_range(size_t byte_start, size_t byte_end, size_t code_point_scan_offset, bool reverse = false) const;
402
403 void get_terminal_size();
404
405 bool m_finish { false };
406
407 RefPtr<Editor> m_search_editor;
408 bool m_is_searching { false };
409 bool m_reset_buffer_on_search_end { true };
410 size_t m_search_offset { 0 };
411 enum class SearchOffsetState {
412 Unbiased,
413 Backwards,
414 Forwards,
415 } m_search_offset_state { SearchOffsetState::Unbiased };
416 size_t m_pre_search_cursor { 0 };
417 Vector<u32, 1024> m_pre_search_buffer;
418
419 Vector<u32, 1024> m_buffer;
420 ByteBuffer m_pending_chars;
421 Vector<char, 512> m_incomplete_data;
422 Optional<Error> m_input_error;
423 DeprecatedString m_returned_line;
424
425 size_t m_cursor { 0 };
426 size_t m_drawn_cursor { 0 };
427 size_t m_drawn_end_of_line_offset { 0 };
428 size_t m_inline_search_cursor { 0 };
429 size_t m_chars_touched_in_the_middle { 0 };
430 size_t m_times_tab_pressed { 0 };
431 size_t m_num_columns { 0 };
432 size_t m_num_lines { 1 };
433 size_t m_previous_num_columns { 0 };
434 size_t m_extra_forward_lines { 0 };
435 size_t m_shown_lines { 0 };
436 StringMetrics m_cached_prompt_metrics;
437 StringMetrics m_old_prompt_metrics;
438 StringMetrics m_cached_buffer_metrics;
439 size_t m_prompt_lines_at_suggestion_initiation { 0 };
440 bool m_cached_prompt_valid { false };
441
442 // Exact position before our prompt in the terminal.
443 size_t m_origin_row { 0 };
444 size_t m_origin_column { 0 };
445 bool m_has_origin_reset_scheduled { false };
446
447 OwnPtr<SuggestionDisplay> m_suggestion_display;
448 Vector<u32, 32> m_remembered_suggestion_static_data;
449
450 DeprecatedString m_new_prompt;
451
452 SuggestionManager m_suggestion_manager;
453
454 bool m_always_refresh { false };
455
456 enum class TabDirection {
457 Forward,
458 Backward,
459 };
460 TabDirection m_tab_direction { TabDirection::Forward };
461
462 KeyCallbackMachine m_callback_machine;
463
464 struct termios m_termios {
465 };
466 struct termios m_default_termios {
467 };
468 bool m_was_interrupted { false };
469 bool m_previous_interrupt_was_handled_as_interrupt { true };
470 bool m_was_resized { false };
471
472 // FIXME: This should be something more take_first()-friendly.
473 struct HistoryEntry {
474 DeprecatedString entry;
475 time_t timestamp;
476 };
477 Vector<HistoryEntry> m_history;
478 size_t m_history_cursor { 0 };
479 size_t m_history_capacity { 1024 };
480 bool m_history_dirty { false };
481
482 enum class InputState {
483 Free,
484 Verbatim,
485 Paste,
486 GotEscape,
487 CSIExpectParameter,
488 CSIExpectIntermediate,
489 CSIExpectFinal,
490 };
491 InputState m_state { InputState::Free };
492 InputState m_previous_free_state { InputState::Free };
493
494 struct Spans {
495 HashMap<u32, HashMap<u32, Style>> m_spans_starting;
496 HashMap<u32, HashMap<u32, Style>> m_spans_ending;
497 HashMap<u32, HashMap<u32, Style>> m_anchored_spans_starting;
498 HashMap<u32, HashMap<u32, Style>> m_anchored_spans_ending;
499
500 bool contains_up_to_offset(Spans const& other, size_t offset) const;
501 } m_drawn_spans, m_current_spans;
502
503 RedBlackTree<u32, Optional<Style::Mask>> m_current_masks;
504
505 RefPtr<Core::Notifier> m_notifier;
506
507 Vector<u32> m_paste_buffer;
508
509 bool m_initialized { false };
510 bool m_refresh_needed { false };
511 Vector<int, 2> m_signal_handlers;
512
513 bool m_is_editing { false };
514 bool m_prohibit_input_processing { false };
515 bool m_have_unprocessed_read_event { false };
516
517 Configuration m_configuration;
518};
519
520}