Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit af8167f

Browse files
authored
Merge pull request #594 from codestoryai/features/add-#-folder-support
features/add # folder support
2 parents 09eaeb1 + 94acd04 commit af8167f

File tree

6 files changed

+283
-9
lines changed

6 files changed

+283
-9
lines changed

extensions/codestory/src/sidecar/client.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,14 @@ export class SideCarClient {
227227
baseUrl.pathname = '/api/agent/followup_chat';
228228
const url = baseUrl.toString();
229229
const activeWindowData = getCurrentActiveWindow();
230+
const folders = folderFromQuery(query);
230231
const sideCarModelConfiguration = await getSideCarModelConfiguration(await vscode.modelSelection.getConfiguration());
231232
const agentSystemInstruction = readCustomSystemInstruction();
232233
const body = {
233234
repo_ref: repoRef.getRepresentation(),
234235
query: query,
235236
thread_id: threadId,
236-
user_context: await convertVSCodeVariableToSidecar(variables),
237+
user_context: await convertVSCodeVariableToSidecar(variables, folders),
237238
project_labels: projectLabels,
238239
active_window_data: activeWindowData,
239240
model_config: sideCarModelConfiguration,
@@ -789,9 +790,35 @@ interface CodeSelectionUriRange {
789790
};
790791
}
791792

793+
function folderFromQuery(query: string): string[] {
794+
const folderRegex = /#folder:([^#\s]+)/g;
795+
const matches = query.matchAll(folderRegex);
796+
const folders: string[] = [];
797+
for (const match of matches) {
798+
// add the workspace root path to the folder so we get the relative path
799+
// here always
800+
console.log(match[1]);
801+
folders.push(match[1]);
802+
}
803+
console.log('folders from query', query);
804+
console.log('folders from query', folders);
805+
const workspaceFolders = folders.map((folder) => {
806+
const rootPath = vscode.workspace.rootPath;
807+
if (rootPath) {
808+
const newPath = path.join(rootPath, folder);
809+
return newPath;
810+
} else {
811+
return folder;
812+
}
813+
});
814+
console.log('workspace folder', workspaceFolders);
815+
return workspaceFolders;
816+
}
817+
792818
async function convertVSCodeVariableToSidecar(
793819
variables: readonly vscode.ChatResolvedVariable[],
794-
): Promise<{ variables: SidecarVariableTypes[]; file_content_map: { file_path: string; file_content: string; language: string }[]; terminal_selection: string | undefined }> {
820+
folders: string[],
821+
): Promise<{ variables: SidecarVariableTypes[]; file_content_map: { file_path: string; file_content: string; language: string }[]; terminal_selection: string | undefined; folder_paths: string[] }> {
795822
const sidecarVariables: SidecarVariableTypes[] = [];
796823
let terminalSelection: string | undefined = undefined;
797824
const fileCache: Map<string, vscode.TextDocument> = new Map();
@@ -863,6 +890,7 @@ async function convertVSCodeVariableToSidecar(
863890
};
864891
}),
865892
terminal_selection: terminalSelection,
893+
folder_paths: folders,
866894
};
867895
}
868896

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
303303

304304
const variables = [
305305
...chatVariablesService.getVariables(),
306-
{ name: 'file', description: nls.localize('file', "Choose a file in the workspace") }
306+
{ name: 'file', description: nls.localize('file', "Choose a file in the workspace") },
307+
{ name: 'folder', description: nls.localize('folder', "Choose a folder in the workspace") },
307308
];
308309
const variableText = variables
309310
.map(v => `* \`${chatVariableLeader}${v.name}\` - ${v.description}`)

src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts

+89
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,95 @@ export class SelectAndInsertFileAction extends Action2 {
213213
}
214214
registerAction2(SelectAndInsertFileAction);
215215

