irb-autosuggestions 0.2.1 → 0.2.3
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/CHANGELOG.md +79 -0
- data/Gemfile.lock +15 -14
- data/Gemfile_2_7.lock +14 -13
- data/README.md +41 -2
- data/lib/irb/autosuggestions/line_editor_patch.rb +268 -36
- data/lib/irb/autosuggestions/version.rb +1 -1
- data/readme.png +0 -0
- data/sig/lib/irb/autosuggestions/line_editor_patch.rbs +17 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4707ca01caa7501953cc12ea344aa7b4ebcc818c3e505a72f5f102f5e702e215
|
|
4
|
+
data.tar.gz: 343171aa866561a3e4af8502df4f02a45ba4df53b45487da08a78fa9d9dad764
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8921326d3b52cba86e73a10fc8ca7eb88322897ad4296dafe2e34e2cd440c88de61086fb522ebf1a06367ec380dc6f1bb2f08afe420347a152d43bb68f393207
|
|
7
|
+
data.tar.gz: 3596a6d3c5a74336f68a21c1f13bff4e664ed2fbd3bd1728e84023a37a2bc2e64dd3ee74b153b810c253f8d6f85f61e5fc962c57bf9927afad4ada3037e77fc1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.3] - 2026-08-21
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Text duplication and stale ghost fragments when typing lines that wrap past the terminal edge; ghost preview is now
|
|
8
|
+
hidden for suggestions that do not fit the current row instead of corrupting the display
|
|
9
|
+
- Dim/color state leaking into subsequent input after accepting a suggestion
|
|
10
|
+
|
|
11
|
+
## [0.2.2] - 2026-07-08
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Alternative key bindings to accept ghost suggestion: Tab, Ctrl+F, Ctrl+E (configurable via
|
|
16
|
+
`IRB.conf[:AUTOSUGGESTION_ACCEPT_KEYS]`) ([#15], [#157])
|
|
17
|
+
- Configurable ghost color/style via `IRB.conf[:AUTOSUGGESTION_GHOST_COLOR]` (ANSI string) and
|
|
18
|
+
`IRB.conf[:AUTOSUGGESTION_GHOST_STYLE]` (declarative hash) ([#16], [#163])
|
|
19
|
+
- GitHub issue templates (bug report, feature request) and pull request template
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Ghost text artifacts and cursor misalignment after accepting multiline (10+ lines) suggestions ([#19], [#256])
|
|
24
|
+
- Auto-execute guard — verified Enter always fires exactly once regardless of ghost visibility ([#18], [#159])
|
|
25
|
+
|
|
26
|
+
## [0.2.1] - 2026-07-05
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Ruby 4.0 compatibility: arrow (`ed_next_char`) not erasing after suggestion accept ([#13])
|
|
31
|
+
|
|
32
|
+
## [0.2.0] - 2026-06-30
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Syntax-colored ghost text via `IRB::Color.colorize_code` (dimmed)
|
|
37
|
+
- Prefix-filtered history navigation (up/down arrows match typed prefix)
|
|
38
|
+
- Ruby 2.7 support
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Ghost text now uses dim syntax colors when `IRB.conf[:USE_COLORIZE]` is enabled
|
|
43
|
+
|
|
44
|
+
## [0.1.1] - 2026-06-25
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `IRB.conf[:USE_AUTOSUGGESTIONS]` toggle
|
|
49
|
+
- Environment variable `IRB_AUTOSUGGESTIONS` override
|
|
50
|
+
- RBS type signatures
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Gem metadata (licenses, homepage)
|
|
55
|
+
|
|
56
|
+
## [0.1.0] - 2026-06-20
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- Initial release: fish-like autosuggestions from `Reline::HISTORY`
|
|
61
|
+
- Right arrow key accepts ghost suggestion
|
|
62
|
+
- Gray ghost text (`\e[90m`)
|
|
63
|
+
- Configuration via `IRB.conf[:USE_AUTOSUGGESTIONS]`
|
|
64
|
+
|
|
65
|
+
[#15]: https://github.com/unurgunite/irb-autosuggestions/pull/15
|
|
66
|
+
|
|
67
|
+
[#16]: https://github.com/unurgunite/irb-autosuggestions/pull/16
|
|
68
|
+
|
|
69
|
+
[#18]: https://github.com/unurgunite/irb-autosuggestions/pull/18
|
|
70
|
+
|
|
71
|
+
[#19]: https://github.com/unurgunite/irb-autosuggestions/pull/19
|
|
72
|
+
|
|
73
|
+
[#157]: https://github.com/unurgunite/irb-autosuggestions/issues/157
|
|
74
|
+
|
|
75
|
+
[#163]: https://github.com/unurgunite/irb-autosuggestions/issues/163
|
|
76
|
+
|
|
77
|
+
[#159]: https://github.com/unurgunite/irb-autosuggestions/issues/159
|
|
78
|
+
|
|
79
|
+
[#256]: https://github.com/unurgunite/irb-autosuggestions/issues/256
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
irb-autosuggestions (0.2.
|
|
4
|
+
irb-autosuggestions (0.2.3)
|
|
5
5
|
reline
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -9,16 +9,16 @@ GEM
|
|
|
9
9
|
specs:
|
|
10
10
|
ast (2.4.3)
|
|
11
11
|
diff-lcs (1.6.2)
|
|
12
|
-
docscribe (1.
|
|
12
|
+
docscribe (1.6.0)
|
|
13
13
|
parser (>= 3.3)
|
|
14
14
|
prism (~> 1.8)
|
|
15
|
-
io-console (0.
|
|
16
|
-
json (2.
|
|
17
|
-
language_server-protocol (3.17.0.
|
|
15
|
+
io-console (0.9.2)
|
|
16
|
+
json (2.21.2)
|
|
17
|
+
language_server-protocol (3.17.0.6)
|
|
18
18
|
lint_roller (1.1.0)
|
|
19
19
|
logger (1.7.0)
|
|
20
20
|
parallel (1.28.0)
|
|
21
|
-
parser (3.3.
|
|
21
|
+
parser (3.3.12.0)
|
|
22
22
|
ast (~> 2.4.1)
|
|
23
23
|
racc
|
|
24
24
|
prism (1.9.0)
|
|
@@ -28,7 +28,7 @@ GEM
|
|
|
28
28
|
rbs (3.6.1)
|
|
29
29
|
logger
|
|
30
30
|
regexp_parser (2.12.0)
|
|
31
|
-
reline (0.
|
|
31
|
+
reline (0.7.0)
|
|
32
32
|
io-console (~> 0.5)
|
|
33
33
|
rspec (3.13.2)
|
|
34
34
|
rspec-core (~> 3.13.0)
|
|
@@ -43,7 +43,7 @@ GEM
|
|
|
43
43
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
44
44
|
rspec-support (~> 3.13.0)
|
|
45
45
|
rspec-support (3.13.7)
|
|
46
|
-
rubocop (1.
|
|
46
|
+
rubocop (1.89.0)
|
|
47
47
|
json (~> 2.3)
|
|
48
48
|
language_server-protocol (~> 3.17.0.2)
|
|
49
49
|
lint_roller (~> 1.1.0)
|
|
@@ -54,23 +54,24 @@ GEM
|
|
|
54
54
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
55
55
|
ruby-progressbar (~> 1.7)
|
|
56
56
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
57
|
-
rubocop-ast (1.
|
|
57
|
+
rubocop-ast (1.50.0)
|
|
58
58
|
parser (>= 3.3.7.2)
|
|
59
59
|
prism (~> 1.7)
|
|
60
60
|
rubocop-rake (0.7.1)
|
|
61
61
|
lint_roller (~> 1.1)
|
|
62
62
|
rubocop (>= 1.72.1)
|
|
63
|
-
rubocop-rspec (3.
|
|
63
|
+
rubocop-rspec (3.10.2)
|
|
64
64
|
lint_roller (~> 1.1)
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
regexp_parser (>= 2.0)
|
|
66
|
+
rubocop (~> 1.86, >= 1.86.2)
|
|
67
|
+
rubocop-sorted_methods_by_call (1.2.3)
|
|
67
68
|
lint_roller
|
|
68
69
|
rubocop (>= 1.72.0)
|
|
69
70
|
ruby-progressbar (1.13.0)
|
|
70
71
|
unicode-display_width (3.2.0)
|
|
71
72
|
unicode-emoji (~> 4.1)
|
|
72
73
|
unicode-emoji (4.2.0)
|
|
73
|
-
yard (0.9.
|
|
74
|
+
yard (0.9.45)
|
|
74
75
|
|
|
75
76
|
PLATFORMS
|
|
76
77
|
arm64-darwin-24
|
|
@@ -91,4 +92,4 @@ DEPENDENCIES
|
|
|
91
92
|
yard (>= 0.9.38)
|
|
92
93
|
|
|
93
94
|
BUNDLED WITH
|
|
94
|
-
|
|
95
|
+
2.4.22
|
data/Gemfile_2_7.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
irb-autosuggestions (0.2.
|
|
4
|
+
irb-autosuggestions (0.2.3)
|
|
5
5
|
reline
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -9,15 +9,15 @@ GEM
|
|
|
9
9
|
specs:
|
|
10
10
|
ast (2.4.3)
|
|
11
11
|
diff-lcs (1.6.2)
|
|
12
|
-
docscribe (1.
|
|
12
|
+
docscribe (1.6.0)
|
|
13
13
|
parser (>= 3.3)
|
|
14
14
|
prism (~> 1.8)
|
|
15
|
-
io-console (0.
|
|
16
|
-
json (2.
|
|
17
|
-
language_server-protocol (3.17.0.
|
|
15
|
+
io-console (0.9.2)
|
|
16
|
+
json (2.21.2)
|
|
17
|
+
language_server-protocol (3.17.0.6)
|
|
18
18
|
lint_roller (1.1.0)
|
|
19
19
|
parallel (1.28.0)
|
|
20
|
-
parser (3.3.
|
|
20
|
+
parser (3.3.12.0)
|
|
21
21
|
ast (~> 2.4.1)
|
|
22
22
|
racc
|
|
23
23
|
prism (1.9.0)
|
|
@@ -25,7 +25,7 @@ GEM
|
|
|
25
25
|
rainbow (3.1.1)
|
|
26
26
|
rake (13.4.2)
|
|
27
27
|
regexp_parser (2.12.0)
|
|
28
|
-
reline (0.
|
|
28
|
+
reline (0.7.0)
|
|
29
29
|
io-console (~> 0.5)
|
|
30
30
|
rspec (3.13.2)
|
|
31
31
|
rspec-core (~> 3.13.0)
|
|
@@ -40,7 +40,7 @@ GEM
|
|
|
40
40
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
41
41
|
rspec-support (~> 3.13.0)
|
|
42
42
|
rspec-support (3.13.7)
|
|
43
|
-
rubocop (1.
|
|
43
|
+
rubocop (1.89.0)
|
|
44
44
|
json (~> 2.3)
|
|
45
45
|
language_server-protocol (~> 3.17.0.2)
|
|
46
46
|
lint_roller (~> 1.1.0)
|
|
@@ -51,23 +51,24 @@ GEM
|
|
|
51
51
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
52
52
|
ruby-progressbar (~> 1.7)
|
|
53
53
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
54
|
-
rubocop-ast (1.
|
|
54
|
+
rubocop-ast (1.50.0)
|
|
55
55
|
parser (>= 3.3.7.2)
|
|
56
56
|
prism (~> 1.7)
|
|
57
57
|
rubocop-rake (0.7.1)
|
|
58
58
|
lint_roller (~> 1.1)
|
|
59
59
|
rubocop (>= 1.72.1)
|
|
60
|
-
rubocop-rspec (3.
|
|
60
|
+
rubocop-rspec (3.10.2)
|
|
61
61
|
lint_roller (~> 1.1)
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
regexp_parser (>= 2.0)
|
|
63
|
+
rubocop (~> 1.86, >= 1.86.2)
|
|
64
|
+
rubocop-sorted_methods_by_call (1.2.3)
|
|
64
65
|
lint_roller
|
|
65
66
|
rubocop (>= 1.72.0)
|
|
66
67
|
ruby-progressbar (1.13.0)
|
|
67
68
|
unicode-display_width (3.2.0)
|
|
68
69
|
unicode-emoji (~> 4.1)
|
|
69
70
|
unicode-emoji (4.2.0)
|
|
70
|
-
yard (0.9.
|
|
71
|
+
yard (0.9.45)
|
|
71
72
|
|
|
72
73
|
PLATFORMS
|
|
73
74
|
arm64-darwin-25
|
data/README.md
CHANGED
|
@@ -18,7 +18,9 @@ No need to explain. Fish-like autosuggestions for IRB — ghost text from histor
|
|
|
18
18
|
* [Usage](#usage)
|
|
19
19
|
* [Prefix-filtered history navigation](#prefix-filtered-history-navigation)
|
|
20
20
|
* [Configuration](#configuration)
|
|
21
|
+
* [Ghost color](#ghost-color)
|
|
21
22
|
* [Colors](#colors)
|
|
23
|
+
* [Accept keys](#accept-keys)
|
|
22
24
|
* [How it works](#how-it-works)
|
|
23
25
|
* [Development](#development)
|
|
24
26
|
* [License](#license)
|
|
@@ -48,7 +50,8 @@ irb(main):002* el.succ <- "cc" in gray
|
|
|
48
50
|
irb(main):003> end <- "d" in gray
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
Press **right arrow** (`->`) to accept the full multiline suggestion.
|
|
53
|
+
Press **right arrow** (`->`), **Tab**, **Ctrl+F**, or **Ctrl+E** to accept the full multiline suggestion. Tab falls
|
|
54
|
+
back to normal completion when no suggestion is visible. Accept keys are configurable (see below).
|
|
52
55
|
|
|
53
56
|
### Prefix-filtered history navigation
|
|
54
57
|
|
|
@@ -96,6 +99,29 @@ Or via environment variable:
|
|
|
96
99
|
export IRB_AUTOSUGGESTIONS=0
|
|
97
100
|
```
|
|
98
101
|
|
|
102
|
+
### Ghost color
|
|
103
|
+
|
|
104
|
+
The plain gray fallback color can be customized. Syntax-colored ghost text (when `IRB::Color` is available) always
|
|
105
|
+
takes precedence over these settings.
|
|
106
|
+
|
|
107
|
+
Set a raw ANSI escape code:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
IRB.conf[:AUTOSUGGESTION_GHOST_COLOR] = "\e[32m" # green
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or use the declarative style hash:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
IRB.conf[:AUTOSUGGESTION_GHOST_STYLE] = { fg: :bright_black, italic: true }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Supported colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `default`,
|
|
120
|
+
`bright_black`, `bright_red`, `bright_green`, `bright_yellow`, `bright_blue`, `bright_magenta`,
|
|
121
|
+
`bright_cyan`, `bright_white`.
|
|
122
|
+
|
|
123
|
+
Supported attributes: `bold`, `dim`, `italic`, `underline`, `blink`, `reverse`.
|
|
124
|
+
|
|
99
125
|
### Colors
|
|
100
126
|
|
|
101
127
|
Syntax-colored ghost text is **enabled by default** when `IRB::Color` is available and `IRB.conf[:USE_COLORIZE]` is
|
|
@@ -118,6 +144,19 @@ irb --nocolorize
|
|
|
118
144
|
> schemes. If you notice visual artifacts (e.g., wrong colors, underlines, or unusual brightness), try disabling the
|
|
119
145
|
> feature or switch to the gray fallback or create new issue.
|
|
120
146
|
|
|
147
|
+
### Accept keys
|
|
148
|
+
|
|
149
|
+
Configure which keys accept the ghost suggestion:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
IRB.conf[:AUTOSUGGESTION_ACCEPT_KEYS] = %i[ed_next_char tab ed_end_of_line]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Defaults: `[:ed_next_char, :ed_end_of_line, :tab]` (right arrow/Ctrl+F, Ctrl+E, Tab).
|
|
156
|
+
|
|
157
|
+
Available symbols: `:ed_next_char` (right arrow/Ctrl+F), `:ed_end_of_line` (Ctrl+E), `:tab`, or
|
|
158
|
+
any Reline method symbol. When no suggestion is visible, the key falls through to its normal behavior.
|
|
159
|
+
|
|
121
160
|
### How it works
|
|
122
161
|
|
|
123
162
|
- Each keystroke queries `Reline::HISTORY` for the most recent entry whose prefix matches the current buffer.
|
|
@@ -127,7 +166,7 @@ irb --nocolorize
|
|
|
127
166
|
- Extra ghost lines (for multiline history entries) are drawn below the prompt.
|
|
128
167
|
- Ghost artifacts are cleared each frame using cursor save/restore (`\e[s`/`\e[u`) and per-line clearing (`\e[2K`),
|
|
129
168
|
avoiding `\e[J` which interfered with Reline's cursor tracking.
|
|
130
|
-
-
|
|
169
|
+
- Configurable accept keys (right arrow, Tab, Ctrl+F, Ctrl+E by default) replace the buffer with the ghost text.
|
|
131
170
|
- Up/down arrows use prefix-filtered navigation when the buffer is non-empty, falling back to unfiltered browsing
|
|
132
171
|
when nothing was typed.
|
|
133
172
|
|
|
@@ -14,40 +14,66 @@ module Irb
|
|
|
14
14
|
ENV_KEY = 'IRB_AUTOSUGGESTIONS'
|
|
15
15
|
CONFIG_NAV_KEY = :USE_PREFIX_HISTORY_NAVIGATION
|
|
16
16
|
ENV_NAV_KEY = 'IRB_PREFIX_HISTORY_NAVIGATION'
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
ACCEPT_KEYS_CONFIG = :AUTOSUGGESTION_ACCEPT_KEYS
|
|
18
|
+
DEFAULT_ACCEPT_KEYS = %i[ed_next_char ed_end_of_line tab].freeze
|
|
19
|
+
GHOST_COLOR_CONFIG = :AUTOSUGGESTION_GHOST_COLOR
|
|
20
|
+
GHOST_STYLE_CONFIG = :AUTOSUGGESTION_GHOST_STYLE
|
|
21
|
+
DEFAULT_GHOST_COLOR = "\e[90m"
|
|
22
|
+
FG_MAP = {
|
|
23
|
+
black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37,
|
|
24
|
+
default: 39,
|
|
25
|
+
bright_black: 90, bright_red: 91, bright_green: 92, bright_yellow: 93,
|
|
26
|
+
bright_blue: 94, bright_magenta: 95, bright_cyan: 96, bright_white: 97
|
|
27
|
+
}.freeze
|
|
28
|
+
ATTR_MAP = {
|
|
29
|
+
bold: 1, dim: 2, italic: 3, underline: 4, blink: 5, reverse: 7
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# Intercepts key input to accept autosuggestions on configured keys
|
|
19
33
|
# and clears the prefix navigation anchor on non-history keys.
|
|
20
34
|
#
|
|
21
35
|
# @param [Object] key A Reline key event.
|
|
22
36
|
# @return [Object]
|
|
23
37
|
def input_key(key)
|
|
38
|
+
# Some keys (Enter, submission) bypass rerender, so ghost is cleared
|
|
39
|
+
# on every key to avoid stale ghost lines leftover on screen.
|
|
40
|
+
clear_previous_ghost if enabled?
|
|
41
|
+
|
|
24
42
|
clear_prefix_anchor if navigation_enabled? && !history_navigation_key?(key)
|
|
25
43
|
|
|
26
|
-
if enabled? &&
|
|
27
|
-
buffer = whole_buffer
|
|
28
|
-
suggestion = find_suggestion(buffer)
|
|
29
|
-
|
|
30
|
-
if suggestion && suggestion != buffer
|
|
31
|
-
accept_suggestion(suggestion)
|
|
32
|
-
return
|
|
33
|
-
end
|
|
34
|
-
end
|
|
44
|
+
return if enabled? && accept_key?(key) && accept_matching_suggestion?
|
|
35
45
|
|
|
36
46
|
super
|
|
37
47
|
end
|
|
38
48
|
|
|
49
|
+
# Accepts the current suggestion when it extends the buffer.
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] true when a suggestion was accepted
|
|
52
|
+
def accept_matching_suggestion?
|
|
53
|
+
buffer = whole_buffer
|
|
54
|
+
suggestion = find_suggestion(buffer)
|
|
55
|
+
return false unless suggestion && suggestion != buffer
|
|
56
|
+
|
|
57
|
+
accept_suggestion(suggestion)
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
39
61
|
# Injects ghost text into terminal output after Reline finishes rendering.
|
|
40
62
|
# Must be public because Reline::Core calls +rerender+ externally.
|
|
41
63
|
# Works on both Reline 0.3.x (rerender is the main rendering entry) and
|
|
42
64
|
# Reline 0.4+ (rerender calls render internally).
|
|
43
65
|
#
|
|
66
|
+
# Ghost is cleared BEFORE super to avoid cursor-position-dependent
|
|
67
|
+
# escapes targeting wrong lines after accept_suggestion changes buffer.
|
|
68
|
+
#
|
|
44
69
|
# @return [Object] The result of +super+.
|
|
45
70
|
def rerender
|
|
71
|
+
clear_previous_ghost if enabled?
|
|
72
|
+
|
|
46
73
|
result = super
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
74
|
+
|
|
75
|
+
render_ghost_suggestion if enabled?
|
|
76
|
+
|
|
51
77
|
result
|
|
52
78
|
end
|
|
53
79
|
|
|
@@ -212,14 +238,34 @@ module Irb
|
|
|
212
238
|
end
|
|
213
239
|
end
|
|
214
240
|
|
|
215
|
-
# Checks if a key event
|
|
241
|
+
# Checks if a key event matches any configured accept key.
|
|
242
|
+
# Defaults to right arrow (:ed_next_char).
|
|
243
|
+
# Tab is matched specially — method_symbol == :ed_insert + char == "\t".
|
|
216
244
|
#
|
|
217
245
|
# @private
|
|
218
246
|
# @param [Object] key A Reline key event.
|
|
219
247
|
# @return [Boolean]
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
248
|
+
def accept_key?(key)
|
|
249
|
+
accept_keys = IRB.conf.fetch(ACCEPT_KEYS_CONFIG, DEFAULT_ACCEPT_KEYS)
|
|
250
|
+
accept_keys.any? { |k| key_match?(key, k) }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Checks if a key matches a given config symbol.
|
|
254
|
+
#
|
|
255
|
+
# @private
|
|
256
|
+
# @param [Object] key A Reline key event.
|
|
257
|
+
# @param [Symbol] config_symbol One of :ed_next_char, :ed_end_of_line, :tab, etc.
|
|
258
|
+
# @return [Boolean]
|
|
259
|
+
def key_match?(key, config_symbol)
|
|
260
|
+
return false unless key.respond_to?(:method_symbol)
|
|
261
|
+
|
|
262
|
+
if config_symbol == :tab
|
|
263
|
+
key.method_symbol == :ed_insert &&
|
|
264
|
+
key.respond_to?(:char) &&
|
|
265
|
+
key.char == "\t"
|
|
266
|
+
else
|
|
267
|
+
key.method_symbol == config_symbol
|
|
268
|
+
end
|
|
223
269
|
end
|
|
224
270
|
|
|
225
271
|
# Finds the index of the previous (older) history entry starting with +buffer+.
|
|
@@ -283,6 +329,13 @@ module Irb
|
|
|
283
329
|
|
|
284
330
|
# Renders ghost text for the current buffer, if a suggestion exists.
|
|
285
331
|
#
|
|
332
|
+
# The ghost is reduced to fit the terminal width so it never wraps
|
|
333
|
+
# onto the following row. A ghost that wraps corrupts Reline's
|
|
334
|
+
# single-row cursor model (duplicated text, stale ANSI state),
|
|
335
|
+
# because Reline does not track visual wrapping of long lines.
|
|
336
|
+
# Suggestions remain accept-able via the configured keys even when
|
|
337
|
+
# the ghost is truncated.
|
|
338
|
+
#
|
|
286
339
|
# @private
|
|
287
340
|
# @return [void]
|
|
288
341
|
def render_ghost_suggestion
|
|
@@ -296,7 +349,99 @@ module Irb
|
|
|
296
349
|
ghost = suggestion[buffer.size..]
|
|
297
350
|
return if ghost.nil? || ghost.empty?
|
|
298
351
|
|
|
299
|
-
render_ghost(ghost, suggestion)
|
|
352
|
+
render_ghost(truncate_ghost_to_terminal(ghost), suggestion, suggestion.bytesize - ghost.bytesize)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Truncates ghost text so no visible line exceeds the terminal width:
|
|
356
|
+
# the inline part is capped at the space left on the current line,
|
|
357
|
+
# each following line at the space left after the prompt.
|
|
358
|
+
#
|
|
359
|
+
# @private
|
|
360
|
+
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
361
|
+
# @return [String]
|
|
362
|
+
def truncate_ghost_to_terminal(ghost)
|
|
363
|
+
inline_limit = terminal_width - current_line_width - 1
|
|
364
|
+
return String.new if inline_limit <= 0
|
|
365
|
+
|
|
366
|
+
lines = ghost.split("\n", -1)
|
|
367
|
+
truncate_to_width(lines.first, inline_limit) + truncate_rest_lines(lines)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Truncates every ghost line after the first at the width left
|
|
371
|
+
# after the prompt, keeping line breaks intact.
|
|
372
|
+
#
|
|
373
|
+
# @private
|
|
374
|
+
# @param [Array<String>] lines
|
|
375
|
+
# @return [String]
|
|
376
|
+
def truncate_rest_lines(lines)
|
|
377
|
+
rest_limit = terminal_width - prompt_width - 1
|
|
378
|
+
rest = lines.drop(1).map { |line| truncate_to_width(line, rest_limit) }
|
|
379
|
+
rest.empty? ? String.new : "\n#{rest.join("\n")}"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Display width of the first ghost line.
|
|
383
|
+
#
|
|
384
|
+
# @private
|
|
385
|
+
# Truncates a string to the given display width, appending an
|
|
386
|
+
# ellipsis when cut. Counts Unicode display width, so multi-column
|
|
387
|
+
# characters are handled correctly.
|
|
388
|
+
#
|
|
389
|
+
# @private
|
|
390
|
+
# @param [String] str
|
|
391
|
+
# @param [Integer] max_width
|
|
392
|
+
# @return [String]
|
|
393
|
+
def truncate_to_width(str, max_width)
|
|
394
|
+
return String.new if max_width.negative?
|
|
395
|
+
return str if max_width.zero? || Reline::Unicode.calculate_width(str) <= max_width
|
|
396
|
+
|
|
397
|
+
"#{chars_up_to(str, max_width - 1)}…"
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Characters of +str+ whose combined display width does not
|
|
401
|
+
# exceed +max_width+.
|
|
402
|
+
#
|
|
403
|
+
# @private
|
|
404
|
+
# @param [String] str
|
|
405
|
+
# @param [Integer] max_width
|
|
406
|
+
# @return [String]
|
|
407
|
+
def chars_up_to(str, max_width)
|
|
408
|
+
width = 0
|
|
409
|
+
str.each_char.with_object(+'') do |char, out|
|
|
410
|
+
char_width = Reline::Unicode.calculate_width(char)
|
|
411
|
+
break out if width + char_width > max_width
|
|
412
|
+
|
|
413
|
+
width += char_width
|
|
414
|
+
out << char
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Width of the prompt (in display columns).
|
|
419
|
+
#
|
|
420
|
+
# @private
|
|
421
|
+
# @return [Integer]
|
|
422
|
+
def prompt_width
|
|
423
|
+
@prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Display width of the prompt plus the current buffer line.
|
|
427
|
+
#
|
|
428
|
+
# @private
|
|
429
|
+
# @return [Integer]
|
|
430
|
+
def current_line_width
|
|
431
|
+
line = @buffer_of_lines[@line_index] || ''
|
|
432
|
+
prompt_width + Reline::Unicode.calculate_width(line)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Terminal width in columns, falling back to 80 when unknown.
|
|
436
|
+
#
|
|
437
|
+
# @private
|
|
438
|
+
# @return [Integer]
|
|
439
|
+
def terminal_width
|
|
440
|
+
if defined?(Reline::IOGate) && Reline::IOGate.respond_to?(:get_screen_size)
|
|
441
|
+
Reline::IOGate.get_screen_size[1]
|
|
442
|
+
else
|
|
443
|
+
80
|
|
444
|
+
end
|
|
300
445
|
end
|
|
301
446
|
|
|
302
447
|
# Finds the most recent history entry that starts with the given buffer.
|
|
@@ -333,10 +478,12 @@ module Irb
|
|
|
333
478
|
# @private
|
|
334
479
|
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
335
480
|
# @param [String, nil] suggestion The full matching history entry.
|
|
481
|
+
# @param [Integer, nil] ghost_byte_start Byte offset of the ghost within the
|
|
482
|
+
# full suggestion (pre-truncation), used to extract the colored suffix.
|
|
336
483
|
# @return [void]
|
|
337
|
-
def render_ghost(ghost, suggestion = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
484
|
+
def render_ghost(ghost, suggestion = nil, ghost_byte_start = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
338
485
|
output = Reline.core.instance_variable_get(:@output)
|
|
339
|
-
display_lines = ghost_display_lines(ghost, suggestion)
|
|
486
|
+
display_lines = ghost_display_lines(ghost, suggestion, ghost_byte_start)
|
|
340
487
|
@ghost_line_count = display_lines.size - 1
|
|
341
488
|
@has_inline_ghost = true
|
|
342
489
|
|
|
@@ -354,21 +501,47 @@ module Irb
|
|
|
354
501
|
#
|
|
355
502
|
# When colorization is enabled, the full suggestion is colorized via
|
|
356
503
|
# IRB::Color and the ghost portion is extracted from the colored output.
|
|
357
|
-
# Otherwise, each line is wrapped in GRAY/RESET.
|
|
358
504
|
#
|
|
359
505
|
# @private
|
|
360
506
|
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
361
507
|
# @param [String, nil] suggestion The full matching history entry.
|
|
508
|
+
# @param [Integer, nil] ghost_byte_start Byte offset of the ghost within the
|
|
509
|
+
# full suggestion (pre-truncation), used to extract the colored suffix.
|
|
362
510
|
# @raise [StandardError]
|
|
363
511
|
# @return [Array<String>]
|
|
364
|
-
def ghost_display_lines(ghost, suggestion)
|
|
512
|
+
def ghost_display_lines(ghost, suggestion, ghost_byte_start = nil)
|
|
365
513
|
if suggestion && use_colorize?
|
|
366
|
-
colorize_ghost_lines(ghost, suggestion)
|
|
514
|
+
colorize_ghost_lines(ghost, suggestion, ghost_byte_start)
|
|
367
515
|
else
|
|
368
|
-
ghost.split("\n").map { |line| "#{
|
|
516
|
+
ghost.split("\n").map { |line| "#{ghost_color}#{line}#{RESET}" }
|
|
369
517
|
end
|
|
370
518
|
rescue StandardError
|
|
371
|
-
ghost.split("\n").map { |line| "#{
|
|
519
|
+
ghost.split("\n").map { |line| "#{ghost_color}#{line}#{RESET}" }
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Returns the effective ANSI color code for ghost text.
|
|
523
|
+
# Checks GHOST_STYLE_CONFIG first, then GHOST_COLOR_CONFIG, then default.
|
|
524
|
+
#
|
|
525
|
+
# @private
|
|
526
|
+
# @return [String]
|
|
527
|
+
def ghost_color
|
|
528
|
+
style = IRB.conf[GHOST_STYLE_CONFIG]
|
|
529
|
+
return resolve_ghost_style(style) if style.is_a?(Hash)
|
|
530
|
+
|
|
531
|
+
IRB.conf.fetch(GHOST_COLOR_CONFIG, DEFAULT_GHOST_COLOR)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Converts a style hash like +{ fg: :bright_black, italic: true }+
|
|
535
|
+
# to an ANSI escape sequence.
|
|
536
|
+
#
|
|
537
|
+
# @private
|
|
538
|
+
# @param [Hash] hash Style hash with +:fg+ color and optional attribute keys.
|
|
539
|
+
# @return [String]
|
|
540
|
+
def resolve_ghost_style(hash)
|
|
541
|
+
codes = []
|
|
542
|
+
codes << FG_MAP[hash[:fg]] if hash[:fg]
|
|
543
|
+
ATTR_MAP.each { |attr, code| codes << code if hash[attr] }
|
|
544
|
+
"\e[#{codes.join(';')}m"
|
|
372
545
|
end
|
|
373
546
|
|
|
374
547
|
# Checks whether syntax coloring is available and enabled.
|
|
@@ -383,15 +556,78 @@ module Irb
|
|
|
383
556
|
|
|
384
557
|
# Colorizes the full suggestion and extracts the ghost portion.
|
|
385
558
|
#
|
|
559
|
+
# +ghost_byte_start+ is the visible-byte offset of the ghost within
|
|
560
|
+
# the suggestion. It must come from the full (untruncated) ghost:
|
|
561
|
+
# using a truncated ghost shifts the offset into the middle of the
|
|
562
|
+
# suggestion and the colored suffix no longer matches the buffer.
|
|
563
|
+
# Colored lines are then truncated to fit the terminal width, same
|
|
564
|
+
# as the plain-text ghost.
|
|
565
|
+
#
|
|
386
566
|
# @private
|
|
387
567
|
# @param [String] ghost The ghost text (suffix of the suggestion).
|
|
388
568
|
# @param [String] suggestion The full matching history entry.
|
|
569
|
+
# @param [Integer, nil] ghost_byte_start Visible-byte offset of the ghost.
|
|
389
570
|
# @return [Array<String>] Colorized ghost lines with ANSI codes.
|
|
390
|
-
def colorize_ghost_lines(ghost, suggestion)
|
|
571
|
+
def colorize_ghost_lines(ghost, suggestion, ghost_byte_start = nil)
|
|
391
572
|
colored = IRB::Color.colorize_code(suggestion)
|
|
392
|
-
|
|
393
|
-
colored_ghost = extract_ansi_colored_suffix(colored,
|
|
394
|
-
colored_ghost
|
|
573
|
+
start = ghost_byte_start || (suggestion.bytesize - ghost.bytesize)
|
|
574
|
+
colored_ghost = extract_ansi_colored_suffix(colored, start)
|
|
575
|
+
truncate_colored_ghost(colored_ghost).map { |line| dim_line(line) }
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Truncates colorized ghost lines so no line exceeds the terminal
|
|
579
|
+
# width, mirroring +truncate_ghost_to_terminal+ for plain text.
|
|
580
|
+
#
|
|
581
|
+
# @private
|
|
582
|
+
# @param [String] colored_ghost ANSI-colored ghost text.
|
|
583
|
+
# @return [Array<String>]
|
|
584
|
+
def truncate_colored_ghost(colored_ghost)
|
|
585
|
+
lines = colored_ghost.split("\n")
|
|
586
|
+
rest_limit = terminal_width - prompt_width - 1
|
|
587
|
+
first = truncate_colored_to_width(lines.first, terminal_width - current_line_width - 1)
|
|
588
|
+
[first] + lines.drop(1).map { |line| truncate_colored_to_width(line, rest_limit) }
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Truncates an ANSI-colored line to the given display width, counting
|
|
592
|
+
# escape codes as zero-width, and appending an ellipsis when cut.
|
|
593
|
+
#
|
|
594
|
+
# @private
|
|
595
|
+
# @param [String] line ANSI-colored line.
|
|
596
|
+
# @param [Integer] max_width
|
|
597
|
+
# @return [String]
|
|
598
|
+
def truncate_colored_to_width(line, max_width)
|
|
599
|
+
return String.new if max_width <= 0
|
|
600
|
+
return line if Reline::Unicode.calculate_width(line, true) <= max_width
|
|
601
|
+
|
|
602
|
+
"#{colored_chars_up_to(line, max_width - 1)}…"
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Characters of +line+ (with ANSI codes preserved) whose combined
|
|
606
|
+
# display width does not exceed +max_width+.
|
|
607
|
+
#
|
|
608
|
+
# @private
|
|
609
|
+
# @param [String] line ANSI-colored line.
|
|
610
|
+
# @param [Integer] max_width
|
|
611
|
+
# @return [String]
|
|
612
|
+
def colored_chars_up_to(line, max_width) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
613
|
+
width = 0
|
|
614
|
+
out = +''
|
|
615
|
+
rest = line.dup
|
|
616
|
+
until rest.empty?
|
|
617
|
+
if rest.start_with?("\e")
|
|
618
|
+
code = rest[/\e\[[0-9;]*m/]
|
|
619
|
+
out << code
|
|
620
|
+
rest = code ? rest[code.length..] : ''
|
|
621
|
+
next
|
|
622
|
+
end
|
|
623
|
+
char = rest.each_char.first
|
|
624
|
+
break if width + Reline::Unicode.calculate_width(char) > max_width
|
|
625
|
+
|
|
626
|
+
width += Reline::Unicode.calculate_width(char)
|
|
627
|
+
out << char
|
|
628
|
+
rest = rest[char.length..]
|
|
629
|
+
end
|
|
630
|
+
out
|
|
395
631
|
end
|
|
396
632
|
|
|
397
633
|
# Prepends each ANSI foreground color code with +2;+ (dim)
|
|
@@ -450,7 +686,6 @@ module Irb
|
|
|
450
686
|
def write_extra_ghost_lines(lines)
|
|
451
687
|
return if lines.empty?
|
|
452
688
|
|
|
453
|
-
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
454
689
|
output = Reline.core.instance_variable_get(:@output)
|
|
455
690
|
|
|
456
691
|
lines.each do |line|
|
|
@@ -466,7 +701,6 @@ module Irb
|
|
|
466
701
|
# @private
|
|
467
702
|
# @return [Integer]
|
|
468
703
|
def cursor_column_at_byte_pointer
|
|
469
|
-
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
470
704
|
current_line = @buffer_of_lines[@line_index] || ''
|
|
471
705
|
prompt_width + Reline::Unicode.calculate_width(current_line[0...@byte_pointer])
|
|
472
706
|
end
|
|
@@ -476,10 +710,8 @@ module Irb
|
|
|
476
710
|
# @private
|
|
477
711
|
# @return [String]
|
|
478
712
|
def move_to_buffer_end
|
|
479
|
-
prompt_width = @prompt ? Reline::Unicode.calculate_width(@prompt) : 0
|
|
480
713
|
current_line = @buffer_of_lines[@line_index] || ''
|
|
481
|
-
|
|
482
|
-
"\e[0G\e[#{buf_end}C"
|
|
714
|
+
"\e[0G\e[#{prompt_width + Reline::Unicode.calculate_width(current_line)}C"
|
|
483
715
|
end
|
|
484
716
|
end
|
|
485
717
|
end
|
data/readme.png
CHANGED
|
Binary file
|
|
@@ -10,24 +10,39 @@ module Irb
|
|
|
10
10
|
CONFIG_KEY: Symbol
|
|
11
11
|
ENV_KEY: String
|
|
12
12
|
RESET_COLOR: String
|
|
13
|
+
ACCEPT_KEYS_CONFIG: Symbol
|
|
14
|
+
DEFAULT_ACCEPT_KEYS: Array[Symbol]
|
|
15
|
+
GHOST_COLOR_CONFIG: Symbol
|
|
16
|
+
GHOST_STYLE_CONFIG: Symbol
|
|
17
|
+
DEFAULT_GHOST_COLOR: String
|
|
18
|
+
FG_MAP: Hash[Symbol, Integer]
|
|
19
|
+
ATTR_MAP: Hash[Symbol, Integer]
|
|
13
20
|
|
|
14
21
|
@buffer_of_lines: Array[String]
|
|
15
22
|
@byte_pointer: Integer
|
|
16
23
|
@line_index: Integer
|
|
17
24
|
|
|
18
25
|
def input_key: (untyped key) -> untyped
|
|
26
|
+
def rerender: () -> untyped
|
|
19
27
|
def render: (*untyped) -> untyped
|
|
20
28
|
def enabled?: () -> bool
|
|
29
|
+
def navigation_enabled?: () -> bool
|
|
21
30
|
def use_colorize?: () -> bool
|
|
22
|
-
def
|
|
31
|
+
def accept_key?: (untyped key) -> bool
|
|
32
|
+
def key_match?: (untyped key, Symbol config_symbol) -> bool
|
|
33
|
+
def ghost_color: () -> String
|
|
34
|
+
def resolve_ghost_style: (Hash[Symbol, untyped] hash) -> String
|
|
35
|
+
def render_ghost_suggestion: () -> void
|
|
23
36
|
def render_ghost: (String ghost, ?String suggestion) -> void
|
|
24
37
|
def ghost_display_lines: (String ghost, ?String suggestion) -> Array[String]
|
|
25
38
|
def colorize_ghost_lines: (String ghost, String suggestion) -> Array[String]
|
|
39
|
+
def dim_line: (String line) -> String
|
|
26
40
|
def extract_ansi_colored_suffix: (String colored_text, Integer visible_byte_offset) -> String
|
|
27
41
|
def write_extra_ghost_lines: (Array[String] lines) -> void
|
|
28
|
-
def restore_cursor_after: (Array[String] lines) -> void
|
|
29
42
|
def find_suggestion: (String buffer) -> (String | nil)
|
|
30
43
|
def accept_suggestion: (String suggestion) -> void
|
|
44
|
+
def find_prev_match: (String buffer, Integer? from_pointer) -> Integer?
|
|
45
|
+
def find_next_match: (String buffer, Integer? from_pointer) -> Integer?
|
|
31
46
|
end
|
|
32
47
|
end
|
|
33
48
|
end
|
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.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- unurgunite
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: reline
|
|
@@ -145,6 +145,7 @@ files:
|
|
|
145
145
|
- ".rspec"
|
|
146
146
|
- ".rubocop.yml"
|
|
147
147
|
- ".ruby-version"
|
|
148
|
+
- CHANGELOG.md
|
|
148
149
|
- CODE_OF_CONDUCT.md
|
|
149
150
|
- Gemfile
|
|
150
151
|
- Gemfile.lock
|