Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mapped edits: support de/serialization of mapped edits context so that it can be used across vscode sessions #194572

Merged
merged 4 commits into from Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -255,10 +255,10 @@ export function registerChatCodeBlockActions() {

// try applying workspace edit that was returned by a MappedEditsProvider, else simply insert at selection

let workspaceEdit: WorkspaceEdit | null = null;
let mappedEdits: WorkspaceEdit | null = null;

if (mappedEditsProviders.length > 0) {
const mostRelevantProvider = mappedEditsProviders[0];
const mostRelevantProvider = mappedEditsProviders[0]; // TODO@ulugbekna: should we try all providers?

// 0th sub-array - editor selections array if there are any selections
// 1st sub-array - array with documents used to get the chat reply
Expand Down Expand Up @@ -287,21 +287,23 @@ export function registerChatCodeBlockActions() {

const cancellationTokenSource = new CancellationTokenSource();

workspaceEdit = await mostRelevantProvider.provideMappedEdits(
mappedEdits = await mostRelevantProvider.provideMappedEdits(
activeModel,
[chatCodeBlockActionContext.code],
{ documents: docRefs },
cancellationTokenSource.token);
}

if (workspaceEdit) {
await bulkEditService.apply(workspaceEdit);
if (mappedEdits) {
await bulkEditService.apply(mappedEdits);
} else {
const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1);
await bulkEditService.apply([new ResourceTextEdit(activeModel.uri, {
range: activeSelection,
text: chatCodeBlockActionContext.code,
})]);
await bulkEditService.apply([
new ResourceTextEdit(activeModel.uri, {
range: activeSelection,
text: chatCodeBlockActionContext.code,
}),
]);
}
codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus();
}
Expand Down
27 changes: 15 additions & 12 deletions src/vs/workbench/contrib/chat/common/chatModel.ts
Expand Up @@ -13,7 +13,7 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { ILogService } from 'vs/platform/log/common/log';
import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService';

export interface IChatRequestModel {
readonly id: string;
Expand Down Expand Up @@ -62,14 +62,6 @@ export interface IChatResponseModel {
setVote(vote: InteractiveSessionVoteDirection): void;
}

export function isRequest(item: unknown): item is IChatRequestModel {
return !!item && typeof (item as IChatRequestModel).message !== 'undefined';
}

export function isResponse(item: unknown): item is IChatResponseModel {
return !isRequest(item);
}

export class ChatRequestModel implements IChatRequestModel {
private static nextId = 0;

Expand Down Expand Up @@ -355,6 +347,8 @@ export interface ISerializableChatRequestData {
followups: IChatFollowup[] | undefined;
isCanceled: boolean | undefined;
vote: InteractiveSessionVoteDirection | undefined;
/** For backward compat: should be optional */
usedContext?: IUsedContext;
}

export interface IExportableChatData {
Expand Down Expand Up @@ -386,7 +380,10 @@ export function isSerializableSessionData(obj: unknown): obj is ISerializableCha
const data = obj as ISerializableChatData;
return isExportableSessionData(obj) &&
typeof data.creationDate === 'number' &&
typeof data.sessionId === 'string';
typeof data.sessionId === 'string' &&
obj.requests.every((request: ISerializableChatRequestData) =>
!request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext)
);
}

export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent | IChatRemoveRequestEvent;
Expand Down Expand Up @@ -523,12 +520,17 @@ export class ChatModel extends Disposable implements IChatModel {

try {
return requests.map((raw: ISerializableChatRequestData) => {
const parsedRequest = typeof raw.message === 'string' ? this.getParsedRequestFromString(raw.message) :
reviveParsedChatRequest(raw.message);
const parsedRequest =
typeof raw.message === 'string'
? this.getParsedRequestFromString(raw.message)
: reviveParsedChatRequest(raw.message);
const request = new ChatRequestModel(this, parsedRequest, raw.providerRequestId);
if (raw.response || raw.responseErrorDetails) {
const agent = raw.agent && this.chatAgentService.getAgents().find(a => a.id === raw.agent!.id); // TODO do something reasonable if this agent has disappeared since the last session
request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups);
if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?
request.response.updateContent(raw.usedContext);
}
}
return request;
});
Expand Down Expand Up @@ -708,6 +710,7 @@ export class ChatModel extends Disposable implements IChatModel {
fullName: r.response.agent.metadata.fullName,
icon: r.response.agent.metadata.icon
} : undefined,
usedContext: r.response?.response.usedContext,
};
}),
providerId: this.providerId,
Expand Down
22 changes: 21 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatService.ts
Expand Up @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { Range, IRange } from 'vs/editor/common/core/range';
import { ProviderResult } from 'vs/editor/common/languages';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatModel, ChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
Expand Down Expand Up @@ -59,10 +59,30 @@ export type IDocumentContext = {
ranges: IRange[];
};

export function isIDocumentContext(obj: unknown): obj is IDocumentContext {
return (
!!obj &&
typeof obj === 'object' &&
'uri' in obj && obj.uri instanceof URI &&
'version' in obj && typeof obj.version === 'number' &&
'ranges' in obj && Array.isArray(obj.ranges) && obj.ranges.every(Range.isIRange)
);
}

export type IUsedContext = {
documents: IDocumentContext[];
};

export function isIUsedContext(obj: unknown): obj is IUsedContext {
return (
!!obj &&
typeof obj === 'object' &&
'documents' in obj &&
Array.isArray(obj.documents) &&
obj.documents.every(isIDocumentContext)
);
}

export type IChatProgress =
| { content: string | IMarkdownString }
| { requestId: string }
Expand Down
@@ -0,0 +1,68 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [
{
providerRequestId: undefined,
message: {
text: "test request",
parts: [
{
range: {
start: 0,
endExclusive: 12
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 13
},
text: "test request",
kind: "text"
}
]
},
response: [
{
value: "",
isTrusted: false,
supportThemeIcons: false,
supportHtml: false
}
],
responseErrorDetails: undefined,
followups: undefined,
isCanceled: false,
vote: undefined,
agent: undefined,
usedContext: { documents: [
{
uri: {
scheme: "file",
authority: "",
path: "/test/path/to/file",
query: "",
fragment: "",
_formatted: null,
_fsPath: null
},
version: 3,
ranges: [
{
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 2
}
]
}
] }
}
],
providerId: "ChatProviderWithUsedContext",
providerState: undefined
}
@@ -0,0 +1,10 @@
{
requesterUsername: "",
requesterAvatarIconUri: undefined,
responderUsername: "",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [ ],
providerId: "ChatProviderWithUsedContext",
providerState: undefined
}
@@ -0,0 +1,68 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [
{
providerRequestId: undefined,
message: {
parts: [
{
range: {
start: 0,
endExclusive: 12
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 13
},
text: "test request",
kind: "text"
}
],
text: "test request"
},
response: [
{
value: "",
isTrusted: false,
supportThemeIcons: false,
supportHtml: false
}
],
responseErrorDetails: undefined,
followups: undefined,
isCanceled: false,
vote: undefined,
agent: undefined,
usedContext: { documents: [
{
uri: {
scheme: "file",
authority: "",
path: "/test/path/to/file",
query: "",
fragment: "",
_formatted: null,
_fsPath: null
},
version: 3,
ranges: [
{
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 2
}
]
}
] }
}
],
providerId: "ChatProviderWithUsedContext",
providerState: undefined
}