Skip to content

Commit

Permalink
Use ICodeMapperService for 'Apply In Editor'
Browse files Browse the repository at this point in the history
  • Loading branch information
aeschli committed Jan 8, 2025
1 parent 8ad5ac5 commit 4f2c166
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 309 deletions.
6 changes: 3 additions & 3 deletions src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCodeMapper);
}

$registerCodeMapperProvider(handle: number): void {
$registerCodeMapperProvider(handle: number, displayName: string): void {
const impl: ICodeMapperProvider = {
displayName,
mapCode: async (uiRequest: ICodeMapperRequest, response: ICodeMapperResponse, token: CancellationToken) => {
const requestId = String(MainThreadChatCodemapper._requestHandlePool++);
this._responseMap.set(requestId, response);
const extHostRequest: ICodeMapperRequestDto = {
requestId,
codeBlocks: uiRequest.codeBlocks,
conversation: uiRequest.conversation
codeBlocks: uiRequest.codeBlocks
};
try {
return await this._proxy.$mapCode(handle, extHostRequest, token).then((result) => result ?? undefined);
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,7 +1299,7 @@ export interface ICodeMapperTextEdit {
export type ICodeMapperProgressDto = Dto<ICodeMapperTextEdit>;

export interface MainThreadCodeMapperShape extends IDisposable {
$registerCodeMapperProvider(handle: number): void;
$registerCodeMapperProvider(handle: number, displayName: string): void;
$unregisterCodeMapperProvider(handle: number): void;
$handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise<void>;
}
Expand Down
19 changes: 2 additions & 17 deletions src/vs/workbench/api/common/extHostCodeMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js';
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js';
import * as extHostProtocol from './extHost.protocol.js';
import { ChatAgentResult, DocumentContextItem, TextEdit } from './extHostTypeConverters.js';
import { TextEdit } from './extHostTypeConverters.js';
import { URI } from '../../../base/common/uri.js';

export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape {
Expand Down Expand Up @@ -48,21 +48,6 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape
resource: URI.revive(block.resource),
markdownBeforeBlock: block.markdownBeforeBlock
};
}),
conversation: internalRequest.conversation.map(item => {
if (item.type === 'request') {
return {
type: 'request',
message: item.message
} satisfies vscode.ConversationRequest;
} else {
return {
type: 'response',
message: item.message,
result: item.result ? ChatAgentResult.to(item.result) : undefined,
references: item.references?.map(DocumentContextItem.to)
} satisfies vscode.ConversationResponse;
}
})
};

Expand All @@ -72,7 +57,7 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape

registerMappedEditsProvider(extension: IExtensionDescription, provider: vscode.MappedEditsProvider2): vscode.Disposable {
const handle = ExtHostCodeMapper._providerHandlePool++;
this._proxy.$registerCodeMapperProvider(handle);
this._proxy.$registerCodeMapperProvider(handle, extension.displayName ?? extension.name);
this.providers.set(handle, provider);
return {
dispose: () => {
Expand Down
198 changes: 55 additions & 143 deletions src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,34 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { coalesce } from '../../../../../base/common/arrays.js';
import { AsyncIterableObject } from '../../../../../base/common/async.js';
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
import { CharCode } from '../../../../../base/common/charCode.js';
import { isCancellationError } from '../../../../../base/common/errors.js';
import { isEqual } from '../../../../../base/common/resources.js';
import * as strings from '../../../../../base/common/strings.js';
import { URI } from '../../../../../base/common/uri.js';
import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js';
import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { ConversationRequest, ConversationResponse, DocumentContextItem, IWorkspaceFileEdit, IWorkspaceTextEdit } from '../../../../../editor/common/languages.js';
import { TextEdit } from '../../../../../editor/common/languages.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { ITextModel } from '../../../../../editor/common/model.js';
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
import { localize } from '../../../../../nls.js';
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js';
import { insertCell } from '../../../notebook/browser/controller/cellOperations.js';
import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js';
import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js';
import { getReferencesAsDocumentContext } from '../../common/chatCodeMapperService.js';
import { ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js';
import { ChatUserAction, IChatService } from '../../common/chatService.js';
import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js';
import { isResponseVM } from '../../common/chatViewModel.js';
import { ICodeBlockActionContext } from '../codeBlockPart.js';

export class InsertCodeBlockOperation {
Expand Down Expand Up @@ -98,7 +96,7 @@ export class InsertCodeBlockOperation {
}
}

type IComputeEditsResult = { readonly edits?: Array<IWorkspaceTextEdit | IWorkspaceFileEdit>; readonly codeMapper?: string };
type IComputeEditsResult = { readonly editsProposed: boolean; readonly codeMapper?: string };

export class ApplyCodeBlockOperation {

Expand All @@ -107,15 +105,13 @@ export class ApplyCodeBlockOperation {
constructor(
@IEditorService private readonly editorService: IEditorService,
@ITextFileService private readonly textFileService: ITextFileService,
@IBulkEditService private readonly bulkEditService: IBulkEditService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
@IChatService private readonly chatService: IChatService,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IProgressService private readonly progressService: IProgressService,
@ILanguageService private readonly languageService: ILanguageService,
@IFileService private readonly fileService: IFileService,
@IDialogService private readonly dialogService: IDialogService,
@ILogService private readonly logService: ILogService,
@ICodeMapperService private readonly codeMapperService: ICodeMapperService
) {
}

Expand Down Expand Up @@ -166,7 +162,7 @@ export class ApplyCodeBlockOperation {
codeBlockIndex: context.codeBlockIndex,
totalCharacters: context.code.length,
codeMapper: result?.codeMapper,
editsProposed: !!result?.edits,
editsProposed: !!result?.editsProposed
});
}

Expand All @@ -187,101 +183,60 @@ export class ApplyCodeBlockOperation {
return undefined;
}

const result = await this.computeEdits(codeEditor, codeBlockContext);
if (result.edits) {
const showWithPreview = await this.applyWithInlinePreview(result.edits, codeEditor);
if (!showWithPreview) {
await this.bulkEditService.apply(result.edits, { showPreview: true });
const activeModel = codeEditor.getModel();
this.codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus();
}
}
return result;
}
const cancellationTokenSource = new CancellationTokenSource();
try {
const activeModel = codeEditor.getModel();
const resource = codeBlockContext.codemapperUri ?? activeModel.uri;
let codeMapper: string | undefined;

private async computeEdits(codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise<IComputeEditsResult> {
const activeModel = codeEditor.getModel();

const mappedEditsProviders = this.languageFeaturesService.mappedEditsProvider.ordered(activeModel);
if (mappedEditsProviders.length > 0) {

// 0th sub-array - editor selections array if there are any selections
// 1st sub-array - array with documents used to get the chat reply
const docRefs: DocumentContextItem[][] = [];
collectDocumentContextFromSelections(codeEditor, docRefs);
collectDocumentContextFromContext(codeBlockActionContext, docRefs);

const cancellationTokenSource = new CancellationTokenSource();
let codeMapper; // the last used code mapper
try {
const result = await this.progressService.withProgress<IComputeEditsResult | undefined>(
{ location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true },
async progress => {
for (const provider of mappedEditsProviders) {
codeMapper = provider.displayName;
progress.report({ message: localize('applyCodeBlock.progress', "Applying code block using {0}...", codeMapper) });
const mappedEdits = await provider.provideMappedEdits(
activeModel,
[codeBlockActionContext.code],
{
documents: docRefs,
conversation: getChatConversation(codeBlockActionContext),
},
cancellationTokenSource.token
);
if (mappedEdits) {
return { edits: mappedEdits.edits, codeMapper };
}
}
return undefined;
},
() => cancellationTokenSource.cancel()
);
if (result) {
return result;
}
} catch (e) {
if (!isCancellationError(e)) {
this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message));
const iterable = new AsyncIterableObject<TextEdit[]>(async executor => {
const request: ICodeMapperRequest = {
codeBlocks: [{ code: codeBlockContext.code, resource, markdownBeforeBlock: undefined }]
};
const response: ICodeMapperResponse = {
textEdit: (target: URI, edit: TextEdit[]) => {
executor.emitOne(edit);
}
};
const result = await this.codeMapperService.mapCode(request, response, cancellationTokenSource.token);
codeMapper = result?.providerName;
if (result?.errorMessage) {
executor.reject(new Error(result.errorMessage));
}
} finally {
cancellationTokenSource.dispose();
});

const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor);
let result = false;
if (editorToApply && editorToApply.hasModel()) {
result = await this.applyWithInlinePreview(iterable, editorToApply, cancellationTokenSource);
}
return { edits: [], codeMapper };
return { editsProposed: result, codeMapper };
} catch (e) {
if (!isCancellationError(e)) {
this.notify(localize('applyCodeBlock.error', "An error occurred while applying the code block."));
}
} finally {
cancellationTokenSource.dispose();
}
return { edits: [], codeMapper: undefined };
return undefined;

}
private async applyWithInlinePreview(edits: AsyncIterable<TextEdit[]>, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise<boolean> {
const inlineChatController = InlineChatController.get(codeEditor);
if (inlineChatController) {
let isOpen = true;
const promise = inlineChatController.reviewEdits(codeEditor.getSelection(), edits, tokenSource.token);
promise.finally(() => {
isOpen = false;
tokenSource.dispose();
});
this.inlineChatPreview = {
promise,
isOpen: () => isOpen,
cancel: () => tokenSource.cancel(),
};
return true;

private async applyWithInlinePreview(edits: Array<IWorkspaceTextEdit | IWorkspaceFileEdit>, codeEditor: IActiveCodeEditor): Promise<boolean> {
const firstEdit = edits[0];
if (!ResourceTextEdit.is(firstEdit)) {
return false;
}
const resource = firstEdit.resource;
const textEdits = coalesce(edits.map(edit => ResourceTextEdit.is(edit) && isEqual(resource, edit.resource) ? edit.textEdit : undefined));
if (textEdits.length !== edits.length) { // more than one file has changed, fall back to bulk edit preview
return false;
}
const editorToApply = await this.codeEditorService.openCodeEditor({ resource }, codeEditor);
if (editorToApply) {
const inlineChatController = InlineChatController.get(editorToApply);
if (inlineChatController) {
const tokenSource = new CancellationTokenSource();
let isOpen = true;
const firstEdit = textEdits[0];
editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber);
const promise = inlineChatController.reviewEdits(textEdits[0].range, AsyncIterableObject.fromArray([textEdits]), tokenSource.token);
promise.finally(() => {
isOpen = false;
tokenSource.dispose();
});
this.inlineChatPreview = {
promise,
isOpen: () => isOpen,
cancel: () => tokenSource.cancel(),
};
return true;
}
}
return false;
}
Expand Down Expand Up @@ -360,49 +315,6 @@ function isReadOnly(model: ITextModel, textFileService: ITextFileService): boole
return !!activeTextModel?.isReadonly();
}

function collectDocumentContextFromSelections(codeEditor: IActiveCodeEditor, result: DocumentContextItem[][]): void {
const activeModel = codeEditor.getModel();
const currentDocUri = activeModel.uri;
const currentDocVersion = activeModel.getVersionId();
const selections = codeEditor.getSelections();
if (selections.length > 0) {
result.push([
{
uri: currentDocUri,
version: currentDocVersion,
ranges: selections,
}
]);
}
}


function collectDocumentContextFromContext(context: ICodeBlockActionContext, result: DocumentContextItem[][]): void {
if (isResponseVM(context.element) && context.element.usedContext?.documents) {
result.push(context.element.usedContext.documents);
}
}

function getChatConversation(context: ICodeBlockActionContext): (ConversationRequest | ConversationResponse)[] {
// TODO@aeschli for now create a conversation with just the current element
// this will be expanded in the future to include the request and any other responses

if (isResponseVM(context.element)) {
return [{
type: 'response',
message: context.element.response.getMarkdown(),
references: getReferencesAsDocumentContext(context.element.contentReferences)
}];
} else if (isRequestVM(context.element)) {
return [{
type: 'request',
message: context.element.messageText,
}];
} else {
return [];
}
}

function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number): string {
const newContent = strings.splitLines(codeBlockContent);
if (newContent.length === 0) {
Expand Down
3 changes: 1 addition & 2 deletions src/vs/workbench/contrib/chat/browser/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ class EditTool implements IToolData, IToolImpl {
}

const result = await this.codeMapperService.mapCode({
codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }],
conversation: []
codeBlocks: [{ code: parameters.code, resource: uri, markdownBeforeBlock: parameters.explanation }]
}, {
textEdit: (target, edits) => {
model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits });
Expand Down
Loading

0 comments on commit 4f2c166

Please sign in to comment.