216+
export class SelectAndInsertFolderAction extends Action2 {
217+
static readonly ID = 'workbench.action.chat.selectAndInsertFolder';
218+
219+
constructor() {
220+
super({
221+
id: SelectAndInsertFolderAction.ID,
222+
title: '' // not displayed
223+
});
224+
}
225+
226+
async run(accessor: ServicesAccessor, ...args: any[]) {
227+
const textModelService = accessor.get(ITextModelService);
228+
const logService = accessor.get(ILogService);
229+
const quickInputService = accessor.get(IQuickInputService);
230+
const chatVariablesService = accessor.get(IChatVariablesService);
231+
232+
const context = args[0];
233+
if (!isSelectAndInsertFileActionContext(context)) {
234+
return;
235+
}
236+
237+
const doCleanup = () => {
238+
// Failed, remove the dangling `folder`
239+
context.widget.inputEditor.executeEdits('chatInsertFolder', [{ range: context.range, text: `` }]);
240+
};
241+
242+
let options: IQuickAccessOptions | undefined;
243+
const foldersVariableName = 'folders';
244+
const foldersItem = {
245+
label: localize('allFolders', 'All Folder'),
246+
description: localize('allFoldersDescription', 'Search for relevant folders in the workspace and provide context from them'),
247+
};
248+
// If we have a `files` variable, add an option to select all files in the picker.
249+
// This of course assumes that the `files` variable has the behavior that it searches
250+
// through files in the workspace.
251+
if (chatVariablesService.hasVariable(foldersVariableName)) {
252+
options = {
253+
providerOptions: <AnythingQuickAccessProviderRunOptions>{
254+
additionPicks: [foldersItem, { type: 'separator' }]
255+
},
256+
};
257+
}
258+
// TODO: have dedicated UX for this instead of using the quick access picker
259+
const picks = await quickInputService.quickAccess.pick('', options);
260+
if (!picks?.length) {
261+
logService.trace('SelectAndInsertFolderAction: no file selected');
262+
doCleanup();
263+
return;
264+
}
265+
266+
const editor = context.widget.inputEditor;
267+
const range = context.range;
268+
269+
// Handle the special case of selecting all files
270+
if (picks[0] === foldersItem) {
271+
const text = `#${foldersVariableName}`;
272+
const success = editor.executeEdits('chatInsertFolder', [{ range, text: text + ' ' }]);
273+
if (!success) {
274+
logService.trace(`SelectAndInsertFolderAction: failed to insert "${text}"`);
275+
doCleanup();
276+
}
277+
return;
278+
}
279+
280+
// Handle the case of selecting a specific file
281+
const resource = (picks[0] as unknown as { resource: unknown }).resource as URI;
282+
if (!textModelService.canHandleResource(resource)) {
283+
logService.trace('SelectAndInsertFolderAction: non-text resource selected');
284+
doCleanup();
285+
return;
286+
}
287+
288+
const fileName = basename(resource);
289+
const text = `#folder:${fileName}`;
290+
const success = editor.executeEdits('chatInsertFolder', [{ range, text: text + ' ' }]);
291+
if (!success) {
292+
logService.trace(`SelectAndInsertFolderAction: failed to insert "${text}"`);
293+
doCleanup();
294+
return;
295+
}
296+
297+
context.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID)?.addReference({
298+
range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length },
299+
data: [{ level: 'full', value: resource }]
300+
});
301+
}
302+
}
303+
registerAction2(SelectAndInsertFolderAction);
304+
216305
export interface IAddDynamicVariableContext {
217306
widget: IChatWidget;
218307
range: IRange;

src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExec
2525
import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
2626
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
2727
import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
28-
import { SelectAndInsertFileAction, dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
28+
import { SelectAndInsertFileAction, SelectAndInsertFolderAction, dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
2929
import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
3030
import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors';
3131
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
@@ -542,7 +542,8 @@ class BuiltinDynamicCompletions extends Disposable {
542542
return null;
543543
}
544544

545-
const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length);
545+
const fileAfterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length);
546+
const folderAfterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#folder:'.length);
546547
return <CompletionList>{
547548
suggestions: [
548549
<CompletionItem>{
@@ -551,7 +552,16 @@ class BuiltinDynamicCompletions extends Disposable {
551552
detail: localize('pickFileLabel', "Pick a file"),
552553
range,
553554
kind: CompletionItemKind.Text,
554-
command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] },
555+
command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: fileAfterRange }] },
556+
sortText: 'z'
557+
},
558+
<CompletionItem>{
559+
label: `${chatVariableLeader}folder`,
560+
insertText: `${chatVariableLeader}folder:`,
561+
detail: localize('pickFolderLabel', "Pick a folder"),
562+
range,
563+
kind: CompletionItemKind.Text,
564+
command: { id: SelectAndInsertFolderAction.ID, title: SelectAndInsertFolderAction.ID, arguments: [{ widget, range: folderAfterRange }] },
555565
sortText: 'z'
556566
}
557567
]

