Skip to content

Commit 90234e7

Browse files
authored
pick where code blocks without existing code uri are applied (microsoft#239208)
1 parent e1526ec commit 90234e7

File tree

2 files changed

+86
-42
lines changed

2 files changed

+86
-42
lines changed

src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class CodeBlockActionRendering extends Disposable implements IWorkbenchCo
108108
const context = this._context;
109109
if (isCodeBlockActionContext(context) && context.codemapperUri) {
110110
const label = labelService.getUriLabel(context.codemapperUri, { relative: true });
111-
return localize('interactive.applyInEditorWithURL.label', "Apply in {0}", label);
111+
return localize('interactive.applyInEditorWithURL.label', "Apply to {0}", label);
112112
}
113113
return super.getTooltip();
114114
}

src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import { ICodeMapperCodeBlock, ICodeMapperRequest, ICodeMapperResponse, ICodeMap
3232
import { ChatUserAction, IChatService } from '../../common/chatService.js';
3333
import { isResponseVM } from '../../common/chatViewModel.js';
3434
import { ICodeBlockActionContext } from '../codeBlockPart.js';
35+
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
36+
import { ILabelService } from '../../../../../platform/label/common/label.js';
3537

3638
export class InsertCodeBlockOperation {
3739
constructor(
@@ -104,48 +106,49 @@ export class ApplyCodeBlockOperation {
104106
constructor(
105107
@IEditorService private readonly editorService: IEditorService,
106108
@ITextFileService private readonly textFileService: ITextFileService,
107-
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
108109
@IChatService private readonly chatService: IChatService,
109110
@ILanguageService private readonly languageService: ILanguageService,
110111
@IFileService private readonly fileService: IFileService,
111112
@IDialogService private readonly dialogService: IDialogService,
112113
@ILogService private readonly logService: ILogService,
113114
@ICodeMapperService private readonly codeMapperService: ICodeMapperService,
114-
@IProgressService private readonly progressService: IProgressService
115+
@IProgressService private readonly progressService: IProgressService,
116+
@IQuickInputService private readonly quickInputService: IQuickInputService,
117+
@ILabelService private readonly labelService: ILabelService,
115118
) {
116119
}
117120

118121
public async run(context: ICodeBlockActionContext): Promise<void> {
119122
let activeEditorControl = getEditableActiveCodeEditor(this.editorService);
120123

121-
if (context.codemapperUri && !isEqual(activeEditorControl?.getModel().uri, context.codemapperUri)) {
122-
// If the code block is from a code mapper, first reveal the target file
123-
try {
124-
// If the file doesn't exist yet, create it
125-
if (!(await this.fileService.exists(context.codemapperUri))) {
126-
// TODO: try to find the file in the workspace
124+
const codemapperUri = await this.evaluateURIToUse(context.codemapperUri, activeEditorControl);
125+
if (!codemapperUri) {
126+
return;
127+
}
127128

128-
await this.fileService.writeFile(context.codemapperUri, VSBuffer.fromString(''));
129-
}
130-
await this.editorService.openEditor({ resource: context.codemapperUri });
129+
if (codemapperUri && !isEqual(activeEditorControl?.getModel().uri, codemapperUri)) {
130+
// reveal the target file
131+
try {
132+
await this.editorService.openEditor({ resource: codemapperUri });
131133

132134
activeEditorControl = getEditableActiveCodeEditor(this.editorService);
133135
if (activeEditorControl) {
134136
this.tryToRevealCodeBlock(activeEditorControl, context.code);
135137
}
136138
} catch (e) {
137-
this.logService.info('[ApplyCodeBlockOperation] error opening code mapper file', context.codemapperUri, e);
139+
this.logService.info('[ApplyCodeBlockOperation] error opening code mapper file', codemapperUri, e);
140+
return;
138141
}
139142
}
140143

141144
let result: IComputeEditsResult | undefined = undefined;
142145

143146
if (activeEditorControl) {
144-
result = await this.handleTextEditor(activeEditorControl, context);
147+
result = await this.handleTextEditor(activeEditorControl, context.code);
145148
} else {
146149
const activeNotebookEditor = getActiveNotebookEditor(this.editorService);
147150
if (activeNotebookEditor) {
148-
result = await this.handleNotebookEditor(activeNotebookEditor, context);
151+
result = await this.handleNotebookEditor(activeNotebookEditor, context.code);
149152
} else {
150153
this.notify(localize('applyCodeBlock.noActiveEditor', "To apply this code block, open a code or notebook editor."));
151154
}
@@ -159,57 +162,98 @@ export class ApplyCodeBlockOperation {
159162
});
160163
}
161164

162-
private async handleNotebookEditor(notebookEditor: IActiveNotebookEditor, codeBlockContext: ICodeBlockActionContext): Promise<IComputeEditsResult | undefined> {
165+
private async evaluateURIToUse(resource: URI | undefined, activeEditorControl: IActiveCodeEditor | undefined): Promise<URI | undefined> {
166+
if (resource && await this.fileService.exists(resource)) {
167+
return resource;
168+
}
169+
170+
const activeEditorOption = activeEditorControl?.getModel().uri ? { label: localize('activeEditor', "Active editor '{0}'", this.labelService.getUriLabel(activeEditorControl.getModel().uri, { relative: true })), id: 'activeEditor' } : undefined;
171+
const untitledEditorOption = { label: localize('newUntitledFile', "New untitled editor"), id: 'newUntitledFile' };
172+
173+
const options = [];
174+
if (resource) {
175+
// code block had an URI, but it doesn't exist
176+
options.push({ label: localize('createFile', "New file '{0}'", this.labelService.getUriLabel(resource, { relative: true })), id: 'createFile' });
177+
options.push(untitledEditorOption);
178+
if (activeEditorOption) {
179+
options.push(activeEditorOption);
180+
}
181+
} else {
182+
// code block had no URI
183+
if (activeEditorOption) {
184+
options.push(activeEditorOption);
185+
}
186+
options.push(untitledEditorOption);
187+
}
188+
189+
const selected = options.length > 1 ? await this.quickInputService.pick(options, { placeHolder: localize('selectOption', "Select where to apply the code block") }) : options[0];
190+
if (selected) {
191+
switch (selected.id) {
192+
case 'createFile':
193+
if (resource) {
194+
try {
195+
await this.fileService.writeFile(resource, VSBuffer.fromString(''));
196+
} catch (error) {
197+
this.notify(localize('applyCodeBlock.fileWriteError', "Failed to create file: {0}", error.message));
198+
return URI.from({ scheme: 'untitled', path: resource.path });
199+
}
200+
}
201+
return resource;
202+
case 'newUntitledFile':
203+
return URI.from({ scheme: 'untitled', path: resource ? resource.path : 'Untitled-1' });
204+
case 'activeEditor':
205+
return activeEditorControl?.getModel().uri;
206+
}
207+
}
208+
return undefined;
209+
}
210+
211+
private async handleNotebookEditor(notebookEditor: IActiveNotebookEditor, code: string): Promise<IComputeEditsResult | undefined> {
163212
if (notebookEditor.isReadOnly) {
164213
this.notify(localize('applyCodeBlock.readonlyNotebook', "Cannot apply code block to read-only notebook editor."));
165214
return undefined;
166215
}
167216
const focusRange = notebookEditor.getFocus();
168217
const next = Math.max(focusRange.end - 1, 0);
169-
insertCell(this.languageService, notebookEditor, next, CellKind.Code, 'below', codeBlockContext.code, true);
218+
insertCell(this.languageService, notebookEditor, next, CellKind.Code, 'below', code, true);
170219
return undefined;
171220
}
172221

173-
private async handleTextEditor(codeEditor: IActiveCodeEditor, codeBlockContext: ICodeBlockActionContext): Promise<IComputeEditsResult | undefined> {
222+
private async handleTextEditor(codeEditor: IActiveCodeEditor, code: string): Promise<IComputeEditsResult | undefined> {
174223
const activeModel = codeEditor.getModel();
175224
if (isReadOnly(activeModel, this.textFileService)) {
176225
this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file."));
177226
return undefined;
178227
}
179228

180-
const resource = codeBlockContext.codemapperUri ?? activeModel.uri;
181-
const codeBlock = { code: codeBlockContext.code, resource, markdownBeforeBlock: undefined };
229+
const codeBlock = { code, resource: activeModel.uri, markdownBeforeBlock: undefined };
182230

183231
const codeMapper = this.codeMapperService.providers[0]?.displayName;
184232
if (!codeMapper) {
185233
this.notify(localize('applyCodeBlock.noCodeMapper', "No code mapper available."));
186234
return undefined;
187235
}
188236
let editsProposed = false;
189-
190-
const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor);
191-
if (editorToApply && editorToApply.hasModel()) {
192-
193-
const cancellationTokenSource = new CancellationTokenSource();
194-
try {
195-
const iterable = await this.progressService.withProgress<AsyncIterable<TextEdit[]>>(
196-
{ location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true },
197-
async progress => {
198-
progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) });
199-
const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token);
200-
return await this.waitForFirstElement(editsIterable);
201-
},
202-
() => cancellationTokenSource.cancel()
203-
);
204-
editsProposed = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource);
205-
} catch (e) {
206-
if (!isCancellationError(e)) {
207-
this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message));
208-
}
209-
} finally {
210-
cancellationTokenSource.dispose();
237+
const cancellationTokenSource = new CancellationTokenSource();
238+
try {
239+
const iterable = await this.progressService.withProgress<AsyncIterable<TextEdit[]>>(
240+
{ location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true },
241+
async progress => {
242+
progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) });
243+
const editsIterable = this.getEdits(codeBlock, cancellationTokenSource.token);
244+
return await this.waitForFirstElement(editsIterable);
245+
},
246+
() => cancellationTokenSource.cancel()
247+
);
248+
editsProposed = await this.applyWithInlinePreview(iterable, codeEditor, cancellationTokenSource);
249+
} catch (e) {
250+
if (!isCancellationError(e)) {
251+
this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message));
211252
}
253+
} finally {
254+
cancellationTokenSource.dispose();
212255
}
256+
213257
return {
214258
editsProposed,
215259
codeMapper

0 commit comments

Comments
 (0)