Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(auto-edits): fix the partial decoration issue when not enough lines in the editor #6582

Merged
merged 7 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion vscode/src/autoedits/analytics-logger/analytics-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const validRequestTransitions = {
started: ['contextLoaded', 'discarded'],
contextLoaded: ['loaded', 'discarded'],
loaded: ['postProcessed', 'discarded'],
postProcessed: ['suggested'],
postProcessed: ['suggested', 'discarded'],
Copy link
Contributor Author

@hitesh-1997 hitesh-1997 Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@valerybugakov added another transition state, since conflictingDecorations and default-renderer can discard.

suggested: ['read', 'accepted', 'rejected'],
read: ['accepted', 'rejected'],
accepted: [],
Expand Down Expand Up @@ -173,6 +173,8 @@ export const autoeditDiscardReason = {
suffixOverlap: 5,
emptyPredictionAfterInlineCompletionExtraction: 6,
noActiveEditor: 7,
conflictingDecorationWithEdits: 8,
noEnoughLinesEditor: 9,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be grammatically correct to use the following name?

Suggested change
noEnoughLinesEditor: 9,
notEnoughLinesEditor: 9,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed as per suggestion

} as const

/** We use numeric keys to send these to the analytics backend */
Expand Down
10 changes: 10 additions & 0 deletions vscode/src/autoedits/renderer/decorators/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export interface AutoEditsDecorator extends vscode.Disposable {
* and how they should be decorated in the editor.
*/
setDecorations(decorationInfo: DecorationInfo): void

/**
* Checks if the decorator can render decorations for the given decoration information.
*
* This method verifies if the current editor state allows for the decorations to be
* rendered properly. Some conditions that might prevent rendering include:
* - Insufficient lines in the editor
* @returns true if decorations can be rendered, false otherwise
*/
canRenderDecoration(decorationInfo: DecorationInfo): boolean
}

/**
Expand Down
89 changes: 78 additions & 11 deletions vscode/src/autoedits/renderer/decorators/default-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,40 @@ interface AddedLinesDecorationInfo {
lineText: string
}

interface DiffDecorationAddedLinesInfo {
/** Information about lines that have been added */
addedLinesDecorationInfo: AddedLinesDecorationInfo[]
/** Starting line number for the decoration */
startLine: number
/** Column position for the replacement text */
replacerCol: number
}

/**
* Information about diff decorations to be applied to lines in the editor
*/
interface DiffDecorationInfo {
/** Ranges of text that have been removed */
removedRangesInfo: vscode.Range[]
/** Information about lines that have been added */
addedLinesInfo?: DiffDecorationAddedLinesInfo
}

