summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibGUI/VimEditingEngine.h
blob: a5ce881b1067dd72a16071e760eb027b33011388 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/*
 * Copyright (c) 2021, the SerenityOS developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#pragma once

#include <AK/Optional.h>
#include <LibCore/Object.h>
#include <LibGUI/EditingEngine.h>
#include <LibGUI/TextRange.h>

namespace GUI {

// Wrapper over TextPosition that makes it easier to move it around as a cursor,
// and to get the current line or character.
class VimCursor {
public:
    VimCursor(TextEditor& editor, TextPosition initial_position, bool forwards)
        : m_editor(editor)
        , m_position(initial_position)
        , m_forwards(forwards)
    {
    }

    void move_forwards();
    void move_backwards();

    // Move a single character in the current direction.
    void move();
    // Move a single character in reverse.
    void move_reverse();
    // Peek a single character in the current direction.
    u32 peek();
    // Peek a single character in reverse.
    u32 peek_reverse();
    // Get the character the cursor is currently on.
    u32 current_char();
    // Get the line the cursor is currently on.
    TextDocumentLine& current_line();
    // Get the current position.
    TextPosition& current_position() { return m_position; }

    // Did we hit the edge of the document?
    bool hit_edge() { return m_hit_edge; }
    // Will the next move cross a line boundary?
    bool will_cross_line_boundary();
    // Did we cross a line boundary?
    bool crossed_line_boundary() { return m_crossed_line_boundary; }
    // Are we on an empty line?
    bool on_empty_line();
    // Are we going forwards?
    bool forwards() { return m_forwards; }

private:
    TextEditor& m_editor;
    TextPosition m_position;
    bool m_forwards;

    u32 m_cached_char { 0 };

    bool m_hit_edge { false };
    bool m_crossed_line_boundary { false };
};

class VimMotion {
public:
    enum class Unit {
        // The motion isn't complete yet, or was invalid.
        Unknown,
        // Document. Anything non-negative is counted as G while anything else is gg.
        Document,
        // Lines.
        Line,
        // A sequence of letters, digits and underscores, or a sequence of other
        // non-blank characters separated by whitespace.
        Word,
        // A sequence of non-blank characters separated by whitespace.
        // This is how Vim separates w from W.
        WORD,
        // End of a word. This is basically the same as a word but it doesn't
        // trim the spaces at the end.
        EndOfWord,
        // End of a WORD.
        EndOfWORD,
        // Characters (or Unicode code points based on how pedantic you want to
        // get).
        Character,
        // Used for find-mode.
        Find
    };
    enum class FindMode {
        /// Find mode is not enabled.
        None,
        /// Finding until the given character.
        To,
        /// Finding through the given character.
        Find
    };

    void add_key_code(KeyCode key, bool ctrl, bool shift, bool alt);
    Optional<TextRange> get_range(class VimEditingEngine& engine, bool normalize_for_position = false);
    Optional<TextRange> get_repeat_range(class VimEditingEngine& engine, Unit, bool normalize_for_position = false);
    Optional<TextPosition> get_position(VimEditingEngine& engine, bool in_visual_mode = false);
    void reset();

    /// Returns whether the motion should consume the next character no matter what.
    /// Used for f and t motions.
    bool should_consume_next_character() { return m_should_consume_next_character; }
    bool is_complete() { return m_is_complete; }
    bool is_cancelled() { return m_is_complete && m_unit == Unit::Unknown; }
    Unit unit() { return m_unit; }
    int amount() { return m_amount; }

    // FIXME: come up with a better way to signal start/end of line than sentinels?
    static constexpr int START_OF_LINE = NumericLimits<int>::min();
    static constexpr int START_OF_NON_WHITESPACE = NumericLimits<int>::min() + 1;
    static constexpr int END_OF_LINE = NumericLimits<int>::max();

private:
    void calculate_document_range(TextEditor&);
    void calculate_line_range(TextEditor&, bool normalize_for_position);
    void calculate_word_range(VimCursor&, int amount, bool normalize_for_position);
    void calculate_WORD_range(VimCursor&, int amount, bool normalize_for_position);
    void calculate_character_range(VimCursor&, int amount, bool normalize_for_position);
    void calculate_find_range(VimCursor&, int amount);

    Unit m_unit { Unit::Unknown };
    int m_amount { 0 };
    bool m_is_complete { false };
    bool m_guirky_mode { false };
    bool m_should_consume_next_character { false };

    FindMode m_find_mode { FindMode::None };
    u32 m_next_character { 0 };

    size_t m_start_line { 0 };
    size_t m_start_column { 0 };
    size_t m_end_line { 0 };
    size_t m_end_column { 0 };
};

class VimEditingEngine final : public EditingEngine {

public:
    virtual CursorWidth cursor_width() const override;

    virtual bool on_key(KeyEvent const& event) override;

private:
    enum VimMode {
        Normal,
        Insert,
        Visual,
        VisualLine
    };

    enum YankType {
        Line,
        Selection
    };

    enum class Casing {
        Uppercase,
        Lowercase,
        Invertcase
    };

    VimMode m_vim_mode { VimMode::Normal };
    VimMotion m_motion;

    YankType m_yank_type {};
    DeprecatedString m_yank_buffer {};
    void yank(YankType);
    void yank(TextRange, YankType yank_type);
    void put_before();
    void put_after();

    TextPosition m_selection_start_position = {};
    void update_selection_on_cursor_move();
    void clamp_cursor_position();
    void clear_visual_mode_data();

    KeyCode m_previous_key {};
    void switch_to_normal_mode();
    void switch_to_insert_mode();
    void switch_to_visual_mode();
    void switch_to_visual_line_mode();
    void move_half_page_up();
    void move_half_page_down();
    void move_to_previous_empty_lines_block();
    void move_to_next_empty_lines_block();

    bool on_key_in_insert_mode(KeyEvent const& event);
    bool on_key_in_normal_mode(KeyEvent const& event);
    bool on_key_in_visual_mode(KeyEvent const& event);
    bool on_key_in_visual_line_mode(KeyEvent const& event);

    void casefold_selection(Casing);

    virtual EngineType engine_type() const override { return EngineType::Vim; }
};

}