@@ -31,6 +31,16 @@ const VARIABLE_RESOLUTION_CONTEXT = { context: 'chat-input-autocomplete' };
3131const VARIABLE_ARGUMENT_PICKER_COMMAND = 'trigger-variable-argument-picker' ;
3232const VARIABLE_ADD_CONTEXT_COMMAND = 'add-context-variable' ;
3333
34+ interface CompletionSource < T > {
35+ triggerCharacter : string ;
36+ getItems : ( ) => T [ ] ;
37+ kind : monaco . languages . CompletionItemKind ;
38+ getId : ( item : T ) => string ;
39+ getName : ( item : T ) => string ;
40+ getDescription : ( item : T ) => string ;
41+ command ?: monaco . languages . Command ;
42+ }
43+
3444@injectable ( )
3545export class ChatViewLanguageContribution implements FrontendApplicationContribution {
3646
@@ -49,37 +59,84 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
4959 onStart ( _app : FrontendApplication ) : MaybePromise < void > {
5060 monaco . languages . register ( { id : CHAT_VIEW_LANGUAGE_ID , extensions : [ CHAT_VIEW_LANGUAGE_EXTENSION ] } ) ;
5161
52- monaco . languages . registerCompletionItemProvider ( CHAT_VIEW_LANGUAGE_ID , {
53- triggerCharacters : [ PromptText . AGENT_CHAR ] ,
54- provideCompletionItems : ( model , position , _context , _token ) : ProviderResult < monaco . languages . CompletionList > => this . provideAgentCompletions ( model , position ) ,
62+ this . registerCompletionProviders ( ) ;
63+
64+ monaco . editor . registerCommand ( VARIABLE_ARGUMENT_PICKER_COMMAND , this . triggerVariableArgumentPicker . bind ( this ) ) ;
65+ monaco . editor . registerCommand ( VARIABLE_ADD_CONTEXT_COMMAND , ( _ , ...args ) => args . length > 1 ? this . addContextVariable ( args [ 0 ] , args [ 1 ] ) : undefined ) ;
66+ }
67+
68+ protected registerCompletionProviders ( ) : void {
69+ this . registerStandardCompletionProvider ( {
70+ triggerCharacter : PromptText . AGENT_CHAR ,
71+ getItems : ( ) => this . agentService . getAgents ( ) ,
72+ kind : monaco . languages . CompletionItemKind . Value ,
73+ getId : agent => `${ agent . id } ` ,
74+ getName : agent => agent . name ,
75+ getDescription : agent => agent . description
5576 } ) ;
56- monaco . languages . registerCompletionItemProvider ( CHAT_VIEW_LANGUAGE_ID , {
57- triggerCharacters : [ PromptText . VARIABLE_CHAR ] ,
58- provideCompletionItems : ( model , position , _context , _token ) : ProviderResult < monaco . languages . CompletionList > => this . provideVariableCompletions ( model , position ) ,
77+
78+ this . registerStandardCompletionProvider ( {
79+ triggerCharacter : PromptText . VARIABLE_CHAR ,
80+ getItems : ( ) => this . variableService . getVariables ( ) ,
81+ kind : monaco . languages . CompletionItemKind . Variable ,
82+ getId : variable => variable . args ?. some ( arg => ! arg . isOptional ) ? variable . name + PromptText . VARIABLE_SEPARATOR_CHAR : `${ variable . name } ` ,
83+ getName : variable => variable . name ,
84+ getDescription : variable => variable . description ,
85+ command : {
86+ title : nls . localize ( 'theia/ai/chat-ui/selectVariableArguments' , 'Select variable arguments' ) ,
87+ id : VARIABLE_ARGUMENT_PICKER_COMMAND ,
88+ }
89+ } ) ;
90+
91+ this . registerStandardCompletionProvider ( {
92+ triggerCharacter : PromptText . FUNCTION_CHAR ,
93+ getItems : ( ) => this . toolInvocationRegistry . getAllFunctions ( ) ,
94+ kind : monaco . languages . CompletionItemKind . Function ,
95+ getId : tool => `${ tool . id } ` ,
96+ getName : tool => tool . name ,
97+ getDescription : tool => tool . description ?? ''
5998 } ) ;
99+
100+ // Register the variable argument completion provider (special case)
60101 monaco . languages . registerCompletionItemProvider ( CHAT_VIEW_LANGUAGE_ID , {
61102 triggerCharacters : [ PromptText . VARIABLE_CHAR , PromptText . VARIABLE_SEPARATOR_CHAR ] ,
62- provideCompletionItems : ( model , position , _context , _token ) : ProviderResult < monaco . languages . CompletionList > => this . provideVariableWithArgCompletions ( model , position ) ,
103+ provideCompletionItems : ( model , position , _context , _token ) : ProviderResult < monaco . languages . CompletionList > =>
104+ this . provideVariableWithArgCompletions ( model , position ) ,
63105 } ) ;
106+ }
107+
108+ protected registerStandardCompletionProvider < T > ( source : CompletionSource < T > ) : void {
64109 monaco . languages . registerCompletionItemProvider ( CHAT_VIEW_LANGUAGE_ID , {
65- triggerCharacters : [ PromptText . FUNCTION_CHAR ] ,
66- provideCompletionItems : ( model , position , _context , _token ) : ProviderResult < monaco . languages . CompletionList > => this . provideToolCompletions ( model , position ) ,
110+ triggerCharacters : [ source . triggerCharacter ] ,
111+ provideCompletionItems : ( model , position , _context , _token ) : ProviderResult < monaco . languages . CompletionList > =>
112+ this . provideCompletions ( model , position , source ) ,
67113 } ) ;
68-
69- monaco . editor . registerCommand ( VARIABLE_ARGUMENT_PICKER_COMMAND , this . triggerVariableArgumentPicker . bind ( this ) ) ;
70- monaco . editor . registerCommand ( VARIABLE_ADD_CONTEXT_COMMAND , ( _ , ...args ) => args . length > 1 ? this . addContextVariable ( args [ 0 ] , args [ 1 ] ) : undefined ) ;
71114 }
72115
73116 getCompletionRange ( model : monaco . editor . ITextModel , position : monaco . Position , triggerCharacter : string ) : monaco . Range | undefined {
74117 const wordInfo = model . getWordUntilPosition ( position ) ;
75118 const lineContent = model . getLineContent ( position . lineNumber ) ;
76119 // one to the left, and -1 for 0-based index
77120 const characterBeforeCurrentWord = lineContent [ wordInfo . startColumn - 1 - 1 ] ;
78- // return suggestions only if the word is directly preceded by the trigger character
121+
79122 if ( characterBeforeCurrentWord !== triggerCharacter ) {
80123 return undefined ;
81124 }
82125
126+ // we are not at the beginning of the line
127+ if ( wordInfo . startColumn > 2 ) {
128+ const charBeforeTrigger = model . getValueInRange ( {
129+ startLineNumber : position . lineNumber ,
130+ startColumn : wordInfo . startColumn - 2 ,
131+ endLineNumber : position . lineNumber ,
132+ endColumn : wordInfo . startColumn - 1
133+ } ) ;
134+ // If the character before the trigger is not whitespace, don't provide completions
135+ if ( ! / \s / . test ( charBeforeTrigger ) ) {
136+ return undefined ;
137+ }
138+ }
139+
83140 return new monaco . Range (
84141 position . lineNumber ,
85142 wordInfo . startColumn ,
@@ -88,73 +145,65 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
88145 ) ;
89146 }
90147
91- private getSuggestions < T > (
148+ protected provideCompletions < T > (
92149 model : monaco . editor . ITextModel ,
93150 position : monaco . Position ,
94- triggerChar : string ,
95- items : T [ ] ,
96- kind : monaco . languages . CompletionItemKind ,
97- getId : ( item : T ) => string ,
98- getName : ( item : T ) => string ,
99- getDescription : ( item : T ) => string ,
100- command ?: monaco . languages . Command
151+ source : CompletionSource < T >
101152 ) : ProviderResult < monaco . languages . CompletionList > {
102- const completionRange = this . getCompletionRange ( model , position , triggerChar ) ;
153+ const completionRange = this . getCompletionRange ( model , position , source . triggerCharacter ) ;
103154 if ( completionRange === undefined ) {
104155 return { suggestions : [ ] } ;
105156 }
157+
158+ const items = source . getItems ( ) ;
106159 const suggestions = items . map ( item => ( {
107- insertText : getId ( item ) ,
108- kind : kind ,
109- label : getName ( item ) ,
160+ insertText : source . getId ( item ) ,
161+ kind : source . kind ,
162+ label : source . getName ( item ) ,
110163 range : completionRange ,
111- detail : getDescription ( item ) ,
112- command
164+ detail : source . getDescription ( item ) ,
165+ command : source . command
113166 } ) ) ;
167+
114168 return { suggestions } ;
115169 }
116170
117- provideAgentCompletions ( model : monaco . editor . ITextModel , position : monaco . Position ) : ProviderResult < monaco . languages . CompletionList > {
118- return this . getSuggestions (
119- model ,
120- position ,
121- PromptText . AGENT_CHAR ,
122- this . agentService . getAgents ( ) ,
123- monaco . languages . CompletionItemKind . Value ,
124- agent => `${ agent . id } ` ,
125- agent => agent . name ,
126- agent => agent . description
127- ) ;
128- }
171+ async provideVariableWithArgCompletions ( model : monaco . editor . ITextModel , position : monaco . Position ) : Promise < monaco . languages . CompletionList > {
172+ // Get the text of the current line up to the cursor position
173+ const textUntilPosition = model . getValueInRange ( {
174+ startLineNumber : position . lineNumber ,
175+ startColumn : 1 ,
176+ endLineNumber : position . lineNumber ,
177+ endColumn : position . column ,
178+ } ) ;
129179
130- provideVariableCompletions ( model : monaco . editor . ITextModel , position : monaco . Position ) : ProviderResult < monaco . languages . CompletionList > {
131- return this . getSuggestions (
132- model ,
133- position ,
134- PromptText . VARIABLE_CHAR ,
135- this . variableService . getVariables ( ) ,
136- monaco . languages . CompletionItemKind . Variable ,
137- variable => variable . args ?. some ( arg => ! arg . isOptional ) ? variable . name + PromptText . VARIABLE_SEPARATOR_CHAR : `${ variable . name } ` ,
138- variable => variable . name ,
139- variable => variable . description ,
140- {
141- title : nls . localize ( 'theia/ai/chat-ui/selectVariableArguments' , 'Select variable arguments' ) ,
142- id : VARIABLE_ARGUMENT_PICKER_COMMAND ,
143- }
144- ) ;
145- }
180+ // Regex that captures the variable name in contexts like "#varname" or "#var-name:args"
181+ // Matches only when # is at the beginning of the string or after whitespace
182+ const variableRegex = / (?: ^ | \s ) # ( [ \w - ] * ) / ;
183+ const match = textUntilPosition . match ( variableRegex ) ;
184+
185+ if ( ! match ) {
186+ return { suggestions : [ ] } ;
187+ }
188+
189+ const currentVariableName = match [ 1 ] ;
190+ const hasColonSeparator = textUntilPosition . includes ( `${ currentVariableName } :` ) ;
146191
147- async provideVariableWithArgCompletions ( model : monaco . editor . ITextModel , position : monaco . Position ) : Promise < monaco . languages . CompletionList > {
148192 const variables = this . variableService . getVariables ( ) ;
149193 const suggestions : monaco . languages . CompletionItem [ ] = [ ] ;
194+
150195 for ( const variable of variables ) {
196+ // If we have a variable:arg pattern, only process the matching variable
197+ if ( hasColonSeparator && variable . name !== currentVariableName ) {
198+ continue ;
199+ }
200+
151201 const provider = await this . variableService . getArgumentCompletionProvider ( variable . name ) ;
152202 if ( provider ) {
153203 const items = await provider ( model , position ) ;
154204 if ( items ) {
155205 suggestions . push ( ...items . map ( item => ( {
156206 ...item ,
157- // trigger command to check if we should add a context variable
158207 command : {
159208 title : nls . localize ( 'theia/ai/chat-ui/addContextVariable' , 'Add context variable' ) ,
160209 id : VARIABLE_ADD_CONTEXT_COMMAND ,
@@ -164,62 +213,57 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
164213 }
165214 }
166215 }
167- return { suggestions } ;
168- }
169216
170- provideToolCompletions ( model : monaco . editor . ITextModel , position : monaco . Position ) : ProviderResult < monaco . languages . CompletionList > {
171- return this . getSuggestions (
172- model ,
173- position ,
174- PromptText . FUNCTION_CHAR ,
175- this . toolInvocationRegistry . getAllFunctions ( ) ,
176- monaco . languages . CompletionItemKind . Function ,
177- tool => `${ tool . id } ` ,
178- tool => tool . name ,
179- tool => tool . description ?? ''
180- ) ;
217+ return { suggestions } ;
181218 }
182219
183220 protected async triggerVariableArgumentPicker ( ) : Promise < void > {
184221 const inputEditor = monaco . editor . getEditors ( ) . find ( editor => editor . hasTextFocus ( ) ) ;
185222 if ( ! inputEditor ) {
186223 return ;
187224 }
225+
188226 const model = inputEditor . getModel ( ) ;
189227 const position = inputEditor . getPosition ( ) ;
190228 if ( ! model || ! position ) {
191229 return ;
192230 }
193231
232+ // // Get the word at cursor
233+ const wordInfo = model . getWordUntilPosition ( position ) ;
234+
194235 // account for the variable separator character if present
195236 let endOfWordPosition = position . column ;
196- let insertTextPrefix = PromptText . VARIABLE_SEPARATOR_CHAR ;
197- if ( this . getCharacterBeforePosition ( model , position ) === PromptText . VARIABLE_SEPARATOR_CHAR ) {
237+ if ( wordInfo . word === '' && this . getCharacterBeforePosition ( model , position ) === PromptText . VARIABLE_SEPARATOR_CHAR ) {
198238 endOfWordPosition = position . column - 1 ;
199- insertTextPrefix = '' ;
239+ } else {
240+ return ;
200241 }
201242
202243 const variableName = model . getWordAtPosition ( { ...position , column : endOfWordPosition } ) ?. word ;
203244 if ( ! variableName ) {
204245 return ;
205246 }
247+
206248 const provider = await this . variableService . getArgumentPicker ( variableName , VARIABLE_RESOLUTION_CONTEXT ) ;
207249 if ( ! provider ) {
208250 return ;
209251 }
252+
210253 const arg = await provider ( VARIABLE_RESOLUTION_CONTEXT ) ;
211254 if ( ! arg ) {
212255 return ;
213256 }
257+
214258 inputEditor . executeEdits ( 'variable-argument-picker' , [ {
215259 range : new monaco . Range ( position . lineNumber , position . column , position . lineNumber , position . column ) ,
216- text : insertTextPrefix + arg
260+ text : arg
217261 } ] ) ;
262+
218263 await this . addContextVariable ( variableName , arg ) ;
219264 }
220265
221266 protected getCharacterBeforePosition ( model : monaco . editor . ITextModel , position : monaco . Position ) : string {
222- // one to the left, and -1 for 0-based index
223267 return model . getLineContent ( position . lineNumber ) [ position . column - 1 - 1 ] ;
224268 }
225269
@@ -228,6 +272,7 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
228272 if ( ! variable || ! AIContextVariable . is ( variable ) ) {
229273 return ;
230274 }
275+
231276 const widget = this . shell . getWidgetById ( ChatViewWidget . ID ) ;
232277 if ( widget instanceof ChatViewWidget ) {
233278 widget . addContext ( { variable, arg } ) ;
0 commit comments