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(autoedit): Support image rendering for complex diffs #6545

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
377 changes: 364 additions & 13 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions vscode/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ tsconfig.typehacks.json

# these are snapshots that haven't been accepted
*.new.json

# jest-image-snapshot debugging files
__diff_output__
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this generated by the auto-edit e2e tests ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's generated by the new unit tests, we're using jest-image-snapshot which lets us compare the canvas output and ensure we don't regress in the image rendering.

Images are produced like this and commited to the repo:
image

1 change: 1 addition & 0 deletions vscode/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
!dist/*.exe
!dist/*.wasm
!dist/*.node
!dist/*.ttf

!resources/*
!walkthroughs/*.md
Expand Down
11 changes: 8 additions & 3 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"description": "AI coding assistant that uses search & codebase context to help you write code faster. Cody brings you autocomplete, chat, and commands, so you can generate code, write unit tests, create docs, and explain complex code using AI. Choose from the best LLMs, including GPT-4o and Claude 3.5 Sonnet.",
"scripts": {
"build:root": "pnpm -C .. run -s build",
"postinstall": "pnpm download-wasm && pnpm copy-win-ca-roots",
"postinstall": "pnpm download-wasm && pnpm download-fonts && pnpm copy-win-ca-roots",
"dev": "pnpm run -s dev:desktop",
"dev:insiders": "pnpm run -s dev:desktop:insiders",
"start:dev:desktop": "NODE_ENV=development code --extensionDevelopmentPath=$PWD --disable-extension=sourcegraph.cody-ai --disable-extension=github.copilot --inspect-extensions=9333 --new-window . --goto ./src/completions/inline-completion-item-provider.ts:16:5",
Expand All @@ -30,16 +30,17 @@
"build:prod:webviews": "pnpm run -s _build:webviews --mode production",
"watch:build:dev:web": "concurrently \"pnpm run -s _build:esbuild:web --watch\" \"pnpm run -s _build:webviews --mode development --watch\"",
"watch:build:dev:desktop": "concurrently \"pnpm run -s _build:esbuild:desktop --watch\" \"pnpm run -s _build:webviews --mode development --watch\"",
"_build:esbuild:desktop": "pnpm download-wasm && pnpm run -s _build:esbuild:uninstall && pnpm run -s _build:esbuild:node",
"_build:esbuild:desktop": "pnpm download-wasm && pnpm download-fonts && pnpm run -s _build:esbuild:uninstall && pnpm run -s _build:esbuild:node",
"_build:esbuild:node": "esbuild ./src/extension.node.ts --bundle --outfile=dist/extension.node.js --loader:.node=copy --external:vscode --external:typescript --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:lexical=./build/lexical-package-fix --format=cjs --platform=node --sourcemap --target=es2022",
"_build:esbuild:web": "esbuild ./src/extension.web.ts --platform=browser --bundle --outfile=dist/extension.web.js --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:path=path-browserify --external:typescript --alias:node:path=path-browserify --alias:node:os=os-browserify --alias:os=os-browserify --external:vscode --external:node:child_process --external:node:util --external:node:fs --external:node:fs/promises --external:node:process --define:process='{\"env\":{}}' --define:window=self --format=cjs --sourcemap",
"_build:esbuild:uninstall": "node ./uninstall/esbuild.mjs",
"_build:webviews": "vite -c webviews/vite.config.mts build",
"_build:vsix_for_test": "vsce package --no-dependencies --out dist/cody.e2e.vsix",
"release": "ts-node-transpile-only ./scripts/release.ts",
"download-wasm": "ts-node-transpile-only ./scripts/download-wasm-modules.ts",
"download-fonts": "ts-node-transpile-only ./scripts/download-fonts.ts",
"copy-win-ca-roots": "ts-node-transpile-only ./scripts/copy-win-ca-roots.ts",
"release:dry-run": "pnpm run download-wasm && CODY_RELEASE_DRY_RUN=1 ts-node ./scripts/release.ts",
"release:dry-run": "pnpm run download-wasm && pnpm run download-fonts && CODY_RELEASE_DRY_RUN=1 ts-node ./scripts/release.ts",
"storybook": "storybook dev -p 6007 --no-open --no-version-updates",
"test:e2e": "playwright install && tsc --build && node dist/tsc/test/e2e/install-deps.js && pnpm run -s _build:vsix_for_test && pnpm run -s build:dev:desktop && pnpm run -s test:e2e:run",
"test:e2e:run": "playwright test",
Expand Down Expand Up @@ -1435,6 +1436,7 @@
"agent-base": "^7.1.1",
"async-mutex": "^0.4.0",
"axios": "^1.3.6",
"canvaskit-wasm": "^0.39.1",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
Expand Down Expand Up @@ -1471,6 +1473,7 @@
"remark-gfm": "^4.0.0",
"safe-stable-stringify": "^2.5.0",
"semver": "^7.5.4",
"shiki": "^1.26.1",
"signal-exit": "^4.1.0",
"socks-proxy-agent": "^8.0.1",
"tailwind-merge": "^2.3.0",
Expand Down Expand Up @@ -1508,6 +1511,7 @@
"@types/glob": "^8.0.0",
"@types/graceful-fs": "^4.1.9",
"@types/ini": "^4.1.0",
"@types/jest-image-snapshot": "^6.4.0",
"@types/js-levenshtein": "^1.1.1",
"@types/lodash": "^4.14.195",
"@types/marked": "^5.0.0",
Expand Down Expand Up @@ -1549,6 +1553,7 @@
"htmlnano": "^2.1.1",
"http-proxy-middleware": "^3.0.0",
"immer": "^10.1.1",
"jest-image-snapshot": "^6.4.0",
"keytar": "^7.9.0",
"kill-sync": "^1.0.3",
"mocha": "^10.2.0",
Expand Down
Binary file added vscode/resources/DejaVuSansMono.ttf
Binary file not shown.
27 changes: 27 additions & 0 deletions vscode/scripts/download-fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
import path from 'node:path'

const DIST_DIRECTORY = path.join(__dirname, '../dist')
const FONT_PATH = path.join(__dirname, '../resources/DejaVuSansMono.ttf')

export async function main(): Promise<void> {
try {
copyFonts()
console.log('Fonts were successfully copied to dist directory')
} catch (error) {
console.error('Error copying fonts:', error)
process.exit(1)
}
}

void main()

function copyFonts(): void {
const hasDistDir = existsSync(DIST_DIRECTORY)

if (!hasDistDir) {
mkdirSync(DIST_DIRECTORY)
}

copyFileSync(FONT_PATH, path.join(DIST_DIRECTORY, 'DejaVuSansMono.ttf'))
}
3 changes: 3 additions & 0 deletions vscode/scripts/download-wasm-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const TREE_SITTER_WASM_FILE = 'tree-sitter.wasm'
const TREE_SITTER_WASM_PATH = require.resolve(`web-tree-sitter/${TREE_SITTER_WASM_FILE}`)
const JS_GRAMMAR_PATH = require.resolve('@sourcegraph/tree-sitter-wasms/out/tree-sitter-javascript.wasm')
const GRAMMARS_PATH = path.dirname(JS_GRAMMAR_PATH)
const CANVASKIT_WASM_FILE = 'canvaskit.wasm'
const CANVASKIT_WASM_PATH = require.resolve(`canvaskit-wasm/bin/${CANVASKIT_WASM_FILE}`)

export async function main(): Promise<void> {
const hasStoreDir = existsSync(WASM_DIRECTORY)
Expand Down Expand Up @@ -50,4 +52,5 @@ function copyFilesToDistDir(): void {
}

copyFileSync(TREE_SITTER_WASM_PATH, path.join(DIST_DIRECTORY, TREE_SITTER_WASM_FILE))
copyFileSync(CANVASKIT_WASM_PATH, path.join(DIST_DIRECTORY, CANVASKIT_WASM_FILE))
}
4 changes: 2 additions & 2 deletions vscode/scripts/measure-bundle-size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { promises as fs } from 'node:fs'
import { appendFileSync } from 'node:fs'

const SIZE_LIMITS = {
extension: 15 * 1024 * 1024, // 15MB
webview: 10 * 1024 * 1024, // 10MB
extension: 20 * 1024 * 1024, // 20MB
webview: 15 * 1024 * 1024, // 15MB
}
Comment on lines 4 to 7
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Kinda annoying, but Shiki bumps us above these boundaries.

I'm not too worried about this, there's actually a lot more we could do to reduce our bundle size (e.g. properly minify the code), but we're not doing right now.

I did use the fine grained bundle to reduce this as much as possible, but alas still above the limits...

Copy link
Contributor

Choose a reason for hiding this comment

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

Since auto-edit only works for extensions, curious if we should avoid including shiki for webview, or does the dependencies have to be common between both extension and webview


function prettyPrintMB(bytes: number): string {
Expand Down
13 changes: 11 additions & 2 deletions vscode/src/autoedits/autoedits-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,15 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v
private readonly onSelectionChangeDebounced: DebouncedFunc<typeof this.onSelectionChange>
public readonly rendererManager: AutoEditsRendererManager
private readonly modelAdapter: AutoeditsModelAdapter

/**
* Default: Current supported renderer
* Inline: Experimental renderer that uses inline decorations to show additions
* Image: Experimental renderer that uses images to show additions.
*/
private readonly enabledRenderer = vscode.workspace
.getConfiguration()
.get<'default' | 'inline'>('cody.experimental.autoedit.renderer', 'default')
.get<'default' | 'inline' | 'image'>('cody.experimental.autoedit.renderer', 'default')

