From 8220518134e8fa144941f5c0ee19cd30a6504acf Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 9 Feb 2024 18:35:14 +0100 Subject: [PATCH 1/7] fixing bug in get edits generation --- .../browser/inlineCompletionsModel.ts | 109 ++++++++++++++---- .../browser/inlineCompletionsModel.test.ts | 63 ++++++++++ 2 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 2faf2a0f8a9..b23c5718822 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -7,7 +7,7 @@ import { mapFindFirst } from 'vs/base/common/arraysFind'; import { BugIndicatingError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, recomputeInitiallyAndOnChange, observableSignal, observableValue, subtransaction, transaction } from 'vs/base/common/observable'; -import { commonPrefixLength } from 'vs/base/common/strings'; +import { commonPrefixLength, splitLines } from 'vs/base/common/strings'; import { isDefined } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -212,6 +212,7 @@ export class InlineCompletionsModel extends Disposable { const suggestItem = this.selectedSuggestItem.read(reader); if (suggestItem) { + console.log('first if statement'); const suggestCompletionEdit = suggestItem.toSingleTextEdit().removeCommonPrefix(model); const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); @@ -219,29 +220,37 @@ export class InlineCompletionsModel extends Disposable { if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; } const fullEdit = augmentation?.edit ?? suggestCompletionEdit; + console.log('fullEdit : ', fullEdit); const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0; const mode = this._suggestPreviewMode.read(reader); const positions = this._positions.read(reader); - const edits = [fullEdit, ...this._getSecondaryEdits(this.textModel, positions, fullEdit)]; + const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; + console.log('edits : ', edits); const ghostTexts = edits .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], fullEditPreviewLength)) .filter(isDefined); const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); + console.log('primaryGhostText : ', primaryGhostText); + console.log('ghostTexts : ', ghostTexts); return { edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; } else { + console.log('second else statement'); if (!this._isActive.read(reader)) { return undefined; } const inlineCompletion = this.selectedInlineCompletion.read(reader); + console.log('inlineCompletion : ', inlineCompletion); if (!inlineCompletion) { return undefined; } const replacement = inlineCompletion.toSingleTextEdit(reader); const mode = this._inlineSuggestMode.read(reader); const positions = this._positions.read(reader); - const edits = [replacement, ...this._getSecondaryEdits(this.textModel, positions, replacement)]; + const edits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; const ghostTexts = edits .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], 0)) .filter(isDefined); if (!ghostTexts[0]) { return undefined; } + console.log('primaryGhostText : ', ghostTexts[0]); + console.log('ghostTexts : ', ghostTexts); return { edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; } }); @@ -402,6 +411,7 @@ export class InlineCompletionsModel extends Disposable { return; } const ghostText = state.primaryGhostText; + console.log('ghostText : ', ghostText); const completion = state.inlineCompletion.toInlineCompletion(undefined); if (completion.snippetInfo || completion.filterText !== completion.insertText) { @@ -413,13 +423,16 @@ export class InlineCompletionsModel extends Disposable { const firstPart = ghostText.parts[0]; const ghostTextPos = new Position(ghostText.lineNumber, firstPart.column); const ghostTextVal = firstPart.text; + console.log('ghostTextPos : ', ghostTextPos); + console.log('ghostTextVal : ', ghostTextVal); const acceptUntilIndexExclusive = getAcceptUntilIndex(ghostTextPos, ghostTextVal); + console.log('acceptUntilIndexExclusive : ', acceptUntilIndexExclusive); if (acceptUntilIndexExclusive === ghostTextVal.length && ghostText.parts.length === 1) { this.accept(editor); return; } const partialGhostTextVal = ghostTextVal.substring(0, acceptUntilIndexExclusive); - + console.log('partialGhostTextVal : ', partialGhostTextVal); const positions = this._positions.get(); const cursorPosition = positions[0]; @@ -430,9 +443,12 @@ export class InlineCompletionsModel extends Disposable { try { editor.pushUndoStop(); const replaceRange = Range.fromPositions(cursorPosition, ghostTextPos); + console.log('replaceRange : ', replaceRange); const newText = editor.getModel()!.getValueInRange(replaceRange) + partialGhostTextVal; + console.log('newText : ', newText); const primaryEdit = new SingleTextEdit(replaceRange, newText); - const edits = [primaryEdit, ...this._getSecondaryEdits(this.textModel, positions, primaryEdit)]; + console.log('primaryEdit : ', primaryEdit); + const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)]; const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replaceMove(edit.range, edit.text))); editor.setSelections(selections, 'inlineCompletionPartialAccept'); @@ -455,23 +471,6 @@ export class InlineCompletionsModel extends Disposable { } } - private _getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { - const primaryPosition = positions[0]; - const secondaryPositions = positions.slice(1); - const replacedTextAfterPrimaryCursor = textModel - .getLineContent(primaryPosition.lineNumber) - .substring(primaryPosition.column - 1, primaryEdit.range.endColumn - 1); - const secondaryEditText = primaryEdit.text.substring(primaryPosition.column - primaryEdit.range.startColumn); - return secondaryPositions.map(pos => { - const textAfterSecondaryCursor = this.textModel - .getLineContent(pos.lineNumber) - .substring(pos.column - 1); - const l = commonPrefixLength(replacedTextAfterPrimaryCursor, textAfterSecondaryCursor); - const range = Range.fromPositions(pos, pos.delta(0, l)); - return new SingleTextEdit(range, secondaryEditText); - }); - } - public handleSuggestAccepted(item: SuggestItemInfo) { const itemEdit = item.toSingleTextEdit().removeCommonPrefix(this.textModel); const augmentedCompletion = this._computeAugmentation(itemEdit, undefined); @@ -492,3 +491,69 @@ function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Positio const newRanges = sortPerm.inverse().apply(sortedNewRanges); return newRanges.map(range => range.getEndPosition()); } + +export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { + const primaryPosition = positions[0]; + const secondaryPositions = positions.slice(1); + console.log('positions : ', JSON.stringify(positions)); + // replaced text is not calculated correctly + // only works if primary edit line number + // Need to take all the that is replaced in the primary edit range + + const replacedTextAfterPrimaryCursor = textModel.getValueInRange(Range.fromPositions(primaryPosition, primaryEdit.range.getEndPosition())); + console.log('replacedTextAfterPrimaryCursor : ', JSON.stringify(replacedTextAfterPrimaryCursor)); + console.log('primaryEdit : ', JSON.stringify(primaryEdit)); + + // There is an error below too, the secondary edit text is the text after the cursor to the right of it, that needs to be added + // in the test case we would want to add ') {\n\treturn 0;\n}' because we already have fib( written. + // Before it worked because we would have the cursor at the end of function fib(, now the cursor is on the line below it + // Or at the very least, we should insert 'return 0;\n}' because this is to the right of the cursor at the primary cursor position + // So need to find the primary position within the edit, and find all the text to the right of it. The primary position will not necessarily be on the first + // line of the edit text. + // We suppose that the primaryEdit.range always touches the primaryPosition in some manner + + // could find the offset of primary position, the offset of the primary edit start and find thus the secondary edit text + // const _offsetPrimaryPosition = textModel.getOffsetAt(primaryPosition); + // const _offsetPrimaryEditStart = textModel.getOffsetAt(primaryEdit.range.getStartPosition()); + // console.log('_offsetPrimaryPosition : ', _offsetPrimaryPosition); + // console.log('_offsetPrimaryEditStart : ', _offsetPrimaryEditStart); + + // Find offset in a different way + // Split the lines of the text, place it in the context of the whole text + // Find the position in the text where the initial position would be, exactly as is, find the offset, and take the substring + + const newCol = primaryPosition.lineNumber - primaryEdit.range.startLineNumber; + const newLine = newCol === 0 ? primaryPosition.column - primaryEdit.range.startColumn : primaryPosition.column; + + let text = ''; + const _splitLines = splitLines(primaryEdit.text); + for (let i = newLine; i < _splitLines.length; i++) { + if (i === newLine) { + text += _splitLines[i].substring(newCol) + '\n'; + } else { + text += _splitLines[i] + '\n'; + } + } + console.log('text : ', text); + + const secondaryEditText = text; + // primaryEdit.text.substring(primaryPosition.column - primaryEdit.range.startColumn); + // console.log('secondaryEditText : ', JSON.stringify(secondaryEditText)); + return secondaryPositions.map(pos => { + console.log('pos : ', JSON.stringify(pos)); + // Maybe taking the substring on the line content specifically is not enough, so we need to actually take it until the range end, because that is the text we would replace + // the range end is not necessarily on the end of that line either + // const textAfterSecondaryCursor = textModel + // .getLineContent(pos.lineNumber) + // .substring(pos.column - 1); + + const textAfterSecondaryCursor = textModel.getValueInRange(Range.fromPositions(pos, primaryEdit.range.getEndPosition())); + console.log('textAfterSecondaryCursor : ', textAfterSecondaryCursor); + const l = commonPrefixLength(replacedTextAfterPrimaryCursor, textAfterSecondaryCursor); + console.log('l : ', l); + const range = Range.fromPositions(pos, pos.delta(0, l)); + console.log('range : ', JSON.stringify(range)); + return new SingleTextEdit(range, secondaryEditText); + }); +} + diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts new file mode 100644 index 00000000000..22c3a0c1987 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { Position } from 'vs/editor/common/core/position'; +import { getSecondaryEdits } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; +import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { Range } from 'vs/editor/common/core/range'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; + +suite('inlineCompletionModel', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('getSecondaryEdits - basic', async function () { + + const textModel = createTextModel([ + 'function fib(', + 'function fib(' + ].join('\n')); + const positions = [ + new Position(1, 14), + new Position(2, 14) + ]; + const primaryEdit = new SingleTextEdit(new Range(1, 1, 1, 14), 'function fib() {'); + const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit); + assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit( + new Range(2, 14, 2, 14), + ') {' + )]); + textModel.dispose(); + }); + + test('getSecondaryEdits - cursor not on same line as primary edit', async function () { + + const textModel = createTextModel([ + 'function fib(', + '', + 'function fib(', + '' + ].join('\n')); + const positions = [ + new Position(2, 1), + new Position(4, 1) + ]; + const primaryEdit = new SingleTextEdit(new Range(1, 1, 2, 1), [ + 'function fib() {', + ' return 0;', + '}' + ].join('\n')); + const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit); + assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit( + new Range(4, 1, 4, 1), [ + ' return 0;', + '}' + ].join('\n') + )]); + console.log('secondaryEdits : ', JSON.stringify(secondaryEdits)); + textModel.dispose(); + }); +}); From bb78bb1d393e0acb32c509871732117e8e737003 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 9 Feb 2024 18:37:19 +0100 Subject: [PATCH 2/7] removing some logs --- .../browser/inlineCompletionsModel.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index b23c5718822..e4d474d8238 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -212,7 +212,6 @@ export class InlineCompletionsModel extends Disposable { const suggestItem = this.selectedSuggestItem.read(reader); if (suggestItem) { - console.log('first if statement'); const suggestCompletionEdit = suggestItem.toSingleTextEdit().removeCommonPrefix(model); const augmentation = this._computeAugmentation(suggestCompletionEdit, reader); @@ -220,25 +219,19 @@ export class InlineCompletionsModel extends Disposable { if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; } const fullEdit = augmentation?.edit ?? suggestCompletionEdit; - console.log('fullEdit : ', fullEdit); const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0; const mode = this._suggestPreviewMode.read(reader); const positions = this._positions.read(reader); const edits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)]; - console.log('edits : ', edits); const ghostTexts = edits .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], fullEditPreviewLength)) .filter(isDefined); const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); - console.log('primaryGhostText : ', primaryGhostText); - console.log('ghostTexts : ', ghostTexts); return { edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; } else { - console.log('second else statement'); if (!this._isActive.read(reader)) { return undefined; } const inlineCompletion = this.selectedInlineCompletion.read(reader); - console.log('inlineCompletion : ', inlineCompletion); if (!inlineCompletion) { return undefined; } const replacement = inlineCompletion.toSingleTextEdit(reader); @@ -249,8 +242,6 @@ export class InlineCompletionsModel extends Disposable { .map((edit, idx) => edit.computeGhostText(model, mode, positions[idx], 0)) .filter(isDefined); if (!ghostTexts[0]) { return undefined; } - console.log('primaryGhostText : ', ghostTexts[0]); - console.log('ghostTexts : ', ghostTexts); return { edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; } }); @@ -411,7 +402,6 @@ export class InlineCompletionsModel extends Disposable { return; } const ghostText = state.primaryGhostText; - console.log('ghostText : ', ghostText); const completion = state.inlineCompletion.toInlineCompletion(undefined); if (completion.snippetInfo || completion.filterText !== completion.insertText) { @@ -423,16 +413,13 @@ export class InlineCompletionsModel extends Disposable { const firstPart = ghostText.parts[0]; const ghostTextPos = new Position(ghostText.lineNumber, firstPart.column); const ghostTextVal = firstPart.text; - console.log('ghostTextPos : ', ghostTextPos); - console.log('ghostTextVal : ', ghostTextVal); const acceptUntilIndexExclusive = getAcceptUntilIndex(ghostTextPos, ghostTextVal); - console.log('acceptUntilIndexExclusive : ', acceptUntilIndexExclusive); if (acceptUntilIndexExclusive === ghostTextVal.length && ghostText.parts.length === 1) { this.accept(editor); return; } const partialGhostTextVal = ghostTextVal.substring(0, acceptUntilIndexExclusive); - console.log('partialGhostTextVal : ', partialGhostTextVal); + const positions = this._positions.get(); const cursorPosition = positions[0]; @@ -443,11 +430,8 @@ export class InlineCompletionsModel extends Disposable { try { editor.pushUndoStop(); const replaceRange = Range.fromPositions(cursorPosition, ghostTextPos); - console.log('replaceRange : ', replaceRange); const newText = editor.getModel()!.getValueInRange(replaceRange) + partialGhostTextVal; - console.log('newText : ', newText); const primaryEdit = new SingleTextEdit(replaceRange, newText); - console.log('primaryEdit : ', primaryEdit); const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)]; const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p)); editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replaceMove(edit.range, edit.text))); From 92143ea2ddfba4a0aea73f19b8a4374e18da1992 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 9 Feb 2024 18:40:01 +0100 Subject: [PATCH 3/7] adding one more test --- .../browser/inlineCompletionsModel.test.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index 22c3a0c1987..c6bc78de063 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -33,7 +33,7 @@ suite('inlineCompletionModel', () => { textModel.dispose(); }); - test('getSecondaryEdits - cursor not on same line as primary edit', async function () { + test('getSecondaryEdits - cursor not on same line as primary edit 1', async function () { const textModel = createTextModel([ 'function fib(', @@ -60,4 +60,35 @@ suite('inlineCompletionModel', () => { console.log('secondaryEdits : ', JSON.stringify(secondaryEdits)); textModel.dispose(); }); + + test('getSecondaryEdits - cursor not on same line as primary edit 2', async function () { + + const textModel = createTextModel([ + 'class A {', + '', + 'class B {', + '', + 'function f() {}' + ].join('\n')); + const positions = [ + new Position(2, 1), + new Position(4, 1) + ]; + const primaryEdit = new SingleTextEdit(new Range(1, 1, 2, 1), [ + 'class A {', + ' public x: number = 0;', + ' public y: number = 0;', + '}' + ].join('\n')); + const secondaryEdits = getSecondaryEdits(textModel, positions, primaryEdit); + assert.deepStrictEqual(secondaryEdits, [new SingleTextEdit( + new Range(4, 1, 4, 1), [ + ' public x: number = 0;', + ' public y: number = 0;', + '}' + ].join('\n') + )]); + console.log('secondaryEdits : ', JSON.stringify(secondaryEdits)); + textModel.dispose(); + }); }); From c7335f45ec29c41eaed5e98da5163040815c0fdb Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 12 Feb 2024 09:31:33 +0100 Subject: [PATCH 4/7] fixing failing tests --- .../browser/inlineCompletionsModel.ts | 49 ++----------------- .../browser/inlineCompletionsModel.test.ts | 2 - 2 files changed, 4 insertions(+), 47 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index e4d474d8238..87fa7080180 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -479,64 +479,23 @@ function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Positio export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { const primaryPosition = positions[0]; const secondaryPositions = positions.slice(1); - console.log('positions : ', JSON.stringify(positions)); - // replaced text is not calculated correctly - // only works if primary edit line number - // Need to take all the that is replaced in the primary edit range - const replacedTextAfterPrimaryCursor = textModel.getValueInRange(Range.fromPositions(primaryPosition, primaryEdit.range.getEndPosition())); - console.log('replacedTextAfterPrimaryCursor : ', JSON.stringify(replacedTextAfterPrimaryCursor)); - console.log('primaryEdit : ', JSON.stringify(primaryEdit)); - - // There is an error below too, the secondary edit text is the text after the cursor to the right of it, that needs to be added - // in the test case we would want to add ') {\n\treturn 0;\n}' because we already have fib( written. - // Before it worked because we would have the cursor at the end of function fib(, now the cursor is on the line below it - // Or at the very least, we should insert 'return 0;\n}' because this is to the right of the cursor at the primary cursor position - // So need to find the primary position within the edit, and find all the text to the right of it. The primary position will not necessarily be on the first - // line of the edit text. - // We suppose that the primaryEdit.range always touches the primaryPosition in some manner - - // could find the offset of primary position, the offset of the primary edit start and find thus the secondary edit text - // const _offsetPrimaryPosition = textModel.getOffsetAt(primaryPosition); - // const _offsetPrimaryEditStart = textModel.getOffsetAt(primaryEdit.range.getStartPosition()); - // console.log('_offsetPrimaryPosition : ', _offsetPrimaryPosition); - // console.log('_offsetPrimaryEditStart : ', _offsetPrimaryEditStart); - - // Find offset in a different way - // Split the lines of the text, place it in the context of the whole text - // Find the position in the text where the initial position would be, exactly as is, find the offset, and take the substring - - const newCol = primaryPosition.lineNumber - primaryEdit.range.startLineNumber; - const newLine = newCol === 0 ? primaryPosition.column - primaryEdit.range.startColumn : primaryPosition.column; - + const newLine = primaryPosition.lineNumber - primaryEdit.range.startLineNumber; + const newCol = newLine === 0 ? primaryPosition.column - primaryEdit.range.startColumn + 1 : primaryPosition.column; let text = ''; const _splitLines = splitLines(primaryEdit.text); for (let i = newLine; i < _splitLines.length; i++) { if (i === newLine) { - text += _splitLines[i].substring(newCol) + '\n'; + text += _splitLines[i].substring(newCol - 1) + (i === _splitLines.length - 1 ? '' : '\n'); } else { - text += _splitLines[i] + '\n'; + text += _splitLines[i] + (i === _splitLines.length - 1 ? '' : '\n'); } } - console.log('text : ', text); - const secondaryEditText = text; - // primaryEdit.text.substring(primaryPosition.column - primaryEdit.range.startColumn); - // console.log('secondaryEditText : ', JSON.stringify(secondaryEditText)); return secondaryPositions.map(pos => { - console.log('pos : ', JSON.stringify(pos)); - // Maybe taking the substring on the line content specifically is not enough, so we need to actually take it until the range end, because that is the text we would replace - // the range end is not necessarily on the end of that line either - // const textAfterSecondaryCursor = textModel - // .getLineContent(pos.lineNumber) - // .substring(pos.column - 1); - const textAfterSecondaryCursor = textModel.getValueInRange(Range.fromPositions(pos, primaryEdit.range.getEndPosition())); - console.log('textAfterSecondaryCursor : ', textAfterSecondaryCursor); const l = commonPrefixLength(replacedTextAfterPrimaryCursor, textAfterSecondaryCursor); - console.log('l : ', l); const range = Range.fromPositions(pos, pos.delta(0, l)); - console.log('range : ', JSON.stringify(range)); return new SingleTextEdit(range, secondaryEditText); }); } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts index c6bc78de063..67c56bb3945 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsModel.test.ts @@ -57,7 +57,6 @@ suite('inlineCompletionModel', () => { '}' ].join('\n') )]); - console.log('secondaryEdits : ', JSON.stringify(secondaryEdits)); textModel.dispose(); }); @@ -88,7 +87,6 @@ suite('inlineCompletionModel', () => { '}' ].join('\n') )]); - console.log('secondaryEdits : ', JSON.stringify(secondaryEdits)); textModel.dispose(); }); }); From 6992cbb90fc362f0a6f0624bc2cc324018d46171 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 12 Feb 2024 11:34:21 +0100 Subject: [PATCH 5/7] adding function splitLinesIncludeSeparators --- src/vs/base/common/strings.ts | 8 ++++++ .../browser/inlineCompletionsModel.ts | 28 +++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 230e6eb7bdd..a68f48e76f9 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -254,6 +254,14 @@ export function splitLines(str: string): string[] { return str.split(/\r\n|\r|\n/); } +export function splitLinesIncludeSeparators(str: string): { lines: string[]; separators: string[] } { + const lines: string[] = []; + const separators: string[] = []; + const splitLinesAndSeparators = str.split(/(\r\n|\r|\n)/); + splitLinesAndSeparators.forEach((el, idx) => (idx % 2 === 0 ? lines : separators).push(el)); + return { lines, separators }; +} + /** * Returns first index of the string that is not whitespace. * If string is empty or contains only whitespaces, returns -1 diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 87fa7080180..15fd1e0e856 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -7,7 +7,7 @@ import { mapFindFirst } from 'vs/base/common/arraysFind'; import { BugIndicatingError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; import { IObservable, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, recomputeInitiallyAndOnChange, observableSignal, observableValue, subtransaction, transaction } from 'vs/base/common/observable'; -import { commonPrefixLength, splitLines } from 'vs/base/common/strings'; +import { commonPrefixLength, splitLinesIncludeSeparators } from 'vs/base/common/strings'; import { isDefined } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -479,21 +479,21 @@ function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Positio export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { const primaryPosition = positions[0]; const secondaryPositions = positions.slice(1); - const replacedTextAfterPrimaryCursor = textModel.getValueInRange(Range.fromPositions(primaryPosition, primaryEdit.range.getEndPosition())); - const newLine = primaryPosition.lineNumber - primaryEdit.range.startLineNumber; - const newCol = newLine === 0 ? primaryPosition.column - primaryEdit.range.startColumn + 1 : primaryPosition.column; - let text = ''; - const _splitLines = splitLines(primaryEdit.text); - for (let i = newLine; i < _splitLines.length; i++) { - if (i === newLine) { - text += _splitLines[i].substring(newCol - 1) + (i === _splitLines.length - 1 ? '' : '\n'); - } else { - text += _splitLines[i] + (i === _splitLines.length - 1 ? '' : '\n'); - } + const primaryEditEndPosition = primaryEdit.range.getEndPosition(); + const replacedTextAfterPrimaryCursor = textModel.getValueInRange( + Range.fromPositions(primaryPosition, primaryEditEndPosition) + ); + const lineNumberWithinEditText = primaryPosition.lineNumber - primaryEdit.range.startLineNumber; + const columnWithinEditText = lineNumberWithinEditText === 0 ? primaryPosition.column - primaryEdit.range.startColumn : primaryPosition.column - 1; + let secondaryEditText = ''; + const { lines, separators } = splitLinesIncludeSeparators(primaryEdit.text); + for (let i = lineNumberWithinEditText; i < lines.length; i++) { + secondaryEditText += lines[i].substring(i === lineNumberWithinEditText ? columnWithinEditText : 0) + (separators[i] ?? ''); } - const secondaryEditText = text; return secondaryPositions.map(pos => { - const textAfterSecondaryCursor = textModel.getValueInRange(Range.fromPositions(pos, primaryEdit.range.getEndPosition())); + const textAfterSecondaryCursor = textModel.getValueInRange( + Range.fromPositions(pos, primaryEditEndPosition) + ); const l = commonPrefixLength(replacedTextAfterPrimaryCursor, textAfterSecondaryCursor); const range = Range.fromPositions(pos, pos.delta(0, l)); return new SingleTextEdit(range, secondaryEditText); From e37a23a9e4b6dddbb289c43e18134b674203f6d0 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 12 Feb 2024 12:56:23 +0100 Subject: [PATCH 6/7] defining now the subtract positions method and using it --- .../browser/inlineCompletionsModel.ts | 33 ++++++++++--------- .../inlineCompletions/browser/utils.ts | 4 +++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 15fd1e0e856..0773f8224eb 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -22,7 +22,7 @@ import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostT import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsSource'; import { SingleTextEdit } from 'vs/editor/contrib/inlineCompletions/browser/singleTextEdit'; import { SuggestItemInfo } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider'; -import { Permutation, addPositions, getNewRanges, lengthOfText } from 'vs/editor/contrib/inlineCompletions/browser/utils'; +import { Permutation, addPositions, getNewRanges, lengthOfText, subtractPositions } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -469,13 +469,6 @@ export class InlineCompletionsModel extends Disposable { } } -function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { - const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range)); - const sortedNewRanges = getNewRanges(sortPerm.apply(edits)); - const newRanges = sortPerm.inverse().apply(sortedNewRanges); - return newRanges.map(range => range.getEndPosition()); -} - export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryEdit: SingleTextEdit): SingleTextEdit[] { const primaryPosition = positions[0]; const secondaryPositions = positions.slice(1); @@ -483,13 +476,8 @@ export function getSecondaryEdits(textModel: ITextModel, positions: readonly Pos const replacedTextAfterPrimaryCursor = textModel.getValueInRange( Range.fromPositions(primaryPosition, primaryEditEndPosition) ); - const lineNumberWithinEditText = primaryPosition.lineNumber - primaryEdit.range.startLineNumber; - const columnWithinEditText = lineNumberWithinEditText === 0 ? primaryPosition.column - primaryEdit.range.startColumn : primaryPosition.column - 1; - let secondaryEditText = ''; - const { lines, separators } = splitLinesIncludeSeparators(primaryEdit.text); - for (let i = lineNumberWithinEditText; i < lines.length; i++) { - secondaryEditText += lines[i].substring(i === lineNumberWithinEditText ? columnWithinEditText : 0) + (separators[i] ?? ''); - } + const positionWithinTextEdit = subtractPositions(primaryPosition, primaryEdit.range.getStartPosition()); + const secondaryEditText = getTextFromPosition(primaryEdit.text, positionWithinTextEdit); return secondaryPositions.map(pos => { const textAfterSecondaryCursor = textModel.getValueInRange( Range.fromPositions(pos, primaryEditEndPosition) @@ -500,3 +488,18 @@ export function getSecondaryEdits(textModel: ITextModel, positions: readonly Pos }); } +function getTextFromPosition(text: string, pos: Position): string { + let subtext = ''; + const { lines, separators } = splitLinesIncludeSeparators(text); + for (let i = pos.lineNumber - 1; i < lines.length; i++) { + subtext += lines[i].substring(i === pos.lineNumber - 1 ? pos.column - 1 : 0) + (separators[i] ?? ''); + } + return subtext; +} + +function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { + const sortPerm = Permutation.createSortPermutation(edits, (edit1, edit2) => Range.compareRangesUsingStarts(edit1.range, edit2.range)); + const sortedNewRanges = getNewRanges(sortPerm.apply(edits)); + const newRanges = sortPerm.inverse().apply(sortedNewRanges); + return newRanges.map(range => range.getEndPosition()); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 805de9a781a..4a9e78238b6 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -96,6 +96,10 @@ export function addPositions(pos1: Position, pos2: Position): Position { return new Position(pos1.lineNumber + pos2.lineNumber - 1, pos2.lineNumber === 1 ? pos1.column + pos2.column - 1 : pos2.column); } +export function subtractPositions(pos1: Position, pos2: Position): Position { + return new Position(pos1.lineNumber - pos2.lineNumber + 1, pos1.lineNumber - pos2.lineNumber === 0 ? pos1.column - pos2.column + 1 : pos1.column); +} + export function lengthOfText(text: string): Position { let line = 1; let column = 1; From b31cef9e2c98143aad50d8ccbf7ec203cffa925c Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 12 Feb 2024 14:55:26 +0100 Subject: [PATCH 7/7] placing separators with the lines themselves --- src/vs/base/common/strings.ts | 11 ++++++----- .../browser/inlineCompletionsModel.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index a68f48e76f9..6ec11f03919 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -254,12 +254,13 @@ export function splitLines(str: string): string[] { return str.split(/\r\n|\r|\n/); } -export function splitLinesIncludeSeparators(str: string): { lines: string[]; separators: string[] } { - const lines: string[] = []; - const separators: string[] = []; +export function splitLinesIncludeSeparators(str: string): string[] { + const linesWithSeparators: string[] = []; const splitLinesAndSeparators = str.split(/(\r\n|\r|\n)/); - splitLinesAndSeparators.forEach((el, idx) => (idx % 2 === 0 ? lines : separators).push(el)); - return { lines, separators }; + for (let i = 0; i < Math.ceil(splitLinesAndSeparators.length / 2); i++) { + linesWithSeparators.push(splitLinesAndSeparators[2 * i] + (splitLinesAndSeparators[2 * i + 1] ?? '')); + } + return linesWithSeparators; } /** diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 0773f8224eb..e385a0d3c65 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -490,9 +490,9 @@ export function getSecondaryEdits(textModel: ITextModel, positions: readonly Pos function getTextFromPosition(text: string, pos: Position): string { let subtext = ''; - const { lines, separators } = splitLinesIncludeSeparators(text); + const lines = splitLinesIncludeSeparators(text); for (let i = pos.lineNumber - 1; i < lines.length; i++) { - subtext += lines[i].substring(i === pos.lineNumber - 1 ? pos.column - 1 : 0) + (separators[i] ?? ''); + subtext += lines[i].substring(i === pos.lineNumber - 1 ? pos.column - 1 : 0); } return subtext; }