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

Block edit renderer #5988

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,11 @@
"command": "cody.experimental.suggest",
"key": "ctrl+shift+enter",
"when": "cody.activated"
},
{
"command": "cody.supersuggest.testExample",
"key": "ctrl+enter",
"when": "cody.activated"
}
],
"submenus": [
Expand Down
218 changes: 218 additions & 0 deletions vscode/src/autoedits/BlockRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import * as vscode from 'vscode'
import { GHOST_TEXT_COLOR } from '../commands/GhostHintDecorator'

export function registerTestRenderCommand(): vscode.Disposable {
return vscode.commands.registerCommand('cody.supersuggest.testExample', () => {
const editor = vscode.window.activeTextEditor
const document = editor?.document
if (!editor || !document) {
return
}
const selection = editor.selection
const offset = editor.document.offsetAt(selection.start)
const text = editor.document.getText()

// extract replace start line and end line, replacerText, and replacerCol
const ret = ((): [string, string, number, number] | undefined => {
const i = text.substring(0, offset).lastIndexOf('\n<<<<\n', offset)
if (i === -1) {
return undefined
}
const textToReplaceStart = i + '\n<<<<\n'.length

const j = text.indexOf('\n====\n', textToReplaceStart)
if (j === -1) {
return undefined
}
const textToReplaceEnd = j
const replacerTextStart = j + '\n====\n'.length

const k = text.indexOf('\n~~~~\n', textToReplaceEnd)
if (k === -1) {
return undefined
}
const replacerTextEnd = k

const metadataStart = k + '\n~~~~\n'.length
const l = text.indexOf('\n>>>>\n', replacerTextEnd)
if (l === -1) {
return undefined
}
const metadataEnd = l
const metadata = text.slice(metadataStart, metadataEnd)
const parsedMetadata = JSON.parse(metadata)

return [
text.slice(textToReplaceStart, textToReplaceEnd),
text.slice(replacerTextStart, replacerTextEnd),
replacerTextEnd + '\n~~~~\n'.length,
parsedMetadata.minReplacerCol ?? 20,
]
})()
if (!ret) {
return
}
const [textToReplace, replacerText, replacerBlockEnd, minReplacerCol] = ret

// Display decoration
const replaceStartOffset = text.indexOf(textToReplace, replacerBlockEnd)
if (replaceStartOffset === -1) {
console.error('Could not find replacement text')
return
}
const replaceEndOffset = replaceStartOffset + textToReplace.length
const replaceStartLine = editor.document.positionAt(replaceStartOffset).line
const replaceEndLine = editor.document.positionAt(replaceEndOffset).line
const dismissEdit = renderBlockEdit({
editor,
replaceStartLine,
replaceEndLine,
replacerText,
minReplacerCol,
})

const listener = vscode.window.onDidChangeTextEditorSelection(e => {
// TODO(beyang): check context
dismissEdit()
listener.dispose()
})
// TODO(beyang): handle escape and tab

// setTimeout(dismissEdit, 5_000)
})
}

// red with opacity 0.1
const toReplaceBackgroundColor = 'rgb(255, 100, 100, 0.1)'

// green with opacity 0.1
const replacerBackgroundColor = 'rgb(100, 255, 100, 0.1)'

const toReplaceDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: toReplaceBackgroundColor,
})

const replacerDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: 'red', // canary (shouldn't be visible)
before: {
backgroundColor: replacerBackgroundColor,
color: GHOST_TEXT_COLOR,
height: '100%',
},
})

const hideRemainderDecorationType = vscode.window.createTextEditorDecorationType({
opacity: '0',
})

interface BlockEdit {
editor: vscode.TextEditor
replaceStartLine: number
replaceEndLine: number
replacerText: string
minReplacerCol: number
}

// TODO(beyang): deal with tabs...
export function renderBlockEdit({
editor,
replaceStartLine,
replaceEndLine,
replacerText,
minReplacerCol,
}: BlockEdit): () => void {
const replaceDecorations: vscode.DecorationOptions[] = []
const hideRemainderDecorations: vscode.DecorationOptions[] = []

let replacerCol = minReplacerCol
if (editor.selection.end.character > minReplacerCol) {
replacerCol = editor.selection.end.character
}

for (let i = replaceStartLine; i < replaceEndLine; i++) {
const line = editor.document.lineAt(i)
if (line.range.end.character <= replacerCol) {
const options = {
// range: new vscode.Range(i, 0, i, Math.max(line.range.end.character - 1, 0)),
range: new vscode.Range(i, 0, i, line.range.end.character),
}
replaceDecorations.push(options)
} else {
replaceDecorations.push({
range: new vscode.Range(i, 0, i, replacerCol),
})
}
}
editor.setDecorations(toReplaceDecorationType, replaceDecorations)

const replacerDecorations: vscode.DecorationOptions[] = []
// TODO(beyang): handle when not enough remaining lines in the doc
for (let i = 0; i < replacerText.split('\n').length; i++) {
const j = i + replaceStartLine
const line = editor.document.lineAt(j)
if (line.range.end.character <= replacerCol) {
const replacerOptions: vscode.DecorationOptions = {
range: new vscode.Range(j, line.range.end.character, j, line.range.end.character),
renderOptions: {
before: {
contentText:
'\u00A0'.repeat(3) +
replaceLeadingChars(replacerText.split('\n')[i], ' ', '\u00A0'), // TODO(beyang): factor out
margin: `0 0 0 ${replacerCol - line.range.end.character}ch`,
},
},
}
replacerDecorations.push(replacerOptions)
} else {
const replacerOptions: vscode.DecorationOptions = {
range: new vscode.Range(j, replacerCol, j, replacerCol),
renderOptions: {
before: {
contentText:
'\u00A0' + replaceLeadingChars(replacerText.split('\n')[i], ' ', '\u00A0'), // TODO(beyang): factor out
},
},
}
replacerDecorations.push(replacerOptions)
}
}
editor.setDecorations(replacerDecorationType, replacerDecorations)

for (
let i = replaceStartLine;
i < Math.max(replaceEndLine, replaceStartLine + replacerText.split('\n').length);
i++
) {
const line = editor.document.lineAt(i)
if (line.range.end.character > replacerCol) {
const options = {
range: new vscode.Range(i, replacerCol, i, line.range.end.character),
}
hideRemainderDecorations.push(options)
}
}
editor.setDecorations(hideRemainderDecorationType, hideRemainderDecorations)

return () => {
editor.setDecorations(toReplaceDecorationType, [])
editor.setDecorations(replacerDecorationType, [])
editor.setDecorations(hideRemainderDecorationType, [])
}
}

