aboutsummaryrefslogtreecommitdiff
path: root/src/tui/editor.hh
blob: 457b0ca1b2d648c7021bb7f924411c8f8954dd71 (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
/*
 * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
 * Copyright (C) 2026  Richard Qin
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#pragma once

#include "paint.hh"
#include "text.hh"

#include <chrono>
#include <optional>

namespace ricochet::tui::detail {

enum class EditorKind : std::uint8_t
{
  SingleLine,
  MultiLine,
  Password
};

struct EditorOptions
{
  EditorKind kind { EditorKind::SingleLine };
  // The owner supplies history; password and multiline editors never request it.
  bool history { false };
};

enum class EditorCommand : std::uint8_t
{
  Left,
  Right,
  Up,
  Down,
  LineStart,
  LineEnd,
  PreviousWord,
  NextWord,
  Backspace,
  Delete,
  KillToStart,
  KillToEnd,
  KillWord,
  Yank,
  BeginSelection,
  SelectAll,
  CancelSelection,
  Clear,
  Newline,
  Undo,
  Redo
};

// Enter/Tab/Escape are context decisions, never implicit submit/finish operations.
// The owner may translate Enter into Newline for a multiline field.
// Unhandled keys stay with the owning editor context, never fall through to Browse.
enum class EditorIntent : std::uint8_t
{
  Consumed,
  Unhandled,
  Enter,
  Tab,
  BackTab,
  Escape,
  Cancel,
  Menu,
  Redraw,
  HistoryPrevious,
  HistoryNext
};

// One editing session. Owners recreate it when changing fields; its text, undo
// history and kill buffer are never shared with another editor or a view snapshot.
class TextEditor
{
public:
  TextEditor( std::string value, EditorOptions options, const TextService& text, ScreenSize viewport );
  TextEditor( const TextEditor& ) = delete;
  TextEditor& operator=( const TextEditor& ) = delete;
  TextEditor( TextEditor&& ) = delete;
  TextEditor& operator=( TextEditor&& ) = delete;
  ~TextEditor() = default;

  // Business value for the owning field; rendering and copying use the methods below.
  const std::string& value() const;
  std::uint64_t revision() const;
  std::size_t cursor() const;
  ByteRange selection() const;
  std::optional<std::string> copy_selection();
  std::optional<std::chrono::steady_clock::time_point> prefix_deadline() const;

  void resize( ScreenSize viewport );
  void insert( std::u32string_view input );
  // Host history replacement is one undoable edit, not a new editing session.
  void replace_value( std::string value );
  void execute( EditorCommand command, bool extend = false );
  EditorIntent handle( const InputEvent& event,
                       std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now() );
  // Region-local mouse positioning, with extend=true while selecting/dragging.
  void place( CellPoint point, bool extend );
  void draw( Frame& frame, CellPoint origin ) const;

private:
  struct State
  {
    std::string value {};
    std::size_t cursor { 0 };
    std::optional<std::size_t> anchor {};
    bool selecting { false };
  };
  struct Point
  {
    std::size_t row { 0 };
    std::int64_t column { 0 };
  };
  struct Glyph
  {
    Point point {};
    int columns { 0 };
  };

  std::string normalize( std::string_view value ) const;
  std::size_t unit_index() const;
  std::size_t boundary( std::size_t index ) const;
  std::size_t line_start() const;
  std::size_t line_end() const;
  std::size_t nearest( Point point ) const;
  void analyze();
  void layout();
  void reveal();
  void move( std::size_t destination, bool extend, bool preserve_column );
  void replace( ByteRange range, std::string_view replacement );
  void kill( ByteRange range );
  void undo( bool redo );
  EditorIntent key( const Key& key, std::chrono::steady_clock::time_point now );

  EditorOptions options_;
  const TextService& text_;
  ScreenSize viewport_;
  State state_ {};
  TextAnalysis analysis_ {};
  std::vector<Glyph> glyphs_ {};
  std::vector<Point> carets_ {};
  std::size_t top_row_ { 0 };
  std::int64_t left_column_ { 0 };
  std::int64_t desired_column_ { 0 };
  std::vector<State> past_ {};
  std::vector<State> future_ {};
  std::string killed_ {};
  bool killing_ { false };
  std::uint64_t revision_ { 0 };
  std::optional<std::chrono::steady_clock::time_point> prefix_ {};
};

} // namespace ricochet::tui::detail