export class DefaultDecorator implements AutoEditsDecorator {
private readonly decorationTypes: vscode.TextEditorDecorationType[]
private readonly editor: vscode.TextEditor

// Decoration types
private readonly removedTextDecorationType: vscode.TextEditorDecorationType
private readonly modifiedTextDecorationType: vscode.TextEditorDecorationType
private readonly suggesterType: vscode.TextEditorDecorationType
private readonly hideRemainderDecorationType: vscode.TextEditorDecorationType
private readonly addedLinesDecorationType: vscode.TextEditorDecorationType
private readonly insertMarkerDecorationType: vscode.TextEditorDecorationType
private readonly editor: vscode.TextEditor

/**
* Pre-computed information about diff decorations to be applied to lines in the editor.
*/
private diffDecorationInfo: DiffDecorationInfo | undefined

constructor(editor: vscode.TextEditor) {
this.editor = editor
Expand All @@ -34,9 +59,6 @@ export class DefaultDecorator implements AutoEditsDecorator {
before: { color: GHOST_TEXT_COLOR },
after: { color: GHOST_TEXT_COLOR },
})
this.hideRemainderDecorationType = vscode.window.createTextEditorDecorationType({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was redundant and was not getting used anywhere.

opacity: '0',
})
this.addedLinesDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: 'red', // SENTINEL (should not actually appear)
before: {
Expand All @@ -55,12 +77,28 @@ export class DefaultDecorator implements AutoEditsDecorator {
this.removedTextDecorationType,
this.modifiedTextDecorationType,
this.suggesterType,
this.hideRemainderDecorationType,
this.addedLinesDecorationType,
this.insertMarkerDecorationType,
]
}

public canRenderDecoration(decorationInfo: DecorationInfo): boolean {
if (!this.diffDecorationInfo) {
this.diffDecorationInfo = this.getDiffDecorationsInfo(decorationInfo)
}
const { addedLinesInfo } = this.diffDecorationInfo
if (addedLinesInfo) {
// Check if there are enough lines in the editor to render the diff decorations
if (
addedLinesInfo.startLine + addedLinesInfo.addedLinesDecorationInfo.length >
this.editor.document.lineCount
) {
return false
}
}
return true
}

private clearDecorations(): void {
for (const decorationType of this.decorationTypes) {
this.editor.setDecorations(decorationType, [])
Expand Down Expand Up @@ -90,6 +128,27 @@ export class DefaultDecorator implements AutoEditsDecorator {
}

private renderDiffDecorations(decorationInfo: DecorationInfo): void {
if (!this.diffDecorationInfo) {
this.diffDecorationInfo = this.getDiffDecorationsInfo(decorationInfo)
}
this.editor.setDecorations(
this.modifiedTextDecorationType,
this.diffDecorationInfo.removedRangesInfo
)
const addedLinesInfo = this.diffDecorationInfo.addedLinesInfo

if (!addedLinesInfo) {
return
}

this.renderAddedLinesDecorations(
addedLinesInfo.addedLinesDecorationInfo,
addedLinesInfo.startLine,
addedLinesInfo.replacerCol
)
}

private getDiffDecorationsInfo(decorationInfo: DecorationInfo): DiffDecorationInfo {
const { modifiedLines, addedLines, unchangedLines } = decorationInfo

// Display the removed range decorations
Expand Down Expand Up @@ -119,7 +178,6 @@ export class DefaultDecorator implements AutoEditsDecorator {
})
}
}
this.editor.setDecorations(this.modifiedTextDecorationType, removedRanges)

// Handle fully added lines
for (const addedLine of addedLines) {
Expand Down Expand Up @@ -151,13 +209,22 @@ export class DefaultDecorator implements AutoEditsDecorator {
// Sort addedLinesInfo by line number in ascending order
addedLinesInfo.sort((a, b) => a.afterLine - b.afterLine)
if (addedLinesInfo.length === 0) {
return
return { removedRangesInfo: removedRanges }
}
// todo (hitesh): handle case when too many lines to fit in the editor
const oldLines = addedLinesInfo.map(info => this.editor.document.lineAt(info.afterLine))
const oldLines = addedLinesInfo
.filter(info => info.afterLine < this.editor.document.lineCount)
.map(info => this.editor.document.lineAt(info.afterLine))
const replacerCol = Math.max(...oldLines.map(line => line.range.end.character))
const startLine = Math.min(...oldLines.map(line => line.lineNumber))
this.renderAddedLinesDecorations(addedLinesInfo, startLine, replacerCol)

return {
removedRangesInfo: removedRanges,
addedLinesInfo: {
addedLinesDecorationInfo: addedLinesInfo,
startLine,
replacerCol,
},
}
}

private renderAddedLinesDecorations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export class InlineDiffDecorator implements vscode.Disposable, AutoEditsDecorato
this.editor.setDecorations(this.addedTextDecorationType, added)
}

public canRenderDecoration(decorationInfo: DecorationInfo): boolean {
// Inline decorator can render any decoration, so it should always return true.
return true
}

/**
* Process modified lines to create decorations for inserted and deleted text within those lines.
*/
Expand Down
32 changes: 27 additions & 5 deletions vscode/src/autoedits/renderer/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
getLatestVisibilityContext,
isCompletionVisible,
} from '../../completions/is-completion-visible'
import { type AutoeditRequestID, autoeditAnalyticsLogger } from '../analytics-logger'
import {
type AutoeditRequestID,
autoeditAnalyticsLogger,
autoeditDiscardReason,
} from '../analytics-logger'
import { autoeditsProviderConfig } from '../autoedits-config'
import { autoeditsOutputChannelLogger } from '../output-channel-logger'
import type { CodeToReplaceData } from '../prompt/prompt-utils'
Expand Down Expand Up @@ -174,16 +178,34 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager
await this.rejectActiveEdit()

const request = autoeditAnalyticsLogger.getRequest(requestId)
if (!request) {
return
}
if (this.hasConflictingDecorations(request.document, request.codeToReplaceData.range)) {
autoeditAnalyticsLogger.markAsDiscarded({
requestId,
discardReason: autoeditDiscardReason.conflictingDecorationWithEdits,
})
return
}

this.decorator = this.createDecorator(vscode.window.activeTextEditor!)
if (
!request ||
this.hasConflictingDecorations(request.document, request.codeToReplaceData.range)
'decorationInfo' in request &&
request.decorationInfo &&
!this.decorator.canRenderDecoration(request.decorationInfo)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we untether this check from the decorator's instance so that we don't have to create and dispose of it later if we don't have enough lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah the flow doesn't look good that we only create decorator only to dispose it on some condition.
I though about it when first tried to implement this, As per the current implementation, the manager doesn't know the type of decorator it is using and canRenderDecoration should be specific to a decorator, for example image renderer should have issues which our default renderer has, so making it a instance specific method seemed like the right call.
Please let me know if there are any alternate implementation ideas, would be happy to switch to them :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Let's think more about it later when we have more clarity on the logic required to render suggestions differently (default/image/inline).

) {
// If the decorator cannot render the decoration properly, dispose of it and return early.
this.decorator.dispose()
this.decorator = null
autoeditAnalyticsLogger.markAsDiscarded({
requestId,
discardReason: autoeditDiscardReason.noEnoughLinesEditor,
})
return
}

this.activeRequestId = requestId
this.decorator = this.createDecorator(vscode.window.activeTextEditor!)

autoeditAnalyticsLogger.markAsSuggested(requestId)

// Clear any existing timeouts, only one suggestion can be shown at a time
Expand Down
Loading