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-edit): Support unified diff and refactor diff format #7000

Merged
merged 21 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion vscode/src/autoedits/renderer/decorators/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface AddedLineInfo {
modifiedLineNumber: number
}

interface RemovedLineInfo {
export interface RemovedLineInfo {
id: string
type: 'removed'
text: string
Expand Down
28 changes: 11 additions & 17 deletions vscode/src/autoedits/renderer/decorators/default-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as vscode from 'vscode'
import { GHOST_TEXT_COLOR } from '../../../commands/GhostHintDecorator'

import { getEditorInsertSpaces, getEditorTabSize } from '@sourcegraph/cody-shared'
import { isOnlyAddingTextForModifiedLines } from '../diff-utils'
import { generateSuggestionAsImage } from '../image-gen'
import type { AutoEditsDecorator, DecorationInfo, ModifiedLineInfo } from './base'
import { UNICODE_SPACE, blockify } from './blockify'
Expand Down Expand Up @@ -172,7 +173,7 @@ export class DefaultDecorator implements AutoEditsDecorator {

if (this.options.shouldRenderImage) {
this.renderAddedLinesImageDecorations(
addedLinesInfo.addedLinesDecorationInfo,
decorationInfo,
addedLinesInfo.startLine,
addedLinesInfo.replacerCol
)
Expand Down Expand Up @@ -367,15 +368,20 @@ export class DefaultDecorator implements AutoEditsDecorator {
}

private renderAddedLinesImageDecorations(
addedLinesInfo: AddedLinesDecorationInfo[],
decorationInfo: DecorationInfo,
startLine: number,
replacerCol: number
): void {
// Blockify the added lines so they are suitable to be rendered together as a VS Code decoration
const blockifiedAddedLines = blockify(this.editor.document, addedLinesInfo)
// TODO: Diff mode will likely change depending on the environment.
// This should be determined by client capabilities.
// VS Code: 'additions'
// Client capabiliies === image: 'unified'
const diffMode = 'additions'
const { dark, light, pixelRatio } = generateSuggestionAsImage({
decorations: blockifiedAddedLines,
decorations: decorationInfo,
lang: this.editor.document.languageId,
mode: diffMode,
document: this.editor.document,
})
const startLineEndColumn = this.getEndColumn(this.editor.document.lineAt(startLine))

Expand Down Expand Up @@ -457,15 +463,3 @@ export class DefaultDecorator implements AutoEditsDecorator {
}
}
}

/**
* Checks if the only changes for modified lines are additions of text.
*/
function isOnlyAddingTextForModifiedLines(modifiedLines: ModifiedLineInfo[]): boolean {
for (const modifiedLine of modifiedLines) {
if (modifiedLine.changes.some(change => change.type === 'delete')) {
return false
}
}
return true
}
12 changes: 12 additions & 0 deletions vscode/src/autoedits/renderer/diff-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,15 @@ export function getDecorationStats({
unchangedChars: charsStats.unchanged,
}
}

/**
* Checks if the only changes for modified lines are additions of text.
*/
export function isOnlyAddingTextForModifiedLines(modifiedLines: ModifiedLineInfo[]): boolean {
for (const modifiedLine of modifiedLines) {
if (modifiedLine.changes.some(change => change.type === 'delete')) {
return false
}
}
return true
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
177 changes: 177 additions & 0 deletions vscode/src/autoedits/renderer/image-gen/canvas/draw-decorations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type { EmulatedCanvas2D } from 'canvaskit-wasm'
import { canvasKit, fontCache } from '.'
import type { DiffMode } from '..'
import type { VisualDiff, VisualDiffLine } from '../decorated-diff/types'
import { DEFAULT_HIGHLIGHT_COLORS } from '../highlight/constants'
import type { SYNTAX_HIGHLIGHT_THEME } from '../highlight/types'
import { type RenderConfig, type UserProvidedRenderConfig, getRenderConfig } from './render-config'
import type { RenderContext } from './types'
import { getRangesToHighlight } from './utils'

function createCanvas(
options: {
width: number
height: number
fontSize: number
scale?: number
backgroundColor?: string
},
context: RenderContext
): {
canvas: EmulatedCanvas2D
ctx: CanvasRenderingContext2D
} {
const { width, height, fontSize, scale, backgroundColor } = options
const canvas = context.CanvasKit.MakeCanvas(width, height)
canvas.loadFont(context.font, { family: 'DejaVuSansMono' })
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('Failed to get 2D context')
}
ctx.font = `${fontSize}px DejaVuSansMono`
if (scale) {
ctx.scale(scale, scale)
}
if (backgroundColor) {
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, width, height)
}
return { canvas, ctx }
}

function drawText(
ctx: CanvasRenderingContext2D,
line: VisualDiffLine,
position: { x: number; y: number },
mode: SYNTAX_HIGHLIGHT_THEME,
config: RenderConfig
): number {
const highlights = line.syntaxHighlights[mode]

// Handle case with no syntax highlighting
if (highlights.length === 0) {
// No syntax highlighting, we probably don't support this language via Shiki
// Default to white or black depending on the theme
ctx.fillStyle = DEFAULT_HIGHLIGHT_COLORS[mode]
ctx.fillText(line.text, position.x, position.y + config.fontSize)
return ctx.measureText(line.text).width
}

// Draw highlighted text segments
let xPos = position.x
for (const { range, color } of highlights) {
const [start, end] = range
const content = line.text.substring(start, end)
ctx.fillStyle = color
ctx.fillText(content, xPos, position.y + config.fontSize)
xPos += ctx.measureText(content).width
}

return xPos
}

function drawDiffColors(
ctx: CanvasRenderingContext2D,
line: VisualDiffLine,
position: { x: number; y: number },
mode: DiffMode,
config: RenderConfig
): void {
const isRemoval = line.type === 'removed' || line.type === 'modified-removed'
const diffColors = isRemoval ? config.diffColors.removed : config.diffColors.inserted

// For unified diffs, we want to ensure that changed lines also have a background color
if (mode === 'unified' && line.type !== 'unchanged') {
const endOfLine = ctx.measureText(line.text).width
ctx.fillStyle = diffColors.line
ctx.fillRect(position.x, position.y, endOfLine, config.lineHeight)
}

// Get ranges to highlight based on line type
const ranges = getRangesToHighlight(line)
if (ranges.length === 0) {
return
}

// Draw highlights for each range
ctx.fillStyle = diffColors.text
for (const [start, end] of ranges) {
const preHighlightWidth = ctx.measureText(line.text.slice(0, start)).width
const highlightWidth = ctx.measureText(line.text.slice(start, end)).width
ctx.fillRect(position.x + preHighlightWidth, position.y, highlightWidth, config.lineHeight)
}
}

export function drawDecorationsToCanvas(
diff: VisualDiff,
theme: SYNTAX_HIGHLIGHT_THEME,
mode: DiffMode,
userConfig: UserProvidedRenderConfig
): EmulatedCanvas2D {
if (!canvasKit || !fontCache) {
// TODO: Log these errors, useful to see if we run into issues where we're not correctly
// initializing the canvas
throw new Error('Canvas not initialized')
Comment on lines +112 to +114
Copy link
Member

Choose a reason for hiding this comment

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

We should figure out why Sentry error reporting isn't working right now. It'd be helpful to check if this issue occurs on users' machines.

}

const context: RenderContext = {
CanvasKit: canvasKit,
font: fontCache,
}
const config = getRenderConfig(userConfig)

// In order for us to draw to the canvas, we must first determine the correct
// dimensions for the canvas. We can do this with a temporary Canvas that uses the same font
const { ctx: tempCtx } = createCanvas({ height: 10, width: 10, fontSize: config.fontSize }, context)

// Iterate through each token line, and determine the required width of the canvas (maximum line length)
// and the required height of the canvas (number of lines determined by their line height)
let tempYPos = config.padding.y
let requiredWidth = 0
for (const line of diff.lines) {
const measure = tempCtx.measureText(line.text)
requiredWidth = Math.max(requiredWidth, config.padding.x + measure.width)
tempYPos += config.lineHeight
}

// Note: We limit the canvas width to avoid the image getting excessively large.
// We should consider possible strategies here, such as tweaking this value or refusing
// to show image decorations for such large images. This could possibly be an area where we would
// prefer an inline decorator.
const canvasWidth = Math.min(requiredWidth + config.padding.x, config.maxWidth)
const canvasHeight = tempYPos + config.padding.y

// Round to the nearest pixel, using sub-pixels will cause CanvasKit to crash
const height = Math.round(canvasHeight * config.pixelRatio)
const width = Math.round(canvasWidth * config.pixelRatio)

// Now we create the actual canvas, ensuring we scale it accordingly to improve the output resolution.
const { canvas, ctx } = createCanvas(
{
height,
width,
fontSize: config.fontSize,
// We upscale the canvas to improve resolution, this will be brought back to the intended size
// using the `scale` CSS property when the decoration is rendered.
scale: config.pixelRatio,
backgroundColor: config.backgroundColor?.[theme],
},
context
)

// Paint text and colors onto the canvas
let yPos = config.padding.y
for (const line of diff.lines) {
const position = { x: config.padding.x, y: yPos }

// Paint any background diff colors first, we will render the text over the top
drawDiffColors(ctx, line, position, mode, config)

// Draw the text, this may or may not be syntax highlighted depending on language support
drawText(ctx, line, position, theme, config)

yPos += config.lineHeight
}

return canvas
}
Loading
Loading