diff --git a/packages/host/app/components/ai-assistant/code-block.gts b/packages/host/app/components/ai-assistant/code-block.gts index 93ca42bc76..394a8cc544 100644 --- a/packages/host/app/components/ai-assistant/code-block.gts +++ b/packages/host/app/components/ai-assistant/code-block.gts @@ -22,7 +22,10 @@ import { MonacoSDK } from '@cardstack/host/services/monaco-service'; import ApplyButton from '../ai-assistant/apply-button'; +import { CodeData } from './formatted-message'; + import type { ComponentLike } from '@glint/template'; +import type * as _MonacoSDK from 'monaco-editor'; interface CopyCodeButtonSignature { Args: { code?: string; @@ -31,14 +34,14 @@ interface CopyCodeButtonSignature { interface ApplyCodePatchButtonSignature { Args: { - codePatch: string; - fileUrl: string; + codePatch?: string | null; + fileUrl?: string | null; }; } interface CodeBlockActionsSignature { Args: { - code?: string; + codeData?: Partial; }; Blocks: { default: [ @@ -58,8 +61,7 @@ interface CodeBlockEditorSignature { interface Signature { Args: { monacoSDK: MonacoSDK; - code: string; - language: string; + codeData: Partial; }; Blocks: { default: [ @@ -72,41 +74,67 @@ interface Signature { Element: HTMLElement; } -import type * as _MonacoSDK from 'monaco-editor'; - -export default class CodeBlock extends Component { - @tracked copyCodeButtonText: 'Copy' | 'Copied!' = 'Copy'; - - copyCode = restartableTask(async (code: string) => { - this.copyCodeButtonText = 'Copied!'; - await navigator.clipboard.writeText(code); - await timeout(1000); - this.copyCodeButtonText = 'Copy'; - }); +let CodeBlockComponent: TemplateOnlyComponent = ; - -} +export default CodeBlockComponent; interface MonacoEditorSignature { Args: { Named: { - code: string; - language: string; + codeData: Partial; monacoSDK: MonacoSDK; editorDisplayOptions: MonacoEditorOptions; }; }; } +function applyCodeDiffDecorations( + editor: _MonacoSDK.editor.IStandaloneCodeEditor, + monacoSDK: MonacoSDK, + codeData: Partial, +) { + if (codeData.searchStartLine && codeData.searchEndLine) { + editor.deltaDecorations( + [], + [ + { + range: new monacoSDK.Range( + codeData.searchStartLine, + 0, + codeData.searchEndLine, + 1000, // Arbitrary large number to ensure the decoration spans the entire line + ), + options: { inlineClassName: 'line-to-be-replaced' }, + }, + ], + ); + } + + if (codeData.replaceStartLine && codeData.replaceEndLine) { + editor.deltaDecorations( + [], + [ + { + range: new monacoSDK.Range( + codeData.replaceStartLine, + 0, + codeData.replaceEndLine, + 1000, // Arbitrary large number to ensure the decoration spans the entire line + ), + options: { inlineClassName: 'line-to-be-replaced-with' }, + }, + ], + ); + } +} + class MonacoEditor extends Modifier { private monacoState: { editor: _MonacoSDK.editor.IStandaloneCodeEditor; @@ -115,12 +143,15 @@ class MonacoEditor extends Modifier { element: HTMLElement, _positional: [], { - code, - language, + codeData, monacoSDK, editorDisplayOptions, }: MonacoEditorSignature['Args']['Named'], ) { + let { code, language } = codeData; + if (!code || !language) { + return; + } if (this.monacoState) { let { editor } = this.monacoState; let model = editor.getModel()!; @@ -131,37 +162,58 @@ class MonacoEditor extends Modifier { // then apply that delta to the model. Compared to calling setValue() // for every new value, this removes the need for re-tokenizing the code // which is expensive and produces visual annoyances such as flickering. - let currentCode = model.getValue(); - let newCode = code; - let codeDelta = newCode.slice(currentCode.length); - - let lineCount = model.getLineCount(); - let lastLineLength = model.getLineLength(lineCount); - - let range = { - startLineNumber: lineCount, - startColumn: lastLineLength + 1, - endLineNumber: lineCount, - endColumn: lastLineLength + 1, - }; - let editOperation = { - range: range, - text: codeDelta, - forceMoveMarkers: true, - }; - - let withDisabledReadOnly = (readOnlySetting: boolean, fn: () => void) => { - editor.updateOptions({ readOnly: false }); - fn(); - editor.updateOptions({ readOnly: readOnlySetting }); - }; + let currentCode = model.getValue(); + let newCode = code ?? ''; + + if (!newCode.startsWith(currentCode)) { + // This is a safety net for rare cases where the new code streamed in + // does not begin with the current code. This can happen when streaming + // in code with search/replace diff markers and the diff marker in chunk + // is incomplete, for example "<<<<<<< SEAR" instead of + // "<<<<<<< SEARCH". In this case the code diff parsing logic + // in parseCodeContent will not recognize the diff marker and it will + // display "<<<<<<< SEAR" for a brief moment in the editor, before getting + // a chunk with a complete diff marker. In this case we need to reset + // the data otherwise the appending delta will be incorrect and we'll + // see mangled code in the editor (syntax errors with incomplete diff markers). + model.setValue(newCode); + } else { + let codeDelta = newCode.slice(currentCode.length); + + let lineCount = model.getLineCount(); + let lastLineLength = model.getLineLength(lineCount); + + let range = { + startLineNumber: lineCount, + startColumn: lastLineLength + 1, + endLineNumber: lineCount, + endColumn: lastLineLength + 1, + }; + + let editOperation = { + range: range, + text: codeDelta, + forceMoveMarkers: true, + }; + + let withDisabledReadOnly = ( + readOnlySetting: boolean, + fn: () => void, + ) => { + editor.updateOptions({ readOnly: false }); + fn(); + editor.updateOptions({ readOnly: readOnlySetting }); + }; + + withDisabledReadOnly(!!editorDisplayOptions.readOnly, () => { + editor.executeEdits('append-source', [editOperation]); + }); - withDisabledReadOnly(!!editorDisplayOptions.readOnly, () => { - editor.executeEdits('append-source', [editOperation]); - }); + editor.revealLine(lineCount + 1); // Scroll to the end as the code streams - editor.revealLine(lineCount + 1); // Scroll to the end as the code streams + applyCodeDiffDecorations(editor, monacoSDK, codeData); + } } else { let monacoContainer = element; @@ -174,6 +226,7 @@ class MonacoEditor extends Modifier { monacoSDK.editor.setModelLanguage(model, language); model.setValue(code); + applyCodeDiffDecorations(editor, monacoSDK, codeData); this.monacoState = { editor, @@ -202,10 +255,17 @@ class CodeBlockEditor extends Component { enabled: false, }, readOnly: true, + automaticLayout: true, };