Path: blob/main/extensions/copilot/src/extension/context/node/resolvers/inlineChatSelection.ts
13405 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*--------------------------------------------------------------------------------------------*/456import type * as vscode from 'vscode';7import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';8import { TreeSitterOffsetRange } from '../../../../platform/parser/node/nodes';9import { ITabsAndEditorsService } from '../../../../platform/tabs/common/tabsAndEditorsService';10import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';11import { ILanguage, getLanguage } from '../../../../util/common/languages';12import { findCell, findNotebook } from '../../../../util/common/notebooks';13import { Schemas } from '../../../../util/vs/base/common/network';14import { Position, Range } from '../../../../vscodeTypes';15import { CodeContextRegion, CodeContextTracker } from '../../../inlineChat/node/codeContextRegion';16import { IDocumentContext } from '../../../prompt/node/documentContext';1718/**19* Get the lines in the selection, and lines above and below the selection.20* Gives preference to lines above the selection.21* Limits the above/below context to 100 lines.22* Limits the total char count to 1/3rd of the max tokens size.23*24* @param range selection range expanded to the encompassing function(s) but with line limit25*/26export function getSelectionAndCodeAroundSelection(27document: TextDocumentSnapshot,28selection: vscode.Range,29range: vscode.Range,30limitRange: vscode.Range,31language: ILanguage,32tracker: CodeContextTracker33): {34language: ILanguage;35above: CodeContextRegion;36range: CodeContextRegion;37below: CodeContextRegion;38} {3940if (range.start.line !== range.end.line && range.end.character === 0) {41// The range ends at the start of a line, we don't need to include that EOL char42const lastLine = document.lineAt(range.end.line - 1);43range = new Range(range.start, new Position(range.end.line - 1, lastLine.text.length));44} else if (45selection.end.character === 046&& selection.end.line !== selection.start.line47&& (48(range.start.line === selection.start.line49&& range.start.character === 050&& range.end.line === selection.end.line51&& range.end.character === document.lineAt(range.end.line).text.length52)53||54(range.isEqual(selection))55)56) {57// The selection ends at the start of a line, we don't need to include that line58// The range was computed from the selection, expanding it59const lastLine = document.lineAt(range.end.line - 1);60range = new Range(range.start, new Position(range.end.line - 1, lastLine.text.length));61}6263const rangeInfo = new CodeContextRegion(tracker, document, language);64const aboveInfo = new CodeContextRegion(tracker, document, language);65const belowInfo = new CodeContextRegion(tracker, document, language);6667const finish = () => {68aboveInfo.trim();69rangeInfo.trim(selection);70belowInfo.trim();7172return { language, above: aboveInfo, range: rangeInfo, below: belowInfo };73};7475// the selection might not fit, so we iterate from its bottom76for (let lineIndex = range.end.line; lineIndex >= range.start.line; lineIndex--) {77if (!rangeInfo.prependLine(lineIndex)) {78// didn't fit79return finish();80}81}8283const constraints = {84aboveLineIndex: range.start.line - 1,85belowLineIndex: range.end.line + 1,86minimumLineIndex: Math.max(0, limitRange.start.line),87maximumLineIndex: Math.min(document.lineCount - 1, limitRange.end.line)88};8990processCodeAroundSelection(constraints, aboveInfo, belowInfo);9192return finish();93}9495export function processCodeAroundSelection(96constraints: { aboveLineIndex: number; belowLineIndex: number; minimumLineIndex: number; maximumLineIndex: number },97aboveInfo: CodeContextRegion,98belowInfo: CodeContextRegion99) {100101let aboveLineIndex = constraints.aboveLineIndex;102let canGoAbove = true;103let belowLineIndex = constraints.belowLineIndex;104let canGoBelow = true;105for (let step = 0; step < 100 && (canGoAbove || canGoBelow); step++) {106// For each line below the selection, we add 3 lines above it107const goBelow = !canGoAbove || (canGoBelow && step % 4 === 3);108109if (goBelow) {110// add line from below111if (belowLineIndex <= constraints.maximumLineIndex && belowInfo.appendLine(belowLineIndex)) {112belowLineIndex++;113} else {114canGoBelow = false;115}116} else {117// add a line from above118if (aboveLineIndex >= constraints.minimumLineIndex && aboveInfo.prependLine(aboveLineIndex)) {119aboveLineIndex--;120} else {121canGoAbove = false;122}123}124}125aboveInfo.isComplete = aboveLineIndex < constraints.minimumLineIndex; // all lines above are included126belowInfo.isComplete = belowLineIndex > constraints.maximumLineIndex; // all lines below are included127}128129export function removeBodiesOutsideRange(130src: string,131functionBodies: TreeSitterOffsetRange[],132rangeToMaintain: { startOffset: number; endOffset: number },133replaceBodyWith: string134): { outlineAbove: string; outlineBelow: string } {135// remove nodes that are outside the range `rangeToMaintain`136// by copying undeleted chunks of `src` into `above` and `below`137// depending on position of deleted chunk relative to `rangeToMaintain`138139let lastOffsetAbove = 0;140let outlineAbove = '';141142let lastOffsetBelow = rangeToMaintain.endOffset;143let outlineBelow = '';144145for (const rangeToDelete of functionBodies) {146if (rangeToDelete.endIndex < rangeToMaintain.startOffset) {147// range is above - delete148149outlineAbove += src.substring(lastOffsetAbove, rangeToDelete.startIndex);150outlineAbove += replaceBodyWith;151lastOffsetAbove = rangeToDelete.endIndex;152} else if (rangeToDelete.startIndex > rangeToMaintain.endOffset) {153// range is below - delete154155outlineBelow += src.substring(lastOffsetBelow, rangeToDelete.startIndex);156outlineBelow += replaceBodyWith;157lastOffsetBelow = rangeToDelete.endIndex;158} else {159// intersection - do not delete160continue;161}162}163164outlineAbove += src.substring(lastOffsetAbove, rangeToMaintain.startOffset);165outlineBelow += src.substring(lastOffsetBelow, src.length);166167return { outlineAbove, outlineBelow };168}169170export function generateNotebookCellContext(171tabAndEditorService: ITabsAndEditorsService,172workspaceService: IWorkspaceService,173documentContext: IDocumentContext,174initialContext: { language: ILanguage; above: CodeContextRegion; range: CodeContextRegion; below: CodeContextRegion },175initialTracker: CodeContextTracker176): {177language: ILanguage;178aboveCells: CodeContextRegion[];179belowCells: CodeContextRegion[];180} {181const emptyContext = {182...initialContext,183aboveCells: [],184belowCells: [],185};186let notebook: vscode.NotebookDocument | undefined;187let aboveCellIndex: number | undefined;188let belowCellIndex: number | undefined;189190if (documentContext.document.uri.scheme === Schemas.vscodeNotebookCell) {191// inline192notebook = findNotebook(documentContext.document.uri, workspaceService.notebookDocuments);193194const cellIndex = notebook && findCell(documentContext.document.uri, notebook)?.index;195196if (cellIndex === undefined || cellIndex === -1) {197return emptyContext;198}199200aboveCellIndex = cellIndex - 1;201belowCellIndex = cellIndex + 1;202} else {203// floating widget204if (tabAndEditorService.activeNotebookEditor?.notebook.uri.path !== documentContext.document.uri.path) {205return emptyContext;206}207208const notebookEditor = tabAndEditorService.activeNotebookEditor;209notebook = notebookEditor?.notebook;210const insertIndex = notebookEditor.selection.start;211aboveCellIndex = insertIndex - 1;212belowCellIndex = insertIndex;213}214215if (!notebook) {216return emptyContext;217}218219const { language, above: aboveInfo, range: rangeInfo, below: belowInfo } = initialContext;220const usedSteps = aboveInfo.lines.length + rangeInfo.lines.length + belowInfo.lines.length;221const aboveCells: CodeContextRegion[] = [];222const belowCells: CodeContextRegion[] = [];223224const finish = () => {225aboveCells.forEach(cell => cell.trim());226belowCells.forEach(cell => cell.trim());227228return {229language,230aboveCells,231belowCells,232};233};234235let canGoAboveCell = true;236let canGoBelowCell = true;237238for (let step = usedSteps; step < 100 && (canGoAboveCell || canGoBelowCell); step++) {239if (canGoAboveCell) {240// add lines from above cell is always preferred over cells below241if (aboveCellIndex >= 0) {242// prepend the cell content243const cell = notebook.cellAt(aboveCellIndex);244const _cellDocument = cell.document;245const cellDocument = TextDocumentSnapshot.create(_cellDocument);246const cellContextRegion = new CodeContextRegion(247initialTracker,248cellDocument,249getLanguage(cellDocument)250);251for (let i = 0; i < cellDocument.lineCount; i++) {252cellContextRegion.appendLine(i);253}254aboveCells.unshift(cellContextRegion);255256aboveCellIndex--;257} else {258canGoAboveCell = false;259}260} else {261// add lines from below cell262if (belowCellIndex < notebook.cellCount) {263// append the cell content264const cell = notebook.cellAt(belowCellIndex);265const _cellDocument = cell.document;266const cellDocument = TextDocumentSnapshot.create(_cellDocument);267268const cellContextRegion = new CodeContextRegion(269initialTracker,270cellDocument,271getLanguage(cellDocument)272);273for (let i = 0; i < cellDocument.lineCount; i++) {274cellContextRegion.appendLine(i);275}276belowCells.push(cellContextRegion);277278belowCellIndex++;279} else {280canGoBelowCell = false;281}282}283}284285return finish();286}287288289