private readonly promptStrategy = new ShortTermPromptStrategy()
public readonly filterPrediction = new FilterPredictionBasedOnRecentEdits()
Expand All @@ -108,7 +114,10 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v
fixupController
)
: new AutoEditsDefaultRendererManager(
(editor: vscode.TextEditor) => new DefaultDecorator(editor),
(editor: vscode.TextEditor) =>
new DefaultDecorator(editor, {
imageRendering: this.enabledRenderer === 'image',
}),
fixupController
)

Expand Down
8 changes: 6 additions & 2 deletions vscode/src/autoedits/renderer/decorators/blockify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ function blockifyAndExtractForTest(
expect(highlightedText).toBe(expectedText)

addedLines.push({
ranges: [[range.start.character, range.end.character]],
highlightedRanges: [
{ type: 'diff-added', range: [range.start.character, range.end.character] },
],
afterLine: range.start.line,
lineText: document.lineAt(range.start.line).text,
})
Expand All @@ -30,7 +32,9 @@ function blockifyAndExtractForTest(
const blockified = blockify(document, addedLines)
return {
code: blockified.map(({ lineText }) => lineText).join('\n'),
ranges: blockified.flatMap(({ ranges }) => ranges),
ranges: blockified.flatMap(({ highlightedRanges }) =>
highlightedRanges.map(({ range }) => range)
),
}
}

Expand Down
18 changes: 10 additions & 8 deletions vscode/src/autoedits/renderer/decorators/blockify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,21 @@ export function convertToSpaceIndentation(
// this as we are converting this text to use spaces.
// 1. Account for the fact that each tab is being replaced with tabSize spaces
// 2. Adjust the position based on how many tabs appear before the range
const newRanges = line.ranges.map(([start, end]) => {
const newRanges = line.highlightedRanges.map(({ range: [start, end], ...rest }) => {
// Count tabs before the start and end positions
const tabsBeforeStart = (line.lineText.slice(0, start).match(/\t/g) || []).length
const tabsBeforeEnd = (line.lineText.slice(0, end).match(/\t/g) || []).length

// Each tab expands to tabSize spaces, so we need to add (tabSize - 1) for each tab
const adjustedStart = start + tabsBeforeStart * (tabSize - 1)
const adjustedEnd = end + tabsBeforeEnd * (tabSize - 1)

return [adjustedStart, adjustedEnd] as [number, number]
return { ...rest, range: [adjustedStart, adjustedEnd] as [number, number] }
})

return {
...line,
lineText: newLineText,
ranges: newRanges,
highlightedRanges: newRanges,
}
})
}
Expand Down Expand Up @@ -110,10 +109,13 @@ function removeLeadingWhitespaceBlock(
return addedLines.map(line => ({
...line,
lineText: line.lineText.replace(leastCommonWhitespacePrefix, ''),
ranges: line.ranges.map(([start, end]) => [
start - leastCommonWhitespacePrefix.length,
end - leastCommonWhitespacePrefix.length,
]),
highlightedRanges: line.highlightedRanges.map(({ range: [start, end], ...rest }) => ({
...rest,
range: [
Math.max(0, start - leastCommonWhitespacePrefix.length),
Math.max(0, end - leastCommonWhitespacePrefix.length),
],
})),
}))
}

Expand Down
Loading
Loading