/**
* Replaces leading occurrences of a character with another string
* @param str The input string to process
* @param oldS The character to replace
* @param newS The character/string to replace with
* @returns The string with leading characters replaced
*/
function replaceLeadingChars(str: string, oldS: string, newS: string): string {
for (let i = 0; i < str.length; i++) {
if (str[i] !== oldS) {
// a string that is `newS` repeated i times
return newS.repeat(i) + str.substring(i)
}
}
return str
}
15 changes: 7 additions & 8 deletions vscode/src/autoedits/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { structuredPatch } from 'diff'
import * as vscode from 'vscode'
import { createGitDiff } from '../../../lib/shared/src/editor/create-git-diff'
import { GHOST_TEXT_COLOR } from '../commands/GhostHintDecorator'
import { registerTestRenderCommand } from './BlockRenderer'
import type { AutoEditsProviderOptions } from './autoedits-provider'
import type { CodeToReplaceData } from './prompt-utils'

Expand Down Expand Up @@ -30,18 +31,16 @@ export class AutoEditsRenderer implements vscode.Disposable {
private activeProposedChange: ProposedChange | null = null

constructor() {
this.disposables.push(registerTestRenderCommand())

this.disposables.push(
vscode.commands.registerCommand(
'cody.supersuggest.accept',
() => this.acceptProposedChange(),
this.disposables
vscode.commands.registerCommand('cody.supersuggest.accept', () =>
this.acceptProposedChange()
)
)
this.disposables.push(
vscode.commands.registerCommand(
'cody.supersuggest.dismiss',
() => this.dismissProposedChange(),
this.disposables
vscode.commands.registerCommand('cody.supersuggest.dismiss', () =>
this.dismissProposedChange()
)
)
}
Expand Down
90 changes: 90 additions & 0 deletions vscode/webviews/chat/cells/contextCell/ContextCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,45 @@ import { Cell } from '../Cell'
import { NON_HUMAN_CELL_AVATAR_SIZE } from '../messageCell/assistant/AssistantMessageCell'
import styles from './ContextCell.module.css'

/*
<<<<
}> = memo(
({
contextItems,
contextAlternatives,
model,
isForFirstMessage,
className,
defaultOpen,
__storybook__initialOpen,
reSubmitWithChatIntent,
showSnippets = false,
isContextLoading,
onAddToFollowupChat,
onManuallyEditContext,
editContextText,
}) => {
====
}> = memo(
({
contextItems,
contextAlternatives,
model,
isForFirstMessage,
className,
defaultOpen,
__storybook__initialOpen,
reSubmitWithChatIntent,
isContextLoading,
onAddToFollowupChat,
onManuallyEditContext,
editContextText,
}) => {
~~~~
{ "minReplacerCol": 40 }
>>>>
*/

/**
* A component displaying the context for a human message.
*/
Expand Down Expand Up @@ -62,6 +101,36 @@ export const ContextCell: FunctionComponent<{
onManuallyEditContext,
editContextText,
}) => {
/*
<<<<
const [selectedAlternative, setSelectedAlternative] = useState<number | undefined>(undefined)
const incrementSelectedAlternative = useCallback(
(increment: number): void => {
if (!contextAlternatives) {
return
}
====
const [selectedAlternative, setSelectedAlternative] = useState<number | undefined>(undefined)
const incrementSelectedAlternative = useCallback(
(increment: number): void => {
if (!contextAlternatives) {
return
}
const basis = contextAlternatives.length + 1
const idx = selectedAlternative === undefined ? 0 : selectedAlternative + 1
const newIdx = (idx + increment + basis) % basis
setSelectedAlternative(newIdx - 1 < 0 ? undefined : newIdx - 1)
},
[contextAlternatives, selectedAlternative]
)
const nextSelectedAlternative = useCallback(
() => incrementSelectedAlternative(1),
[incrementSelectedAlternative]
)
~~~~
{ "minReplacerCol": 80 }
>>>>
*/
const [selectedAlternative, setSelectedAlternative] = useState<number | undefined>(undefined)
const incrementSelectedAlternative = useCallback(
(increment: number): void => {
Expand All @@ -75,6 +144,27 @@ export const ContextCell: FunctionComponent<{
},
[contextAlternatives, selectedAlternative]
)

/*
<<<<
const prevSelectedAlternative = useCallback(
() => incrementSelectedAlternative(-1),
[incrementSelectedAlternative]
)
====
const prevSelectedAlternative = useCallback(
() => incrementSelectedAlternative(-1),
[incrementSelectedAlternative]
)
const anotherOne = useCallback(
() => incrementSelectedAlternative(-1),
[incrementSelectedAlternative]
)
~~~~
{ "minReplacerCol": 60 }
>>>>
*/

const nextSelectedAlternative = useCallback(
() => incrementSelectedAlternative(1),
[incrementSelectedAlternative]
Expand Down
Loading