Path: blob/main/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { URI } from '../../../../base/common/uri.js';6import { isEqual } from '../../../../base/common/resources.js';7import { ICodeEditor, IDiffEditor } from '../../../../editor/browser/editorBrowser.js';8import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';9import { IRange } from '../../../../editor/common/core/range.js';10import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js';11import { EditorResourceAccessor, SideBySideEditor } from '../../../../workbench/common/editor.js';12import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js';13import { editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js';14import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';15import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';16import { MultiDiffEditorInput } from '../../../../workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.js';17import { ISessionFileChange } from '../../../services/sessions/common/session.js';1819/**20* Find the session that contains the given resource by checking editing sessions,21* sessions providers, and agent sessions.22*/23export function getSessionForResource(24resourceUri: URI,25chatEditingService: IChatEditingService,26sessionsManagementService: ISessionsManagementService,27): URI | undefined {28for (const editingSession of chatEditingService.editingSessionsObs.get()) {29if (editingEntriesContainResource(editingSession.entries.get(), resourceUri)) {30return editingSession.chatSessionResource;31}32}33for (const session of sessionsManagementService.getSessions()) {34const changes = session.changes.get();35if (changes.some(change => changeMatchesResource(change, resourceUri))) {36return session.resource;37}38}3940return undefined;41}4243export interface IAgentFeedbackContext {44readonly codeSelection?: string;45readonly diffHunks?: string;46}4748export function changeMatchesResource(change: ISessionFileChange, resourceUri: URI): boolean {49if (isIChatSessionFileChange2(change)) {50return isEqual(change.uri, resourceUri)51|| isEqual(change.modifiedUri, resourceUri)52|| isEqual(change.originalUri, resourceUri);53}5455return isEqual(change.modifiedUri, resourceUri)56|| isEqual(change.originalUri, resourceUri);57}5859export function getSessionChangeForResource(60sessionResource: URI | undefined,61resourceUri: URI,62sessionsManagementService: ISessionsManagementService,63): ISessionFileChange | undefined {64if (!sessionResource) {65return undefined;66}6768const sessionData = sessionsManagementService.getSession(sessionResource);69if (sessionData) {70const changes = sessionData.changes.get();71return changes.find(change => changeMatchesResource(change, resourceUri));72}7374return undefined;75}7677export function createAgentFeedbackContext(78editor: ICodeEditor,79codeEditorService: ICodeEditorService,80resourceUri: URI,81range: IRange,82): IAgentFeedbackContext {83const codeSelection = getCodeSelection(editor, codeEditorService, resourceUri, range);84const diffHunks = getDiffHunks(editor, codeEditorService, resourceUri, range);85return { codeSelection, diffHunks };86}8788function getCodeSelection(89editor: ICodeEditor,90codeEditorService: ICodeEditorService,91resourceUri: URI,92range: IRange,93): string | undefined {94const model = getModelForResource(editor, codeEditorService, resourceUri);95if (!model) {96return undefined;97}9899const selection = model.getValueInRange(range);100return selection.length > 0 ? selection : undefined;101}102103function getDiffHunks(104editor: ICodeEditor,105codeEditorService: ICodeEditorService,106resourceUri: URI,107range: IRange,108): string | undefined {109const diffEditor = getContainingDiffEditor(editor, codeEditorService);110if (!diffEditor) {111return undefined;112}113114const originalModel = diffEditor.getOriginalEditor().getModel();115const modifiedModel = diffEditor.getModifiedEditor().getModel();116if (!originalModel || !modifiedModel) {117return undefined;118}119120const selectionIsInOriginal = isEqual(resourceUri, originalModel.uri);121const selectionIsInModified = isEqual(resourceUri, modifiedModel.uri);122if (!selectionIsInOriginal && !selectionIsInModified) {123return undefined;124}125126const diffResult = diffEditor.getDiffComputationResult();127if (!diffResult) {128return undefined;129}130131const selectionIsEmpty = range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn;132const relevantGroups = groupChanges(diffResult.changes2).filter(group => {133const changeTouchesSelection = (change: DetailedLineRangeMapping) => rangeTouchesChange(range, selectionIsInOriginal ? change.original : change.modified);134return selectionIsEmpty ? group.some(changeTouchesSelection) : group.every(changeTouchesSelection);135});136if (relevantGroups.length === 0) {137return undefined;138}139140const originalText = originalModel.getValue();141const modifiedText = modifiedModel.getValue();142const originalEndsWithNewline = originalText.length > 0 && originalText.endsWith('\n');143const modifiedEndsWithNewline = modifiedText.length > 0 && modifiedText.endsWith('\n');144const originalLines = originalText.split('\n');145const modifiedLines = modifiedText.split('\n');146147if (originalEndsWithNewline && originalLines[originalLines.length - 1] === '') {148originalLines.pop();149}150if (modifiedEndsWithNewline && modifiedLines[modifiedLines.length - 1] === '') {151modifiedLines.pop();152}153154return relevantGroups.map(group => renderHunkGroup(group, originalLines, modifiedLines, originalEndsWithNewline, modifiedEndsWithNewline)).join('\n');155}156157function getContainingDiffEditor(editor: ICodeEditor, codeEditorService: ICodeEditorService): IDiffEditor | undefined {158return codeEditorService.listDiffEditors().find(diffEditor =>159diffEditor.getModifiedEditor() === editor || diffEditor.getOriginalEditor() === editor160);161}162163function getModelForResource(editor: ICodeEditor, codeEditorService: ICodeEditorService, resourceUri: URI) {164const currentModel = editor.getModel();165if (currentModel && isEqual(currentModel.uri, resourceUri)) {166return currentModel;167}168169const diffEditor = getContainingDiffEditor(editor, codeEditorService);170const originalModel = diffEditor?.getOriginalEditor().getModel();171if (originalModel && isEqual(originalModel.uri, resourceUri)) {172return originalModel;173}174175const modifiedModel = diffEditor?.getModifiedEditor().getModel();176if (modifiedModel && isEqual(modifiedModel.uri, resourceUri)) {177return modifiedModel;178}179180return undefined;181}182183function groupChanges(changes: readonly DetailedLineRangeMapping[]): DetailedLineRangeMapping[][] {184const contextSize = 3;185const groups: DetailedLineRangeMapping[][] = [];186let currentGroup: DetailedLineRangeMapping[] = [];187188for (const change of changes) {189if (currentGroup.length === 0) {190currentGroup.push(change);191continue;192}193194const lastChange = currentGroup[currentGroup.length - 1];195const lastContextEnd = lastChange.original.endLineNumberExclusive - 1 + contextSize;196const currentContextStart = change.original.startLineNumber - contextSize;197if (currentContextStart <= lastContextEnd + 1) {198currentGroup.push(change);199} else {200groups.push(currentGroup);201currentGroup = [change];202}203}204205if (currentGroup.length > 0) {206groups.push(currentGroup);207}208209return groups;210}211212function rangeTouchesChange(213range: IRange,214lineRange: { startLineNumber: number; endLineNumberExclusive: number; isEmpty: boolean; contains(lineNumber: number): boolean },215): boolean {216const isEmptySelection = range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn;217if (isEmptySelection) {218return !lineRange.isEmpty && lineRange.contains(range.startLineNumber);219}220221const selectionStart = range.startLineNumber;222const selectionEndExclusive = range.endLineNumber + 1;223return selectionStart <= lineRange.startLineNumber && lineRange.endLineNumberExclusive <= selectionEndExclusive;224}225226function renderHunkGroup(227group: readonly DetailedLineRangeMapping[],228originalLines: string[],229modifiedLines: string[],230originalEndsWithNewline: boolean,231modifiedEndsWithNewline: boolean,232): string {233const contextSize = 3;234const firstChange = group[0];235const lastChange = group[group.length - 1];236const hunkOrigStart = Math.max(1, firstChange.original.startLineNumber - contextSize);237const hunkOrigEnd = Math.min(originalLines.length, lastChange.original.endLineNumberExclusive - 1 + contextSize);238const hunkModStart = Math.max(1, firstChange.modified.startLineNumber - contextSize);239240const hunkLines: string[] = [];241let lastOriginalLineIndex = -1;242let lastModifiedLineIndex = -1;243let origLineNum = hunkOrigStart;244let origCount = 0;245let modCount = 0;246247for (const change of group) {248const origStart = change.original.startLineNumber;249const origEnd = change.original.endLineNumberExclusive;250const modStart = change.modified.startLineNumber;251const modEnd = change.modified.endLineNumberExclusive;252253while (origLineNum < origStart) {254const idx = hunkLines.length;255hunkLines.push(` ${originalLines[origLineNum - 1]}`);256if (origLineNum === originalLines.length) {257lastOriginalLineIndex = idx;258}259const modLineNum = hunkModStart + modCount;260if (modLineNum === modifiedLines.length) {261lastModifiedLineIndex = idx;262}263origLineNum++;264origCount++;265modCount++;266}267268for (let i = origStart; i < origEnd; i++) {269const idx = hunkLines.length;270hunkLines.push(`-${originalLines[i - 1]}`);271if (i === originalLines.length) {272lastOriginalLineIndex = idx;273}274origLineNum++;275origCount++;276}277278for (let i = modStart; i < modEnd; i++) {279const idx = hunkLines.length;280hunkLines.push(`+${modifiedLines[i - 1]}`);281if (i === modifiedLines.length) {282lastModifiedLineIndex = idx;283}284modCount++;285}286}287288while (origLineNum <= hunkOrigEnd) {289const idx = hunkLines.length;290hunkLines.push(` ${originalLines[origLineNum - 1]}`);291if (origLineNum === originalLines.length) {292lastOriginalLineIndex = idx;293}294const modLineNum = hunkModStart + modCount;295if (modLineNum === modifiedLines.length) {296lastModifiedLineIndex = idx;297}298origLineNum++;299origCount++;300modCount++;301}302303const header = `@@ -${hunkOrigStart},${origCount} +${hunkModStart},${modCount} @@`;304const result = [header, ...hunkLines];305306if (!originalEndsWithNewline && lastOriginalLineIndex >= 0) {307result.splice(lastOriginalLineIndex + 2, 0, '\\ No newline at end of file');308} else if (!modifiedEndsWithNewline && lastModifiedLineIndex >= 0) {309result.splice(lastModifiedLineIndex + 2, 0, '\\ No newline at end of file');310}311312return result.join('\n');313}314315export function getActiveResourceCandidates(input: Parameters<typeof EditorResourceAccessor.getOriginalUri>[0]): URI[] {316const result: URI[] = [];317318if (input instanceof MultiDiffEditorInput) {319const items = input.resources.get();320if (items) {321for (const item of items) {322if (item.originalUri) { result.push(item.originalUri); }323if (item.modifiedUri) { result.push(item.modifiedUri); }324}325}326return result;327}328329const resources = EditorResourceAccessor.getOriginalUri(input, { supportSideBySide: SideBySideEditor.BOTH });330if (!resources) {331return result;332}333334if (URI.isUri(resources)) {335result.push(resources);336return result;337}338339if (resources.secondary) {340result.push(resources.secondary);341}342if (resources.primary) {343result.push(resources.primary);344}345346return result;347}348349350