irb-autosuggestions 0.1.1 → 0.2.1
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 +6 -4
- data/Gemfile_2_7 +6 -0
- data/Gemfile_2_7.lock +88 -0
- data/README.md +66 -3
- data/lib/irb/autosuggestions/line_editor_patch.rb +380 -56
- 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: b9d8279c02d62499ce7d58d2b8c4c47c7f8289fa4e601b140106219ea4d0d472
|
|
4
|
+
data.tar.gz: f35c372e76961a2b91fc65bdf1fe94e6ec6daa7fec810461b49eb8dd11c34379
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 838a7272d857247def598a2989cd8c43eb35b8272183ce3623c25c53faa8e3bb2c375ed3b0006ec9942c2a34537e8fdc644961ab61477dfd86118fbc9f4ccf33
|
|
7
|
+
data.tar.gz: 85643f9c47045b2accbceb5a0f84b4eb13ae988b39550a541a21e320fec2f81d75e56128a73a59a9ab5061f0ccce1a2c8e984fcebb8b5a083b2927fcd0c6171f
|
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.1)
|
|
5
5
|
reline
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -70,17 +70,19 @@ 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
|
|
78
|
+
ruby
|
|
77
79
|
x86_64-linux
|
|
78
80
|
|
|
79
81
|
DEPENDENCIES
|
|
80
82
|
docscribe
|
|
81
83
|
irb-autosuggestions!
|
|
82
84
|
rake
|
|
83
|
-
rbs
|
|
85
|
+
rbs (~> 3.6.0)
|
|
84
86
|
rspec (~> 3.0)
|
|
85
87
|
rubocop
|
|
86
88
|
rubocop-rake
|
|
@@ -89,4 +91,4 @@ DEPENDENCIES
|
|
|
89
91
|
yard (>= 0.9.38)
|
|
90
92
|
|
|
91
93
|
BUNDLED WITH
|
|
92
|
-
|
|
94
|
+
4.0.11
|
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.1)
|
|
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
|
+
arm64-darwin-25
|
|
74
|
+
x86_64-linux
|
|
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,40 @@ 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]
|
|
117
|
+
> Colorized ghost rendering may behave differently across terminal emulators, Ruby versions, and IRB color
|
|
118
|
+
> schemes. If you notice visual artifacts (e.g., wrong colors, underlines, or unusual brightness), try disabling the
|
|
119
|
+
> feature or switch to the gray fallback or create new issue.
|
|
120
|
+
|
|
63
121
|
### How it works
|
|
64
122
|
|
|
65
123
|
- Each keystroke queries `Reline::HISTORY` for the most recent entry whose prefix matches the current buffer.
|
|
66
|
-
- The suggestion is rendered inline as
|
|
124
|
+
- The suggestion is rendered inline as ghost text without modifying the buffer.
|
|
125
|
+
- When available, the ghost uses `IRB::Color.colorize_code` to match IRB's syntax colors, dimmed via ANSI escape codes
|
|
126
|
+
for visual distinction.
|
|
67
127
|
- Extra ghost lines (for multiline history entries) are drawn below the prompt.
|
|
68
|
-
- `\e[
|
|
128
|
+
- Ghost artifacts are cleared each frame using cursor save/restore (`\e[s`/`\e[u`) and per-line clearing (`\e[2K`),
|
|
129
|
+
avoiding `\e[J` which interfered with Reline's cursor tracking.
|
|
69
130
|
- Right arrow triggers `ed_next_char`, which replaces the buffer with the ghost text.
|
|
131
|
+
- Up/down arrows use prefix-filtered navigation when the buffer is non-empty, falling back to unfiltered browsing
|
|
132
|
+
when nothing was typed.
|
|
70
133
|
|
|
71
134
|
## Development
|
|
72
135
|
|
|
@@ -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)
|
|
@@ -27,25 +36,166 @@ module Irb
|
|
|
27
36
|
super
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
private
|
|
31
|
-
|
|
32
39
|
# Injects ghost text into terminal output after Reline finishes rendering.
|
|
40
|
+
# Must be public because Reline::Core calls +rerender+ externally.
|
|
41
|
+
# Works on both Reline 0.3.x (rerender is the main rendering entry) and
|
|
42
|
+
# Reline 0.4+ (rerender calls render internally).
|
|
33
43
|
#
|
|
34
|
-
# @private
|
|
35
44
|
# @return [Object] The result of +super+.
|
|
36
|
-
def
|
|
45
|
+
def rerender
|
|
37
46
|
result = super
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
if enabled?
|
|
48
|
+
clear_previous_ghost
|
|
49
|
+
render_ghost_suggestion
|
|
50
|
+
end
|
|
51
|
+
result
|
|
52
|
+
end
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
return result if buffer.empty?
|
|
54
|
+
private
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
88
|
+
|
|
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. Restores cursor column
|
|
113
|
+
# afterwards so Reline's cursor tracking is not disturbed.
|
|
114
|
+
#
|
|
115
|
+
# @private
|
|
116
|
+
# @return [void]
|
|
117
|
+
def clear_previous_ghost
|
|
118
|
+
clear_inline_ghost
|
|
119
|
+
return unless @ghost_line_count&.positive?
|
|
120
|
+
|
|
121
|
+
origin_column = cursor_column_at_byte_pointer
|
|
122
|
+
output = Reline.core.instance_variable_get(:@output)
|
|
123
|
+
@ghost_line_count.times { output.write("\e[1B\e[2K") }
|
|
124
|
+
output.write("\e[#{@ghost_line_count}A")
|
|
125
|
+
output.write("\e[0G\e[#{origin_column}C")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Clears inline ghost text from the buffer end to end of line.
|
|
129
|
+
#
|
|
130
|
+
# @private
|
|
131
|
+
# @return [void]
|
|
132
|
+
def clear_inline_ghost
|
|
133
|
+
return unless @has_inline_ghost
|
|
134
|
+
|
|
135
|
+
output = Reline.core.instance_variable_get(:@output)
|
|
136
|
+
buf_end = move_to_buffer_end
|
|
137
|
+
origin_column = cursor_column_at_byte_pointer
|
|
138
|
+
output.write("#{buf_end}\e[K")
|
|
139
|
+
output.write("\e[0G\e[#{origin_column}C")
|
|
140
|
+
@has_inline_ghost = false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Walks backward through history, loading each matching entry.
|
|
144
|
+
#
|
|
145
|
+
# @private
|
|
146
|
+
# @param [String] buffer The prefix anchor.
|
|
147
|
+
# @param [Integer] arg Repeat count.
|
|
148
|
+
# @return [Object]
|
|
149
|
+
def walk_history_back(buffer, arg)
|
|
150
|
+
arg.times do
|
|
151
|
+
pointer = find_prev_match(buffer, @history_pointer)
|
|
152
|
+
break unless pointer
|
|
153
|
+
|
|
154
|
+
move_history(pointer, line: :end, cursor: :end)
|
|
155
|
+
@prefix_buffer = buffer
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Walks forward through history. Returns to base when exhausted.
|
|
160
|
+
#
|
|
161
|
+
# @private
|
|
162
|
+
# @param [Integer] arg Repeat count.
|
|
163
|
+
# @return [Object]
|
|
164
|
+
def walk_history_forward(arg)
|
|
165
|
+
arg.times do
|
|
166
|
+
pointer = find_next_match(@prefix_buffer, @history_pointer)
|
|
167
|
+
if pointer
|
|
168
|
+
move_history(pointer, line: :start, cursor: :end)
|
|
169
|
+
else
|
|
170
|
+
move_history(Reline::HISTORY.size, line: :start, cursor: :end)
|
|
171
|
+
break
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Removes the prefix anchor so the next up/down starts fresh.
|
|
177
|
+
#
|
|
178
|
+
# @private
|
|
179
|
+
# @return [void]
|
|
180
|
+
def clear_prefix_anchor
|
|
181
|
+
return unless instance_variable_defined?(:@prefix_buffer)
|
|
182
|
+
|
|
183
|
+
remove_instance_variable(:@prefix_buffer)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Whether prefix-filtered history navigation is enabled.
|
|
187
|
+
# Falls back to the value of +enabled?+ when not explicitly set.
|
|
188
|
+
#
|
|
189
|
+
# @private
|
|
190
|
+
# @return [Boolean]
|
|
191
|
+
def navigation_enabled?
|
|
192
|
+
case ENV.fetch(ENV_NAV_KEY, nil)
|
|
193
|
+
when '0' then false
|
|
194
|
+
when '1' then true
|
|
195
|
+
else
|
|
196
|
+
val = IRB.conf[CONFIG_NAV_KEY]
|
|
197
|
+
val.nil? ? enabled? : val
|
|
198
|
+
end
|
|
49
199
|
end
|
|
50
200
|
|
|
51
201
|
# Checks whether autosuggestions are enabled via IRB.conf or env var.
|
|
@@ -56,7 +206,9 @@ module Irb
|
|
|
56
206
|
case ENV.fetch(ENV_KEY, nil)
|
|
57
207
|
when '0' then false
|
|
58
208
|
when '1' then true
|
|
59
|
-
else
|
|
209
|
+
else
|
|
210
|
+
val = IRB.conf[CONFIG_KEY]
|
|
211
|
+
val.nil? || val
|
|
60
212
|
end
|
|
61
213
|
end
|
|
62
214
|
|
|
@@ -70,69 +222,81 @@ module Irb
|
|
|
70
222
|
key.method_symbol == :ed_next_char
|
|
71
223
|
end
|
|
72
224
|
|
|
73
|
-
#
|
|
225
|
+
# Finds the index of the previous (older) history entry starting with +buffer+.
|
|
74
226
|
#
|
|
75
227
|
# @private
|
|
76
|
-
# @param [String] buffer
|
|
77
|
-
# @
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
228
|
+
# @param [String] buffer Search prefix.
|
|
229
|
+
# @param [Integer, nil] from_pointer Current history pointer (nil = base buffer).
|
|
230
|
+
# @return [Integer, nil]
|
|
231
|
+
def find_prev_match(buffer, from_pointer)
|
|
232
|
+
start_idx = (from_pointer || Reline::HISTORY.size) - 1
|
|
233
|
+
return nil if start_idx.negative?
|
|
81
234
|
|
|
82
|
-
|
|
83
|
-
|
|
235
|
+
start_idx.downto(0) do |i|
|
|
236
|
+
entry = Reline::HISTORY[i]
|
|
237
|
+
next if entry.nil? || (dedup?(buffer) && duplicate_of_newer?(i, entry))
|
|
84
238
|
|
|
85
|
-
|
|
239
|
+
return i if entry.start_with?(buffer)
|
|
240
|
+
end
|
|
241
|
+
nil
|
|
86
242
|
end
|
|
87
243
|
|
|
88
|
-
#
|
|
244
|
+
# Finds the index of the next (newer) history entry starting with +buffer+.
|
|
89
245
|
#
|
|
90
246
|
# @private
|
|
91
|
-
# @param [String]
|
|
92
|
-
# @
|
|
93
|
-
|
|
94
|
-
|
|
247
|
+
# @param [String] buffer Search prefix.
|
|
248
|
+
# @param [Integer, nil] from_pointer Current history pointer.
|
|
249
|
+
# @return [Integer, nil]
|
|
250
|
+
def find_next_match(buffer, from_pointer) # rubocop:disable Metrics/CyclomaticComplexity
|
|
251
|
+
return nil unless from_pointer
|
|
95
252
|
|
|
96
|
-
|
|
253
|
+
start_idx = from_pointer + 1
|
|
254
|
+
return nil if start_idx > Reline::HISTORY.size - 1
|
|
97
255
|
|
|
98
|
-
|
|
99
|
-
|
|
256
|
+
(start_idx...Reline::HISTORY.size).each do |i|
|
|
257
|
+
entry = Reline::HISTORY[i]
|
|
258
|
+
next if entry.nil? || (dedup?(buffer) && duplicate_of_newer?(i, entry))
|
|
100
259
|
|
|
101
|
-
|
|
260
|
+
return i if entry.start_with?(buffer)
|
|
261
|
+
end
|
|
262
|
+
nil
|
|
102
263
|
end
|
|
103
264
|
|
|
104
|
-
#
|
|
265
|
+
# Whether duplicate collapsing is active (only for prefix search).
|
|
105
266
|
#
|
|
106
267
|
# @private
|
|
107
|
-
# @param [
|
|
108
|
-
# @return [
|
|
109
|
-
def
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
113
|
-
output = Reline.core.instance_variable_get(:@output)
|
|
268
|
+
# @param [String] buffer Search prefix.
|
|
269
|
+
# @return [Boolean]
|
|
270
|
+
def dedup?(buffer)
|
|
271
|
+
!buffer.empty?
|
|
272
|
+
end
|
|
114
273
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
274
|
+
# Whether +entry+ at index +idx+ is a consecutive duplicate of the next entry.
|
|
275
|
+
#
|
|
276
|
+
# @private
|
|
277
|
+
# @param [Integer] idx
|
|
278
|
+
# @param [String] entry
|
|
279
|
+
# @return [Boolean]
|
|
280
|
+
def duplicate_of_newer?(idx, entry)
|
|
281
|
+
idx < Reline::HISTORY.size - 1 && entry == Reline::HISTORY[idx + 1]
|
|
120
282
|
end
|
|
121
283
|
|
|
122
|
-
#
|
|
284
|
+
# Renders ghost text for the current buffer, if a suggestion exists.
|
|
123
285
|
#
|
|
124
286
|
# @private
|
|
125
|
-
# @param [Array<String>] lines The ghost split into lines.
|
|
126
287
|
# @return [void]
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
output = Reline.core.instance_variable_get(:@output)
|
|
288
|
+
def render_ghost_suggestion
|
|
289
|
+
buffer = whole_buffer
|
|
290
|
+
@ghost_line_count = 0
|
|
291
|
+
return if buffer.empty?
|
|
132
292
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
293
|
+
suggestion = find_suggestion(buffer)
|
|
294
|
+
return unless suggestion
|
|
295
|
+
|
|
296
|
+
ghost = suggestion[buffer.size..]
|
|
297
|
+
return if ghost.nil? || ghost.empty?
|
|
298
|
+
|
|
299
|
+
render_ghost(ghost, suggestion)
|
|
136
300
|
end
|
|
137
301
|
|
|
138
302
|
# Finds the most recent history entry that starts with the given buffer.
|
|
@@ -158,6 +322,166 @@ module Irb
|
|
|
158
322
|
@byte_pointer = sug_lines.last.bytesize
|
|
159
323
|
rerender
|
|
160
324
|
end
|
|
325
|
+
|
|
326
|
+
# Writes the ghost text (inline + extra lines) to terminal output.
|
|
327
|
+
# Saves and restores cursor column so Reline's cursor tracking
|
|
328
|
+
# (used for left/right arrow positioning) is not disturbed.
|
|
329
|
+
#
|
|
330
|
+
# If +suggestion+ is provided and colorization is enabled, the ghost
|
|
331
|
+
# is rendered with syntax highlighting via IRB::Color.
|
|
332
|
+
#
|
|
333
|
+
# @private
|
|
334
|
+
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
335
|
+
# @param [String, nil] suggestion The full matching history entry.
|
|
336
|
+
# @return [void]
|
|
337
|
+
def render_ghost(ghost, suggestion = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
338
|
+
output = Reline.core.instance_variable_get(:@output)
|
|
339
|
+
display_lines = ghost_display_lines(ghost, suggestion)
|
|
340
|
+
@ghost_line_count = display_lines.size - 1
|
|
341
|
+
@has_inline_ghost = true
|
|
342
|
+
|
|
343
|
+
output.write(move_to_buffer_end)
|
|
344
|
+
first_line = display_lines.first
|
|
345
|
+
output.write(first_line) if first_line && !first_line.empty?
|
|
346
|
+
write_extra_ghost_lines(display_lines.drop(1))
|
|
347
|
+
origin_column = cursor_column_at_byte_pointer
|
|
348
|
+
output.write("\e[#{@ghost_line_count}A") if @ghost_line_count.positive?
|
|
349
|
+
output.write("\e[0G\e[#{origin_column}C")
|
|
350
|
+
output.flush
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Returns ghost lines ready for terminal output (with ANSI codes).
|
|
354
|
+
#
|
|
355
|
+
# When colorization is enabled, the full suggestion is colorized via
|
|
356
|
+
# IRB::Color and the ghost portion is extracted from the colored output.
|
|
357
|
+
# Otherwise, each line is wrapped in GRAY/RESET.
|
|
358
|
+
#
|
|
359
|
+
# @private
|
|
360
|
+
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
361
|
+
# @param [String, nil] suggestion The full matching history entry.
|
|
362
|
+
# @raise [StandardError]
|
|
363
|
+
# @return [Array<String>]
|
|
364
|
+
def ghost_display_lines(ghost, suggestion)
|
|
365
|
+
if suggestion && use_colorize?
|
|
366
|
+
colorize_ghost_lines(ghost, suggestion)
|
|
367
|
+
else
|
|
368
|
+
ghost.split("\n").map { |line| "#{GRAY}#{line}#{RESET}" }
|
|
369
|
+
end
|
|
370
|
+
rescue StandardError
|
|
371
|
+
ghost.split("\n").map { |line| "#{GRAY}#{line}#{RESET}" }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Checks whether syntax coloring is available and enabled.
|
|
375
|
+
#
|
|
376
|
+
# @private
|
|
377
|
+
# @return [Boolean]
|
|
378
|
+
def use_colorize?
|
|
379
|
+
defined?(IRB::Color) &&
|
|
380
|
+
IRB::Color.colorable? &&
|
|
381
|
+
IRB.conf.fetch(:USE_COLORIZE, true)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Colorizes the full suggestion and extracts the ghost portion.
|
|
385
|
+
#
|
|
386
|
+
# @private
|
|
387
|
+
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
388
|
+
# @param [String] suggestion The full matching history entry.
|
|
389
|
+
# @return [Array<String>] Colorized ghost lines with ANSI codes.
|
|
390
|
+
def colorize_ghost_lines(ghost, suggestion)
|
|
391
|
+
colored = IRB::Color.colorize_code(suggestion)
|
|
392
|
+
ghost_byte_start = suggestion.bytesize - ghost.bytesize
|
|
393
|
+
colored_ghost = extract_ansi_colored_suffix(colored, ghost_byte_start)
|
|
394
|
+
colored_ghost.split("\n").map { |line| dim_line(line) }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Prepends each ANSI foreground color code with +2;+ (dim)
|
|
398
|
+
# and strips non-color attributes (bold, underline, reverse…).
|
|
399
|
+
# Inner full resets are replaced with +RESET_COLOR+ so dim
|
|
400
|
+
# stays active across token boundaries.
|
|
401
|
+
#
|
|
402
|
+
# @private
|
|
403
|
+
# @param [String] line ANSI-colored line.
|
|
404
|
+
# @return [String] Dimmed ANSI-colored line.
|
|
405
|
+
def dim_line(line)
|
|
406
|
+
inner = line.gsub(/\e\[(\d+(?:;\d+)*)m/) do
|
|
407
|
+
params = Regexp.last_match(1)
|
|
408
|
+
next RESET_COLOR if params == '0'
|
|
409
|
+
|
|
410
|
+
color = params.split(';').map(&:to_i).select { |p| FG_COLORS.include?(p) }
|
|
411
|
+
color.empty? ? '' : "\e[2;#{color.join(';')}m"
|
|
412
|
+
end
|
|
413
|
+
"#{DIM}#{inner}#{RESET}"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Extracts the suffix of an ANSI-colored string starting at a given
|
|
417
|
+
# visible byte offset, preserving all ANSI codes.
|
|
418
|
+
#
|
|
419
|
+
# @private
|
|
420
|
+
# @param [String] colored_text Text with embedded ANSI escape sequences.
|
|
421
|
+
# @param [Integer] visible_byte_offset Offset in visible (non-ANSI) bytes.
|
|
422
|
+
# @return [String]
|
|
423
|
+
def extract_ansi_colored_suffix(colored_text, visible_byte_offset) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
424
|
+
pos = 0
|
|
425
|
+
visible = 0
|
|
426
|
+
pending_code = nil
|
|
427
|
+
|
|
428
|
+
while visible < visible_byte_offset && pos < colored_text.length
|
|
429
|
+
if colored_text[pos] == "\e"
|
|
430
|
+
code_start = pos
|
|
431
|
+
pos = colored_text.index('m', pos)&.succ || colored_text.length
|
|
432
|
+
code = colored_text[code_start...pos]
|
|
433
|
+
|
|
434
|
+
pending_code = [RESET, "\e[m"].include?(code) ? nil : code
|
|
435
|
+
else
|
|
436
|
+
visible += 1
|
|
437
|
+
pos += 1
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
suffix = colored_text[pos..] || String.new
|
|
442
|
+
pending_code ? "#{pending_code}#{suffix}" : suffix
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Writes extra ghost lines below the current buffer line with prompt-width alignment.
|
|
446
|
+
#
|
|
447
|
+
# @private
|
|
448
|
+
# @param [Array<String>] lines Extra lines with ANSI codes (excluding first inline line).
|
|
449
|
+
# @return [void]
|
|
450
|
+
def write_extra_ghost_lines(lines)
|
|
451
|
+
return if lines.empty?
|
|
452
|
+
|
|
453
|
+
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
454
|
+
output = Reline.core.instance_variable_get(:@output)
|
|
455
|
+
|
|
456
|
+
lines.each do |line|
|
|
457
|
+
output.write("\n\e[K")
|
|
458
|
+
output.write("\e[#{prompt_width}C") if prompt_width.positive?
|
|
459
|
+
output.write(line)
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Returns the terminal column where the cursor should sit (after the
|
|
464
|
+
# prompt and currently typed text at +@byte_pointer+).
|
|
465
|
+
#
|
|
466
|
+
# @private
|
|
467
|
+
# @return [Integer]
|
|
468
|
+
def cursor_column_at_byte_pointer
|
|
469
|
+
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
470
|
+
current_line = @buffer_of_lines[@line_index] || ''
|
|
471
|
+
prompt_width + Reline::Unicode.calculate_width(current_line[0...@byte_pointer])
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Moves cursor to the end of the buffer line (for writing inline ghost).
|
|
475
|
+
#
|
|
476
|
+
# @private
|
|
477
|
+
# @return [String]
|
|
478
|
+
def move_to_buffer_end
|
|
479
|
+
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
480
|
+
current_line = @buffer_of_lines[@line_index] || ''
|
|
481
|
+
buf_end = prompt_width + Reline::Unicode.calculate_width(current_line)
|
|
482
|
+
"\e[0G\e[#{buf_end}C"
|
|
483
|
+
end
|
|
161
484
|
end
|
|
162
485
|
end
|
|
163
486
|
end
|
|
487
|
+
# 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.1
|
|
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-30 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.
|