textbringer 14 → 15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +197 -0
- data/lib/textbringer/buffer.rb +26 -4
- data/lib/textbringer/commands/buffers.rb +10 -1
- data/lib/textbringer/commands/isearch.rb +9 -3
- data/lib/textbringer/commands/ispell.rb +77 -54
- data/lib/textbringer/commands/misc.rb +4 -7
- data/lib/textbringer/commands/replace.rb +8 -2
- data/lib/textbringer/config.rb +1 -0
- data/lib/textbringer/global_minor_mode.rb +58 -0
- data/lib/textbringer/modes/completion_list_mode.rb +1 -4
- data/lib/textbringer/modes/transient_mark_mode.rb +101 -0
- data/lib/textbringer/utils.rb +8 -6
- data/lib/textbringer/version.rb +1 -1
- data/lib/textbringer/window.rb +4 -4
- data/lib/textbringer.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ad5b7d9a5cc2022075f802db23f24781dcb3be366a4ba3ee27015a66ada834d
|
|
4
|
+
data.tar.gz: 1933c54c72c4e189b4f0a6ba81d6186d7618c9eaaa3eccbe5b61925d8959c108
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44736e20de4174e865ad160107e48461e26d46cd495e74000138c724753dafaae65b5e530902e8f1ad1f38659e301cfe2fbec71208913f85d01af5e23b124bc5
|
|
7
|
+
data.tar.gz: b07e7c3533f540b88157c268d498f7ec6bd077db6fce0e3adb154acc02fb9d960cee0bfb23cd91342a6dce12f0c8af1083dac1434fc67c9fddc361bd03623ded
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Textbringer is an Emacs-like text editor written in Ruby. It is extensible by Ruby instead of Lisp and runs in the terminal using ncurses.
|
|
8
|
+
|
|
9
|
+
**Ruby Version**: Requires Ruby >= 3.2
|
|
10
|
+
|
|
11
|
+
## Development Commands
|
|
12
|
+
|
|
13
|
+
### Setup
|
|
14
|
+
```bash
|
|
15
|
+
bundle install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For ncursesw support (required for multibyte characters):
|
|
19
|
+
```bash
|
|
20
|
+
sudo apt-get install libncursesw5-dev
|
|
21
|
+
gem install curses
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Running the Editor
|
|
25
|
+
```bash
|
|
26
|
+
# Run the main executable
|
|
27
|
+
./exe/txtb
|
|
28
|
+
|
|
29
|
+
# Or after installation
|
|
30
|
+
txtb
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Testing
|
|
34
|
+
```bash
|
|
35
|
+
# Run all tests
|
|
36
|
+
bundle exec rake test
|
|
37
|
+
|
|
38
|
+
# Or simply (default task)
|
|
39
|
+
bundle exec rake
|
|
40
|
+
|
|
41
|
+
# On Ubuntu/Linux (for CI)
|
|
42
|
+
xvfb-run bundle exec rake test
|
|
43
|
+
|
|
44
|
+
# Run a single test file
|
|
45
|
+
ruby -Ilib:test test/textbringer/test_buffer.rb
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Build and Release
|
|
49
|
+
```bash
|
|
50
|
+
# Install gem locally
|
|
51
|
+
bundle exec rake install
|
|
52
|
+
|
|
53
|
+
# Bump version and create release
|
|
54
|
+
bundle exec rake bump
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
### Core Components
|
|
60
|
+
|
|
61
|
+
**Buffer** (`lib/textbringer/buffer.rb`)
|
|
62
|
+
- The fundamental text container, similar to Emacs buffers
|
|
63
|
+
- Uses a gap buffer implementation (GAP_SIZE = 256) for efficient text editing
|
|
64
|
+
- Supports undo/redo with UNDO_LIMIT = 1000
|
|
65
|
+
- Handles encoding detection (UTF-8, EUC-JP, Windows-31J) and file format conversion
|
|
66
|
+
- Manages marks, point (cursor position), and the kill ring
|
|
67
|
+
- Class methods maintain global buffer list (@@list, @@current, @@minibuffer)
|
|
68
|
+
|
|
69
|
+
**Window** (`lib/textbringer/window.rb`)
|
|
70
|
+
- Display abstraction using curses for terminal UI
|
|
71
|
+
- Multiple windows can display different buffers or the same buffer
|
|
72
|
+
- Window.current tracks the active window
|
|
73
|
+
- Echo area (@@echo_area) for messages and minibuffer input
|
|
74
|
+
- Manages cursor position and window splitting/deletion
|
|
75
|
+
|
|
76
|
+
**Controller** (`lib/textbringer/controller.rb`)
|
|
77
|
+
- The main event loop and command dispatcher
|
|
78
|
+
- Reads key sequences and dispatches to commands
|
|
79
|
+
- Handles prefix arguments, keyboard macros, and recursive editing
|
|
80
|
+
- Maintains command execution state (this_command, last_command)
|
|
81
|
+
- Pre/post command hooks for extensibility
|
|
82
|
+
|
|
83
|
+
**Mode** (`lib/textbringer/mode.rb`)
|
|
84
|
+
- Buffer modes define context-specific behavior and syntax highlighting
|
|
85
|
+
- Modes inherit from Mode class (FundamentalMode, ProgrammingMode, RubyMode, CMode, etc.)
|
|
86
|
+
- Each mode has its own keymap and syntax table
|
|
87
|
+
- `define_local_command` creates mode-specific commands
|
|
88
|
+
- Modes are automatically selected based on file_name_pattern or interpreter_name_pattern
|
|
89
|
+
|
|
90
|
+
**Keymap** (`lib/textbringer/keymap.rb`)
|
|
91
|
+
- Tree structure for key bindings (supports multi-stroke sequences)
|
|
92
|
+
- Uses `kbd()` function to parse Emacs-style key notation
|
|
93
|
+
- Key sequences can bind to commands (symbols) or nested keymaps
|
|
94
|
+
|
|
95
|
+
**Commands** (`lib/textbringer/commands.rb` and `lib/textbringer/commands/*.rb`)
|
|
96
|
+
- Commands are defined using `define_command(name, doc:)`
|
|
97
|
+
- Available as module functions in the Commands module
|
|
98
|
+
- Command groups: buffers, windows, files, isearch, replace, rectangle, etc.
|
|
99
|
+
- All commands accessible via Alt+x or key bindings
|
|
100
|
+
|
|
101
|
+
### Plugin System
|
|
102
|
+
|
|
103
|
+
Plugins are loaded from `~/.textbringer/plugins/` via `Plugin.load_plugins`. Examples:
|
|
104
|
+
- Mournmail (mail client)
|
|
105
|
+
- MedicineShield (Mastodon client)
|
|
106
|
+
- textbringer-presentation
|
|
107
|
+
- textbringer-ghost_text
|
|
108
|
+
|
|
109
|
+
### Configuration
|
|
110
|
+
|
|
111
|
+
User configuration is loaded from:
|
|
112
|
+
1. `~/.textbringer/init.rb` (loaded first)
|
|
113
|
+
2. `~/.textbringer.rb` (loaded after plugins)
|
|
114
|
+
|
|
115
|
+
Global configuration hash: `CONFIG` in `lib/textbringer/config.rb`
|
|
116
|
+
|
|
117
|
+
Key settings:
|
|
118
|
+
- `east_asian_ambiguous_width`: Character width (1 or 2)
|
|
119
|
+
- `tab_width`, `indent_tabs_mode`: Indentation
|
|
120
|
+
- `syntax_highlight`, `highlight_buffer_size_limit`: Syntax highlighting
|
|
121
|
+
- `default_input_method`: Input method for non-ASCII text
|
|
122
|
+
|
|
123
|
+
### Input Methods
|
|
124
|
+
|
|
125
|
+
Support for non-ASCII input:
|
|
126
|
+
- T-Code (`lib/textbringer/input_methods/t_code_input_method.rb`)
|
|
127
|
+
- Hiragana (`lib/textbringer/input_methods/hiragana_input_method.rb`)
|
|
128
|
+
- Hangul (`lib/textbringer/input_methods/hangul_input_method.rb`)
|
|
129
|
+
|
|
130
|
+
### Testing Infrastructure
|
|
131
|
+
|
|
132
|
+
Tests use `Test::Unit` with custom `Textbringer::TestCase` base class in `test/test_helper.rb`.
|
|
133
|
+
|
|
134
|
+
Key test helpers:
|
|
135
|
+
- `FakeController`: Test controller with `test_key_buffer` for simulating input
|
|
136
|
+
- `FakeCursesWindow`: Mock curses window for headless testing
|
|
137
|
+
- `push_keys(keys)`: Simulate keyboard input
|
|
138
|
+
- `mkcdtmpdir`: Create temporary directory for file tests
|
|
139
|
+
- `Window.setup_for_test`: Initialize test environment
|
|
140
|
+
|
|
141
|
+
Tests are organized mirroring lib structure: `test/textbringer/**/*`.
|
|
142
|
+
|
|
143
|
+
## Code Patterns
|
|
144
|
+
|
|
145
|
+
### Defining Commands
|
|
146
|
+
```ruby
|
|
147
|
+
define_command(:command_name, doc: "Description") do
|
|
148
|
+
# Command implementation
|
|
149
|
+
# Access current buffer: Buffer.current
|
|
150
|
+
# Get prefix arg: current_prefix_arg
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Mode-Specific Commands
|
|
155
|
+
```ruby
|
|
156
|
+
class MyMode < Mode
|
|
157
|
+
define_local_command(:my_command) do
|
|
158
|
+
# Mode-specific implementation
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Key Bindings
|
|
164
|
+
```ruby
|
|
165
|
+
GLOBAL_MAP.define_key("\C-x\C-f", :find_file)
|
|
166
|
+
MODE_MAP.define_key("C-c C-c", :compile)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Buffer Operations
|
|
170
|
+
- Always use `Buffer.current` to get the active buffer
|
|
171
|
+
- `@buffer.point` is the cursor position
|
|
172
|
+
- `@buffer.mark` for region operations
|
|
173
|
+
- `@buffer.insert(text)`, `@buffer.delete_char(n)` for modifications
|
|
174
|
+
- `@buffer.save_point` and `@buffer.goto_char(pos)` for navigation
|
|
175
|
+
|
|
176
|
+
### Window Management
|
|
177
|
+
- `Window.current` is the active window
|
|
178
|
+
- `Window.redisplay` updates the display
|
|
179
|
+
- `Window.echo_area` for messages
|
|
180
|
+
- `message(text)` to display in echo area
|
|
181
|
+
|
|
182
|
+
## File Organization
|
|
183
|
+
|
|
184
|
+
- `lib/textbringer.rb`: Main entry point, requires all components
|
|
185
|
+
- `lib/textbringer/commands/*.rb`: Command implementations by category
|
|
186
|
+
- `lib/textbringer/modes/*.rb`: Major and minor modes
|
|
187
|
+
- `lib/textbringer/faces/*.rb`: Syntax highlighting face definitions
|
|
188
|
+
- `exe/txtb`: Main executable
|
|
189
|
+
- `exe/tbclient`: Client for server mode
|
|
190
|
+
- `exe/tbtags`: Tag file generator
|
|
191
|
+
|
|
192
|
+
## Notes
|
|
193
|
+
|
|
194
|
+
- The editor is designed to mimic Emacs conventions and terminology
|
|
195
|
+
- Key sequences use Emacs notation: C- (Control), M- (Meta/Alt), S- (Shift)
|
|
196
|
+
- The codebase uses extensive metaprogramming for command registration and mode definition
|
|
197
|
+
- All user-facing text editing operations should go through Buffer methods to maintain undo/redo support
|
data/lib/textbringer/buffer.rb
CHANGED
|
@@ -10,7 +10,8 @@ module Textbringer
|
|
|
10
10
|
|
|
11
11
|
attr_accessor :mode, :keymap
|
|
12
12
|
attr_reader :name, :file_name, :file_encoding, :file_format, :point, :marks
|
|
13
|
-
attr_reader :current_line, :current_column, :visible_mark
|
|
13
|
+
attr_reader :current_line, :current_column, :visible_mark, :mark_active
|
|
14
|
+
attr_reader :last_match
|
|
14
15
|
attr_reader :input_method
|
|
15
16
|
|
|
16
17
|
GAP_SIZE = 256
|
|
@@ -253,8 +254,10 @@ module Textbringer
|
|
|
253
254
|
@save_point_level = 0
|
|
254
255
|
@match_offsets = []
|
|
255
256
|
@visible_mark = nil
|
|
257
|
+
@mark_active = false
|
|
256
258
|
@read_only = read_only
|
|
257
259
|
@callbacks = {}
|
|
260
|
+
@last_match = nil
|
|
258
261
|
@input_method = nil
|
|
259
262
|
end
|
|
260
263
|
|
|
@@ -945,6 +948,20 @@ module Textbringer
|
|
|
945
948
|
end
|
|
946
949
|
end
|
|
947
950
|
|
|
951
|
+
def activate_mark
|
|
952
|
+
@mark_active = true
|
|
953
|
+
set_visible_mark(@mark.location) if @mark
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def deactivate_mark
|
|
957
|
+
@mark_active = false
|
|
958
|
+
delete_visible_mark
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def mark_active?
|
|
962
|
+
@mark_active
|
|
963
|
+
end
|
|
964
|
+
|
|
948
965
|
def self.region_boundaries(s, e)
|
|
949
966
|
if s > e
|
|
950
967
|
[e, s]
|
|
@@ -1159,6 +1176,7 @@ module Textbringer
|
|
|
1159
1176
|
|
|
1160
1177
|
def byteindex(forward, re, pos)
|
|
1161
1178
|
@match_offsets = []
|
|
1179
|
+
@last_match = nil
|
|
1162
1180
|
method = forward ? :byteindex : :byterindex
|
|
1163
1181
|
adjust_gap(0, 0)
|
|
1164
1182
|
s = @contents.byteslice(@gap_end..-1)
|
|
@@ -1167,9 +1185,9 @@ module Textbringer
|
|
|
1167
1185
|
end
|
|
1168
1186
|
i = s.send(method, re, pos)
|
|
1169
1187
|
if i
|
|
1170
|
-
|
|
1171
|
-
(0 ..
|
|
1172
|
-
@match_offsets.push(
|
|
1188
|
+
@last_match = Regexp.last_match
|
|
1189
|
+
(0 .. @last_match.size - 1).each do |j|
|
|
1190
|
+
@match_offsets.push(@last_match.byteoffset(j))
|
|
1173
1191
|
end
|
|
1174
1192
|
i
|
|
1175
1193
|
else
|
|
@@ -1294,6 +1312,10 @@ module Textbringer
|
|
|
1294
1312
|
end
|
|
1295
1313
|
end
|
|
1296
1314
|
|
|
1315
|
+
def minor_mode_active?(mode_class)
|
|
1316
|
+
@minor_modes.any? { |mode| mode.instance_of?(mode_class) }
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1297
1319
|
def mode_names
|
|
1298
1320
|
names = []
|
|
1299
1321
|
names.push(mode&.name || 'None')
|
|
@@ -93,7 +93,12 @@ module Textbringer
|
|
|
93
93
|
|
|
94
94
|
define_command(:exchange_point_and_mark,
|
|
95
95
|
doc: "Exchange the positions of point and mark.") do
|
|
96
|
-
Buffer.current
|
|
96
|
+
buffer = Buffer.current
|
|
97
|
+
buffer.exchange_point_and_mark
|
|
98
|
+
# Activate mark if transient mark mode is enabled
|
|
99
|
+
if TransientMarkMode.enabled?
|
|
100
|
+
buffer.activate_mark
|
|
101
|
+
end
|
|
97
102
|
end
|
|
98
103
|
|
|
99
104
|
define_command(:copy_region,
|
|
@@ -147,6 +152,10 @@ module Textbringer
|
|
|
147
152
|
buffer.pop_to_mark
|
|
148
153
|
else
|
|
149
154
|
buffer.push_mark
|
|
155
|
+
# Activate mark if transient mark mode is enabled
|
|
156
|
+
if TransientMarkMode.enabled?
|
|
157
|
+
buffer.activate_mark
|
|
158
|
+
end
|
|
150
159
|
message("Mark set")
|
|
151
160
|
end
|
|
152
161
|
end
|
|
@@ -77,7 +77,10 @@ module Textbringer
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def isearch_done
|
|
80
|
-
|
|
80
|
+
# Don't delete visible_mark if mark is active (transient mark mode)
|
|
81
|
+
unless Buffer.current.mark_active?
|
|
82
|
+
Buffer.current.delete_visible_mark
|
|
83
|
+
end
|
|
81
84
|
Controller.current.overriding_map = nil
|
|
82
85
|
remove_hook(:pre_command_hook, :isearch_pre_command_hook)
|
|
83
86
|
ISEARCH_STATUS[:last_string] = ISEARCH_STATUS[:string]
|
|
@@ -154,8 +157,11 @@ module Textbringer
|
|
|
154
157
|
if Buffer.current != Buffer.minibuffer
|
|
155
158
|
message(isearch_prompt + ISEARCH_STATUS[:string], log: false)
|
|
156
159
|
end
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
# Don't update visible_mark if mark is already active (transient mark mode)
|
|
161
|
+
unless Buffer.current.mark_active?
|
|
162
|
+
Buffer.current.set_visible_mark(forward ? match_beginning(0) :
|
|
163
|
+
match_end(0))
|
|
164
|
+
end
|
|
159
165
|
goto_char(forward ? match_end(0) : match_beginning(0))
|
|
160
166
|
else
|
|
161
167
|
if Buffer.current != Buffer.minibuffer
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module Textbringer
|
|
6
7
|
module Commands
|
|
@@ -8,13 +9,17 @@ module Textbringer
|
|
|
8
9
|
def initialize
|
|
9
10
|
@personal_dictionary_modified = false
|
|
10
11
|
@stdin, @stdout, @stderr, @wait_thr =
|
|
11
|
-
Open3.popen3(
|
|
12
|
+
Open3.popen3(CONFIG[:ispell_command])
|
|
12
13
|
@stdout.gets # consume the banner
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def check_word(word)
|
|
16
17
|
send_command("^" + word)
|
|
17
18
|
result = @stdout.gets
|
|
19
|
+
if result.nil? || result == "\n"
|
|
20
|
+
# aspell can't handle word, which may contain multibyte characters
|
|
21
|
+
return [word, nil]
|
|
22
|
+
end
|
|
18
23
|
@stdout.gets
|
|
19
24
|
case result
|
|
20
25
|
when /\A&\s+([^\s]+)\s+\d+\s+\d+:\s+(.*)/
|
|
@@ -70,66 +75,54 @@ module Textbringer
|
|
|
70
75
|
ISPELL_MODE_MAP.define_key(?i, :ispell_insert)
|
|
71
76
|
ISPELL_MODE_MAP.define_key(" ", :ispell_skip)
|
|
72
77
|
ISPELL_MODE_MAP.define_key(?q, :ispell_quit)
|
|
78
|
+
ISPELL_MODE_MAP.define_key("\C-g", :ispell_quit)
|
|
73
79
|
|
|
74
80
|
ISPELL_STATUS = {}
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
_original, suggestions = ispell.check_word(word)
|
|
96
|
-
if suggestions.nil? || suggestions.empty?
|
|
97
|
-
message("#{word.inspect} is spelled correctly.")
|
|
98
|
-
else
|
|
99
|
-
s = read_from_minibuffer("Correct #{word} with: ",
|
|
100
|
-
completion_proc: ->(s) {
|
|
101
|
-
suggestions.grep(/^#{Regexp.quote(s)}/)
|
|
102
|
-
})
|
|
103
|
-
if s
|
|
104
|
-
buffer.composite_edit do
|
|
105
|
-
buffer.delete_region(start_pos, end_pos)
|
|
106
|
-
buffer.insert(s)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
ensure
|
|
111
|
-
ispell.close
|
|
112
|
-
end
|
|
113
|
-
end
|
|
82
|
+
URI_REGEXP = URI::RFC2396_PARSER.make_regexp(["http", "https", "ftp", "mailto"])
|
|
83
|
+
EMAIL_REGEXP = /
|
|
84
|
+
# local-part
|
|
85
|
+
( # dot-atom
|
|
86
|
+
(?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)
|
|
87
|
+
(\.\g<atom>)*
|
|
88
|
+
| # quoted-string
|
|
89
|
+
\"([\x20\x21\x23-\x5b\x5d-\x7e]
|
|
90
|
+
|\\[\x20-\x7e])*\"
|
|
91
|
+
)@
|
|
92
|
+
# domain
|
|
93
|
+
(?<sub_domain>[0-9a-z]([0-9a-z-]*[0-9a-z])?)
|
|
94
|
+
(\.\g<sub_domain>)*
|
|
95
|
+
/ix
|
|
96
|
+
ISPELL_WORD_REGEXP = /
|
|
97
|
+
(?<uri>#{URI_REGEXP})
|
|
98
|
+
| (?<email>#{EMAIL_REGEXP})
|
|
99
|
+
| (?<word>[[:alpha:]]+(?:'[[:alpha:]]+)*)
|
|
100
|
+
/x
|
|
114
101
|
|
|
115
102
|
define_command(:ispell_buffer) do |recursive_edit: false|
|
|
103
|
+
ISPELL_STATUS[:recursive_edit] = false
|
|
116
104
|
Buffer.current.beginning_of_buffer
|
|
117
105
|
ispell_mode
|
|
118
|
-
ispell_forward
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
if !ispell_forward
|
|
107
|
+
ISPELL_STATUS[:recursive_edit] = recursive_edit
|
|
108
|
+
if recursive_edit
|
|
109
|
+
recursive_edit()
|
|
110
|
+
end
|
|
122
111
|
end
|
|
123
112
|
end
|
|
124
113
|
|
|
125
114
|
def ispell_done
|
|
126
|
-
|
|
115
|
+
# Don't delete visible_mark if mark is active (transient mark mode)
|
|
116
|
+
unless Buffer.current.mark_active?
|
|
117
|
+
Buffer.current.delete_visible_mark
|
|
118
|
+
end
|
|
127
119
|
Controller.current.overriding_map = nil
|
|
128
120
|
ISPELL_STATUS[:ispell]&.close
|
|
129
121
|
ISPELL_STATUS[:ispell] = nil
|
|
130
122
|
if ISPELL_STATUS[:recursive_edit]
|
|
131
123
|
exit_recursive_edit
|
|
132
124
|
end
|
|
125
|
+
ISPELL_STATUS[:recursive_edit] = false
|
|
133
126
|
end
|
|
134
127
|
|
|
135
128
|
def ispell_mode
|
|
@@ -141,8 +134,15 @@ module Textbringer
|
|
|
141
134
|
buffer = Buffer.current
|
|
142
135
|
while buffer.re_search_forward(ISPELL_WORD_REGEXP, raise_error: false,
|
|
143
136
|
goto_beginning: true)
|
|
137
|
+
if buffer.last_match[:word].nil?
|
|
138
|
+
buffer.goto_char(buffer.match_end(0))
|
|
139
|
+
next
|
|
140
|
+
end
|
|
144
141
|
ispell_beginning = buffer.point
|
|
145
|
-
|
|
142
|
+
# Don't update visible_mark if mark is already active (transient mark mode)
|
|
143
|
+
unless buffer.mark_active?
|
|
144
|
+
buffer.set_visible_mark
|
|
145
|
+
end
|
|
146
146
|
buffer.goto_char(buffer.match_end(0))
|
|
147
147
|
word = buffer.match_string(0)
|
|
148
148
|
_original, suggestions = ISPELL_STATUS[:ispell].check_word(word)
|
|
@@ -150,9 +150,9 @@ module Textbringer
|
|
|
150
150
|
ISPELL_STATUS[:beginning] = ispell_beginning
|
|
151
151
|
ISPELL_STATUS[:word] = word
|
|
152
152
|
ISPELL_STATUS[:suggestions] = suggestions
|
|
153
|
-
|
|
153
|
+
message_misspelled
|
|
154
154
|
recenter
|
|
155
|
-
return
|
|
155
|
+
return false
|
|
156
156
|
end
|
|
157
157
|
end
|
|
158
158
|
Controller.current.overriding_map = nil
|
|
@@ -162,17 +162,25 @@ module Textbringer
|
|
|
162
162
|
end
|
|
163
163
|
message("Finished spelling check.")
|
|
164
164
|
ispell_done
|
|
165
|
+
true
|
|
165
166
|
end
|
|
166
167
|
|
|
167
168
|
define_command(:ispell_replace) do
|
|
169
|
+
ensure_ispell_active
|
|
168
170
|
word = ISPELL_STATUS[:word]
|
|
169
171
|
suggestions = ISPELL_STATUS[:suggestions]
|
|
170
172
|
Controller.current.overriding_map = nil
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
begin
|
|
174
|
+
s = read_from_minibuffer("Correct #{word} with: ",
|
|
175
|
+
completion_proc: ->(s) {
|
|
176
|
+
suggestions.grep(/^#{Regexp.quote(s)}/)
|
|
177
|
+
})
|
|
178
|
+
rescue Quit
|
|
179
|
+
message_misspelled
|
|
180
|
+
return
|
|
181
|
+
ensure
|
|
182
|
+
Controller.current.overriding_map = ISPELL_MODE_MAP
|
|
183
|
+
end
|
|
176
184
|
if !s.empty?
|
|
177
185
|
buffer = Buffer.current
|
|
178
186
|
pos = buffer.point
|
|
@@ -186,28 +194,43 @@ module Textbringer
|
|
|
186
194
|
end
|
|
187
195
|
|
|
188
196
|
define_command(:ispell_accept) do
|
|
197
|
+
ensure_ispell_active
|
|
189
198
|
ISPELL_STATUS[:ispell].add_to_session_dictionary(ISPELL_STATUS[:word])
|
|
190
199
|
ispell_forward
|
|
191
200
|
end
|
|
192
201
|
|
|
193
202
|
define_command(:ispell_insert) do
|
|
203
|
+
ensure_ispell_active
|
|
194
204
|
ISPELL_STATUS[:ispell].add_to_personal_dictionary(ISPELL_STATUS[:word])
|
|
195
205
|
ispell_forward
|
|
196
206
|
end
|
|
197
207
|
|
|
198
208
|
define_command(:ispell_skip) do
|
|
209
|
+
ensure_ispell_active
|
|
199
210
|
ispell_forward
|
|
200
211
|
end
|
|
201
212
|
|
|
202
213
|
define_command(:ispell_quit) do
|
|
214
|
+
ensure_ispell_active
|
|
203
215
|
message("Quitting spell check.")
|
|
204
216
|
ispell_done
|
|
205
217
|
end
|
|
206
218
|
|
|
207
219
|
define_command(:ispell_unknown_command) do
|
|
208
|
-
|
|
209
|
-
|
|
220
|
+
ensure_ispell_active
|
|
221
|
+
message_misspelled
|
|
210
222
|
Window.beep
|
|
211
223
|
end
|
|
224
|
+
|
|
225
|
+
def message_misspelled
|
|
226
|
+
word = ISPELL_STATUS[:word]
|
|
227
|
+
message("Misspelled: #{word} [r]eplace, [a]ccept, [i]nsert, [SPC] to skip, [q]uit")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def ensure_ispell_active
|
|
231
|
+
if ISPELL_STATUS[:ispell].nil?
|
|
232
|
+
raise EditorError, "ispell is not active"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
212
235
|
end
|
|
213
236
|
end
|
|
@@ -82,10 +82,9 @@ module Textbringer
|
|
|
82
82
|
|
|
83
83
|
def update_completions(xs)
|
|
84
84
|
if xs.size > 1
|
|
85
|
-
if COMPLETION[:
|
|
85
|
+
if COMPLETION[:completions_window].nil?
|
|
86
|
+
Window.list.last.split
|
|
86
87
|
COMPLETION[:completions_window] = Window.list.last
|
|
87
|
-
COMPLETION[:original_buffer] =
|
|
88
|
-
COMPLETION[:completions_window].buffer
|
|
89
88
|
end
|
|
90
89
|
completions = Buffer.find_or_new("*Completions*", undo_limit: 0)
|
|
91
90
|
if !completions.mode.is_a?(CompletionListMode)
|
|
@@ -97,15 +96,13 @@ module Textbringer
|
|
|
97
96
|
xs.each do |x|
|
|
98
97
|
completions.insert(x + "\n")
|
|
99
98
|
end
|
|
99
|
+
completions.beginning_of_buffer
|
|
100
100
|
COMPLETION[:completions_window].buffer = completions
|
|
101
101
|
ensure
|
|
102
102
|
completions.read_only = true
|
|
103
103
|
end
|
|
104
104
|
else
|
|
105
|
-
|
|
106
|
-
COMPLETION[:completions_window].buffer =
|
|
107
|
-
COMPLETION[:original_buffer]
|
|
108
|
-
end
|
|
105
|
+
delete_completions_window
|
|
109
106
|
end
|
|
110
107
|
end
|
|
111
108
|
private :update_completions
|
|
@@ -42,7 +42,10 @@ module Textbringer
|
|
|
42
42
|
loop do
|
|
43
43
|
re_search_forward(regexp)
|
|
44
44
|
Window.current.recenter_if_needed
|
|
45
|
-
|
|
45
|
+
# Don't update visible_mark if mark is already active (transient mark mode)
|
|
46
|
+
unless Buffer.current.mark_active?
|
|
47
|
+
Buffer.current.set_visible_mark(match_beginning(0))
|
|
48
|
+
end
|
|
46
49
|
begin
|
|
47
50
|
Window.redisplay
|
|
48
51
|
c = read_single_char("Replace?", [?y, ?n, ?!, ?q, ?.])
|
|
@@ -66,7 +69,10 @@ module Textbringer
|
|
|
66
69
|
break
|
|
67
70
|
end
|
|
68
71
|
ensure
|
|
69
|
-
|
|
72
|
+
# Don't delete visible_mark if mark is active (transient mark mode)
|
|
73
|
+
unless Buffer.current.mark_active?
|
|
74
|
+
Buffer.current.delete_visible_mark
|
|
75
|
+
end
|
|
70
76
|
end
|
|
71
77
|
end
|
|
72
78
|
rescue SearchError
|
data/lib/textbringer/config.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Textbringer
|
|
|
15
15
|
shell_file_name: ENV["SHELL"],
|
|
16
16
|
shell_command_switch: "-c",
|
|
17
17
|
grep_command: "grep -nH -e",
|
|
18
|
+
ispell_command: "aspell -a",
|
|
18
19
|
fill_column: 70,
|
|
19
20
|
read_file_name_completion_ignore_case: RUBY_PLATFORM.match?(/darwin/),
|
|
20
21
|
default_input_method: "t_code"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
# Base class for global minor modes that affect all buffers.
|
|
3
|
+
# Unlike buffer-local MinorMode, global minor modes have a single on/off state.
|
|
4
|
+
class GlobalMinorMode
|
|
5
|
+
extend Commands
|
|
6
|
+
include Commands
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :mode_name
|
|
10
|
+
attr_accessor :command_name
|
|
11
|
+
|
|
12
|
+
def enabled=(val)
|
|
13
|
+
@enabled = val
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enabled? = @enabled
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.inherited(child)
|
|
20
|
+
# Initialize enabled to false immediately
|
|
21
|
+
child.instance_variable_set(:@enabled, false)
|
|
22
|
+
|
|
23
|
+
class_name = child.name
|
|
24
|
+
if class_name.nil? || class_name.empty?
|
|
25
|
+
raise ArgumentError, "GlobalMinorMode subclasses must be named classes (anonymous classes are not supported)"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
base_name = class_name.slice(/[^:]*\z/)
|
|
29
|
+
child.mode_name = base_name.sub(/Mode\z/, "")
|
|
30
|
+
command_name = base_name.sub(/\A[A-Z]/) { |s| s.downcase }.
|
|
31
|
+
gsub(/(?<=[a-z])([A-Z])/) {
|
|
32
|
+
"_" + $1.downcase
|
|
33
|
+
}
|
|
34
|
+
command = command_name.intern
|
|
35
|
+
child.command_name = command
|
|
36
|
+
|
|
37
|
+
# Define the toggle command
|
|
38
|
+
define_command(command) do
|
|
39
|
+
if child.enabled?
|
|
40
|
+
child.disable
|
|
41
|
+
child.enabled = false
|
|
42
|
+
else
|
|
43
|
+
child.enable
|
|
44
|
+
child.enabled = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Override these in subclasses
|
|
50
|
+
def self.enable
|
|
51
|
+
raise EditorError, "Subclass must implement enable"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.disable
|
|
55
|
+
raise EditorError, "Subclass must implement disable"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -24,10 +24,7 @@ module Textbringer
|
|
|
24
24
|
if s.size > 0
|
|
25
25
|
Window.current = Window.echo_area
|
|
26
26
|
complete_minibuffer_with_string(s)
|
|
27
|
-
|
|
28
|
-
COMPLETION[:completions_window].buffer =
|
|
29
|
-
COMPLETION[:original_buffer]
|
|
30
|
-
end
|
|
27
|
+
delete_completions_window
|
|
31
28
|
end
|
|
32
29
|
end
|
|
33
30
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module Textbringer
|
|
4
|
+
# Transient Mark Mode is a global minor mode that highlights
|
|
5
|
+
# the region between mark and point when the mark is active.
|
|
6
|
+
class TransientMarkMode < GlobalMinorMode
|
|
7
|
+
# Commands that should NOT deactivate the mark
|
|
8
|
+
MARK_PRESERVING_COMMANDS = [
|
|
9
|
+
:set_mark_command,
|
|
10
|
+
:exchange_point_and_mark,
|
|
11
|
+
:transient_mark_mode,
|
|
12
|
+
# Navigation commands that should preserve mark
|
|
13
|
+
:beginning_of_line,
|
|
14
|
+
:end_of_line,
|
|
15
|
+
:next_line,
|
|
16
|
+
:previous_line,
|
|
17
|
+
:forward_char,
|
|
18
|
+
:backward_char,
|
|
19
|
+
:forward_word,
|
|
20
|
+
:backward_word,
|
|
21
|
+
:scroll_up_command,
|
|
22
|
+
:scroll_down_command,
|
|
23
|
+
:beginning_of_buffer,
|
|
24
|
+
:end_of_buffer,
|
|
25
|
+
# Search commands
|
|
26
|
+
:isearch_forward,
|
|
27
|
+
:isearch_backward,
|
|
28
|
+
:isearch_repeat_forward,
|
|
29
|
+
:isearch_repeat_backward,
|
|
30
|
+
:isearch_printing_char,
|
|
31
|
+
# Undo/redo
|
|
32
|
+
:undo,
|
|
33
|
+
:redo_command,
|
|
34
|
+
# Other navigation
|
|
35
|
+
:goto_line,
|
|
36
|
+
:goto_char,
|
|
37
|
+
:recenter,
|
|
38
|
+
:move_to_beginning_of_line,
|
|
39
|
+
:move_to_end_of_line
|
|
40
|
+
].to_set.freeze
|
|
41
|
+
|
|
42
|
+
# Hook to deactivate mark before most commands
|
|
43
|
+
PRE_COMMAND_HOOK = -> {
|
|
44
|
+
buffer = Buffer.current
|
|
45
|
+
controller = Controller.current
|
|
46
|
+
|
|
47
|
+
return unless buffer.mark_active?
|
|
48
|
+
|
|
49
|
+
# Check if this command preserves the mark
|
|
50
|
+
command = controller.this_command
|
|
51
|
+
unless MARK_PRESERVING_COMMANDS.include?(command)
|
|
52
|
+
buffer.deactivate_mark
|
|
53
|
+
end
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Hook to update visible mark after commands
|
|
57
|
+
POST_COMMAND_HOOK = -> {
|
|
58
|
+
buffer = Buffer.current
|
|
59
|
+
|
|
60
|
+
# Skip if in isearch or ispell mode (they manage their own highlighting)
|
|
61
|
+
controller = Controller.current
|
|
62
|
+
if controller.overriding_map
|
|
63
|
+
return if Commands.const_defined?(:ISEARCH_MODE_MAP) &&
|
|
64
|
+
controller.overriding_map == Commands::ISEARCH_MODE_MAP
|
|
65
|
+
return if Commands.const_defined?(:ISPELL_MODE_MAP) &&
|
|
66
|
+
controller.overriding_map == Commands::ISPELL_MODE_MAP
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Update visible_mark to reflect mark_active state
|
|
70
|
+
if buffer.mark_active?
|
|
71
|
+
begin
|
|
72
|
+
mark = buffer.mark
|
|
73
|
+
buffer.set_visible_mark(mark.location) if mark
|
|
74
|
+
rescue EditorError
|
|
75
|
+
# Mark is not set, do nothing
|
|
76
|
+
end
|
|
77
|
+
elsif !buffer.mark_active? && buffer.visible_mark
|
|
78
|
+
buffer.delete_visible_mark
|
|
79
|
+
end
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def self.enable
|
|
83
|
+
# Add global hooks (not buffer-local)
|
|
84
|
+
add_hook(:pre_command_hook, PRE_COMMAND_HOOK, local: false)
|
|
85
|
+
add_hook(:post_command_hook, POST_COMMAND_HOOK, local: false)
|
|
86
|
+
message("Transient Mark mode enabled")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.disable
|
|
90
|
+
# Remove global hooks
|
|
91
|
+
remove_hook(:pre_command_hook, PRE_COMMAND_HOOK, local: false)
|
|
92
|
+
remove_hook(:post_command_hook, POST_COMMAND_HOOK, local: false)
|
|
93
|
+
|
|
94
|
+
# Deactivate mark in all buffers
|
|
95
|
+
Buffer.list.each do |buffer|
|
|
96
|
+
buffer.deactivate_mark
|
|
97
|
+
end
|
|
98
|
+
message("Transient Mark mode disabled")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/textbringer/utils.rb
CHANGED
|
@@ -134,7 +134,6 @@ module Textbringer
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
COMPLETION = {
|
|
137
|
-
original_buffer: nil,
|
|
138
137
|
completions_window: nil
|
|
139
138
|
}
|
|
140
139
|
|
|
@@ -186,11 +185,14 @@ module Textbringer
|
|
|
186
185
|
Buffer.minibuffer.keymap = old_minibuffer_map
|
|
187
186
|
Buffer.minibuffer.disable_input_method
|
|
188
187
|
Controller.current.current_prefix_arg = old_current_prefix_arg
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
delete_completions_window
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def delete_completions_window
|
|
193
|
+
if COMPLETION[:completions_window]
|
|
194
|
+
Window.delete_window(COMPLETION[:completions_window])
|
|
195
|
+
COMPLETION[:completions_window] = nil
|
|
194
196
|
end
|
|
195
197
|
end
|
|
196
198
|
|
data/lib/textbringer/version.rb
CHANGED
data/lib/textbringer/window.rb
CHANGED
|
@@ -405,7 +405,7 @@ module Textbringer
|
|
|
405
405
|
@window.erase
|
|
406
406
|
@window.setpos(0, 0)
|
|
407
407
|
@window.attrset(0)
|
|
408
|
-
if @buffer.visible_mark &&
|
|
408
|
+
if current? && @buffer.visible_mark &&
|
|
409
409
|
@buffer.point_after_mark?(@buffer.visible_mark)
|
|
410
410
|
@window.attron(Curses::A_REVERSE)
|
|
411
411
|
end
|
|
@@ -450,7 +450,7 @@ module Textbringer
|
|
|
450
450
|
break if newx == columns && cury == lines - 2
|
|
451
451
|
@buffer.forward_char
|
|
452
452
|
end
|
|
453
|
-
if @buffer.visible_mark
|
|
453
|
+
if current? && @buffer.visible_mark
|
|
454
454
|
@window.attroff(Curses::A_REVERSE)
|
|
455
455
|
end
|
|
456
456
|
@buffer.mark_to_point(@bottom_of_window)
|
|
@@ -679,7 +679,7 @@ module Textbringer
|
|
|
679
679
|
if @buffer.point_at_mark?(point)
|
|
680
680
|
@cursor.y = cury
|
|
681
681
|
@cursor.x = curx
|
|
682
|
-
if @buffer.visible_mark
|
|
682
|
+
if current? && @buffer.visible_mark
|
|
683
683
|
if @buffer.point_after_mark?(@buffer.visible_mark)
|
|
684
684
|
@window.attroff(Curses::A_REVERSE)
|
|
685
685
|
elsif @buffer.point_before_mark?(@buffer.visible_mark)
|
|
@@ -687,7 +687,7 @@ module Textbringer
|
|
|
687
687
|
end
|
|
688
688
|
end
|
|
689
689
|
end
|
|
690
|
-
if @buffer.visible_mark &&
|
|
690
|
+
if current? && @buffer.visible_mark &&
|
|
691
691
|
@buffer.point_at_mark?(@buffer.visible_mark)
|
|
692
692
|
if @buffer.point_after_mark?(point)
|
|
693
693
|
@window.attroff(Curses::A_REVERSE)
|
data/lib/textbringer.rb
CHANGED
|
@@ -37,7 +37,9 @@ require_relative "textbringer/modes/completion_list_mode"
|
|
|
37
37
|
require_relative "textbringer/modes/buffer_list_mode"
|
|
38
38
|
require_relative "textbringer/modes/help_mode"
|
|
39
39
|
require_relative "textbringer/minor_mode"
|
|
40
|
+
require_relative "textbringer/global_minor_mode"
|
|
40
41
|
require_relative "textbringer/modes/overwrite_mode"
|
|
42
|
+
require_relative "textbringer/modes/transient_mark_mode"
|
|
41
43
|
require_relative "textbringer/input_method"
|
|
42
44
|
require_relative "textbringer/input_methods/t_code_input_method"
|
|
43
45
|
require_relative "textbringer/input_methods/hiragana_input_method"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textbringer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '
|
|
4
|
+
version: '15'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shugo Maeda
|
|
@@ -329,6 +329,7 @@ extensions: []
|
|
|
329
329
|
extra_rdoc_files: []
|
|
330
330
|
files:
|
|
331
331
|
- CHANGES.md
|
|
332
|
+
- CLAUDE.md
|
|
332
333
|
- Gemfile
|
|
333
334
|
- LICENSE.txt
|
|
334
335
|
- README.ja.md
|
|
@@ -368,6 +369,7 @@ files:
|
|
|
368
369
|
- lib/textbringer/face.rb
|
|
369
370
|
- lib/textbringer/faces/basic.rb
|
|
370
371
|
- lib/textbringer/faces/programming.rb
|
|
372
|
+
- lib/textbringer/global_minor_mode.rb
|
|
371
373
|
- lib/textbringer/input_method.rb
|
|
372
374
|
- lib/textbringer/input_methods/hangul_input_method.rb
|
|
373
375
|
- lib/textbringer/input_methods/hiragana_input_method.rb
|
|
@@ -385,6 +387,7 @@ files:
|
|
|
385
387
|
- lib/textbringer/modes/overwrite_mode.rb
|
|
386
388
|
- lib/textbringer/modes/programming_mode.rb
|
|
387
389
|
- lib/textbringer/modes/ruby_mode.rb
|
|
390
|
+
- lib/textbringer/modes/transient_mark_mode.rb
|
|
388
391
|
- lib/textbringer/plugin.rb
|
|
389
392
|
- lib/textbringer/ring.rb
|
|
390
393
|
- lib/textbringer/utils.rb
|