@@ -15,24 +15,34 @@ import { ITextModel } from 'vs/editor/common/model';
1515import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures' ;
1616import { localize } from 'vs/nls' ;
1717import { Registry } from 'vs/platform/registry/common/platform' ;
18- import { editorForeground , textLinkForeground } from 'vs/platform/theme/common/colorRegistry' ;
18+ import { editorForeground , textCodeBlockBackground , textLinkForeground } from 'vs/platform/theme/common/colorRegistry' ;
1919import { IThemeService } from 'vs/platform/theme/common/themeService' ;
2020import { IChatWidget , IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat' ;
2121import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget' ;
2222import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle' ;
2323import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart' ;
2424import { IChatService } from 'vs/workbench/contrib/chat/common/chatService' ;
25+ import { ContentWidgetPositionPreference , IContentWidget } from 'vs/editor/browser/editorBrowser' ;
26+ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent' ;
27+ import { KeyCode } from 'vs/base/common/keyCodes' ;
28+ import { Selection } from 'vs/editor/common/core/selection' ;
2529
2630const decorationDescription = 'chat' ;
2731const slashCommandPlaceholderDecorationType = 'chat-session-detail' ;
2832const slashCommandTextDecorationType = 'chat-session-text' ;
33+ const slashCommandContentWidgetId = 'chat-session-content-widget' ;
2934
3035class InputEditorDecorations extends Disposable {
3136
37+ private _slashCommandDomNode = document . createElement ( 'div' ) ;
38+ private _slashCommandContentWidget : IContentWidget | undefined ;
39+ private _previouslyUsedSlashCommands = new Set < string > ( ) ;
40+
3241 constructor (
3342 private readonly widget : IChatWidget ,
3443 @ICodeEditorService private readonly codeEditorService : ICodeEditorService ,
3544 @IThemeService private readonly themeService : IThemeService ,
45+ @IChatService private readonly chatService : IChatService ,
3646 ) {
3747 super ( ) ;
3848
@@ -43,14 +53,25 @@ class InputEditorDecorations extends Disposable {
4353
4454 this . updateInputEditorDecorations ( ) ;
4555 this . _register ( this . widget . inputEditor . onDidChangeModelContent ( ( ) => this . updateInputEditorDecorations ( ) ) ) ;
46- this . _register ( this . widget . onDidChangeViewModel ( ( ) => this . updateInputEditorDecorations ( ) ) ) ;
56+ this . _register ( this . widget . onDidChangeViewModel ( ( ) => {
57+ this . _previouslyUsedSlashCommands . clear ( ) ;
58+ this . updateInputEditorDecorations ( ) ;
59+ } ) ) ;
60+ this . _register ( this . chatService . onDidSubmitSlashCommand ( ( e ) => {
61+ if ( e . sessionId === this . widget . viewModel ?. sessionId && ! this . _previouslyUsedSlashCommands . has ( e . slashCommand ) ) {
62+ this . _previouslyUsedSlashCommands . add ( e . slashCommand ) ;
63+ }
64+ } ) ) ;
4765 }
4866
4967 private updateRegisteredDecorationTypes ( ) {
50- const theme = this . themeService . getColorTheme ( ) ;
5168 this . codeEditorService . removeDecorationType ( slashCommandTextDecorationType ) ;
69+ this . updateInputEditorContentWidgets ( { hide : true } ) ;
5270 this . codeEditorService . registerDecorationType ( decorationDescription , slashCommandTextDecorationType , {
53- color : theme . getColor ( textLinkForeground ) ?. toString ( )
71+ opacity : '0' ,
72+ after : {
73+ contentText : ' ' ,
74+ }
5475 } ) ;
5576 this . updateInputEditorDecorations ( ) ;
5677 }
@@ -62,10 +83,10 @@ class InputEditorDecorations extends Disposable {
6283 }
6384
6485 private async updateInputEditorDecorations ( ) {
65- const value = this . widget . inputEditor . getValue ( ) ;
86+ const inputValue = this . widget . inputEditor . getValue ( ) ;
6687 const slashCommands = await this . widget . getSlashCommands ( ) ; // TODO this async call can lead to a flicker of the placeholder text when switching editor tabs
6788
68- if ( ! value ) {
89+ if ( ! inputValue ) {
6990 const extensionPlaceholder = this . widget . viewModel ?. inputPlaceholder ;
7091 const defaultPlaceholder = slashCommands ?. length ?
7192 localize ( 'interactive.input.placeholderWithCommands' , "Ask a question or type '/' for topics" ) :
@@ -88,32 +109,44 @@ class InputEditorDecorations extends Disposable {
88109 }
89110 ] ;
90111 this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandPlaceholderDecorationType , decoration ) ;
112+ this . updateInputEditorContentWidgets ( { hide : true } ) ;
91113 return ;
92114 }
93115
94- const command = value && slashCommands ?. find ( c => value . startsWith ( `/${ c . command } ` ) ) ;
95- if ( command && command . detail && value === `/${ command . command } ` ) {
96- const decoration : IDecorationOptions [ ] = [
97- {
116+ let slashCommandPlaceholderDecoration : IDecorationOptions [ ] | undefined ;
117+ const command = inputValue && slashCommands ?. find ( c => inputValue . startsWith ( `/${ c . command } ` ) ) ;
118+ if ( command && inputValue === `/${ command . command } ` ) {
119+ const isFollowupSlashCommand = this . _previouslyUsedSlashCommands . has ( command . command ) ;
120+ const shouldRenderFollowupPlaceholder = command . followupPlaceholder && isFollowupSlashCommand ;
121+ if ( shouldRenderFollowupPlaceholder || command . detail ) {
122+ slashCommandPlaceholderDecoration = [ {
98123 range : {
99124 startLineNumber : 1 ,
100125 endLineNumber : 1 ,
101- startColumn : command . command . length + 2 ,
126+ startColumn : command && typeof command !== 'string' ? ( command ? .command . length + 2 ) : 1 ,
102127 endColumn : 1000
103128 } ,
104129 renderOptions : {
105130 after : {
106- contentText : command . detail ,
107- color : this . getPlaceholderColor ( )
131+ contentText : shouldRenderFollowupPlaceholder ? command . followupPlaceholder : command . detail ,
132+ color : this . getPlaceholderColor ( ) ,
133+ padding : '0 0 0 5px'
108134 }
109135 }
110- }
111- ] ;
112- this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandPlaceholderDecorationType , decoration ) ;
113- } else {
136+ } ] ;
137+ this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandPlaceholderDecorationType , slashCommandPlaceholderDecoration ) ;
138+ }
139+ }
140+ if ( ! slashCommandPlaceholderDecoration ) {
114141 this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandPlaceholderDecorationType , [ ] ) ;
115142 }
116143
144+ if ( command && inputValue . startsWith ( `/${ command . command } ` ) ) {
145+ this . updateInputEditorContentWidgets ( { command : command . command } ) ;
146+ } else {
147+ this . updateInputEditorContentWidgets ( { hide : true } ) ;
148+ }
149+
117150 if ( command && command . detail ) {
118151 const textDecoration : IDecorationOptions [ ] = [
119152 {
@@ -130,6 +163,40 @@ class InputEditorDecorations extends Disposable {
130163 this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandTextDecorationType , [ ] ) ;
131164 }
132165 }
166+
167+ private async updateInputEditorContentWidgets ( arg : { command : string } | { hide : true } ) {
168+ const domNode = this . _slashCommandDomNode ;
169+
170+ if ( this . _slashCommandContentWidget && 'hide' in arg ) {
171+ domNode . toggleAttribute ( 'hidden' , true ) ;
172+ this . widget . inputEditor . removeContentWidget ( this . _slashCommandContentWidget ) ;
173+ return ;
174+ } else if ( 'command' in arg ) {
175+ const theme = this . themeService . getColorTheme ( ) ;
176+ domNode . style . padding = '0 0.4em' ;
177+ domNode . style . borderRadius = '3px' ;
178+ domNode . style . backgroundColor = theme . getColor ( textCodeBlockBackground ) ?. toString ( ) ?? '' ;
179+ domNode . style . color = theme . getColor ( textLinkForeground ) ?. toString ( ) ?? '' ;
180+ domNode . innerText = `${ arg . command } ` ;
181+ domNode . toggleAttribute ( 'hidden' , false ) ;
182+
183+ this . _slashCommandContentWidget = {
184+ getId ( ) { return slashCommandContentWidgetId ; } ,
185+ getDomNode ( ) { return domNode ; } ,
186+ getPosition ( ) {
187+ return {
188+ position : {
189+ lineNumber : 1 ,
190+ column : 1
191+ } ,
192+ preference : [ ContentWidgetPositionPreference . EXACT ]
193+ } ;
194+ } ,
195+ } ;
196+
197+ this . widget . inputEditor . addContentWidget ( this . _slashCommandContentWidget ) ;
198+ }
199+ }
133200}
134201
135202class InputEditorSlashCommandFollowups extends Disposable {
@@ -139,6 +206,7 @@ class InputEditorSlashCommandFollowups extends Disposable {
139206 ) {
140207 super ( ) ;
141208 this . _register ( this . chatService . onDidSubmitSlashCommand ( ( { slashCommand, sessionId } ) => this . repopulateSlashCommand ( slashCommand , sessionId ) ) ) ;
209+ this . _register ( this . widget . inputEditor . onKeyUp ( ( e ) => this . handleKeyUp ( e ) ) ) ;
142210 }
143211
144212 private async repopulateSlashCommand ( slashCommand : string , sessionId : string ) {
@@ -159,6 +227,22 @@ class InputEditorSlashCommandFollowups extends Disposable {
159227
160228 }
161229 }
230+
231+ private handleKeyUp ( e : IKeyboardEvent ) {
232+ if ( e . keyCode !== KeyCode . Backspace ) {
233+ return ;
234+ }
235+
236+ const value = this . widget . inputEditor . getValue ( ) . split ( ' ' ) [ 0 ] ;
237+ const currentSelection = this . widget . inputEditor . getSelection ( ) ;
238+ if ( ! value . startsWith ( '/' ) || ! currentSelection ?. isEmpty ( ) || currentSelection ?. startLineNumber !== 1 || currentSelection ?. startColumn !== value . length + 1 ) {
239+ return ;
240+ }
241+
242+ if ( this . widget . getSlashCommandsSync ( ) ?. find ( ( command ) => `/${ command . command } ` === value ) ) {
243+ this . widget . inputEditor . executeEdits ( 'chat-input-editor-slash-commands' , [ { range : new Range ( 1 , 1 , 1 , currentSelection . startColumn ) , text : null } ] , [ new Selection ( 1 , 1 , 1 , 1 ) ] ) ;
244+ }
245+ }
162246}
163247
164248ChatWidget . CONTRIBS . push ( InputEditorDecorations , InputEditorSlashCommandFollowups ) ;
0 commit comments