11import logging
2+ import sys
23from prompt_toolkit .enums import EditingMode
34from prompt_toolkit .key_binding import KeyBindings
5+ from prompt_toolkit .key_binding .vi_state import InputMode , ViState
46from prompt_toolkit .filters import (
57 completion_is_selected ,
68 is_searching ,
79 has_completions ,
810 has_selection ,
911 vi_mode ,
12+ vi_insert_mode ,
13+ vi_navigation_mode ,
1014)
1115
1216from .pgbuffer import buffer_should_be_handled , safe_multi_line_mode
1317
1418_logger = logging .getLogger (__name__ )
1519
20+ # Track whether ViState has been patched to avoid redundant monkeypatching
21+ _vim_cursor_shapes_configured = False
22+
23+
24+ def _set_cursor_shape (shape_code ):
25+ """
26+ Send terminal escape sequence to change cursor shape.
27+
28+ Args:
29+ shape_code: Cursor shape code (1=block, 3=underline, 5=beam)
30+ """
31+ out = getattr (sys .stdout , 'buffer' , sys .stdout )
32+ try :
33+ # Write escape sequence as bytes
34+ out .write (f'\x1b [{ shape_code } q' .encode ('ascii' ))
35+ sys .stdout .flush ()
36+ except (AttributeError , OSError ):
37+ # Silently ignore if terminal doesn't support cursor shape changes
38+ pass
39+
40+
41+ def setup_vim_cursor_shapes ():
42+ """
43+ Configure cursor shape changes for vim modes (idempotent).
44+
45+ Uses terminal escape sequences to change cursor appearance:
46+ - Block cursor (█) in navigation/normal mode
47+ - Beam cursor (|) in insert mode
48+ - Underline cursor (_) in replace mode
49+
50+ This function can be called multiple times safely - it only patches ViState once.
51+ """
52+ global _vim_cursor_shapes_configured
53+
54+ # Only patch ViState once to avoid issues with multiple calls
55+ if _vim_cursor_shapes_configured :
56+ return
57+
58+ def set_input_mode (self , mode ):
59+ # Cursor shape codes: 1=block, 3=underline, 5=beam
60+ shape = {
61+ InputMode .NAVIGATION : 1 , # Block cursor for normal mode
62+ InputMode .REPLACE : 3 , # Underline cursor for replace mode
63+ InputMode .INSERT : 5 , # Beam cursor for insert mode
64+ }.get (mode , 5 )
65+
66+ _set_cursor_shape (shape )
67+ self ._input_mode = mode
68+
69+ # Patch ViState to include cursor shape changes
70+ ViState ._input_mode = InputMode .INSERT
71+ ViState .input_mode = property (lambda self : self ._input_mode , set_input_mode )
72+
73+ _vim_cursor_shapes_configured = True
74+
1675
1776def pgcli_bindings (pgcli ):
1877 """Custom key bindings for pgcli."""
@@ -39,6 +98,11 @@ def _(event):
3998 pgcli .vi_mode = not pgcli .vi_mode
4099 event .app .editing_mode = EditingMode .VI if pgcli .vi_mode else EditingMode .EMACS
41100
101+ if pgcli .vi_mode :
102+ setup_vim_cursor_shapes ()
103+ else :
104+ _set_cursor_shape (5 )
105+
42106 @kb .add ("f5" )
43107 def _ (event ):
44108 """Toggle between Vi and Emacs mode."""
@@ -73,21 +137,43 @@ def _(event):
73137 @kb .add ("c-space" )
74138 def _ (event ):
75139 """
76- Initialize autocompletion at cursor.
140+ Toggle autocompletion at cursor.
77141
78142 If the autocompletion menu is not showing, display it with the
79143 appropriate completions for the context.
80144
81- If the menu is showing, select the next completion .
145+ If the menu is showing, close it (toggle off) .
82146 """
83147 _logger .debug ("Detected <C-Space> key." )
84148
85149 b = event .app .current_buffer
86150 if b .complete_state :
87- b .complete_next ()
151+ # Close completion menu (toggle off)
152+ b .complete_state = None
88153 else :
154+ # Open completion menu (toggle on)
89155 b .start_completion (select_first = False )
90156
157+ @kb .add ("c-j" , filter = has_completions )
158+ def _ (event ):
159+ """
160+ Navigate to next completion (down) in autocomplete menu.
161+
162+ Works like Ctrl+n but uses Vim-style j (down) binding.
163+ """
164+ _logger .debug ("Detected <C-j> key." )
165+ event .current_buffer .complete_next ()
166+
167+ @kb .add ("c-k" , filter = has_completions )
168+ def _ (event ):
169+ """
170+ Navigate to previous completion (up) in autocomplete menu.
171+
172+ Works like Ctrl+p but uses Vim-style k (up) binding.
173+ """
174+ _logger .debug ("Detected <C-k> key." )
175+ event .current_buffer .complete_previous ()
176+
91177 @kb .add ("enter" , filter = completion_is_selected )
92178 def _ (event ):
93179 """Makes the enter key work as the tab key only when showing the menu.
@@ -129,4 +215,49 @@ def _(event):
129215 """Move down in history."""
130216 event .current_buffer .history_forward (count = event .arg )
131217
218+ # Add these bindings with eager=True to take precedence when suggestions are available
219+ # This is key for fish/zsh-style autosuggestion acceptance in vim normal mode
220+ from prompt_toolkit .filters import Condition
221+
222+ @Condition
223+ def has_suggestion_at_end ():
224+ """Check if there's a suggestion and cursor is at end of line."""
225+ from prompt_toolkit .application .current import get_app
226+ app = get_app ()
227+ buffer = app .current_buffer
228+ return (
229+ buffer .suggestion is not None
230+ and buffer .document .is_cursor_at_the_end_of_line
231+ )
232+
233+ @kb .add ("l" , filter = vi_navigation_mode & has_suggestion_at_end , eager = True )
234+ def _ (event ):
235+ """
236+ Accept autosuggestion with 'l' in vi normal mode when at end of line.
237+
238+ This takes precedence over normal 'l' movement when a suggestion is available.
239+ """
240+ _logger .debug ("Accepting suggestion with 'l' in normal mode" )
241+ buff = event .current_buffer
242+ buff .insert_text (buff .suggestion .text )
243+
244+ @kb .add ("l" , filter = vi_navigation_mode , eager = False )
245+ def _ (event ):
246+ """Normal 'l' forward movement when no suggestion or not at end."""
247+ buff = event .current_buffer
248+ buff .cursor_position += buff .document .get_cursor_right_position ()
249+
250+ @kb .add ("right" , filter = vi_navigation_mode & has_suggestion_at_end , eager = True )
251+ def _ (event ):
252+ """Accept autosuggestion with right arrow in vi normal mode when at end of line."""
253+ _logger .debug ("Accepting suggestion with right arrow in normal mode" )
254+ buff = event .current_buffer
255+ buff .insert_text (buff .suggestion .text )
256+
257+ @kb .add ("right" , filter = vi_navigation_mode , eager = False )
258+ def _ (event ):
259+ """Normal right arrow forward movement when no suggestion or not at end."""
260+ buff = event .current_buffer
261+ buff .cursor_position += buff .document .get_cursor_right_position ()
262+
132263 return kb
0 commit comments