@@ -15,24 +15,34 @@ import { ITextModel } from 'vs/editor/common/model';
15
15
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures' ;
16
16
import { localize } from 'vs/nls' ;
17
17
import { 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' ;
19
19
import { IThemeService } from 'vs/platform/theme/common/themeService' ;
20
20
import { IChatWidget , IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat' ;
21
21
import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget' ;
22
22
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle' ;
23
23
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart' ;
24
24
import { 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' ;
25
29
26
30
const decorationDescription = 'chat' ;
27
31
const slashCommandPlaceholderDecorationType = 'chat-session-detail' ;
28
32
const slashCommandTextDecorationType = 'chat-session-text' ;
33
+ const slashCommandContentWidgetId = 'chat-session-content-widget' ;
29
34
30
35
class InputEditorDecorations extends Disposable {
31
36
37
+ private _slashCommandDomNode = document . createElement ( 'div' ) ;
38
+ private _slashCommandContentWidget : IContentWidget | undefined ;
39
+ private _previouslyUsedSlashCommands = new Set < string > ( ) ;
40
+
32
41
constructor (
33
42
private readonly widget : IChatWidget ,
34
43
@ICodeEditorService private readonly codeEditorService : ICodeEditorService ,
35
44
@IThemeService private readonly themeService : IThemeService ,
45
+ @IChatService private readonly chatService : IChatService ,
36
46
) {
37
47
super ( ) ;
38
48
@@ -43,14 +53,25 @@ class InputEditorDecorations extends Disposable {
43
53
44
54
this . updateInputEditorDecorations ( ) ;
45
55
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
+ } ) ) ;
47
65
}
48
66
49
67
private updateRegisteredDecorationTypes ( ) {
50
- const theme = this . themeService . getColorTheme ( ) ;
51
68
this . codeEditorService . removeDecorationType ( slashCommandTextDecorationType ) ;
69
+ this . updateInputEditorContentWidgets ( { hide : true } ) ;
52
70
this . codeEditorService . registerDecorationType ( decorationDescription , slashCommandTextDecorationType , {
53
- color : theme . getColor ( textLinkForeground ) ?. toString ( )
71
+ opacity : '0' ,
72
+ after : {
73
+ contentText : ' ' ,
74
+ }
54
75
} ) ;
55
76
this . updateInputEditorDecorations ( ) ;
56
77
}
@@ -62,10 +83,10 @@ class InputEditorDecorations extends Disposable {
62
83
}
63
84
64
85
private async updateInputEditorDecorations ( ) {
65
- const value = this . widget . inputEditor . getValue ( ) ;
86
+ const inputValue = this . widget . inputEditor . getValue ( ) ;
66
87
const slashCommands = await this . widget . getSlashCommands ( ) ; // TODO this async call can lead to a flicker of the placeholder text when switching editor tabs
67
88
68
- if ( ! value ) {
89
+ if ( ! inputValue ) {
69
90
const extensionPlaceholder = this . widget . viewModel ?. inputPlaceholder ;
70
91
const defaultPlaceholder = slashCommands ?. length ?
71
92
localize ( 'interactive.input.placeholderWithCommands' , "Ask a question or type '/' for topics" ) :
@@ -88,32 +109,44 @@ class InputEditorDecorations extends Disposable {
88
109
}
89
110
] ;
90
111
this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandPlaceholderDecorationType , decoration ) ;
112
+ this . updateInputEditorContentWidgets ( { hide : true } ) ;
91
113
return ;
92
114
}
93
115
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 = [ {
98
123
range : {
99
124
startLineNumber : 1 ,
100
125
endLineNumber : 1 ,
101
- startColumn : command . command . length + 2 ,
126
+ startColumn : command && typeof command !== 'string' ? ( command ? .command . length + 2 ) : 1 ,
102
127
endColumn : 1000
103
128
} ,
104
129
renderOptions : {
105
130
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'
108
134
}
109
135
}
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 ) {
114
141
this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandPlaceholderDecorationType , [ ] ) ;
115
142
}
116
143
144
+ if ( command && inputValue . startsWith ( `/${ command . command } ` ) ) {
145
+ this . updateInputEditorContentWidgets ( { command : command . command } ) ;
146
+ } else {
147
+ this . updateInputEditorContentWidgets ( { hide : true } ) ;
148
+ }
149
+
117
150
if ( command && command . detail ) {
118
151
const textDecoration : IDecorationOptions [ ] = [
119
152
{
@@ -130,6 +163,40 @@ class InputEditorDecorations extends Disposable {
130
163
this . widget . inputEditor . setDecorationsByType ( decorationDescription , slashCommandTextDecorationType , [ ] ) ;
131
164
}
132
165
}
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
+ }
133
200
}
134
201
135
202
class InputEditorSlashCommandFollowups extends Disposable {
@@ -139,6 +206,7 @@ class InputEditorSlashCommandFollowups extends Disposable {
139
206
) {
140
207
super ( ) ;
141
208
this . _register ( this . chatService . onDidSubmitSlashCommand ( ( { slashCommand, sessionId } ) => this . repopulateSlashCommand ( slashCommand , sessionId ) ) ) ;
209
+ this . _register ( this . widget . inputEditor . onKeyUp ( ( e ) => this . handleKeyUp ( e ) ) ) ;
142
210
}
143
211
144
212
private async repopulateSlashCommand ( slashCommand : string , sessionId : string ) {
@@ -159,6 +227,22 @@ class InputEditorSlashCommandFollowups extends Disposable {
159
227
160
228
}
161
229
}
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
+ }
162
246
}
163
247
164
248
ChatWidget . CONTRIBS . push ( InputEditorDecorations , InputEditorSlashCommandFollowups ) ;
0 commit comments