@@ -19,39 +19,68 @@ export class ShortcutHandler {
1919 private setupEventListeners ( ) : void {
2020 this . input . addEventListener ( 'input' , ( ) => this . handleInput ( ) ) ;
2121 this . input . addEventListener ( 'keydown' , ( e ) => this . handleKeydown ( e ) ) ;
22+
23+ // Add support for Tab key to accept first suggestion
24+ this . input . addEventListener ( 'keydown' , ( e ) => {
25+ if ( e . key === 'Tab' && this . matches . length > 0 && this . dropdown ) {
26+ e . preventDefault ( ) ;
27+ this . applyShortcut ( this . matches [ this . selectedIndex ] . command ) ;
28+ }
29+ } ) ;
30+
2231 document . addEventListener ( 'click' , ( e ) => {
23- if ( e . target !== this . input && this . dropdown ) {
32+ if ( e . target !== this . input && this . dropdown && ! this . dropdown . contains ( e . target as Node ) ) {
2433 this . hideDropdown ( ) ;
2534 }
2635 } ) ;
2736 }
2837
2938 private async handleInput ( ) : Promise < void > {
3039 const text = this . input . value ;
31- const lastWord = text . split ( ' ' ) . pop ( ) || '' ;
32-
33- if ( lastWord . startsWith ( '/' ) ) {
34- // Get both default shortcuts and custom prompts
35- const [ settings , { customPrompts = { } } ] = await Promise . all ( [
36- getSettings ( ) ,
37- new Promise < { customPrompts ?: Record < string , string > } > ( resolve => {
38- chrome . storage . sync . get ( [ 'customPrompts' ] , resolve ) ;
39- } )
40- ] ) ;
41-
42- // Combine default shortcuts and custom prompts
43- const allShortcuts = {
44- ...settings . shortcuts ,
45- ...customPrompts
46- } ;
47-
48- this . matches = Object . entries ( allShortcuts )
49- . filter ( ( [ command ] ) => command . startsWith ( lastWord ) )
50- . map ( ( [ command , description ] ) => ( { command, description } ) ) ;
51-
52- if ( this . matches . length > 0 ) {
53- this . showDropdown ( ) ;
54- } else {
40+ const cursorPos = this . input . selectionStart ;
41+
42+ // Find the word at the cursor position
43+ const beforeCursor = text . substring ( 0 , cursorPos ) ;
44+ const afterCursor = text . substring ( cursorPos ) ;
45+
46+ // Find the start of the current word
47+ const lastSpaceIndex = beforeCursor . lastIndexOf ( ' ' ) ;
48+ const currentWord = beforeCursor . substring ( lastSpaceIndex + 1 ) ;
49+
50+ if ( currentWord . startsWith ( '/' ) ) {
51+ try {
52+ // Get both default shortcuts and custom prompts
53+ const [ settings , { customPrompts = { } } ] = await Promise . all ( [
54+ getSettings ( ) ,
55+ new Promise < { customPrompts ?: Record < string , string > } > ( resolve => {
56+ chrome . storage . sync . get ( [ 'customPrompts' ] , resolve ) ;
57+ } )
58+ ] ) ;
59+
60+ // Combine default shortcuts and custom prompts
61+ const allShortcuts = {
62+ ...settings . shortcuts ,
63+ ...customPrompts
64+ } ;
65+
66+ // If just '/', show all shortcuts, otherwise filter by match
67+ this . matches = currentWord . length === 1
68+ ? Object . entries ( allShortcuts )
69+ . map ( ( [ command , description ] ) => ( { command, description } ) )
70+ . sort ( ( a , b ) => a . command . localeCompare ( b . command ) )
71+ : Object . entries ( allShortcuts )
72+ . filter ( ( [ command ] ) => command . toLowerCase ( ) . startsWith ( currentWord . toLowerCase ( ) ) )
73+ . map ( ( [ command , description ] ) => ( { command, description } ) )
74+ . sort ( ( a , b ) => a . command . localeCompare ( b . command ) ) ;
75+
76+ if ( this . matches . length > 0 ) {
77+ this . selectedIndex = 0 ; // Reset selection to first item
78+ this . showDropdown ( ) ;
79+ } else {
80+ this . hideDropdown ( ) ;
81+ }
82+ } catch ( error ) {
83+ console . error ( 'Error loading shortcuts:' , error ) ;
5584 this . hideDropdown ( ) ;
5685 }
5786 } else {
@@ -92,7 +121,13 @@ export class ShortcutHandler {
92121 if ( ! this . dropdown ) {
93122 this . dropdown = document . createElement ( 'div' ) ;
94123 this . dropdown . className = 'shortcut-autocomplete' ;
95- this . input . parentElement ?. appendChild ( this . dropdown ) ;
124+ // Find the input section to append the dropdown there
125+ const inputSection = this . input . closest ( '.ai-input-section' ) ;
126+ if ( inputSection ) {
127+ inputSection . appendChild ( this . dropdown ) ;
128+ } else {
129+ this . input . parentElement ?. appendChild ( this . dropdown ) ;
130+ }
96131 }
97132
98133 this . dropdown . innerHTML = this . matches
@@ -132,9 +167,35 @@ export class ShortcutHandler {
132167 private applyShortcut ( command : string ) : void {
133168 // Replace the last word with the full command
134169 const words = this . input . value . split ( ' ' ) ;
135- words [ words . length - 1 ] = command ;
136- this . input . value = words . join ( ' ' ) ;
170+ const lastWordIndex = words . length - 1 ;
171+
172+ // Find the position of the current word
173+ const beforeCursor = this . input . value . substring ( 0 , this . input . selectionStart ) ;
174+ const afterCursor = this . input . value . substring ( this . input . selectionStart ) ;
175+
176+ // Find the start of the current word
177+ const lastSpaceIndex = beforeCursor . lastIndexOf ( ' ' ) ;
178+ const currentWord = beforeCursor . substring ( lastSpaceIndex + 1 ) ;
179+
180+ if ( currentWord . startsWith ( '/' ) ) {
181+ // Replace just the command part
182+ const newValue = beforeCursor . substring ( 0 , lastSpaceIndex + 1 ) + command + ' ' + afterCursor ;
183+ this . input . value = newValue ;
184+
185+ // Set cursor position after the command
186+ const newCursorPos = lastSpaceIndex + 1 + command . length + 1 ;
187+ this . input . setSelectionRange ( newCursorPos , newCursorPos ) ;
188+ } else {
189+ // Fallback to word replacement
190+ words [ lastWordIndex ] = command ;
191+ this . input . value = words . join ( ' ' ) + ' ' ;
192+
193+ // Move cursor to end
194+ this . input . setSelectionRange ( this . input . value . length , this . input . value . length ) ;
195+ }
196+
137197 this . hideDropdown ( ) ;
198+ this . input . focus ( ) ;
138199
139200 // Handle special commands
140201 switch ( command ) {
0 commit comments