From ee88336bae1e3eb29c64afb5ad88b5b5ab55d342 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 18 Sep 2024 23:48:33 +0200 Subject: [PATCH 1/4] Implements inline edits as part of inline completions --- src/vs/base/common/hotReloadHelpers.ts | 23 +- src/vs/base/common/strings.ts | 6 + src/vs/editor/browser/observableCodeEditor.ts | 1 + .../widget/codeEditor/codeEditorWidget.ts | 4 +- src/vs/editor/common/core/textEdit.ts | 28 +- src/vs/editor/common/core/textLength.ts | 11 + .../defaultLinesDiffComputer.ts | 90 +--- src/vs/editor/common/diff/rangeMapping.ts | 110 ++++- src/vs/editor/common/languages.ts | 2 + src/vs/editor/common/model/textModelText.ts | 6 +- .../editor/common/viewModel/viewModelImpl.ts | 9 +- .../browser/controller/commands.ts | 68 ++- .../controller/inlineCompletionContextKeys.ts | 14 + .../controller/inlineCompletionsController.ts | 76 +++- .../browser/inlineCompletions.contribution.ts | 17 +- .../browser/model/inlineCompletionsModel.ts | 95 +++- .../browser/model/inlineEdit.ts | 20 + .../browser/model/inlineEditsAdapter.ts | 80 ++++ .../browser/model/provideInlineCompletions.ts | 2 +- .../browser/model/singleTextEditHelpers.ts | 5 +- .../view/{ => ghostText}/ghostTextView.css | 0 .../view/{ => ghostText}/ghostTextView.ts | 44 +- .../view/inlineEdits/inlineEditsView.css | 134 ++++++ .../view/inlineEdits/inlineEditsView.ts | 418 ++++++++++++++++++ .../browser/view/inlineEdits/utils.ts | 86 ++++ .../inlineEdit/browser/ghostTextWidget.ts | 2 +- .../browser/inlineEditController.ts | 10 +- .../diffing/defaultLinesDiffComputer.test.ts | 20 +- .../observable/common/wrapInHotClass.ts | 70 +++ 29 files changed, 1293 insertions(+), 158 deletions(-) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts rename src/vs/editor/contrib/inlineCompletions/browser/view/{ => ghostText}/ghostTextView.css (100%) rename src/vs/editor/contrib/inlineCompletions/browser/view/{ => ghostText}/ghostTextView.ts (87%) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts create mode 100644 src/vs/platform/observable/common/wrapInHotClass.ts diff --git a/src/vs/base/common/hotReloadHelpers.ts b/src/vs/base/common/hotReloadHelpers.ts index 17753174e683e..f4f7cf11eaee4 100644 --- a/src/vs/base/common/hotReloadHelpers.ts +++ b/src/vs/base/common/hotReloadHelpers.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isHotReloadEnabled, registerHotReloadHandler } from './hotReload.js'; -import { IReader, observableSignalFromEvent } from './observable.js'; +import { constObservable, IObservable, IReader, ISettableObservable, observableSignalFromEvent, observableValue } from './observable.js'; export function readHotReloadableExport(value: T, reader: IReader | undefined): T { observeHotReloadableExports([value], reader); @@ -28,3 +28,24 @@ export function observeHotReloadableExports(values: any[], reader: IReader | und o.read(reader); } } + +const classes = new Map>(); + +export function createHotClass(clazz: T): IObservable { + if (!isHotReloadEnabled()) { + return constObservable(clazz); + } + + const id = (clazz as any).name; + + let existing = classes.get(id); + if (!existing) { + existing = observableValue(id, clazz); + classes.set(id, existing); + } else { + setTimeout(() => { + existing!.set(clazz, undefined); + }, 0); + } + return existing as IObservable; +} diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 8952a776f74d8..a9b4486cdecd3 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -304,6 +304,12 @@ export function lastNonWhitespaceIndex(str: string, startIndex: number = str.len return -1; } +export function getIndentationLength(str: string): number { + const idx = firstNonWhitespaceIndex(str); + if (idx === -1) { return str.length; } + return idx; +} + /** * Function that works identically to String.prototype.replace, except, the * replace function is allowed to be async and return a Promise. diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 9641c87228ba3..5747a39c438bc 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -181,6 +181,7 @@ export class ObservableCodeEditor extends Disposable { public readonly valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; }); public readonly cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); public readonly cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); + public readonly cursorLineNumber = derived(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null); public readonly onDidType = observableSignal(this); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index bf72eb8c8ab46..59f027dd434ed 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -584,8 +584,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, maxCol, includeViewZones); } - public setHiddenAreas(ranges: IRange[], source?: unknown): void { - this._modelData?.viewModel.setHiddenAreas(ranges.map(r => Range.lift(r)), source); + public setHiddenAreas(ranges: IRange[], source?: unknown, forceUpdate?: boolean): void { + this._modelData?.viewModel.setHiddenAreas(ranges.map(r => Range.lift(r)), source, forceUpdate); } public getVisibleColumnFromPosition(rawPosition: IPosition): number { diff --git a/src/vs/editor/common/core/textEdit.ts b/src/vs/editor/common/core/textEdit.ts index be6b9e65a5abd..2dcc4ec3dc752 100644 --- a/src/vs/editor/common/core/textEdit.ts +++ b/src/vs/editor/common/core/textEdit.ts @@ -16,6 +16,10 @@ export class TextEdit { return new TextEdit([new SingleTextEdit(originalRange, newText)]); } + public static insert(position: Position, newText: string): TextEdit { + return new TextEdit([new SingleTextEdit(Range.fromPositions(position, position), newText)]); + } + constructor(public readonly edits: readonly SingleTextEdit[]) { assertFn(() => checkAdjacentItems(edits, (a, b) => a.range.getEndPosition().isBeforeOrEqual(b.range.getStartPosition()))); } @@ -43,12 +47,12 @@ export class TextEdit { for (const edit of this.edits) { const start = edit.range.getStartPosition(); - const end = edit.range.getEndPosition(); if (position.isBeforeOrEqual(start)) { break; } + const end = edit.range.getEndPosition(); const len = TextLength.ofText(edit.text); if (position.isBefore(end)) { const startPos = new Position(start.lineNumber + lineDelta, start.column + (start.lineNumber + lineDelta === curLine ? columnDeltaInCurLine : 0)); @@ -56,6 +60,10 @@ export class TextEdit { return rangeFromPositions(startPos, endPos); } + if (start.lineNumber + lineDelta !== curLine) { + columnDeltaInCurLine = 0; + } + lineDelta += len.lineCount - (edit.range.endLineNumber - edit.range.startLineNumber); if (len.lineCount === 0) { @@ -173,6 +181,14 @@ export class SingleTextEdit { text: this.text, }; } + + public toEdit(): TextEdit { + return new TextEdit([this]); + } + + public equals(other: SingleTextEdit): boolean { + return SingleTextEdit.equals(this, other); + } } function rangeFromPositions(start: Position, end: Position): Range { @@ -195,6 +211,10 @@ export abstract class AbstractText { getValue() { return this.getValueOfRange(this.length.toRange()); } + + getLineLength(lineNumber: number): number { + return this.getValueOfRange(new Range(lineNumber, 1, lineNumber, Number.MAX_SAFE_INTEGER)).length; + } } export class LineBasedText extends AbstractText { @@ -207,7 +227,7 @@ export class LineBasedText extends AbstractText { super(); } - getValueOfRange(range: Range): string { + override getValueOfRange(range: Range): string { if (range.startLineNumber === range.endLineNumber) { return this._getLineContent(range.startLineNumber).substring(range.startColumn - 1, range.endColumn - 1); } @@ -219,6 +239,10 @@ export class LineBasedText extends AbstractText { return result; } + override getLineLength(lineNumber: number): number { + return this._getLineContent(lineNumber).length; + } + get length(): TextLength { const lastLine = this._getLineContent(this._lineCount); return new TextLength(this._lineCount - 1, lastLine.length); diff --git a/src/vs/editor/common/core/textLength.ts b/src/vs/editor/common/core/textLength.ts index f1635b93ebb56..91dbdd79637a9 100644 --- a/src/vs/editor/common/core/textLength.ts +++ b/src/vs/editor/common/core/textLength.ts @@ -30,6 +30,10 @@ export class TextLength { } } + public static fromPosition(pos: Position): TextLength { + return new TextLength(pos.lineNumber - 1, pos.column - 1); + } + public static ofRange(range: Range) { return TextLength.betweenPositions(range.getStartPosition(), range.getEndPosition()); } @@ -117,6 +121,13 @@ export class TextLength { } } + public addToRange(range: Range): Range { + return Range.fromPositions( + this.addToPosition(range.getStartPosition()), + this.addToPosition(range.getEndPosition()) + ); + } + toString() { return `${this.lineCount},${this.columnCount}`; } diff --git a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts index 7b058590f0ccc..3d3a14eb86bbb 100644 --- a/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts @@ -3,21 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals, groupAdjacentBy } from '../../../../base/common/arrays.js'; -import { assertFn, checkAdjacentItems } from '../../../../base/common/assert.js'; +import { equals } from '../../../../base/common/arrays.js'; +import { assertFn } from '../../../../base/common/assert.js'; import { LineRange } from '../../core/lineRange.js'; import { OffsetRange } from '../../core/offsetRange.js'; import { Position } from '../../core/position.js'; import { Range } from '../../core/range.js'; -import { DateTimeout, ITimeout, InfiniteTimeout, SequenceDiff } from './algorithms/diffAlgorithm.js'; +import { ArrayText } from '../../core/textEdit.js'; +import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js'; +import { DetailedLineRangeMapping, LineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../rangeMapping.js'; +import { DateTimeout, InfiniteTimeout, ITimeout, SequenceDiff } from './algorithms/diffAlgorithm.js'; import { DynamicProgrammingDiffing } from './algorithms/dynamicProgrammingDiffing.js'; import { MyersDiffAlgorithm } from './algorithms/myersDiffAlgorithm.js'; import { computeMovedLines } from './computeMovedLines.js'; import { extendDiffsToEntireWordIfAppropriate, optimizeSequenceDiffs, removeShortMatches, removeVeryShortMatchingLinesBetweenDiffs, removeVeryShortMatchingTextBetweenLongDiffs } from './heuristicSequenceOptimizations.js'; import { LineSequence } from './lineSequence.js'; import { LinesSliceCharSequence } from './linesSliceCharSequence.js'; -import { ILinesDiffComputer, ILinesDiffComputerOptions, LinesDiff, MovedText } from '../linesDiffComputer.js'; -import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../rangeMapping.js'; export class DefaultLinesDiffComputer implements ILinesDiffComputer { private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing(); @@ -140,7 +141,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { scanForWhitespaceChanges(originalLines.length - seq1LastStart); - const changes = lineRangeMappingFromRangeMappings(alignments, originalLines, modifiedLines); + const changes = lineRangeMappingFromRangeMappings(alignments, new ArrayText(originalLines), new ArrayText(modifiedLines)); let moves: MovedText[] = []; if (options.computeMoves) { @@ -203,7 +204,7 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { m.original.toOffsetRange(), m.modified.toOffsetRange(), ), timeout, considerWhitespaceChanges); - const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); + const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, new ArrayText(originalLines), new ArrayText(modifiedLines), true); return new MovedText(m, mappings); }); return movesWithDiffs; @@ -252,81 +253,6 @@ export class DefaultLinesDiffComputer implements ILinesDiffComputer { } } -export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[], originalLines: string[], modifiedLines: string[], dontAssertStartLine: boolean = false): DetailedLineRangeMapping[] { - const changes: DetailedLineRangeMapping[] = []; - for (const g of groupAdjacentBy( - alignments.map(a => getLineRangeMapping(a, originalLines, modifiedLines)), - (a1, a2) => - a1.original.overlapOrTouch(a2.original) - || a1.modified.overlapOrTouch(a2.modified) - )) { - const first = g[0]; - const last = g[g.length - 1]; - - changes.push(new DetailedLineRangeMapping( - first.original.join(last.original), - first.modified.join(last.modified), - g.map(a => a.innerChanges![0]), - )); - } - - assertFn(() => { - if (!dontAssertStartLine && changes.length > 0) { - if (changes[0].modified.startLineNumber !== changes[0].original.startLineNumber) { - return false; - } - if (modifiedLines.length - changes[changes.length - 1].modified.endLineNumberExclusive !== originalLines.length - changes[changes.length - 1].original.endLineNumberExclusive) { - return false; - } - } - return checkAdjacentItems(changes, - (m1, m2) => m2.original.startLineNumber - m1.original.endLineNumberExclusive === m2.modified.startLineNumber - m1.modified.endLineNumberExclusive && - // There has to be an unchanged line in between (otherwise both diffs should have been joined) - m1.original.endLineNumberExclusive < m2.original.startLineNumber && - m1.modified.endLineNumberExclusive < m2.modified.startLineNumber, - ); - }); - - return changes; -} - -export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: string[], modifiedLines: string[]): DetailedLineRangeMapping { - let lineStartDelta = 0; - let lineEndDelta = 0; - - // rangeMapping describes the edit that replaces `rangeMapping.originalRange` with `newText := getText(modifiedLines, rangeMapping.modifiedRange)`. - - // original: ]xxx \n <- this line is not modified - // modified: ]xx \n - if (rangeMapping.modifiedRange.endColumn === 1 && rangeMapping.originalRange.endColumn === 1 - && rangeMapping.originalRange.startLineNumber + lineStartDelta <= rangeMapping.originalRange.endLineNumber - && rangeMapping.modifiedRange.startLineNumber + lineStartDelta <= rangeMapping.modifiedRange.endLineNumber) { - // We can only do this if the range is not empty yet - lineEndDelta = -1; - } - - // original: xxx[ \n <- this line is not modified - // modified: xxx[ \n - if (rangeMapping.modifiedRange.startColumn - 1 >= modifiedLines[rangeMapping.modifiedRange.startLineNumber - 1].length - && rangeMapping.originalRange.startColumn - 1 >= originalLines[rangeMapping.originalRange.startLineNumber - 1].length - && rangeMapping.originalRange.startLineNumber <= rangeMapping.originalRange.endLineNumber + lineEndDelta - && rangeMapping.modifiedRange.startLineNumber <= rangeMapping.modifiedRange.endLineNumber + lineEndDelta) { - // We can only do this if the range is not empty yet - lineStartDelta = 1; - } - - const originalLineRange = new LineRange( - rangeMapping.originalRange.startLineNumber + lineStartDelta, - rangeMapping.originalRange.endLineNumber + 1 + lineEndDelta - ); - const modifiedLineRange = new LineRange( - rangeMapping.modifiedRange.startLineNumber + lineStartDelta, - rangeMapping.modifiedRange.endLineNumber + 1 + lineEndDelta - ); - - return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]); -} - function toLineRangeMapping(sequenceDiff: SequenceDiff) { return new LineRangeMapping( new LineRange(sequenceDiff.seq1Range.start + 1, sequenceDiff.seq1Range.endExclusive + 1), diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index 2e341b8aef223..80c18b9eddf40 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { groupAdjacentBy } from '../../../base/common/arrays.js'; +import { assertFn, checkAdjacentItems } from '../../../base/common/assert.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { LineRange } from '../core/lineRange.js'; import { Position } from '../core/position.js'; import { Range } from '../core/range.js'; -import { AbstractText, SingleTextEdit } from '../core/textEdit.js'; +import { AbstractText, SingleTextEdit, TextEdit } from '../core/textEdit.js'; /** * Maps a line range in the original text model to a line range in the modified text model. @@ -226,6 +228,29 @@ export class DetailedLineRangeMapping extends LineRangeMapping { * Maps a range in the original text model to a range in the modified text model. */ export class RangeMapping { + public static fromEdit(edit: TextEdit): RangeMapping[] { + const newRanges = edit.getNewRanges(); + const result = edit.edits.map((e, idx) => new RangeMapping(e.range, newRanges[idx])); + return result; + } + + public static fromEditJoin(edit: TextEdit): RangeMapping { + const newRanges = edit.getNewRanges(); + const result = edit.edits.map((e, idx) => new RangeMapping(e.range, newRanges[idx])); + return RangeMapping.join(result); + } + + public static join(rangeMappings: RangeMapping[]): RangeMapping { + if (rangeMappings.length === 0) { + throw new BugIndicatingError('Cannot join an empty list of range mappings'); + } + let result = rangeMappings[0]; + for (let i = 1; i < rangeMappings.length; i++) { + result = result.join(rangeMappings[i]); + } + return result; + } + public static assertSorted(rangeMappings: RangeMapping[]): void { for (let i = 1; i < rangeMappings.length; i++) { const previous = rangeMappings[i - 1]; @@ -272,4 +297,87 @@ export class RangeMapping { const newText = modified.getValueOfRange(this.modifiedRange); return new SingleTextEdit(this.originalRange, newText); } + + public join(other: RangeMapping): RangeMapping { + return new RangeMapping( + this.originalRange.plusRange(other.originalRange), + this.modifiedRange.plusRange(other.modifiedRange) + ); + } +} + +export function lineRangeMappingFromRangeMappings(alignments: readonly RangeMapping[], originalLines: AbstractText, modifiedLines: AbstractText, dontAssertStartLine: boolean = false): DetailedLineRangeMapping[] { + const changes: DetailedLineRangeMapping[] = []; + for (const g of groupAdjacentBy( + alignments.map(a => getLineRangeMapping(a, originalLines, modifiedLines)), + (a1, a2) => + a1.original.overlapOrTouch(a2.original) + || a1.modified.overlapOrTouch(a2.modified) + )) { + const first = g[0]; + const last = g[g.length - 1]; + + changes.push(new DetailedLineRangeMapping( + first.original.join(last.original), + first.modified.join(last.modified), + g.map(a => a.innerChanges![0]), + )); + } + + assertFn(() => { + if (!dontAssertStartLine && changes.length > 0) { + if (changes[0].modified.startLineNumber !== changes[0].original.startLineNumber) { + return false; + } + + if (modifiedLines.length.lineCount - changes[changes.length - 1].modified.endLineNumberExclusive !== originalLines.length.lineCount - changes[changes.length - 1].original.endLineNumberExclusive) { + return false; + } + } + return checkAdjacentItems(changes, + (m1, m2) => m2.original.startLineNumber - m1.original.endLineNumberExclusive === m2.modified.startLineNumber - m1.modified.endLineNumberExclusive && + // There has to be an unchanged line in between (otherwise both diffs should have been joined) + m1.original.endLineNumberExclusive < m2.original.startLineNumber && + m1.modified.endLineNumberExclusive < m2.modified.startLineNumber, + ); + }); + + return changes; +} + +export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: AbstractText, modifiedLines: AbstractText): DetailedLineRangeMapping { + let lineStartDelta = 0; + let lineEndDelta = 0; + + // rangeMapping describes the edit that replaces `rangeMapping.originalRange` with `newText := getText(modifiedLines, rangeMapping.modifiedRange)`. + + // original: ]xxx \n <- this line is not modified + // modified: ]xx \n + if (rangeMapping.modifiedRange.endColumn === 1 && rangeMapping.originalRange.endColumn === 1 + && rangeMapping.originalRange.startLineNumber + lineStartDelta <= rangeMapping.originalRange.endLineNumber + && rangeMapping.modifiedRange.startLineNumber + lineStartDelta <= rangeMapping.modifiedRange.endLineNumber) { + // We can only do this if the range is not empty yet + lineEndDelta = -1; + } + + // original: xxx[ \n <- this line is not modified + // modified: xxx[ \n + if (rangeMapping.modifiedRange.startColumn - 1 >= modifiedLines.getLineLength(rangeMapping.modifiedRange.startLineNumber) + && rangeMapping.originalRange.startColumn - 1 >= originalLines.getLineLength(rangeMapping.originalRange.startLineNumber) + && rangeMapping.originalRange.startLineNumber <= rangeMapping.originalRange.endLineNumber + lineEndDelta + && rangeMapping.modifiedRange.startLineNumber <= rangeMapping.modifiedRange.endLineNumber + lineEndDelta) { + // We can only do this if the range is not empty yet + lineStartDelta = 1; + } + + const originalLineRange = new LineRange( + rangeMapping.originalRange.startLineNumber + lineStartDelta, + rangeMapping.originalRange.endLineNumber + 1 + lineEndDelta + ); + const modifiedLineRange = new LineRange( + rangeMapping.modifiedRange.startLineNumber + lineStartDelta, + rangeMapping.modifiedRange.endLineNumber + 1 + lineEndDelta + ); + + return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]); } diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 66614f0f5d292..533010f8cbe70 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -759,6 +759,8 @@ export interface InlineCompletion { * Defaults to `false`. */ readonly completeBracketPairs?: boolean; + + readonly isInlineEdit?: boolean; } export interface InlineCompletions { diff --git a/src/vs/editor/common/model/textModelText.ts b/src/vs/editor/common/model/textModelText.ts index 0dad2b9b6b1e2..0a4d6de53646c 100644 --- a/src/vs/editor/common/model/textModelText.ts +++ b/src/vs/editor/common/model/textModelText.ts @@ -13,10 +13,14 @@ export class TextModelText extends AbstractText { super(); } - getValueOfRange(range: Range): string { + override getValueOfRange(range: Range): string { return this._textModel.getValueInRange(range); } + override getLineLength(lineNumber: number): number { + return this._textModel.getLineLength(lineNumber); + } + get length(): TextLength { const lastLineNumber = this._textModel.getLineCount(); const lastLineLen = this._textModel.getLineLength(lastLineNumber); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index bbdd92aab933a..96043722d84c5 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -476,10 +476,15 @@ export class ViewModel extends Disposable implements IViewModel { private readonly hiddenAreasModel = new HiddenAreasModel(); private previousHiddenAreas: readonly Range[] = []; - public setHiddenAreas(ranges: Range[], source?: unknown): void { + /** + * @param forceUpdate If true, the hidden areas will be updated even if the new ranges are the same as the previous ranges. + * This is because the model might have changed, which resets the hidden areas, but not the last cached value. + * This needs a better fix in the future. + */ + public setHiddenAreas(ranges: Range[], source?: unknown, forceUpdate?: boolean): void { this.hiddenAreasModel.setHiddenAreas(source, ranges); const mergedRanges = this.hiddenAreasModel.getMergedRanges(); - if (mergedRanges === this.previousHiddenAreas) { + if (mergedRanges === this.previousHiddenAreas && !forceUpdate) { return; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 595ab703f2891..4e59d278b7e3c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -138,22 +138,79 @@ export class AcceptInlineCompletion extends EditorAction { id: inlineSuggestCommitId, label: nls.localize('action.inlineSuggest.accept', "Accept Inline Suggestion"), alias: 'Accept Inline Suggestion', - precondition: InlineCompletionContextKeys.inlineSuggestionVisible, + precondition: ContextKeyExpr.or(InlineCompletionContextKeys.inlineSuggestionVisible, InlineCompletionContextKeys.inlineEditVisible), menuOpts: [{ menuId: MenuId.InlineSuggestionToolbar, title: nls.localize('accept', "Accept"), group: 'primary', order: 1, + }, { + menuId: MenuId.InlineEditsActions, + title: nls.localize('accept', "Accept"), + group: 'primary', + order: 1, }], kbOpts: { primary: KeyCode.Tab, weight: 200, + kbExpr: ContextKeyExpr.or( + ContextKeyExpr.and( + InlineCompletionContextKeys.inlineSuggestionVisible, + EditorContextKeys.tabMovesFocus.toNegated(), + SuggestContext.Visible.toNegated(), + EditorContextKeys.hoverFocused.toNegated(), + + InlineCompletionContextKeys.inlineSuggestionHasIndentationLessThanTabSize, + ), + ContextKeyExpr.and( + InlineCompletionContextKeys.inlineEditVisible, + EditorContextKeys.tabMovesFocus.toNegated(), + SuggestContext.Visible.toNegated(), + EditorContextKeys.hoverFocused.toNegated(), + + //InlineCompletionContextKeys.cursorInIndentation.toNegated(), + InlineCompletionContextKeys.hasSelection.toNegated(), + InlineCompletionContextKeys.cursorAtInlineEdit, + ) + ), + } + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { + const controller = InlineCompletionsController.get(editor); + if (controller) { + controller.model.get()?.accept(controller.editor); + controller.editor.focus(); + } + } +} + +export class JumpToNextInlineEdit extends EditorAction { + constructor() { + super({ + id: 'editor.action.inlineSuggest.jump', + label: nls.localize('action.inlineSuggest.jump', "Jump to next inline edit"), + alias: 'Jump to next inline edit', + precondition: InlineCompletionContextKeys.inlineEditVisible, + menuOpts: [{ + menuId: MenuId.InlineEditsActions, + title: nls.localize('jump', "Jump"), + group: 'primary', + order: 2, + when: InlineCompletionContextKeys.cursorAtInlineEdit.toNegated(), + }], + kbOpts: { + primary: KeyCode.Tab, + weight: 201, kbExpr: ContextKeyExpr.and( - InlineCompletionContextKeys.inlineSuggestionVisible, + InlineCompletionContextKeys.inlineEditVisible, + //InlineCompletionContextKeys.cursorInIndentation.toNegated(), + InlineCompletionContextKeys.hasSelection.toNegated(), EditorContextKeys.tabMovesFocus.toNegated(), - InlineCompletionContextKeys.inlineSuggestionHasIndentationLessThanTabSize, SuggestContext.Visible.toNegated(), EditorContextKeys.hoverFocused.toNegated(), + InlineCompletionContextKeys.cursorAtInlineEdit.toNegated(), ), } }); @@ -162,8 +219,7 @@ export class AcceptInlineCompletion extends EditorAction { public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise { const controller = InlineCompletionsController.get(editor); if (controller) { - controller.model.get()?.accept(controller.editor); - controller.editor.focus(); + controller.jump(); } } } @@ -176,7 +232,7 @@ export class HideInlineCompletion extends EditorAction { id: HideInlineCompletion.ID, label: nls.localize('action.inlineSuggest.hide', "Hide Inline Suggestion"), alias: 'Hide Inline Suggestion', - precondition: InlineCompletionContextKeys.inlineSuggestionVisible, + precondition: ContextKeyExpr.or(InlineCompletionContextKeys.inlineSuggestionVisible, InlineCompletionContextKeys.inlineEditVisible), kbOpts: { weight: 100, primary: KeyCode.Escape, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts index adbc09f3eb57c..18ac9852887b3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts @@ -10,13 +10,21 @@ import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import { RawContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../nls.js'; +import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; export class InlineCompletionContextKeys extends Disposable { + public static readonly inlineSuggestionVisible = new RawContextKey('inlineSuggestionVisible', false, localize('inlineSuggestionVisible', "Whether an inline suggestion is visible")); public static readonly inlineSuggestionHasIndentation = new RawContextKey('inlineSuggestionHasIndentation', false, localize('inlineSuggestionHasIndentation', "Whether the inline suggestion starts with whitespace")); public static readonly inlineSuggestionHasIndentationLessThanTabSize = new RawContextKey('inlineSuggestionHasIndentationLessThanTabSize', true, localize('inlineSuggestionHasIndentationLessThanTabSize', "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab")); public static readonly suppressSuggestions = new RawContextKey('inlineSuggestionSuppressSuggestions', undefined, localize('suppressSuggestions', "Whether suggestions should be suppressed for the current suggestion")); + public static readonly cursorInIndentation = new RawContextKey('cursorInIndentation', false, localize('cursorInIndentation', "Whether the cursor is in indentation")); + public static readonly hasSelection = new RawContextKey('editor.hasSelection', false, localize('editor.hasSelection', "Whether the editor has a selection")); + public static readonly cursorAtInlineEdit = new RawContextKey('cursorAtInlineEdit', false, localize('cursorAtInlineEdit', "Whether the cursor is at an inline edit")); + public static readonly inlineEditVisible = new RawContextKey('inlineEditIsVisible', false, localize('inlineEditVisible', "Whether an inline edit is visible")); + + public readonly inlineCompletionVisible = InlineCompletionContextKeys.inlineSuggestionVisible.bindTo(this.contextKeyService); public readonly inlineCompletionSuggestsIndentation = InlineCompletionContextKeys.inlineSuggestionHasIndentation.bindTo(this.contextKeyService); public readonly inlineCompletionSuggestsIndentationLessThanTabSize = InlineCompletionContextKeys.inlineSuggestionHasIndentationLessThanTabSize.bindTo(this.contextKeyService); @@ -28,6 +36,12 @@ export class InlineCompletionContextKeys extends Disposable { ) { super(); + this._register(bindContextKey( + InlineCompletionContextKeys.inlineEditVisible, + this.contextKeyService, + reader => this.model.read(reader)?.stateInlineEdit.read(reader) !== undefined + )); + this._register(autorun(reader => { /** @description update context key: inlineCompletionVisible, suppressSuggestions */ const model = this.model.read(reader); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index c96abd181b75a..53e1dca17478f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -7,7 +7,7 @@ import { createStyleSheetFromObservable } from '../../../../../base/browser/domO import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { timeout } from '../../../../../base/common/async.js'; import { cancelOnDispose } from '../../../../../base/common/cancellation.js'; -import { readHotReloadableExport } from '../../../../../base/common/hotReloadHelpers.js'; +import { createHotClass, readHotReloadableExport } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ITransaction, autorun, constObservable, derived, derivedDisposable, derivedObservableWithCache, mapObservableArrayCached, observableFromEvent, observableSignal, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; import { isUndefined } from '../../../../../base/common/types.js'; @@ -19,10 +19,13 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { hotClassGetOriginalInstance } from '../../../../../platform/observable/common/wrapInHotClass.js'; import { CoreEditingCommands } from '../../../../browser/coreCommands.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../common/core/lineRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { CursorChangeReason } from '../../../../common/cursorEvents.js'; @@ -32,15 +35,18 @@ import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import { SuggestWidgetAdaptor } from '../model/suggestWidgetAdaptor.js'; import { convertItemsToStableObservables } from '../utils.js'; -import { GhostTextView } from '../view/ghostTextView.js'; +import { GhostTextView } from '../view/ghostText/ghostTextView.js'; +import { InlineEditsViewAndDiffProducer } from '../view/inlineEdits/inlineEditsView.js'; import { inlineSuggestCommitId } from './commandIds.js'; import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; export class InlineCompletionsController extends Disposable { + public static hot = createHotClass(InlineCompletionsController); + static ID = 'editor.contrib.inlineCompletionsController'; public static get(editor: ICodeEditor): InlineCompletionsController | null { - return editor.getContribution(InlineCompletionsController.ID); + return hotClassGetOriginalInstance(editor.getContribution(InlineCompletionsController.ID)); } private readonly _editorObs = observableCodeEditor(this.editor); @@ -113,9 +119,23 @@ export class InlineCompletionsController extends Disposable { ).recomputeInitiallyAndOnChange(store) ).recomputeInitiallyAndOnChange(this._store); + private readonly _inlineEdit = derived(this, reader => { + const s = this.model.read(reader)?.stateWithInlineEdit.read(reader); + if (s?.kind === 'inlineEdit') { + return s.inlineEdit; + } + return undefined; + }); + private readonly _everHadInlineEdit = derivedObservableWithCache(this, (reader, last) => last || !!this._inlineEdit.read(reader)); + protected readonly _inlineEditWidget = derivedDisposable(reader => { + if (!this._everHadInlineEdit.read(reader)) { return undefined; } + return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this.editor, this._inlineEdit); + }) + .recomputeInitiallyAndOnChange(this._store); + private readonly _playAccessibilitySignal = observableSignal(this); - private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).fontFamily); + private readonly _fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily); constructor( public readonly editor: ICodeEditor, @@ -240,8 +260,43 @@ export class InlineCompletionsController extends Disposable { } })); this.editor.updateOptions({ inlineCompletionsAccessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.inlineCompletions') }); + + this._register(bindContextKey( + InlineCompletionContextKeys.cursorInIndentation, + this._contextKeyService, + reader => this._cursorIsInIndentation.read(reader), + )); + + this._register(bindContextKey( + InlineCompletionContextKeys.hasSelection, + this._contextKeyService, + reader => !this._editorObs.cursorSelection.read(reader)?.isEmpty(), + )); + + this._register(bindContextKey( + InlineCompletionContextKeys.cursorAtInlineEdit, + this._contextKeyService, + reader => { + const cursorPos = this._editorObs.cursorPosition.read(reader); + if (cursorPos === null) { return false; } + const edit = this.model.read(reader)?.stateInlineEdit.read(reader); + if (!edit) { return false; } + return LineRange.fromRangeInclusive(edit.inlineEdit.range).contains(cursorPos.lineNumber); + } + )); + } + private readonly _cursorIsInIndentation = derived(this, reader => { + const cursorPos = this._editorObs.cursorPosition.read(reader); + if (cursorPos === null) { return false; } + const model = this._editorObs.model.read(reader); + if (!model) { return false; } + this._editorObs.versionId.read(reader); + const indentMaxColumn = model.getLineIndentColumn(cursorPos.lineNumber); + return cursorPos.column <= indentMaxColumn; + }); + public playAccessibilitySignal(tx: ITransaction) { this._playAccessibilitySignal.trigger(tx); } @@ -273,4 +328,17 @@ export class InlineCompletionsController extends Disposable { this.model.get()?.stop(tx); }); } + + public jump(): void { + const m = this.model.get(); + const s = m?.stateInlineEdit.get(); + if (!s) { return; } + + transaction(tx => { + m!.dontRefetchSignal.trigger(tx); + this.editor.setPosition(s.inlineEdit.range.getStartPosition(), 'inlineCompletions.jump'); + this.editor.revealLine(s.inlineEdit.range.startLineNumber); + this.editor.focus(); + }); + } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts index 27e88e8b0896d..321a2d2e3d7f4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts @@ -3,16 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { wrapInHotClass1 } from '../../../../platform/observable/common/wrapInHotClass.js'; import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from '../../../browser/editorExtensions.js'; import { HoverParticipantRegistry } from '../../hover/browser/hoverTypes.js'; -import { TriggerInlineSuggestionAction, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, AcceptNextWordOfInlineCompletion, AcceptInlineCompletion, HideInlineCompletion, ToggleAlwaysShowInlineSuggestionToolbar, AcceptNextLineOfInlineCompletion } from './controller/commands.js'; +import { AcceptInlineCompletion, AcceptNextLineOfInlineCompletion, AcceptNextWordOfInlineCompletion, HideInlineCompletion, JumpToNextInlineEdit, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, ToggleAlwaysShowInlineSuggestionToolbar, TriggerInlineSuggestionAction } from './controller/commands.js'; +import { InlineCompletionsController } from './controller/inlineCompletionsController.js'; import { InlineCompletionsHoverParticipant } from './hintsWidget/hoverParticipant.js'; import { InlineCompletionsAccessibleView } from './inlineCompletionsAccessibleView.js'; -import { InlineCompletionsController } from './controller/inlineCompletionsController.js'; -import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; -import { registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { InlineEditsAdapterContribution } from './model/inlineEditsAdapter.js'; + +registerEditorContribution(InlineEditsAdapterContribution.ID, InlineEditsAdapterContribution, EditorContributionInstantiation.Eventually); -registerEditorContribution(InlineCompletionsController.ID, InlineCompletionsController, EditorContributionInstantiation.Eventually); + +registerEditorContribution(InlineCompletionsController.ID, wrapInHotClass1(InlineCompletionsController.hot), EditorContributionInstantiation.Eventually); registerEditorAction(TriggerInlineSuggestionAction); registerEditorAction(ShowNextInlineSuggestionAction); @@ -21,8 +26,8 @@ registerEditorAction(AcceptNextWordOfInlineCompletion); registerEditorAction(AcceptNextLineOfInlineCompletion); registerEditorAction(AcceptInlineCompletion); registerEditorAction(HideInlineCompletion); +registerEditorAction(JumpToNextInlineEdit); registerAction2(ToggleAlwaysShowInlineSuggestionToolbar); HoverParticipantRegistry.register(InlineCompletionsHoverParticipant); - AccessibleViewRegistry.register(new InlineCompletionsAccessibleView()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 8768d067135c7..786529449fd1c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -30,6 +30,8 @@ import { addPositions, getEndPositionsAfterApplying, substringPos, subtractPosit import { computeGhostText } from './computeGhostText.js'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from './inlineCompletionsSource.js'; +import { InlineEdit } from './inlineEdit.js'; +import { InlineCompletionItem } from './provideInlineCompletions.js'; import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; import { SuggestItemInfo } from './suggestWidgetAdaptor.js'; @@ -92,9 +94,12 @@ export class InlineCompletionsModel extends Disposable { return VersionIdChangeReason.Other; } + public readonly dontRefetchSignal = observableSignal(this); + private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({ owner: this, createEmptyChangeSummary: () => ({ + dontRefetch: false, preserveCurrentCompletion: false, inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic }), @@ -104,10 +109,13 @@ export class InlineCompletionsModel extends Disposable { changeSummary.preserveCurrentCompletion = true; } else if (ctx.didChange(this._forceUpdateExplicitlySignal)) { changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit; + } else if (ctx.didChange(this.dontRefetchSignal)) { + changeSummary.dontRefetch = true; } return true; }, }, (reader, changeSummary) => { + this.dontRefetchSignal.read(reader); this._forceUpdateExplicitlySignal.read(reader); const shouldUpdate = (this._enabled.read(reader) && this.selectedSuggestItem.read(reader)) || this._isActive.read(reader); if (!shouldUpdate) { @@ -131,6 +139,10 @@ export class InlineCompletionsModel extends Disposable { } const cursorPosition = this._primaryPosition.read(reader); + if (changeSummary.dontRefetch) { + return Promise.resolve(true); + } + const context: InlineCompletionContext = { triggerKind: changeSummary.inlineCompletionTriggerKind, selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(), @@ -161,12 +173,30 @@ export class InlineCompletionsModel extends Disposable { }); } - private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => { const c = this._source.inlineCompletions.read(reader); - if (!c) { return []; } + if (!c) { return undefined; } const cursorPosition = this._primaryPosition.read(reader); - const filteredCompletions = c.inlineCompletions.filter(c => c.isVisible(this.textModel, cursorPosition, reader)); - return filteredCompletions; + let inlineEditCompletion: InlineCompletionWithUpdatedRange | undefined = undefined; + const filteredCompletions: InlineCompletionWithUpdatedRange[] = []; + for (const completion of c.inlineCompletions) { + if (!completion.inlineCompletion.sourceInlineCompletion.isInlineEdit) { + if (completion.isVisible(this.textModel, cursorPosition, reader)) { + filteredCompletions.push(completion); + } + } else if (filteredCompletions.length === 0 && completion.inlineCompletion.sourceInlineCompletion.isInlineEdit) { + inlineEditCompletion = completion; + } + } + return { + items: filteredCompletions, + inlineEditCompletion, + }; + }); + + private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + const c = this._inlineCompletionItems.read(reader); + return c?.items ?? []; }); public readonly selectedInlineCompletionIndex = derived(this, (reader) => { @@ -203,23 +233,42 @@ export class InlineCompletionsModel extends Disposable { } }); - public readonly state = derivedOpts<{ + public readonly stateWithInlineEdit = derivedOpts<{ + kind: 'ghostText'; edits: readonly SingleTextEdit[]; primaryGhostText: GhostTextOrReplacement; ghostTexts: readonly GhostTextOrReplacement[]; suggestItem: SuggestItemInfo | undefined; inlineCompletion: InlineCompletionWithUpdatedRange | undefined; + } | { + kind: 'inlineEdit'; + edits: readonly SingleTextEdit[]; + inlineEdit: InlineEdit; + inlineCompletion: InlineCompletionWithUpdatedRange; } | undefined>({ owner: this, equalsFn: (a, b) => { if (!a || !b) { return a === b; } - return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) - && a.inlineCompletion === b.inlineCompletion - && a.suggestItem === b.suggestItem; + + if (a.kind === 'ghostText' && b.kind === 'ghostText') { + return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) + && a.inlineCompletion === b.inlineCompletion + && a.suggestItem === b.suggestItem; + } else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') { + return a.inlineEdit.edit.equals(b.inlineEdit.edit); + } + return false; } }, (reader) => { const model = this.textModel; + const item = this._inlineCompletionItems.read(reader); + if (item?.inlineEditCompletion) { + let edit = item.inlineEditCompletion.toSingleTextEdit(reader); + edit = singleTextRemoveCommonPrefix(edit, model); + return { kind: 'inlineEdit', inlineEdit: new InlineEdit(edit), inlineCompletion: item.inlineEditCompletion, edits: [edit] }; + } + const suggestItem = this.selectedSuggestItem.read(reader); if (suggestItem) { const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.toSingleTextEdit(), model); @@ -238,7 +287,7 @@ export class InlineCompletionsModel extends Disposable { .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength)) .filter(isDefined); const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); - return { edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; + return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; } else { if (!this._isActive.read(reader)) { return undefined; } const inlineCompletion = this.selectedInlineCompletion.read(reader); @@ -252,10 +301,22 @@ export class InlineCompletionsModel extends Disposable { .map((edit, idx) => computeGhostText(edit, model, mode, positions[idx], 0)) .filter(isDefined); if (!ghostTexts[0]) { return undefined; } - return { edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; + return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; } }); + public readonly state = derived(reader => { + const s = this.stateWithInlineEdit.read(reader); + if (!s || s.kind !== 'ghostText') { return undefined; } + return s; + }); + + public readonly stateInlineEdit = derived(reader => { + const s = this.stateWithInlineEdit.read(reader); + if (!s || s.kind !== 'inlineEdit') { return undefined; } + return s; + }); + private _computeAugmentation(suggestCompletion: SingleTextEdit, reader: IReader | undefined) { const model = this.textModel; const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(reader); @@ -313,11 +374,19 @@ export class InlineCompletionsModel extends Disposable { throw new BugIndicatingError(); } - const state = this.state.get(); - if (!state || state.primaryGhostText.isEmpty() || !state.inlineCompletion) { + let completion: InlineCompletionItem; + + const state = this.stateWithInlineEdit.get(); + if (state?.kind === 'ghostText') { + if (!state || state.primaryGhostText.isEmpty() || !state.inlineCompletion) { + return; + } + completion = state.inlineCompletion.toInlineCompletion(undefined); + } else if (state?.kind === 'inlineEdit') { + completion = state.inlineCompletion.toInlineCompletion(undefined); + } else { return; } - const completion = state.inlineCompletion.toInlineCompletion(undefined); if (completion.command) { // Make sure the completion list will not be disposed. diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts new file mode 100644 index 0000000000000..2daf10bd10e36 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SingleTextEdit } from '../../../../common/core/textEdit.js'; + +export class InlineEdit { + constructor( + public readonly edit: SingleTextEdit, + ) { } + + public get range() { + return this.edit.range; + } + + public get text() { + return this.edit.text; + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts new file mode 100644 index 0000000000000..ad131e62b4ccb --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEditsAdapter.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { autorunWithStore, observableSignalFromEvent } from '../../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { ICodeEditor } from '../../../../browser/editorBrowser.js'; +import { IInlineEdit, InlineCompletions, InlineCompletionsProvider, InlineEditProvider, InlineEditTriggerKind } from '../../../../common/languages.js'; +import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; + +export class InlineEditsAdapterContribution extends Disposable { + public static ID = 'editor.contrib.inlineEditsAdapter'; + public static isFirst = true; + + constructor( + _editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + if (InlineEditsAdapterContribution.isFirst) { + InlineEditsAdapterContribution.isFirst = false; + this.instantiationService.createInstance(InlineEditsAdapter); + } + } +} + +export class InlineEditsAdapter extends Disposable { + public static experimentalInlineEditsEnabled = 'editor.inlineSuggest.experimentalInlineEditsEnabled'; + private readonly _inlineCompletionInlineEdits = observableConfigValue(InlineEditsAdapter.experimentalInlineEditsEnabled, false, this._configurationService); + + constructor( + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + + const didChangeSignal = observableSignalFromEvent('didChangeSignal', this._languageFeaturesService.inlineEditProvider.onDidChange); + + this._register(autorunWithStore((reader, store) => { + if (!this._inlineCompletionInlineEdits.read(reader)) { return; } + didChangeSignal.read(reader); + + store.add(this._languageFeaturesService.inlineCompletionsProvider.register('*', { + provideInlineCompletions: async (model, position, context, token) => { + const allInlineEditProvider = _languageFeaturesService.inlineEditProvider.all(model); + const inlineEdits = await Promise.all(allInlineEditProvider.map(async provider => { + const result = await provider.provideInlineEdit(model, { + triggerKind: InlineEditTriggerKind.Automatic, + }, token); + if (!result) { return undefined; } + return { result, provider }; + })); + + const definedEdits = inlineEdits.filter(e => !!e); + return { + edits: definedEdits, + items: definedEdits.map(e => { + return { + range: e.result.range, + insertText: e.result.text, + command: e.result.accepted, + isInlineEdit: true, + }; + }), + }; + }, + freeInlineCompletions: (c) => { + for (const e of c.edits) { + e.provider.freeInlineEdit(e.result); + } + }, + } as InlineCompletionsProvider }[] }>)); + })); + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index af59786ab8a9b..fc7e6b42ee1c8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -75,7 +75,7 @@ export async function provideInlineCompletions( return undefined; } - function processProvider(provider: InlineCompletionsProvider): Result { + function processProvider(provider: InlineCompletionsProvider): Result { const state = states.get(provider); if (state) { return state; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts index 33931934c1bee..3c4d51ef8d900 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts @@ -14,10 +14,11 @@ export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextM if (!modelRange) { return edit; } + const normalizedText = edit.text.replaceAll('\r\n', '\n'); const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); - const commonPrefixLen = commonPrefixLength(valueToReplace, edit.text); + const commonPrefixLen = commonPrefixLength(valueToReplace, normalizedText); const start = TextLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); - const text = edit.text.substring(commonPrefixLen); + const text = normalizedText.substring(commonPrefixLen); const range = Range.fromPositions(start, edit.range.getEndPosition()); return new SingleTextEdit(range, text); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostTextView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css similarity index 100% rename from src/vs/editor/contrib/inlineCompletions/browser/view/ghostTextView.css rename to src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts similarity index 87% rename from src/vs/editor/contrib/inlineCompletions/browser/view/ghostTextView.ts rename to src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 4851968a3bca5..8bff9b3efbaaf 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -3,29 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createTrustedTypesPolicy } from '../../../../../base/browser/trustedTypes.js'; -import { Event } from '../../../../../base/common/event.js'; -import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, derived, observableSignalFromEvent, observableValue } from '../../../../../base/common/observable.js'; -import * as strings from '../../../../../base/common/strings.js'; +import { createTrustedTypesPolicy } from '../../../../../../base/browser/trustedTypes.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { Disposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, autorun, derived, observableSignalFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import * as strings from '../../../../../../base/common/strings.js'; +import { applyFontInfo } from '../../../../../browser/config/domFontInfo.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from '../../../../../common/config/editorOptions.js'; +import { OffsetEdit, SingleOffsetEdit } from '../../../../../common/core/offsetEdit.js'; +import { Position } from '../../../../../common/core/position.js'; +import { Range } from '../../../../../common/core/range.js'; +import { StringBuilder } from '../../../../../common/core/stringBuilder.js'; +import { ILanguageService } from '../../../../../common/languages/language.js'; +import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops, PositionAffinity } from '../../../../../common/model.js'; +import { LineEditWithAdditionalLines } from '../../../../../common/tokenizationTextModelPart.js'; +import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; +import { LineDecoration } from '../../../../../common/viewLayout/lineDecorations.js'; +import { RenderLineInput, renderViewLine } from '../../../../../common/viewLayout/viewLineRenderer.js'; +import { InlineDecorationType } from '../../../../../common/viewModel.js'; +import { GhostText, GhostTextReplacement } from '../../model/ghostText.js'; +import { ColumnRange } from '../../utils.js'; import './ghostTextView.css'; -import { applyFontInfo } from '../../../../browser/config/domFontInfo.js'; -import { ICodeEditor } from '../../../../browser/editorBrowser.js'; -import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from '../../../../common/config/editorOptions.js'; -import { Position } from '../../../../common/core/position.js'; -import { Range } from '../../../../common/core/range.js'; -import { StringBuilder } from '../../../../common/core/stringBuilder.js'; -import { ILanguageService } from '../../../../common/languages/language.js'; -import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops, PositionAffinity } from '../../../../common/model.js'; -import { LineTokens } from '../../../../common/tokens/lineTokens.js'; -import { LineDecoration } from '../../../../common/viewLayout/lineDecorations.js'; -import { RenderLineInput, renderViewLine } from '../../../../common/viewLayout/viewLineRenderer.js'; -import { InlineDecorationType } from '../../../../common/viewModel.js'; -import { GhostText, GhostTextReplacement } from '../model/ghostText.js'; -import { ColumnRange } from '../utils.js'; -import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js'; -import { OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; -import { LineEditWithAdditionalLines } from '../../../../common/tokenizationTextModelPart.js'; export interface IGhostTextWidgetModel { readonly targetTextModel: IObservable; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css new file mode 100644 index 0000000000000..bb57778ae1e63 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + /* + @keyframes blink { 50% { border-color: orange; } } +*/ + + .monaco-editor div.inline-edits-view-indicator { + display: flex; + + z-index: 1000000; + height: 20px; + + color: var(--vscode-editorHoverWidget-foreground); + background-color: var(--vscode-editorHoverWidget-background); + border: 1px solid var(--vscode-editorHoverWidget-border); + border-radius: 3px; + + align-items: center; + padding: 2px; + padding-right: 10px; + margin: 0 4px; + + /* + animation: blink 1s; + animation-iteration-count: 3; + */ + + opacity: 0; + + &.contained { + transition: opacity 0.2s ease-in-out; + transition-delay: 0.4s; + } + + + &.top { + opacity: 1; + .icon { + transform: rotate(90deg); + } + } + &.bottom { + opacity: 1; + .icon { + transform: rotate(-90deg); + } + } + + .icon { + display: flex; + align-items: center; + margin: 0 2px; + transform: none; + transition: transform 0.2s ease-in-out; + } + + .label { + margin: 0 2px; + + display: flex; + justify-content: center; + width: 100%; + } + } + +.monaco-editor div.inline-edits-view { + --widget-color: var(--vscode-editorHoverWidget-background); + + &.toolbarDropdownVisible, .editorContainer:hover { + .toolbar { + display: block; + } + } + + .editorContainer { + color: var(--vscode-editorHoverWidget-foreground); + background-color: var(--vscode-editorHoverWidget-background); + border: 1px solid var(--vscode-editorHoverWidget-border); + border-radius: 3px; + + .toolbar { + display: none; + border-top: 1px solid rgba(69, 69, 69, 0.5); + background-color: var(--vscode-editorHoverWidget-statusBarBackground); + + a { + color: var(--vscode-foreground); + } + + a:hover { + color: var(--vscode-foreground); + } + + .keybinding { + display: flex; + margin-left: 4px; + opacity: 0.6; + } + + .keybinding .monaco-keybinding-key { + font-size: 8px; + padding: 2px 3px; + } + + .availableSuggestionCount a { + display: flex; + min-width: 19px; + justify-content: center; + } + + .inlineSuggestionStatusBarItemLabel { + margin-right: 2px; + } + + } + + .preview { + .monaco-editor { + .view-overlays .current-line-exact { + border: none; + } + + .current-line-margin { + border: none; + } + + --vscode-editor-background: var(--widget-color); + } + } + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts new file mode 100644 index 0000000000000..fc42e61d8a8fa --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -0,0 +1,418 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from '../../../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { numberComparator } from '../../../../../../base/common/arrays.js'; +import { findFirstMin } from '../../../../../../base/common/arraysFind.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { createHotClass } from '../../../../../../base/common/hotReloadHelpers.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun, constObservable, derived, derivedDisposable, derivedWithCancellationToken, IObservable, observableFromEvent, ObservablePromise } from '../../../../../../base/common/observable.js'; +import { getIndentationLength, splitLines } from '../../../../../../base/common/strings.js'; +import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { EmbeddedCodeEditorWidget } from '../../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; +import { IDiffProviderFactoryService } from '../../../../../browser/widget/diffEditor/diffProviderFactoryService.js'; +import { diffAddDecoration, diffAddDecorationEmpty, diffDeleteDecorationEmpty, diffWholeLineAddDecoration, diffWholeLineDeleteDecoration } from '../../../../../browser/widget/diffEditor/registrations.contribution.js'; +import { appendRemoveOnDispose, applyStyle } from '../../../../../browser/widget/diffEditor/utils.js'; +import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { OffsetRange } from '../../../../../common/core/offsetRange.js'; +import { Position } from '../../../../../common/core/position.js'; +import { Range } from '../../../../../common/core/range.js'; +import { ArrayText, SingleTextEdit, TextEdit } from '../../../../../common/core/textEdit.js'; +import { TextLength } from '../../../../../common/core/textLength.js'; +import { lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; +import { IModelDeltaDecoration } from '../../../../../common/model.js'; +import { TextModel } from '../../../../../common/model/textModel.js'; +import { TextModelText } from '../../../../../common/model/textModelText.js'; +import { IModelService } from '../../../../../common/services/model.js'; +import { InlineEdit } from '../../model/inlineEdit.js'; +import './inlineEditsView.css'; +import { applyEditToModifiedRangeMappings, maxLeftInRange, Point, StatusBarViewItem, UniqueUriGenerator } from './utils.js'; + +export class InlineEditsViewAndDiffProducer extends Disposable { + public static readonly hot = createHotClass(InlineEditsViewAndDiffProducer); + + private readonly _modelUriGenerator = new UniqueUriGenerator('inline-edits'); + + private readonly _originalModel = derivedDisposable(() => this._modelService.createModel( + '', null, this._modelUriGenerator.getUniqueUri())).keepObserved(this._store); + private readonly _modifiedModel = derivedDisposable(() => this._modelService.createModel( + '', null, this._modelUriGenerator.getUniqueUri())).keepObserved(this._store); + + private readonly _inlineEditPromise = derivedWithCancellationToken | undefined>(this, (reader, token) => { + const edit = this._edit.read(reader); + if (!edit) { return undefined; } + const range = edit.range; + if (edit.text.trim() === '') { + return undefined; + } + + this._originalModel.get().setValue(this._editor.getModel()!.getValueInRange(range)); + this._modifiedModel.get().setValue(edit.text); + + const d = this._diffProviderFactoryService.createDiffProvider({ diffAlgorithm: 'advanced' }); + return ObservablePromise.fromFn(async () => { + const result = await d.computeDiff(this._originalModel.get(), this._modifiedModel.get(), { + computeMoves: false, + ignoreTrimWhitespace: false, + maxComputationTimeMs: 1000, + }, token); + + if (result.identical) { return undefined; } + + const rangeStartPos = Range.lift(edit.range).getStartPosition(); + const innerChanges = result.changes.flatMap(c => c.innerChanges!); + + function addRangeToPos(pos: Position, range: Range): Range { + const start = TextLength.fromPosition(range.getStartPosition()); + return TextLength.ofRange(range).createRange(start.addToPosition(pos)); + } + + const edits = innerChanges.map(c => new SingleTextEdit(addRangeToPos(rangeStartPos, c.originalRange), this._modifiedModel.get()!.getValueInRange(c.modifiedRange))); + + /*if (edit.range.startColumn !== 1) { + const range = edit.range; + const textBefore = this._editor.getModel()!.getValueInRange(new Range(range.startLineNumber, 1, range.startLineNumber, range.startColumn)); + const skippedTextEdit = TextEdit.insert(new Position(1, 1), textBefore); + innerChanges = applyEditToOriginalRangeMappings(innerChanges, skippedTextEdit); + }*/ + + return new InlineEditWithChanges(new TextEdit(edits)); + }); + }); + + private readonly _inlineEdit = this._inlineEditPromise.map((p, reader) => p?.promiseResult?.read(reader)?.data); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _edit: IObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IDiffProviderFactoryService private readonly _diffProviderFactoryService: IDiffProviderFactoryService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._register(new InlineEditsView(this._editor, this._inlineEdit, this._instantiationService)); + } +} + +export class InlineEditWithChanges { + public readonly originalLineRange = LineRange.fromRangeInclusive(RangeMapping.fromEditJoin(this.diffedTextEdit).originalRange); + public readonly modifiedLineRange = LineRange.fromRangeInclusive(RangeMapping.fromEditJoin(this.diffedTextEdit).modifiedRange); + + constructor( + public readonly diffedTextEdit: TextEdit, + ) { + } +} + +export class InlineEditsView extends Disposable { + private readonly _editorObs = observableCodeEditor(this._editor); + + private readonly _elements = h('div.inline-edits-view', { + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + }, + }, [ + h('div.editorContainer@editorContainer', { style: { position: 'absolute' } }, [ + h('div.preview@editor', { style: {} }), + h('div.toolbar@toolbar', { style: {} }), + ]), + ]); + + private readonly _indicator = h('div.inline-edits-view-indicator', { + style: { + position: 'absolute', + overflow: 'visible', + }, + }, [ + h('div.icon', {}, [ + renderIcon(Codicon.arrowLeft), + ]), + h('div.label', {}, [ + ' inline edit' + ]) + ]); + + private readonly _previewEditorWidth = derived(this, reader => { + const edit = this._edit.read(reader); + if (!edit) { return 0; } + + return maxLeftInRange(this._previewEditorObs, edit.modifiedLineRange, reader); + }); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _edit: IObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + const visible = derived(this, reader => this._edit.read(reader) !== undefined); + this._register(applyStyle(this._elements.root, { + display: derived(this, reader => visible.read(reader) ? 'block' : 'none') + })); + + this._register(appendRemoveOnDispose(this._editor.getDomNode()!, this._elements.root)); + + this._register(observableCodeEditor(_editor).createOverlayWidget({ + domNode: this._elements.root, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: derived(reader => { + const x = this._layout1.read(reader)?.left; + if (x === undefined) { return 0; } + const width = this._previewEditorWidth.read(reader); + return x + width; + }), + })); + + this._register(observableCodeEditor(_editor).createOverlayWidget({ + domNode: this._indicator.root, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + })); + + this._previewEditor.setModel(this._previewTextModel); + + this._register(this._previewEditorObs.setDecorations(this._decorations.map(d => d?.modifiedDecorations ?? []))); + + this._register(observableCodeEditor(_editor).setDecorations(this._decorations.map(d => d?.originalDecorations ?? []))); + + this._register(autorun(reader => { + const layoutInfo = this._layout.read(reader); + if (!layoutInfo) { + this._indicator.root.style.visibility = 'hidden'; + return; + } + this._indicator.root.style.visibility = ''; + + const { topEdit, editHeight } = layoutInfo; + + this._elements.editorContainer.style.top = `${topEdit.y}px`; + this._elements.editorContainer.style.left = `${topEdit.x}px`; + + const width = this._previewEditorWidth.read(reader); + this._previewEditor.layout({ height: editHeight, width }); + + const i = this._editorObs.layoutInfo.read(reader); + + + const range = new OffsetRange(0, i.height - 30); + + this._indicator.root.classList.toggle('top', topEdit.y < range.start); + this._indicator.root.classList.toggle('bottom', topEdit.y > range.endExclusive); + this._indicator.root.classList.toggle('contained', range.contains(topEdit.y)); + + + this._indicator.root.style.top = `${range.clip(topEdit.y)}px`; + this._indicator.root.style.right = `${i.minimap.minimapWidth + i.verticalScrollbarWidth}px`; + })); + + const toolbarDropdownVisible = observableFromEvent(this, this._toolbar.onDidChangeDropdownVisibility, (e) => e ?? false); + + this._register(autorun(reader => { + this._elements.root.classList.toggle('toolbarDropdownVisible', toolbarDropdownVisible.read(reader)); + })); + } + + private readonly _uiState = derived(this, reader => { + const edit = this._edit.read(reader); + if (!edit) { return undefined; } + + let newText = edit.diffedTextEdit.apply(new TextModelText(this._editor.getModel()!)); + + let mappings = RangeMapping.fromEdit(edit.diffedTextEdit); + + const newLines = splitLines(newText); + + function offsetRangeToRange(offsetRange: OffsetRange, startPos: Position): Range { + return new Range( + startPos.lineNumber, + startPos.column + offsetRange.start, + startPos.lineNumber, + startPos.column + offsetRange.endExclusive, + ); + } + + const edits: SingleTextEdit[] = []; + const minIndent = findFirstMin(edit.modifiedLineRange.mapToLineArray(l => getIndentationLength(newLines[l - 1])), numberComparator)!; + edit.modifiedLineRange.forEach(lineNumber => { + edits.push(new SingleTextEdit(offsetRangeToRange(new OffsetRange(0, minIndent), new Position(lineNumber, 1)), '')); + }); + const indentationAdjustmentEdit = new TextEdit(edits); + + newText = indentationAdjustmentEdit.applyToString(newText); + mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit); + + const diff = lineRangeMappingFromRangeMappings(mappings, new TextModelText(this._editor.getModel()!), new ArrayText(newLines)); + + return { + diff, + edit, + newText, + newTextLineCount: edit.modifiedLineRange.length, + }; + }); + + protected readonly _toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar, MenuId.InlineEditsActions, { + menuOptions: { renderShortTitle: true }, + toolbarOptions: { + primaryGroup: g => g.startsWith('primary'), + }, + actionViewItemProvider: (action, options) => { + if (action instanceof MenuItemAction) { + return this._instantiationService.createInstance(StatusBarViewItem, action, undefined); + } + return undefined; + }, + })); + private readonly _previewTextModel = this._register(this._instantiationService.createInstance( + TextModel, + '', + this._editor.getModel()!.getLanguageId(), + { ...TextModel.DEFAULT_CREATION_OPTIONS, bracketPairColorizationOptions: { enabled: true, independentColorPoolPerBracketType: false } }, + null + )); + + private readonly _previewEditor = this._register(this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._elements.editor, + { + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + bracketPairColorization: { enabled: true, independentColorPoolPerBracketType: false }, + scrollBeyondLastLine: false, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + }, + readOnly: true, + }, + { contributions: [], }, + this._editor + )); + + private readonly _previewEditorObs = observableCodeEditor(this._previewEditor); + + private readonly _ensureModelTextIsSet = derived(reader => { + const uiState = this._uiState.read(reader); + if (!uiState) { return; } + + this._previewTextModel.setValue(uiState.newText); + const range = uiState.edit.originalLineRange; + + this._previewEditor.setHiddenAreas([ + new Range(1, 1, range.startLineNumber - 1, 1), + new Range(range.startLineNumber + uiState.newTextLineCount, 1, this._previewTextModel.getLineCount() + 1, 1), + ], undefined, true); + + }).recomputeInitiallyAndOnChange(this._store); + + private readonly _decorations = derived(this, (reader) => { + this._ensureModelTextIsSet.read(reader); + const s = this._uiState.read(reader); + if (!s) { return undefined; } + const diff = s.diff; + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + for (const m of diff) { + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber)) { + originalDecorations.push({ + range: i.originalRange, options: i.originalRange.isEmpty() ? diffDeleteDecorationEmpty : { + className: 'char-delete', + description: 'char-delete', + shouldFillLineOnLineBreak: false, + } + }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ range: i.modifiedRange, options: i.modifiedRange.isEmpty() ? diffAddDecorationEmpty : diffAddDecoration }); + } + } + } + } + + return { originalDecorations, modifiedDecorations }; + }); + + private readonly _layout1 = derived(this, reader => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const maxLeft = maxLeftInRange(this._editorObs, inlineEdit.originalLineRange, reader); + + const contentLeft = this._editorObs.layoutInfoContentLeft.read(reader); + return { left: contentLeft + maxLeft }; + }); + + private readonly _layout = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + if (!inlineEdit) { return null; } + + const range = inlineEdit.originalLineRange; + + const scrollLeft = this._editorObs.scrollLeft.read(reader); + + const left = this._layout1.read(reader)!.left + 20 - scrollLeft; + + const selectionTop = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const selectionBottom = this._editor.getTopForLineNumber(range.endLineNumberExclusive) - this._editorObs.scrollTop.read(reader); + + const topCode = new Point(left, selectionTop); + const bottomCode = new Point(left, selectionBottom); + const codeHeight = selectionBottom - selectionTop; + + const codeEditDist = 50; + const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length; + + const topEdit = new Point(left + codeEditDist, selectionTop); + const bottomEdit = new Point(left + codeEditDist, selectionBottom); + + return { + topCode, + bottomCode, + codeHeight, + topEdit, + bottomEdit, + editHeight, + }; + }); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts new file mode 100644 index 0000000000000..ee3c11fcd1e8d --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from '../../../../../../base/browser/dom.js'; +import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; +import { IReader } from '../../../../../../base/common/observable.js'; +import { OS } from '../../../../../../base/common/platform.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { MenuEntryActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ObservableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { TextEdit } from '../../../../../common/core/textEdit.js'; +import { RangeMapping } from '../../../../../common/diff/rangeMapping.js'; + +export function maxLeftInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader): number { + editor.layoutInfo.read(reader); + editor.value.read(reader); + const model = editor.model.get()!; + if (!model) { return 0; } + let maxLeft = 0; + + editor.scrollTop.read(reader); + for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { + const column = model.getLineMaxColumn(i); + const left = editor.editor.getOffsetForColumn(i, column); + maxLeft = Math.max(maxLeft, left); + } + return maxLeft; +} + +export class StatusBarViewItem extends MenuEntryActionViewItem { + protected override updateLabel() { + const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService); + if (!kb) { + return super.updateLabel(); + } + if (this.label) { + const div = h('div.keybinding').root; + const keybindingLabel = this._register(new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions })); + keybindingLabel.set(kb); + this.label.textContent = this._action.label; + this.label.appendChild(div); + this.label.classList.add('inlineSuggestionStatusBarItemLabel'); + } + } + + protected override updateTooltip(): void { + // NOOP, disable tooltip + } +} + +export class Point { + constructor( + public readonly x: number, + public readonly y: number, + ) { } + + public add(other: Point): Point { + return new Point(this.x + other.x, this.y + other.y); + } + + public deltaX(delta: number): Point { + return new Point(this.x + delta, this.y); + } +} +export class UniqueUriGenerator { + private static _modelId = 0; + + constructor( + public readonly scheme: string + ) { } + + public getUniqueUri(): URI { + return URI.from({ scheme: this.scheme, path: new Date().toString() + String(UniqueUriGenerator._modelId++) }); + } +} +export function applyEditToModifiedRangeMappings(rangeMapping: RangeMapping[], edit: TextEdit): RangeMapping[] { + const updatedMappings: RangeMapping[] = []; + for (const m of rangeMapping) { + const updatedRange = edit.mapRange(m.modifiedRange); + updatedMappings.push(new RangeMapping(m.originalRange, updatedRange)); + } + return updatedMappings; +} diff --git a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts index abe204adc1e79..5941538ef69c4 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts @@ -13,7 +13,7 @@ import { ILanguageService } from '../../../common/languages/language.js'; import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from '../../../common/model.js'; import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { InlineDecorationType } from '../../../common/viewModel.js'; -import { AdditionalLinesWidget, LineData } from '../../inlineCompletions/browser/view/ghostTextView.js'; +import { AdditionalLinesWidget, LineData } from '../../inlineCompletions/browser/view/ghostText/ghostTextView.js'; import { GhostText } from '../../inlineCompletions/browser/model/ghostText.js'; import { ColumnRange } from '../../inlineCompletions/browser/utils.js'; import { diffDeleteDecoration, diffLineDeleteDecorationBackgroundWithIndicator } from '../../../browser/widget/diffEditor/registrations.contribution.js'; diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index b0cf6e51db82c..fdd3995abdfc3 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -7,11 +7,12 @@ import { createStyleSheet2 } from '../../../../base/browser/dom.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ISettableObservable, autorun, constObservable, derivedDisposable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { ISettableObservable, autorun, constObservable, derived, derivedDisposable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -22,6 +23,7 @@ import { IInlineEdit, InlineEditTriggerKind } from '../../../common/languages.js import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { IModelService } from '../../../common/services/model.js'; import { GhostText, GhostTextPart } from '../../inlineCompletions/browser/model/ghostText.js'; +import { InlineEditsAdapter } from '../../inlineCompletions/browser/model/inlineEditsAdapter.js'; import { GhostTextWidget } from './ghostTextWidget.js'; import { InlineEditHintsWidget } from './inlineEditHintsWidget.js'; import { InlineEditSideBySideWidget } from './inlineEditSideBySideWidget.js'; @@ -72,7 +74,11 @@ export class InlineEditController extends Disposable { private _jumpBackPosition: Position | undefined; private _isAccepting: ISettableObservable = observableValue(this, false); - private readonly _enabled = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); + private readonly _inlineCompletionInlineEdits = observableConfigValue(InlineEditsAdapter.experimentalInlineEditsEnabled, false, this._configurationService); + + private readonly _inlineEditEnabled = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).enabled); + private readonly _enabled = derived(this, reader => this._inlineEditEnabled.read(reader) && !this._inlineCompletionInlineEdits.read(reader)); + private readonly _fontFamily = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineEdit).fontFamily); diff --git a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index e6a2bf1011848..7e93197efec40 100644 --- a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts +++ b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts @@ -5,13 +5,13 @@ import assert from 'assert'; import { Range } from '../../../common/core/range.js'; -import { RangeMapping } from '../../../common/diff/rangeMapping.js'; +import { getLineRangeMapping, RangeMapping } from '../../../common/diff/rangeMapping.js'; import { OffsetRange } from '../../../common/core/offsetRange.js'; -import { getLineRangeMapping } from '../../../common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.js'; import { LinesSliceCharSequence } from '../../../common/diff/defaultLinesDiffComputer/linesSliceCharSequence.js'; import { MyersDiffAlgorithm } from '../../../common/diff/defaultLinesDiffComputer/algorithms/myersDiffAlgorithm.js'; import { DynamicProgrammingDiffing } from '../../../common/diff/defaultLinesDiffComputer/algorithms/dynamicProgrammingDiffing.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { ArrayText } from '../../../common/core/textEdit.js'; suite('myers', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -35,15 +35,15 @@ suite('lineRangeMapping', () => { new Range(2, 1, 3, 1), new Range(2, 1, 2, 1) ), - [ + new ArrayText([ 'const abc = "helloworld".split("");', '', '' - ], - [ + ]), + new ArrayText([ 'const asciiLower = "helloworld".split("");', '' - ] + ]) ).toString(), "{[2,3)->[2,2)}" ); @@ -56,16 +56,16 @@ suite('lineRangeMapping', () => { new Range(2, 1, 2, 1), new Range(2, 1, 4, 1), ), - [ + new ArrayText([ '', '', - ], - [ + ]), + new ArrayText([ '', '', '', '', - ] + ]) ).toString(), "{[2,2)->[2,4)}" ); diff --git a/src/vs/platform/observable/common/wrapInHotClass.ts b/src/vs/platform/observable/common/wrapInHotClass.ts new file mode 100644 index 0000000000000..75e706ed1b08f --- /dev/null +++ b/src/vs/platform/observable/common/wrapInHotClass.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { isHotReloadEnabled } from '../../../base/common/hotReload.js'; +import { IDisposable } from '../../../base/common/lifecycle.js'; +import { autorunWithStore, IObservable } from '../../../base/common/observable.js'; +import { BrandedService, GetLeadingNonServiceArgs, IInstantiationService } from '../../instantiation/common/instantiation.js'; + +export function hotClassGetOriginalInstance(value: T): T { + if (value instanceof BaseClass) { + return value._instance as any; + } + return value; +} + +/** + * Wrap a class in a reloadable wrapper. + * When the wrapper is created, the original class is created. + * When the original class changes, the instance is re-created. +*/ +export function wrapInHotClass0(clazz: IObservable>): Result> { + return !isHotReloadEnabled() ? clazz.get() : createWrapper(clazz, BaseClass0); +} + +type Result = new (...args: TArgs) => IDisposable; + +class BaseClass { + public _instance: unknown; + + constructor( + public readonly instantiationService: IInstantiationService, + ) { } + + public init(...params: any[]): void { } +} + +function createWrapper(clazz: IObservable, B: new (...args: T) => BaseClass) { + return (class ReloadableWrapper extends B { + private _autorun: IDisposable | undefined = undefined; + + override init(...params: any[]) { + this._autorun = autorunWithStore((reader, store) => { + const clazz_ = clazz.read(reader); + this._instance = store.add(this.instantiationService.createInstance(clazz_, ...params) as IDisposable); + }); + } + + dispose(): void { + this._autorun?.dispose(); + } + }) as any; +} + +class BaseClass0 extends BaseClass { + constructor(@IInstantiationService i: IInstantiationService) { super(i); this.init(); } +} + +/** + * Wrap a class in a reloadable wrapper. + * When the wrapper is created, the original class is created. + * When the original class changes, the instance is re-created. +*/ +export function wrapInHotClass1(clazz: IObservable>): Result> { + return !isHotReloadEnabled() ? clazz.get() : createWrapper(clazz, BaseClass1); +} + +class BaseClass1 extends BaseClass { + constructor(param1: any, @IInstantiationService i: IInstantiationService,) { super(i); this.init(param1); } +} From d6743bf1f86ec9a99a4b2fa83e4af92cc67085d5 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 Sep 2024 00:02:44 +0200 Subject: [PATCH 2/4] Fixes CI --- src/vs/monaco.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 0aeba15fab00b..851575355f801 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7234,6 +7234,7 @@ declare namespace monaco.languages { * Defaults to `false`. */ readonly completeBracketPairs?: boolean; + readonly isInlineEdit?: boolean; } export interface InlineCompletions { From e958572d2c1ebdcfb5eb20f7d2d517d1f428f817 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 Sep 2024 00:25:12 +0200 Subject: [PATCH 3/4] Fixes CI --- .../browser/view/inlineEdits/inlineEditsView.css | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css index bb57778ae1e63..4126d4fa71915 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - /* +/* @keyframes blink { 50% { border-color: orange; } } */ - .monaco-editor div.inline-edits-view-indicator { +.monaco-editor div.inline-edits-view-indicator { display: flex; z-index: 1000000; @@ -38,12 +38,15 @@ &.top { opacity: 1; + .icon { transform: rotate(90deg); } } + &.bottom { opacity: 1; + .icon { transform: rotate(-90deg); } @@ -64,12 +67,13 @@ justify-content: center; width: 100%; } - } +} .monaco-editor div.inline-edits-view { --widget-color: var(--vscode-editorHoverWidget-background); - &.toolbarDropdownVisible, .editorContainer:hover { + &.toolbarDropdownVisible, + .editorContainer:hover { .toolbar { display: block; } From f6615e490a80dd89e664fece06436fba79518e4c Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 19 Sep 2024 10:14:54 +0200 Subject: [PATCH 4/4] Fixes CI --- .../browser/view/inlineEdits/inlineEditsView.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css index 4126d4fa71915..6c65ff7d81257 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ /* - @keyframes blink { 50% { border-color: orange; } } + @keyframes blink { 50% { border-color: orange; } } */ .monaco-editor div.inline-edits-view-indicator { @@ -25,7 +25,7 @@ /* animation: blink 1s; - animation-iteration-count: 3; + animation-iteration-count: 3; */ opacity: 0;