irb-autosuggestions 0.1.1 → 0.2.0
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/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +5 -4
- data/Gemfile_2_7 +6 -0
- data/Gemfile_2_7.lock +88 -0
- data/README.md +65 -3
- data/lib/irb/autosuggestions/line_editor_patch.rb +363 -49
- data/lib/irb/autosuggestions/version.rb +1 -1
- data/rbs_collection.lock.yaml +0 -4
- data/sig/lib/irb/autosuggestions/line_editor_patch.rbs +10 -2
- metadata +6 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48eb1b9edb63b8d65e26cb9917c5631ec58ccac72e05ab0c47efa7c2521e84c0
|
|
4
|
+
data.tar.gz: 7369bbf6293a3ff1c99f733b5026d3d0d6a68305af4330af8ebe5feff8944041
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d362044adf712df1f2f36a7305554c837263001d557fe6d999614f51cfd43901b336402326c3008d20c2191c89c60fb54618e79dd785626ceb0abd11eb7d43f4
|
|
7
|
+
data.tar.gz: d7aee55ff2cd6ec1c49c0b4adeeb890010cda94ffe0b354acb7d63c50081bb6c74c79f8553f820564b592c2ca47cdf8239a492ac1f1a97754be60190ab641635
|
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2.7.8
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
irb-autosuggestions (0.
|
|
4
|
+
irb-autosuggestions (0.2.0)
|
|
5
5
|
reline
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -70,9 +70,10 @@ GEM
|
|
|
70
70
|
unicode-display_width (3.2.0)
|
|
71
71
|
unicode-emoji (~> 4.1)
|
|
72
72
|
unicode-emoji (4.2.0)
|
|
73
|
-
yard (0.9.
|
|
73
|
+
yard (0.9.44)
|
|
74
74
|
|
|
75
75
|
PLATFORMS
|
|
76
|
+
arm64-darwin-24
|
|
76
77
|
arm64-darwin-25
|
|
77
78
|
x86_64-linux
|
|
78
79
|
|
|
@@ -80,7 +81,7 @@ DEPENDENCIES
|
|
|
80
81
|
docscribe
|
|
81
82
|
irb-autosuggestions!
|
|
82
83
|
rake
|
|
83
|
-
rbs
|
|
84
|
+
rbs (~> 3.6.0)
|
|
84
85
|
rspec (~> 3.0)
|
|
85
86
|
rubocop
|
|
86
87
|
rubocop-rake
|
|
@@ -89,4 +90,4 @@ DEPENDENCIES
|
|
|
89
90
|
yard (>= 0.9.38)
|
|
90
91
|
|
|
91
92
|
BUNDLED WITH
|
|
92
|
-
|
|
93
|
+
4.0.12
|
data/Gemfile_2_7
ADDED
data/Gemfile_2_7.lock
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
irb-autosuggestions (0.2.0)
|
|
5
|
+
reline
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
ast (2.4.3)
|
|
11
|
+
diff-lcs (1.6.2)
|
|
12
|
+
docscribe (1.3.3)
|
|
13
|
+
parser (>= 3.3)
|
|
14
|
+
prism (~> 1.8)
|
|
15
|
+
io-console (0.8.2)
|
|
16
|
+
json (2.19.5)
|
|
17
|
+
language_server-protocol (3.17.0.5)
|
|
18
|
+
lint_roller (1.1.0)
|
|
19
|
+
parallel (1.28.0)
|
|
20
|
+
parser (3.3.11.1)
|
|
21
|
+
ast (~> 2.4.1)
|
|
22
|
+
racc
|
|
23
|
+
prism (1.9.0)
|
|
24
|
+
racc (1.8.1)
|
|
25
|
+
rainbow (3.1.1)
|
|
26
|
+
rake (13.4.2)
|
|
27
|
+
regexp_parser (2.12.0)
|
|
28
|
+
reline (0.6.3)
|
|
29
|
+
io-console (~> 0.5)
|
|
30
|
+
rspec (3.13.2)
|
|
31
|
+
rspec-core (~> 3.13.0)
|
|
32
|
+
rspec-expectations (~> 3.13.0)
|
|
33
|
+
rspec-mocks (~> 3.13.0)
|
|
34
|
+
rspec-core (3.13.6)
|
|
35
|
+
rspec-support (~> 3.13.0)
|
|
36
|
+
rspec-expectations (3.13.5)
|
|
37
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
38
|
+
rspec-support (~> 3.13.0)
|
|
39
|
+
rspec-mocks (3.13.8)
|
|
40
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
41
|
+
rspec-support (~> 3.13.0)
|
|
42
|
+
rspec-support (3.13.7)
|
|
43
|
+
rubocop (1.86.2)
|
|
44
|
+
json (~> 2.3)
|
|
45
|
+
language_server-protocol (~> 3.17.0.2)
|
|
46
|
+
lint_roller (~> 1.1.0)
|
|
47
|
+
parallel (>= 1.10)
|
|
48
|
+
parser (>= 3.3.0.2)
|
|
49
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
50
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
51
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
52
|
+
ruby-progressbar (~> 1.7)
|
|
53
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
54
|
+
rubocop-ast (1.49.1)
|
|
55
|
+
parser (>= 3.3.7.2)
|
|
56
|
+
prism (~> 1.7)
|
|
57
|
+
rubocop-rake (0.7.1)
|
|
58
|
+
lint_roller (~> 1.1)
|
|
59
|
+
rubocop (>= 1.72.1)
|
|
60
|
+
rubocop-rspec (3.9.0)
|
|
61
|
+
lint_roller (~> 1.1)
|
|
62
|
+
rubocop (~> 1.81)
|
|
63
|
+
rubocop-sorted_methods_by_call (1.2.2)
|
|
64
|
+
lint_roller
|
|
65
|
+
rubocop (>= 1.72.0)
|
|
66
|
+
ruby-progressbar (1.13.0)
|
|
67
|
+
unicode-display_width (3.2.0)
|
|
68
|
+
unicode-emoji (~> 4.1)
|
|
69
|
+
unicode-emoji (4.2.0)
|
|
70
|
+
yard (0.9.44)
|
|
71
|
+
|
|
72
|
+
PLATFORMS
|
|
73
|
+
x86_64-linux
|
|
74
|
+
arm64-darwin-25
|
|
75
|
+
|
|
76
|
+
DEPENDENCIES
|
|
77
|
+
docscribe
|
|
78
|
+
irb-autosuggestions!
|
|
79
|
+
rake
|
|
80
|
+
rspec (~> 3.0)
|
|
81
|
+
rubocop
|
|
82
|
+
rubocop-rake
|
|
83
|
+
rubocop-rspec
|
|
84
|
+
rubocop-sorted_methods_by_call
|
|
85
|
+
yard (>= 0.9.38)
|
|
86
|
+
|
|
87
|
+
BUNDLED WITH
|
|
88
|
+
2.4.22
|
data/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# Irb::Autosuggestions
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/irb-autosuggestions)
|
|
4
|
+
[](https://rubygems.org/gems/irb-autosuggestions)
|
|
4
5
|
[](https://github.com/unurgunite/irb-autosuggestions/actions)
|
|
6
|
+
[](https://github.com/unurgunite/irb-autosuggestions/blob/master/LICENSE.txt)
|
|
7
|
+
[](#installation)
|
|
5
8
|
|
|
6
9
|

|
|
7
10
|
|
|
@@ -13,7 +16,9 @@ No need to explain. Fish-like autosuggestions for IRB — ghost text from histor
|
|
|
13
16
|
* [Contents](#contents)
|
|
14
17
|
* [Installation](#installation)
|
|
15
18
|
* [Usage](#usage)
|
|
19
|
+
* [Prefix-filtered history navigation](#prefix-filtered-history-navigation)
|
|
16
20
|
* [Configuration](#configuration)
|
|
21
|
+
* [Colors](#colors)
|
|
17
22
|
* [How it works](#how-it-works)
|
|
18
23
|
* [Development](#development)
|
|
19
24
|
* [License](#license)
|
|
@@ -34,7 +39,8 @@ require 'irb-autosuggestions'
|
|
|
34
39
|
|
|
35
40
|
## Usage
|
|
36
41
|
|
|
37
|
-
Start typing in IRB.
|
|
42
|
+
Start typing in IRB. Ghost text appears after the cursor, showing the most recent matching history entry with syntax
|
|
43
|
+
coloring (or gray if colorization is unavailable or disabled):
|
|
38
44
|
|
|
39
45
|
```
|
|
40
46
|
irb(main):001* [1,2,3].map do |el|
|
|
@@ -44,6 +50,36 @@ irb(main):003> end <- "d" in gray
|
|
|
44
50
|
|
|
45
51
|
Press **right arrow** (`->`) to accept the full multiline suggestion.
|
|
46
52
|
|
|
53
|
+
### Prefix-filtered history navigation
|
|
54
|
+
|
|
55
|
+
**Up/down arrows** navigate history filtered by the typed prefix (like zsh). Start typing and press **up** — only
|
|
56
|
+
entries matching your prefix are shown. The prefix is frozen on the first press; subsequent presses keep searching
|
|
57
|
+
within that prefix:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
irb(main):001:0> def <- type "def", press up
|
|
61
|
+
irb(main):001:0> def foo <- press up again then older "def*" entry
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Press **right arrow** to accept the suggestion and exit prefix mode. Any non-history key (letter, enter, etc.) resets
|
|
65
|
+
the prefix anchor, returning arrows to normal unfiltered browsing.
|
|
66
|
+
|
|
67
|
+
Consecutive duplicate entries are collapsed during prefix search so each unique line appears once.
|
|
68
|
+
|
|
69
|
+
Regular unfiltered history browsing (empty buffer + up arrow) is unchanged — all entries are shown including duplicates.
|
|
70
|
+
|
|
71
|
+
To disable prefix navigation while keeping ghost text:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
IRB.conf[:USE_PREFIX_HISTORY_NAVIGATION] = false
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or via environment variable:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
export IRB_PREFIX_HISTORY_NAVIGATION=0
|
|
81
|
+
```
|
|
82
|
+
|
|
47
83
|
## Configuration
|
|
48
84
|
|
|
49
85
|
Autosuggestions are enabled by default. To disable:
|
|
@@ -60,13 +96,39 @@ Or via environment variable:
|
|
|
60
96
|
export IRB_AUTOSUGGESTIONS=0
|
|
61
97
|
```
|
|
62
98
|
|
|
99
|
+
### Colors
|
|
100
|
+
|
|
101
|
+
Syntax-colored ghost text is **enabled by default** when `IRB::Color` is available and `IRB.conf[:USE_COLORIZE]` is
|
|
102
|
+
true.
|
|
103
|
+
|
|
104
|
+
To disable colored ghost text (falls back to plain gray):
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
IRB.conf[:USE_COLORIZE] = false
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or from command line:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
irb --nocolorize
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> [!NOTE] Colorized ghost rendering may behave differently across terminal emulators, Ruby versions, and IRB color
|
|
117
|
+
> schemes. If you notice visual artifacts (e.g., wrong colors, underlines, or unusual brightness), try disabling the
|
|
118
|
+
> feature or switch to the gray fallback or create new issue.
|
|
119
|
+
|
|
63
120
|
### How it works
|
|
64
121
|
|
|
65
122
|
- Each keystroke queries `Reline::HISTORY` for the most recent entry whose prefix matches the current buffer.
|
|
66
|
-
- The suggestion is rendered inline as
|
|
123
|
+
- The suggestion is rendered inline as ghost text without modifying the buffer.
|
|
124
|
+
- When available, the ghost uses `IRB::Color.colorize_code` to match IRB's syntax colors, dimmed via ANSI escape codes
|
|
125
|
+
for visual distinction.
|
|
67
126
|
- Extra ghost lines (for multiline history entries) are drawn below the prompt.
|
|
68
|
-
- `\e[
|
|
127
|
+
- Ghost artifacts are cleared each frame using cursor save/restore (`\e[s`/`\e[u`) and per-line clearing (`\e[2K`),
|
|
128
|
+
avoiding `\e[J` which interfered with Reline's cursor tracking.
|
|
69
129
|
- Right arrow triggers `ed_next_char`, which replaces the buffer with the ghost text.
|
|
130
|
+
- Up/down arrows use prefix-filtered navigation when the buffer is non-empty, falling back to unfiltered browsing
|
|
131
|
+
when nothing was typed.
|
|
70
132
|
|
|
71
133
|
## Development
|
|
72
134
|
|
|
@@ -3,17 +3,26 @@
|
|
|
3
3
|
module Irb
|
|
4
4
|
module Autosuggestions
|
|
5
5
|
# Patches Reline::LineEditor to display fish-like autosuggestions from history.
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength, SortedMethodsByCall/Waterfall
|
|
6
7
|
module LineEditorPatch
|
|
7
8
|
GRAY = "\e[90m"
|
|
9
|
+
DIM = "\e[2m"
|
|
10
|
+
RESET_COLOR = "\e[39;49m"
|
|
8
11
|
RESET = "\e[0m"
|
|
12
|
+
FG_COLORS = ((30..37).to_a + (90..97).to_a + [38, 39]).freeze
|
|
9
13
|
CONFIG_KEY = :USE_AUTOSUGGESTIONS
|
|
10
14
|
ENV_KEY = 'IRB_AUTOSUGGESTIONS'
|
|
15
|
+
CONFIG_NAV_KEY = :USE_PREFIX_HISTORY_NAVIGATION
|
|
16
|
+
ENV_NAV_KEY = 'IRB_PREFIX_HISTORY_NAVIGATION'
|
|
11
17
|
|
|
12
|
-
# Intercepts key input to accept autosuggestions on right arrow
|
|
18
|
+
# Intercepts key input to accept autosuggestions on right arrow
|
|
19
|
+
# and clears the prefix navigation anchor on non-history keys.
|
|
13
20
|
#
|
|
14
21
|
# @param [Object] key A Reline key event.
|
|
15
|
-
# @return [Object]
|
|
22
|
+
# @return [Object]
|
|
16
23
|
def input_key(key)
|
|
24
|
+
clear_prefix_anchor if navigation_enabled? && !history_navigation_key?(key)
|
|
25
|
+
|
|
17
26
|
if enabled? && right_arrow?(key)
|
|
18
27
|
buffer = whole_buffer
|
|
19
28
|
suggestion = find_suggestion(buffer)
|
|
@@ -30,22 +39,149 @@ module Irb
|
|
|
30
39
|
private
|
|
31
40
|
|
|
32
41
|
# Injects ghost text into terminal output after Reline finishes rendering.
|
|
42
|
+
# Clears any previous ghost text (inline and multi-line) first,
|
|
43
|
+
# then renders the new ghost suggestion.
|
|
33
44
|
#
|
|
34
45
|
# @private
|
|
35
46
|
# @return [Object] The result of +super+.
|
|
36
47
|
def render(...)
|
|
37
48
|
result = super
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
if enabled?
|
|
50
|
+
clear_previous_ghost
|
|
51
|
+
render_ghost_suggestion
|
|
52
|
+
end
|
|
53
|
+
result
|
|
54
|
+
end
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
# Prefix-filtered up-arrow history navigation.
|
|
57
|
+
#
|
|
58
|
+
# @private
|
|
59
|
+
# @param [Object] key
|
|
60
|
+
# @param [Integer] arg Repeat count.
|
|
61
|
+
# @return [Object]
|
|
62
|
+
def ed_prev_history(key, arg: 1)
|
|
63
|
+
if navigation_enabled? && (@line_index.zero? || @history_pointer)
|
|
64
|
+
buffer = prefix_buffer_for_nav
|
|
65
|
+
if buffer && !buffer.empty?
|
|
66
|
+
walk_history_back(buffer, arg)
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
clear_prefix_anchor
|
|
71
|
+
super
|
|
72
|
+
end
|
|
43
73
|
|
|
44
|
-
|
|
45
|
-
|
|
74
|
+
# Prefix-filtered down-arrow history navigation.
|
|
75
|
+
#
|
|
76
|
+
# @private
|
|
77
|
+
# @param [Object] key
|
|
78
|
+
# @param [Integer] arg Repeat count.
|
|
79
|
+
# @return [Object]
|
|
80
|
+
def ed_next_history(key, arg: 1)
|
|
81
|
+
if navigation_enabled? && @history_pointer && @prefix_buffer
|
|
82
|
+
walk_history_forward(arg)
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
clear_prefix_anchor
|
|
86
|
+
super
|
|
87
|
+
end
|
|
46
88
|
|
|
47
|
-
|
|
48
|
-
|
|
89
|
+
# Returns the anchor buffer for prefix navigation: the frozen prefix
|
|
90
|
+
# during an ongoing session, +whole_buffer+ on the first press from
|
|
91
|
+
# base buffer, or +nil+ when nothing was typed.
|
|
92
|
+
#
|
|
93
|
+
# @private
|
|
94
|
+
# @return [String, nil]
|
|
95
|
+
def prefix_buffer_for_nav
|
|
96
|
+
return @prefix_buffer if @prefix_buffer
|
|
97
|
+
|
|
98
|
+
whole_buffer unless @history_pointer
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Checks if a key event triggers history navigation (up/down arrow).
|
|
102
|
+
#
|
|
103
|
+
# @private
|
|
104
|
+
# @param [Object] key A Reline key event.
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def history_navigation_key?(key)
|
|
107
|
+
key.respond_to?(:method_symbol) &&
|
|
108
|
+
%i[ed_prev_history ed_next_history].include?(key.method_symbol)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Clears ghost from the previous frame: inline text from buffer end
|
|
112
|
+
# to end of line, and each extra line below. Uses cursor save/restore
|
|
113
|
+
# so it does not interfere with Reline's cursor tracking.
|
|
114
|
+
#
|
|
115
|
+
# @private
|
|
116
|
+
# @return [void]
|
|
117
|
+
def clear_previous_ghost
|
|
118
|
+
clear_inline_ghost
|
|
119
|
+
return unless @ghost_line_count&.positive?
|
|
120
|
+
|
|
121
|
+
output = Reline.core.instance_variable_get(:@output)
|
|
122
|
+
output.write("\e[s")
|
|
123
|
+
@ghost_line_count.times { output.write("\e[1B\e[2K") }
|
|
124
|
+
output.write("\e[u")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Clears inline ghost text from the buffer end to end of line.
|
|
128
|
+
#
|
|
129
|
+
# @private
|
|
130
|
+
# @return [void]
|
|
131
|
+
def clear_inline_ghost
|
|
132
|
+
return unless @has_inline_ghost
|
|
133
|
+
|
|
134
|
+
output = Reline.core.instance_variable_get(:@output)
|
|
135
|
+
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
136
|
+
current_line = @buffer_of_lines[@line_index] || ''
|
|
137
|
+
buf_end = prompt_width + Reline::Unicode.calculate_width(current_line)
|
|
138
|
+
output.write("\e[s")
|
|
139
|
+
output.write("\e[0G\e[#{buf_end}C\e[K")
|
|
140
|
+
output.write("\e[u")
|
|
141
|
+
@has_inline_ghost = false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Walks backward through history, loading each matching entry.
|
|
145
|
+
#
|
|
146
|
+
# @private
|
|
147
|
+
# @param [String] buffer The prefix anchor.
|
|
148
|
+
# @param [Integer] arg Repeat count.
|
|
149
|
+
# @return [Object]
|
|
150
|
+
def walk_history_back(buffer, arg)
|
|
151
|
+
arg.times do
|
|
152
|
+
pointer = find_prev_match(buffer, @history_pointer)
|
|
153
|
+
break unless pointer
|
|
154
|
+
|
|
155
|
+
move_history(pointer, line: :end, cursor: :end)
|
|
156
|
+
@prefix_buffer = buffer
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Walks forward through history. Returns to base when exhausted.
|
|
161
|
+
#
|
|
162
|
+
# @private
|
|
163
|
+
# @param [Integer] arg Repeat count.
|
|
164
|
+
# @return [Object]
|
|
165
|
+
def walk_history_forward(arg)
|
|
166
|
+
arg.times do
|
|
167
|
+
pointer = find_next_match(@prefix_buffer, @history_pointer)
|
|
168
|
+
if pointer
|
|
169
|
+
move_history(pointer, line: :start, cursor: :end)
|
|
170
|
+
else
|
|
171
|
+
move_history(Reline::HISTORY.size, line: :start, cursor: :end)
|
|
172
|
+
break
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Removes the prefix anchor so the next up/down starts fresh.
|
|
178
|
+
#
|
|
179
|
+
# @private
|
|
180
|
+
# @return [void]
|
|
181
|
+
def clear_prefix_anchor
|
|
182
|
+
return unless instance_variable_defined?(:@prefix_buffer)
|
|
183
|
+
|
|
184
|
+
remove_instance_variable(:@prefix_buffer)
|
|
49
185
|
end
|
|
50
186
|
|
|
51
187
|
# Checks whether autosuggestions are enabled via IRB.conf or env var.
|
|
@@ -56,7 +192,24 @@ module Irb
|
|
|
56
192
|
case ENV.fetch(ENV_KEY, nil)
|
|
57
193
|
when '0' then false
|
|
58
194
|
when '1' then true
|
|
59
|
-
else
|
|
195
|
+
else
|
|
196
|
+
val = IRB.conf[CONFIG_KEY]
|
|
197
|
+
val.nil? || val
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Whether prefix-filtered history navigation is enabled.
|
|
202
|
+
# Falls back to the value of +enabled?+ when not explicitly set.
|
|
203
|
+
#
|
|
204
|
+
# @private
|
|
205
|
+
# @return [Boolean]
|
|
206
|
+
def navigation_enabled?
|
|
207
|
+
case ENV.fetch(ENV_NAV_KEY, nil)
|
|
208
|
+
when '0' then false
|
|
209
|
+
when '1' then true
|
|
210
|
+
else
|
|
211
|
+
val = IRB.conf[CONFIG_NAV_KEY]
|
|
212
|
+
val.nil? ? enabled? : val
|
|
60
213
|
end
|
|
61
214
|
end
|
|
62
215
|
|
|
@@ -70,41 +223,225 @@ module Irb
|
|
|
70
223
|
key.method_symbol == :ed_next_char
|
|
71
224
|
end
|
|
72
225
|
|
|
73
|
-
#
|
|
226
|
+
# Finds the index of the previous (older) history entry starting with +buffer+.
|
|
74
227
|
#
|
|
75
228
|
# @private
|
|
76
|
-
# @param [String] buffer
|
|
77
|
-
# @
|
|
78
|
-
|
|
229
|
+
# @param [String] buffer Search prefix.
|
|
230
|
+
# @param [Integer, nil] from_pointer Current history pointer (nil = base buffer).
|
|
231
|
+
# @return [Integer, nil]
|
|
232
|
+
def find_prev_match(buffer, from_pointer)
|
|
233
|
+
start_idx = (from_pointer || Reline::HISTORY.size) - 1
|
|
234
|
+
return nil if start_idx.negative?
|
|
235
|
+
|
|
236
|
+
start_idx.downto(0) do |i|
|
|
237
|
+
entry = Reline::HISTORY[i]
|
|
238
|
+
next if entry.nil? || (dedup?(buffer) && duplicate_of_newer?(i, entry))
|
|
239
|
+
|
|
240
|
+
return i if entry.start_with?(buffer)
|
|
241
|
+
end
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Finds the index of the next (newer) history entry starting with +buffer+.
|
|
246
|
+
#
|
|
247
|
+
# @private
|
|
248
|
+
# @param [String] buffer Search prefix.
|
|
249
|
+
# @param [Integer, nil] from_pointer Current history pointer.
|
|
250
|
+
# @return [Integer, nil]
|
|
251
|
+
def find_next_match(buffer, from_pointer) # rubocop:disable Metrics/CyclomaticComplexity
|
|
252
|
+
return nil unless from_pointer
|
|
253
|
+
|
|
254
|
+
start_idx = from_pointer + 1
|
|
255
|
+
return nil if start_idx > Reline::HISTORY.size - 1
|
|
256
|
+
|
|
257
|
+
(start_idx...Reline::HISTORY.size).each do |i|
|
|
258
|
+
entry = Reline::HISTORY[i]
|
|
259
|
+
next if entry.nil? || (dedup?(buffer) && duplicate_of_newer?(i, entry))
|
|
260
|
+
|
|
261
|
+
return i if entry.start_with?(buffer)
|
|
262
|
+
end
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Whether duplicate collapsing is active (only for prefix search).
|
|
267
|
+
#
|
|
268
|
+
# @private
|
|
269
|
+
# @param [String] buffer Search prefix.
|
|
270
|
+
# @return [Boolean]
|
|
271
|
+
def dedup?(buffer)
|
|
272
|
+
!buffer.empty?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Whether +entry+ at index +idx+ is a consecutive duplicate of the next entry.
|
|
276
|
+
#
|
|
277
|
+
# @private
|
|
278
|
+
# @param [Integer] idx
|
|
279
|
+
# @param [String] entry
|
|
280
|
+
# @return [Boolean]
|
|
281
|
+
def duplicate_of_newer?(idx, entry)
|
|
282
|
+
idx < Reline::HISTORY.size - 1 && entry == Reline::HISTORY[idx + 1]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Renders ghost text for the current buffer, if a suggestion exists.
|
|
286
|
+
#
|
|
287
|
+
# @private
|
|
288
|
+
# @return [void]
|
|
289
|
+
def render_ghost_suggestion
|
|
290
|
+
buffer = whole_buffer
|
|
291
|
+
@ghost_line_count = 0
|
|
292
|
+
return if buffer.empty?
|
|
293
|
+
|
|
79
294
|
suggestion = find_suggestion(buffer)
|
|
80
295
|
return unless suggestion
|
|
81
296
|
|
|
82
297
|
ghost = suggestion[buffer.size..]
|
|
83
298
|
return if ghost.nil? || ghost.empty?
|
|
84
299
|
|
|
85
|
-
ghost
|
|
300
|
+
render_ghost(ghost, suggestion)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Finds the most recent history entry that starts with the given buffer.
|
|
304
|
+
#
|
|
305
|
+
# @private
|
|
306
|
+
# @param [String] buffer The current whole buffer.
|
|
307
|
+
# @return [String, nil] The matching history entry, or nil.
|
|
308
|
+
def find_suggestion(buffer)
|
|
309
|
+
Reline::HISTORY.reverse.find do |h|
|
|
310
|
+
h != buffer && h.start_with?(buffer)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Replaces the entire buffer with the accepted suggestion and triggers a rerender.
|
|
315
|
+
#
|
|
316
|
+
# @private
|
|
317
|
+
# @param [String] suggestion The full multiline suggestion to accept.
|
|
318
|
+
# @return [void]
|
|
319
|
+
def accept_suggestion(suggestion)
|
|
320
|
+
sug_lines = suggestion.split("\n")
|
|
321
|
+
@buffer_of_lines = sug_lines
|
|
322
|
+
@line_index = sug_lines.size - 1
|
|
323
|
+
@byte_pointer = sug_lines.last.bytesize
|
|
324
|
+
rerender
|
|
86
325
|
end
|
|
87
326
|
|
|
88
327
|
# Writes the ghost text (inline + extra lines) to terminal output.
|
|
89
328
|
#
|
|
329
|
+
# If +suggestion+ is provided and colorization is enabled, the ghost
|
|
330
|
+
# is rendered with syntax highlighting via IRB::Color.
|
|
331
|
+
#
|
|
90
332
|
# @private
|
|
91
|
-
# @param [String] ghost The
|
|
333
|
+
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
334
|
+
# @param [String, nil] suggestion The full matching history entry.
|
|
92
335
|
# @return [void]
|
|
93
|
-
def render_ghost(ghost)
|
|
94
|
-
|
|
336
|
+
def render_ghost(ghost, suggestion = nil)
|
|
337
|
+
output = Reline.core.instance_variable_get(:@output)
|
|
338
|
+
display_lines = ghost_display_lines(ghost, suggestion)
|
|
339
|
+
@ghost_line_count = display_lines.size - 1
|
|
340
|
+
@has_inline_ghost = true
|
|
341
|
+
|
|
342
|
+
first_line = display_lines.first
|
|
343
|
+
output.write(first_line) if first_line && !first_line.empty?
|
|
344
|
+
write_extra_ghost_lines(display_lines.drop(1))
|
|
345
|
+
restore_cursor_after(display_lines)
|
|
346
|
+
output.flush
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Returns ghost lines ready for terminal output (with ANSI codes).
|
|
350
|
+
#
|
|
351
|
+
# When colorization is enabled, the full suggestion is colorized via
|
|
352
|
+
# IRB::Color and the ghost portion is extracted from the colored output.
|
|
353
|
+
# Otherwise, each line is wrapped in GRAY/RESET.
|
|
354
|
+
#
|
|
355
|
+
# @private
|
|
356
|
+
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
357
|
+
# @param [String, nil] suggestion The full matching history entry.
|
|
358
|
+
# @raise [StandardError]
|
|
359
|
+
# @return [Array<String>]
|
|
360
|
+
def ghost_display_lines(ghost, suggestion)
|
|
361
|
+
if suggestion && use_colorize?
|
|
362
|
+
colorize_ghost_lines(ghost, suggestion)
|
|
363
|
+
else
|
|
364
|
+
ghost.split("\n").map { |line| "#{GRAY}#{line}#{RESET}" }
|
|
365
|
+
end
|
|
366
|
+
rescue StandardError
|
|
367
|
+
ghost.split("\n").map { |line| "#{GRAY}#{line}#{RESET}" }
|
|
368
|
+
end
|
|
95
369
|
|
|
96
|
-
|
|
370
|
+
# Checks whether syntax coloring is available and enabled.
|
|
371
|
+
#
|
|
372
|
+
# @private
|
|
373
|
+
# @return [Boolean]
|
|
374
|
+
def use_colorize?
|
|
375
|
+
defined?(IRB::Color) &&
|
|
376
|
+
IRB::Color.colorable? &&
|
|
377
|
+
IRB.conf.fetch(:USE_COLORIZE, true)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Colorizes the full suggestion and extracts the ghost portion.
|
|
381
|
+
#
|
|
382
|
+
# @private
|
|
383
|
+
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
384
|
+
# @param [String] suggestion The full matching history entry.
|
|
385
|
+
# @return [Array<String>] Colorized ghost lines with ANSI codes.
|
|
386
|
+
def colorize_ghost_lines(ghost, suggestion)
|
|
387
|
+
colored = IRB::Color.colorize_code(suggestion)
|
|
388
|
+
ghost_byte_start = suggestion.bytesize - ghost.bytesize
|
|
389
|
+
colored_ghost = extract_ansi_colored_suffix(colored, ghost_byte_start)
|
|
390
|
+
colored_ghost.split("\n").map { |line| dim_line(line) }
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Prepends each ANSI foreground color code with +2;+ (dim)
|
|
394
|
+
# and strips non-color attributes (bold, underline, reverse…).
|
|
395
|
+
# Inner full resets are replaced with +RESET_COLOR+ so dim
|
|
396
|
+
# stays active across token boundaries.
|
|
397
|
+
#
|
|
398
|
+
# @private
|
|
399
|
+
# @param [String] line ANSI-colored line.
|
|
400
|
+
# @return [String] Dimmed ANSI-colored line.
|
|
401
|
+
def dim_line(line)
|
|
402
|
+
inner = line.gsub(/\e\[(\d+(?:;\d+)*)m/) do
|
|
403
|
+
params = Regexp.last_match(1)
|
|
404
|
+
next RESET_COLOR if params == '0'
|
|
97
405
|
|
|
98
|
-
|
|
99
|
-
|
|
406
|
+
color = params.split(';').map(&:to_i).select { |p| FG_COLORS.include?(p) }
|
|
407
|
+
color.empty? ? '' : "\e[2;#{color.join(';')}m"
|
|
408
|
+
end
|
|
409
|
+
"#{DIM}#{inner}#{RESET}"
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Extracts the suffix of an ANSI-colored string starting at a given
|
|
413
|
+
# visible byte offset, preserving all ANSI codes.
|
|
414
|
+
#
|
|
415
|
+
# @private
|
|
416
|
+
# @param [String] colored_text Text with embedded ANSI escape sequences.
|
|
417
|
+
# @param [Integer] visible_byte_offset Offset in visible (non-ANSI) bytes.
|
|
418
|
+
# @return [String]
|
|
419
|
+
def extract_ansi_colored_suffix(colored_text, visible_byte_offset) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
420
|
+
pos = 0
|
|
421
|
+
visible = 0
|
|
422
|
+
pending_code = nil
|
|
423
|
+
|
|
424
|
+
while visible < visible_byte_offset && pos < colored_text.length
|
|
425
|
+
if colored_text[pos] == "\e"
|
|
426
|
+
code_start = pos
|
|
427
|
+
pos = colored_text.index('m', pos)&.succ || colored_text.length
|
|
428
|
+
code = colored_text[code_start...pos]
|
|
100
429
|
|
|
101
|
-
|
|
430
|
+
pending_code = [RESET, "\e[m"].include?(code) ? nil : code
|
|
431
|
+
else
|
|
432
|
+
visible += 1
|
|
433
|
+
pos += 1
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
suffix = colored_text[pos..] || String.new
|
|
438
|
+
pending_code ? "#{pending_code}#{suffix}" : suffix
|
|
102
439
|
end
|
|
103
440
|
|
|
104
441
|
# Writes extra ghost lines below the current buffer line with prompt-width alignment.
|
|
105
442
|
#
|
|
106
443
|
# @private
|
|
107
|
-
# @param [Array<String>] lines Extra
|
|
444
|
+
# @param [Array<String>] lines Extra lines with ANSI codes (excluding first inline line).
|
|
108
445
|
# @return [void]
|
|
109
446
|
def write_extra_ghost_lines(lines)
|
|
110
447
|
return if lines.empty?
|
|
@@ -115,7 +452,7 @@ module Irb
|
|
|
115
452
|
lines.each do |line|
|
|
116
453
|
output.write("\n\e[K")
|
|
117
454
|
output.write("\e[#{prompt_width}C") if prompt_width.positive?
|
|
118
|
-
output.write(
|
|
455
|
+
output.write(line)
|
|
119
456
|
end
|
|
120
457
|
end
|
|
121
458
|
|
|
@@ -134,30 +471,7 @@ module Irb
|
|
|
134
471
|
output.write("\e[0G")
|
|
135
472
|
output.write("\e[#{pos}C")
|
|
136
473
|
end
|
|
137
|
-
|
|
138
|
-
# Finds the most recent history entry that starts with the given buffer.
|
|
139
|
-
#
|
|
140
|
-
# @private
|
|
141
|
-
# @param [String] buffer The current whole buffer.
|
|
142
|
-
# @return [String, nil] The matching history entry, or nil.
|
|
143
|
-
def find_suggestion(buffer)
|
|
144
|
-
Reline::HISTORY.reverse.find do |h|
|
|
145
|
-
h != buffer && h.start_with?(buffer)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Replaces the entire buffer with the accepted suggestion and triggers a rerender.
|
|
150
|
-
#
|
|
151
|
-
# @private
|
|
152
|
-
# @param [String] suggestion The full multiline suggestion to accept.
|
|
153
|
-
# @return [void]
|
|
154
|
-
def accept_suggestion(suggestion)
|
|
155
|
-
sug_lines = suggestion.split("\n")
|
|
156
|
-
@buffer_of_lines = sug_lines
|
|
157
|
-
@line_index = sug_lines.size - 1
|
|
158
|
-
@byte_pointer = sug_lines.last.bytesize
|
|
159
|
-
rerender
|
|
160
|
-
end
|
|
161
474
|
end
|
|
162
475
|
end
|
|
163
476
|
end
|
|
477
|
+
# rubocop:enable Metrics/ModuleLength, SortedMethodsByCall/Waterfall
|
data/rbs_collection.lock.yaml
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
module Irb
|
|
2
2
|
module Autosuggestions
|
|
3
3
|
module LineEditorPatch
|
|
4
|
+
CONFIG_NAV_KEY: Symbol
|
|
5
|
+
DIM: String
|
|
6
|
+
ENV_NAV_KEY: String
|
|
7
|
+
FG_COLORS: Array[Integer]
|
|
4
8
|
GRAY: String
|
|
5
9
|
RESET: String
|
|
6
10
|
CONFIG_KEY: Symbol
|
|
7
11
|
ENV_KEY: String
|
|
12
|
+
RESET_COLOR: String
|
|
8
13
|
|
|
9
14
|
@buffer_of_lines: Array[String]
|
|
10
15
|
@byte_pointer: Integer
|
|
@@ -13,9 +18,12 @@ module Irb
|
|
|
13
18
|
def input_key: (untyped key) -> untyped
|
|
14
19
|
def render: (*untyped) -> untyped
|
|
15
20
|
def enabled?: () -> bool
|
|
21
|
+
def use_colorize?: () -> bool
|
|
16
22
|
def right_arrow?: (untyped key) -> bool
|
|
17
|
-
def
|
|
18
|
-
def
|
|
23
|
+
def render_ghost: (String ghost, ?String suggestion) -> void
|
|
24
|
+
def ghost_display_lines: (String ghost, ?String suggestion) -> Array[String]
|
|
25
|
+
def colorize_ghost_lines: (String ghost, String suggestion) -> Array[String]
|
|
26
|
+
def extract_ansi_colored_suffix: (String colored_text, Integer visible_byte_offset) -> String
|
|
19
27
|
def write_extra_ghost_lines: (Array[String] lines) -> void
|
|
20
28
|
def restore_cursor_after: (Array[String] lines) -> void
|
|
21
29
|
def find_suggestion: (String buffer) -> (String | nil)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: irb-autosuggestions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- unurgunite
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: reline
|
|
@@ -52,20 +52,6 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: rbs
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - ">="
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
62
|
-
type: :development
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - ">="
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
69
55
|
- !ruby/object:Gem::Dependency
|
|
70
56
|
name: rspec
|
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -162,6 +148,8 @@ files:
|
|
|
162
148
|
- CODE_OF_CONDUCT.md
|
|
163
149
|
- Gemfile
|
|
164
150
|
- Gemfile.lock
|
|
151
|
+
- Gemfile_2_7
|
|
152
|
+
- Gemfile_2_7.lock
|
|
165
153
|
- LICENSE.txt
|
|
166
154
|
- README.md
|
|
167
155
|
- Rakefile
|
|
@@ -191,14 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
191
179
|
requirements:
|
|
192
180
|
- - ">="
|
|
193
181
|
- !ruby/object:Gem::Version
|
|
194
|
-
version: '
|
|
182
|
+
version: '2.7'
|
|
195
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
196
184
|
requirements:
|
|
197
185
|
- - ">="
|
|
198
186
|
- !ruby/object:Gem::Version
|
|
199
187
|
version: '0'
|
|
200
188
|
requirements: []
|
|
201
|
-
rubygems_version: 3.
|
|
189
|
+
rubygems_version: 3.4.22
|
|
202
190
|
signing_key:
|
|
203
191
|
specification_version: 4
|
|
204
192
|
summary: Fish-like autosuggestions for irb.
|