1- import  type  {  ParsedKey ,   BoxRenderable ,   InputRenderable  }  from  "@opentui/core" 
1+ import  type  {  BoxRenderable ,   TextareaRenderable ,   KeyEvent  }  from  "@opentui/core" 
22import  fuzzysort  from  "fuzzysort" 
33import  {  firstBy  }  from  "remeda" 
44import  {  createMemo ,  createResource ,  createEffect ,  onMount ,  For ,  Show  }  from  "solid-js" 
@@ -12,7 +12,7 @@ import type { PromptInfo } from "./history"
1212
1313export  type  AutocompleteRef  =  { 
1414  onInput : ( value : string )  =>  void 
15-   onKeyDown : ( e : ParsedKey )  =>  void 
15+   onKeyDown : ( e : KeyEvent )  =>  void 
1616  visible : false  |  "@"  |  "/" 
1717} 
1818
@@ -27,9 +27,13 @@ export function Autocomplete(props: {
2727  value : string 
2828  sessionID ?: string 
2929  setPrompt : ( input : ( prompt : PromptInfo )  =>  void )  =>  void 
30+   setExtmark : ( partIndex : number ,  extmarkId : number )  =>  void 
3031  anchor : ( )  =>  BoxRenderable 
31-   input : ( )  =>  InputRenderable 
32+   input : ( )  =>  TextareaRenderable 
3233  ref : ( ref : AutocompleteRef )  =>  void 
34+   fileStyleId : number 
35+   agentStyleId : number 
36+   promptPartTypeId : ( )  =>  number 
3337} )  { 
3438  const  sdk  =  useSDK ( ) 
3539  const  sync  =  useSync ( ) 
@@ -46,6 +50,49 @@ export function Autocomplete(props: {
4650    return  props . value . substring ( store . index  +  1 ) . split ( " " ) [ 0 ] 
4751  } ) 
4852
53+   function  insertPart ( text : string ,  part : PromptInfo [ "parts" ] [ number ] )  { 
54+     const  append  =  "@"  +  text  +  " " 
55+     const  input  =  props . input ( ) 
56+     const  currentCursorOffset  =  input . visualCursor . offset 
57+ 
58+     input . cursorOffset  =  store . index 
59+     const  startCursor  =  input . logicalCursor 
60+     input . cursorOffset  =  currentCursorOffset 
61+     const  endCursor  =  input . logicalCursor 
62+ 
63+     input . deleteRange ( startCursor . row ,  startCursor . col ,  endCursor . row ,  endCursor . col ) 
64+     input . insertText ( append ) 
65+ 
66+     const  virtualText  =  "@"  +  text 
67+     const  extmarkStart  =  store . index 
68+     const  extmarkEnd  =  extmarkStart  +  virtualText . length 
69+ 
70+     const  styleId  =  part . type  ===  "file"  ? props . fileStyleId  : part . type  ===  "agent"  ? props . agentStyleId  : undefined 
71+ 
72+     const  extmarkId  =  input . extmarks . create ( { 
73+       start : extmarkStart , 
74+       end : extmarkEnd , 
75+       virtual : true , 
76+       styleId, 
77+       typeId : props . promptPartTypeId ( ) , 
78+     } ) 
79+ 
80+     props . setPrompt ( ( draft )  =>  { 
81+       if  ( part . type  ===  "file"  &&  part . source ?. text )  { 
82+         part . source . text . start  =  extmarkStart 
83+         part . source . text . end  =  extmarkEnd 
84+         part . source . text . value  =  virtualText 
85+       }  else  if  ( part . type  ===  "agent"  &&  part . source )  { 
86+         part . source . start  =  extmarkStart 
87+         part . source . end  =  extmarkEnd 
88+         part . source . value  =  virtualText 
89+       } 
90+       const  partIndex  =  draft . parts . length 
91+       draft . parts . push ( part ) 
92+       props . setExtmark ( partIndex ,  extmarkId ) 
93+     } ) 
94+   } 
95+ 
4996  const  [ files ]  =  createResource ( 
5097    ( )  =>  [ filter ( ) ] , 
5198    async  ( )  =>  { 
@@ -68,26 +115,20 @@ export function Autocomplete(props: {
68115            ( item ) : AutocompleteOption  =>  ( { 
69116              display : item , 
70117              onSelect : ( )  =>  { 
71-                 const   part :  PromptInfo [ "parts" ] [ number ]   =  { 
118+                 insertPart ( item ,  { 
72119                  type : "file" , 
73120                  mime : "text/plain" , 
74121                  filename : item , 
75122                  url : `file://${ process . cwd ( ) } ${ item }  , 
76123                  source : { 
77124                    type : "file" , 
78125                    text : { 
79-                       start : store . index , 
80-                       end : store . index   +   item . length   +   1 , 
81-                       value : "@"    +   item , 
126+                       start : 0 , 
127+                       end : 0 , 
128+                       value : ""  , 
82129                    } , 
83130                    path : item , 
84131                  } , 
85-                 } 
86-                 props . setPrompt ( ( draft )  =>  { 
87-                   const  append  =  "@"  +  item  +  " " 
88-                   if  ( store . index  ===  0 )  draft . input  =  append 
89-                   if  ( store . index  >  0 )  draft . input  =  draft . input . slice ( 0 ,  store . index )  +  append 
90-                   draft . parts . push ( part ) 
91132                } ) 
92133              } , 
93134            } ) , 
@@ -111,18 +152,14 @@ export function Autocomplete(props: {
111152        ( agent ) : AutocompleteOption  =>  ( { 
112153          display : "@"  +  agent . name , 
113154          onSelect : ( )  =>  { 
114-             props . setPrompt ( ( draft )  =>  { 
115-               const  append  =  "@"  +  agent . name  +  " " 
116-               draft . input  =  append 
117-               draft . parts . push ( { 
118-                 type : "agent" , 
119-                 source : { 
120-                   start : store . index , 
121-                   end : store . index  +  agent . name . length  +  1 , 
122-                   value : "@"  +  agent . name , 
123-                 } , 
124-                 name : agent . name , 
125-               } ) 
155+             insertPart ( agent . name ,  { 
156+               type : "agent" , 
157+               name : agent . name , 
158+               source : { 
159+                 start : 0 , 
160+                 end : 0 , 
161+                 value : "" , 
162+               } , 
126163            } ) 
127164          } , 
128165        } ) , 
@@ -138,8 +175,11 @@ export function Autocomplete(props: {
138175        display : "/"  +  command . name , 
139176        description : command . description , 
140177        onSelect : ( )  =>  { 
141-           props . input ( ) . value  =  "/"  +  command . name  +  " " 
142-           props . input ( ) . cursorPosition  =  props . input ( ) . value . length 
178+           const  newText  =  "/"  +  command . name  +  " " 
179+           const  cursor  =  props . input ( ) . logicalCursor 
180+           props . input ( ) . deleteRange ( 0 ,  0 ,  cursor . row ,  cursor . col ) 
181+           props . input ( ) . insertText ( newText ) 
182+           props . input ( ) . cursorOffset  =  Bun . stringWidth ( newText ) 
143183        } , 
144184      } ) 
145185    } 
@@ -234,13 +274,13 @@ export function Autocomplete(props: {
234274    const  selected  =  options ( ) [ store . selected ] 
235275    if  ( ! selected )  return 
236276    selected . onSelect ?.( ) 
237-     setTimeout ( ( )   =>   hide ( ) ,   0 ) 
277+     hide ( ) 
238278  } 
239279
240280  function  show ( mode : "@"  |  "/" )  { 
241281    setStore ( { 
242282      visible : mode , 
243-       index : props . input ( ) . cursorPosition , 
283+       index : props . input ( ) . visualCursor . offset , 
244284      position : { 
245285        x : props . anchor ( ) . x , 
246286        y : props . anchor ( ) . y , 
@@ -250,7 +290,10 @@ export function Autocomplete(props: {
250290  } 
251291
252292  function  hide ( )  { 
253-     if  ( store . visible  ===  "/"  &&  ! props . value . endsWith ( " " ) )  props . input ( ) . value  =  "" 
293+     if  ( store . visible  ===  "/"  &&  ! props . value . endsWith ( " " ) )  { 
294+       const  cursor  =  props . input ( ) . logicalCursor 
295+       props . input ( ) . deleteRange ( 0 ,  0 ,  cursor . row ,  cursor . col ) 
296+     } 
254297    setStore ( "visible" ,  false ) 
255298  } 
256299
@@ -262,12 +305,13 @@ export function Autocomplete(props: {
262305      onInput ( value : string )  { 
263306        if  ( store . visible  &&  value . length  <=  store . index )  hide ( ) 
264307      } , 
265-       onKeyDown ( e : ParsedKey )  { 
308+       onKeyDown ( e : KeyEvent )  { 
266309        if  ( store . visible )  { 
267310          if  ( e . name  ===  "up" )  move ( - 1 ) 
268311          if  ( e . name  ===  "down" )  move ( 1 ) 
269312          if  ( e . name  ===  "escape" )  hide ( ) 
270313          if  ( e . name  ===  "return" )  select ( ) 
314+           if  ( [ "up" ,  "down" ,  "return" ,  "escape" ] . includes ( e . name ) )  e . preventDefault ( ) 
271315        } 
272316        if  ( ! store . visible )  { 
273317          if  ( e . name  ===  "@" )  { 
@@ -278,7 +322,7 @@ export function Autocomplete(props: {
278322          } 
279323
280324          if  ( e . name  ===  "/" )  { 
281-             if  ( props . input ( ) . cursorPosition  ===  0 )  show ( "/" ) 
325+             if  ( props . input ( ) . visualCursor . offset  ===  0 )  show ( "/" ) 
282326          } 
283327        } 
284328      } , 
0 commit comments