src/vs/workbench/contrib/chat/browser/contrib/csChatDynamicVariables.ts

+59-2
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserT
2525
import { ISymbolQuickPickItem } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess';
2626
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
2727

28+
export const FolderReferenceCompletionProviderName = 'chatInplaceFolderReferenceCompletionProvider';
2829
export const FileReferenceCompletionProviderName = 'chatInplaceFileReferenceCompletionProvider';
2930
export const CodeSymbolCompletionProviderName = 'chatInplaceCodeCompletionProvider';
3031

3132
interface MultiLevelCodeTriggerActionContext {
3233
widget: IChatWidget;
3334
range: IRange;
34-
pick: 'file' | 'code';
35+
pick: 'file' | 'code' | 'folder';
3536
}
3637

3738
function isMultiLevelCodeTriggerActionContext(context: any): context is MultiLevelCodeTriggerActionContext {
@@ -71,7 +72,7 @@ export class MultiLevelCodeTriggerAction extends Action2 {
7172
const completionProviders = languageFeaturesService.completionProvider.getForAllLanguages();
7273
const codeSymbolCompletionProvider = completionProviders.find(
7374
provider => provider._debugDisplayName === (
74-
context.pick === 'code' ? CodeSymbolCompletionProviderName : FileReferenceCompletionProviderName
75+
context.pick === 'code' ? CodeSymbolCompletionProviderName : context.pick === 'file' ? FileReferenceCompletionProviderName : FolderReferenceCompletionProviderName
7576
));
7677

7778
if (!codeSymbolCompletionProvider) {
@@ -94,6 +95,62 @@ function isSelectAndInsertFileActionContext(context: any): context is SelectAndI
9495
return 'widget' in context && 'range' in context && 'uri' in context;
9596
}
9697

98+
export class SelectAndInsertFolderAction extends Action2 {
99+
static readonly ID = 'workbench.action.chat.csSelectAndInsertFolder';
100+
101+
constructor() {
102+
super({
103+
id: SelectAndInsertFolderAction.ID,
104+
title: '' // not displayed
105+
});
106+
}
107+
108+
async run(accessor: ServicesAccessor, ...args: any[]) {
109+
const textModelService = accessor.get(ITextModelService);
110+
const logService = accessor.get(ILogService);
111+
112+
const context = args[0];
113+
if (!isSelectAndInsertFileActionContext(context)) {
114+
return;
115+
}
116+
117+
const doCleanup = () => {
118+
// Failed, remove the dangling `file`
119+
context.widget.inputEditor.executeEdits('chatInsertFolder', [{ range: context.range, text: `` }]);
120+
};
121+
122+
const resource = context.uri;
123+
if (!resource) {
124+
logService.trace('SelectAndInsertFolderAction: no resource selected');
125+
doCleanup();
126+
return;
127+
}
128+
129+
const model = await textModelService.createModelReference(resource);
130+
const fileRange = model.object.textEditorModel.getFullModelRange();
131+
model.dispose();
132+
133+
const fileName = basename(resource);
134+
const editor = context.widget.inputEditor;
135+
const text = `${chatVariableLeader}folder:${fileName}`;
136+
const range = context.range;
137+
const success = editor.executeEdits('chatInsertFolder', [{ range, text: text + ' ' }]);
138+
if (!success) {
139+
logService.trace(`SelectAndInsertFolderAction: failed to insert "${text}"`);
140+
doCleanup();
141+
return;
142+
}
143+
144+
const valueObj = { uri: resource, range: fileRange };
145+
const value = JSON.stringify(valueObj);
146+
context.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID)?.addReference({
147+
range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length },
148+
data: [{ level: 'full', value, kind: 'folder' }]
149+
});
150+
}
151+
}
152+
registerAction2(SelectAndInsertFolderAction);
153+
97154
export class SelectAndInsertFileAction extends Action2 {
98155
static readonly ID = 'workbench.action.chat.csSelectAndInsertFile';
99156

0 commit comments

Comments
 (0)