Browse Source

LibGUI: Indent selected text on tab press

If selected text is less than a whole line, usual delete/replace takes
place. Otherwise, if the selected text is a whole line or spans
multiple lines, the selection will be indented.
huttongrabiel 3 năm trước cách đây
mục cha
commit
2fbaa7996c

+ 27 - 0
Userland/Libraries/LibGUI/TextDocument.cpp

@@ -932,6 +932,33 @@ String ReplaceAllTextCommand::action_text() const
     return m_action_text;
 }
 
+IndentSelection::IndentSelection(TextDocument& document, size_t tab_width, TextRange const& range)
+    : TextDocumentUndoCommand(document)
+    , m_tab_width(tab_width)
+    , m_range(range)
+{
+}
+
+void IndentSelection::redo()
+{
+    auto const tab = String::repeated(' ', m_tab_width);
+
+    for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
+        m_document.insert_at({ i, 0 }, tab, m_client);
+    }
+
+    m_document.set_all_cursors(m_range.start());
+}
+
+void IndentSelection::undo()
+{
+    for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
+        m_document.remove({ { i, 0 }, { i, m_tab_width } });
+    }
+
+    m_document.set_all_cursors(m_range.start());
+}
+
 TextPosition TextDocument::insert_at(TextPosition const& position, StringView text, Client const* client)
 {
     TextPosition cursor = position;

+ 12 - 0
Userland/Libraries/LibGUI/TextDocument.h

@@ -259,4 +259,16 @@ private:
     String m_action_text;
 };
 
+class IndentSelection : public TextDocumentUndoCommand {
+public:
+    IndentSelection(TextDocument&, size_t tab_width, TextRange const&);
+    virtual void undo() override;
+    virtual void redo() override;
+    TextRange const& range() const { return m_range; }
+
+private:
+    size_t m_tab_width { 0 };
+    TextRange m_range;
+};
+
 }

+ 39 - 2
Userland/Libraries/LibGUI/TextEditor.cpp

@@ -867,6 +867,15 @@ void TextEditor::keydown_event(KeyEvent& event)
         return;
     }
 
+    if (event.key() == KeyCode::Key_Tab) {
+        if (has_selection()) {
+            if (is_indenting_selection()) {
+                indent_selection();
+                return;
+            }
+        }
+    }
+
     if (event.key() == KeyCode::Key_Delete) {
         if (!is_editable())
             return;
@@ -952,6 +961,33 @@ void TextEditor::keydown_event(KeyEvent& event)
     event.ignore();
 }
 
+bool TextEditor::is_indenting_selection()
+{
+    auto const selection_start = m_selection.start() > m_selection.end() ? m_selection.end() : m_selection.start();
+    auto const selection_end = m_selection.end() > m_selection.start() ? m_selection.end() : m_selection.start();
+    auto const whole_line_selected = selection_end.column() - selection_start.column() >= current_line().length() - current_line().first_non_whitespace_column();
+    auto const on_same_line = selection_start.line() == selection_end.line();
+
+    if (has_selection() && (whole_line_selected || !on_same_line)) {
+        return true;
+    }
+
+    return false;
+}
+
+void TextEditor::indent_selection()
+{
+    auto const selection_start = m_selection.start() > m_selection.end() ? m_selection.end() : m_selection.start();
+    auto const selection_end = m_selection.end() > m_selection.start() ? m_selection.end() : m_selection.start();
+
+    if (is_indenting_selection()) {
+        execute<IndentSelection>(m_soft_tab_width, TextRange(selection_start, selection_end));
+        m_selection.set_start({ selection_start.line(), selection_start.column() + m_soft_tab_width });
+        m_selection.set_end({ selection_end.line(), selection_end.column() + m_soft_tab_width });
+        set_cursor({ m_cursor.line(), m_cursor.column() + m_soft_tab_width });
+    }
+}
+
 void TextEditor::delete_previous_word()
 {
     TextRange to_erase(document().first_word_before(m_cursor, true), m_cursor);
@@ -1444,7 +1480,7 @@ void TextEditor::insert_at_cursor_or_replace_selection(StringView text)
 {
     ReflowDeferrer defer(*this);
     VERIFY(is_editable());
-    if (has_selection())
+    if (has_selection() && !is_indenting_selection())
         delete_selection();
 
     // Check if adding a newline leaves the previous line as just whitespace.
@@ -1453,7 +1489,8 @@ void TextEditor::insert_at_cursor_or_replace_selection(StringView text)
         && clear_length > 0
         && current_line().leading_spaces() == clear_length;
 
-    execute<InsertTextCommand>(text, m_cursor);
+    if (!is_indenting_selection())
+        execute<InsertTextCommand>(text, m_cursor);
 
     if (should_clear_last_line) { // If it does leave just whitespace, clear it.
         auto const original_cursor_position = cursor();

+ 2 - 0
Userland/Libraries/LibGUI/TextEditor.h

@@ -151,6 +151,8 @@ public:
     void select_current_line();
     virtual void undo();
     virtual void redo();
+    bool is_indenting_selection();
+    void indent_selection();
 
     Function<void()> on_change;
     Function<void(bool modified)> on_modified_change;