From 33909b31431c82df2cc2296521ec3fd3f87567bd Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 28 Dec 2024 20:03:47 -0500 Subject: [PATCH] add ctrl+K --- .../void/browser/inlineDiffsService.ts | 273 ++++++++++++------ .../contrib/void/browser/quickEditActions.ts | 26 +- .../react/src/ctrl-k-tsx/CtrlKChat.tsx | 35 ++- 3 files changed, 235 insertions(+), 99 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 51eaabdd..793e7f86 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -26,7 +26,6 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; -import { ServiceSendLLMFeatureParams } from '../../../../platform/void/common/llmMessageTypes.js'; import { IConsistentItemService } from './helperServices/consistentItemService.js'; import { inlineDiff_systemMessage } from './prompt/prompts.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; @@ -59,14 +58,22 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); // similar to ServiceLLM export type StartStreamingOpts = { featureName: 'Ctrl+K'; - diffareaid: string; // id of the CtrlK area + diffareaid: number; // id of the CtrlK area + userMessage?: undefined; } | { featureName: 'Ctrl+L'; + userMessage: string; } | { featureName: 'Autocomplete'; range: IRange; + userMessage: string; } +export type AddCtrlKOpts = { + startLine: number, + endLine: number, + uri: URI, +} @@ -95,7 +102,7 @@ type CommonZoneProps = { type CtrlKZone = { type: 'CtrlKZone'; originalCode?: undefined; - userText: string; + userText: string | null; } & CommonZoneProps @@ -103,7 +110,7 @@ type DiffZone = { type: 'DiffZone', originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea - _sweepState: { + _streamState: { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; @@ -147,7 +154,9 @@ type HistorySnapshot = { export interface IInlineDiffsService { readonly _serviceBrand: undefined; - startStreaming(opts: StartStreamingOpts, userMessage: string): Promise; + startStreaming(opts: StartStreamingOpts, userMessage: string): number | undefined; + interruptStreaming(diffareaid: number): void; + addCtrlKZone(opts: AddCtrlKOpts): number; } export const IInlineDiffsService = createDecorator('inlineDiffAreasService'); @@ -164,7 +173,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { - _diffareaidPool = 0 // each diffarea has an id _diffidPool = 0 // each diff has an id constructor( @@ -253,11 +261,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type === 'DiffZone') { // add sweep styles to the diffZone - if (diffArea._sweepState.isStreaming) { + if (diffArea._streamState.isStreaming) { // sweepLine ... sweepLine - const fn1 = this._addLineDecoration(model, diffArea._sweepState.line, diffArea._sweepState.line, 'void-sweepIdxBG') + const fn1 = this._addLineDecoration(model, diffArea._streamState.line, diffArea._streamState.line, 'void-sweepIdxBG') // sweepLine+1 ... endLine - const fn2 = this._addLineDecoration(model, diffArea._sweepState.line + 1, diffArea.endLine, 'void-sweepBG') + const fn2 = this._addLineDecoration(model, diffArea._streamState.line + 1, diffArea.endLine, 'void-sweepBG') diffArea._removeStylesFns.add(() => { fn1?.(); fn2?.(); }) } } @@ -273,7 +281,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // domNode.className = 'void-redBG' const viewZone: IViewZone = { // afterLineNumber: computedDiff.startLine - 1, - afterLineNumber: 1, + afterLineNumber: diffArea.startLine, + // __TODO__ heightInPx update dynamically heightInPx: 100, // heightInLines: 1, // minWidthInPx: 200, @@ -286,17 +295,22 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { editor.changeViewZones(accessor => { zoneId = accessor.addZone(viewZone) }) const fn1 = () => editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) + + // __TODO fix this resize part // on resize domNode.onresize = () => { + console.log('RESIZING!!!!') viewZone.heightInPx = domNode.clientHeight editor.changeViewZones(accessor => { if (zoneId) accessor.layoutZone(zoneId) }) } this._instantiationService.invokeFunction(accessor => { const props: QuickEditPropsType = { - quickEditId: diffArea.diffareaid, + diffareaid: diffArea.diffareaid, + onUserUpdateText(text) { diffArea.userText = text; }, } mountCtrlK(domNode, accessor, props) + // __TODO__ dismount }) return () => { fn1(); } @@ -410,7 +424,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { private _getNumLines(uri: URI): number | null { return this._getModel(uri)?.getLineCount() ?? null } - + private _getActiveEditorURI(): URI | null { + const editor = this._editorService.getActiveCodeEditor() + if (!editor) return null + const uri = editor.getModel()?.uri + if (!uri) return null + return uri + } _weAreWriting = false private _writeText(uri: URI, text: string, range: IRange) { @@ -452,9 +472,12 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // delete all current decorations (diffs, sweep styles) so we don't have any unwanted leftover decorations this._clearAllEffects(uri) - // __TODO__ stop streaming if currently streaming - - + // for each diffarea in this uri, stop streaming if currently streaming + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea.type === 'DiffZone') + this._stopIfStreaming(diffArea) + } // restore diffAreaOfId and diffAreasOfModelId this.diffAreaOfId = {} @@ -469,7 +492,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { type: 'DiffZone', _diffOfId: {}, _URI: uri, - _sweepState: { + _streamState: { isStreaming: false, line: null, } as const, @@ -560,6 +583,18 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { + private _diffareaidPool = 0 // each diffarea has an id + private _addDiffArea(diffArea: Omit): T { + const diffareaid = this._diffareaidPool++ + const diffArea2 = { ...diffArea, diffareaid } as T + this.diffAreasOfURI[diffArea2._URI.fsPath].add(diffareaid.toString()) + this.diffAreaOfId[diffareaid] = diffArea2 + return diffArea2 + } + + + + // changes the start/line locations of all DiffAreas on the page (adjust their start/end based on the change) based on the change that was recently made private _realignAllDiffAreasLines(uri: URI, text: string, recentChange: { startLineNumber: number; endLineNumber: number }) { @@ -683,7 +718,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const computedDiffs = findDiffs(diffArea.originalCode, llmText) // if not streaming, just write the new code - if (!diffArea._sweepState.isStreaming) { + if (!diffArea._streamState.isStreaming) { this._writeText(uri, llmText, { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed ) @@ -719,7 +754,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } } - diffArea._sweepState.line = newFileEndLine + diffArea._streamState.line = newFileEndLine // lines are 1-indexed const newFileTop = llmText.split('\n').slice(0, (newFileEndLine - 1)).join('\n') @@ -739,31 +774,77 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { + private _initializeStream(opts: StartStreamingOpts): DiffZone | undefined { - private _initializeStream(featureParams: ServiceSendLLMFeatureParams, diffRepr: string, uri: URI,): DiffZone | undefined { + const { featureName } = opts - // diff area begin and end line - const numLines = this._getNumLines(uri) - if (numLines === null) return + let startLine: number + let endLine: number + let uri: URI + let userMessage: string - const beginLine = 1 - const endLine = numLines + if (featureName === 'Ctrl+L') { - // check if there's overlap with any other diffAreas and return early if there is - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { - const da2 = this.diffAreaOfId[diffareaid] - if (!da2) continue - const noOverlap = da2.startLine > endLine || da2.endLine < beginLine - if (!noOverlap) { - // TODO add a message here that says this to the user too - console.error('Not diffing because found overlap:', this.diffAreasOfURI[uri.fsPath], beginLine, endLine) - return + const uri_ = this._getActiveEditorURI() + if (!uri_) return + uri = uri_ + + // __TODO__ reject all diffs in the diff area + // __TODO__ deselect user's cursor + + // in ctrl+L the start and end lines are the full document + const numLines = this._getNumLines(uri) + if (numLines === null) return + startLine = 1 + endLine = numLines + + // check if there's overlap with any other diffAreas and return early if there is + for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const da2 = this.diffAreaOfId[diffareaid] + if (!da2) continue + const noOverlap = da2.startLine > endLine || da2.endLine < startLine + if (!noOverlap) { + // TODO add a message here that says this to the user too + console.error('Not diffing because found overlap:', this.diffAreasOfURI[uri.fsPath], startLine, endLine) + return + } } + + userMessage = opts.userMessage } + else if (featureName === 'Ctrl+K') { + + const { diffareaid } = opts + + const ctrlKZone = this.diffAreaOfId[diffareaid] + const { startLine: startLine_, endLine: endLine_, _URI, userText } = ctrlKZone + uri = _URI + + startLine = startLine_ + endLine = endLine_ + + // check if there's overlap with any other ctrlKZones and if so, focus them + for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const da2 = this.diffAreaOfId[diffareaid] + if (!da2) continue + const noOverlap = da2.startLine > endLine || da2.endLine < startLine + if (!noOverlap) { + // __TODO__ focus it + return + } + } + + if (!userText) return + userMessage = userText + } + else { + throw new Error(`Void: diff.type not recognized on: ${featureName}`) + } + const currentFileStr = this._readURI(uri) if (currentFileStr === null) return - const originalCode = currentFileStr.split('\n').slice((beginLine - 1), (endLine - 1) + 1).join('\n') + const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') let streamRequestIdRef: { current: string | null } = { current: null } @@ -772,20 +853,24 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // add to history const { onFinishEdit } = this._addToHistory(uri) - // create a diffArea for the stream - const diffareaid = this._diffareaidPool++ - // in ctrl+L the start and end lines are the full document - const diffArea: DiffZone = { + // for Ctrl+K, delete the current ctrlKZone, swapping it out for a diffZone + if (featureName === 'Ctrl+K') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + this._deleteDiffArea(ctrlKZone) + } + + + + + const adding: Omit = { type: 'DiffZone', - diffareaid: diffareaid, - // originalStartLine: beginLine, - // originalEndLine: endLine, originalCode: originalCode, - startLine: beginLine, + startLine: startLine, endLine: endLine, // starts out the same as the current file _URI: uri, - _sweepState: { + _streamState: { isStreaming: true, streamRequestIdRef, line: 1, @@ -793,13 +878,10 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _diffOfId: {}, // added later _removeStylesFns: new Set(), } - - // console.log('adding uri.fspath', uri.fsPath, diffArea.diffareaid.toString()) - this.diffAreasOfURI[uri.fsPath].add(diffArea.diffareaid.toString()) - this.diffAreaOfId[diffArea.diffareaid] = diffArea + const diffZone = this._addDiffArea(adding) // actually call the LLM - const promptContent = `\ + const userContent = featureName === 'Ctrl+L' ? `\ ORIGINAL_CODE \`\`\` ${originalCode} @@ -807,33 +889,40 @@ ${originalCode} DIFF \`\`\` -${diffRepr} +${userMessage} \`\`\` INSTRUCTIONS Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation. +` + : `\ +CTRL K MESSAGE GOES HERE __TODO__! +${userMessage} ` + // __TODO__ make these only move forward + const latestCurrentFileEnd: IPosition = { lineNumber: 1, column: 1 } const latestOriginalFileStart: IPosition = { lineNumber: 1, column: 1 } streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + featureName, logging: { loggingName: 'streamChunk' }, messages: [ { role: 'system', content: inlineDiff_systemMessage, }, // TODO include more context too - { role: 'user', content: promptContent, } + { role: 'user', content: userContent, } ], onText: ({ newText, fullText }) => { - this._writeDiffZoneLLMText(diffArea, fullText, latestCurrentFileEnd, latestOriginalFileStart) + this._writeDiffZoneLLMText(diffZone, fullText, latestCurrentFileEnd, latestOriginalFileStart) this._refreshDiffsInURI(uri) }, onFinalMessage: ({ fullText }) => { this._writeText(uri, fullText, - { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed ) - diffArea._sweepState = { isStreaming: false, line: null } + diffZone._streamState = { isStreaming: false, line: null } this._refreshDiffsInURI(uri) onFinishEdit() }, @@ -843,71 +932,81 @@ Please finish writing the new file by applying the diff to the original file. Re if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) - diffArea._sweepState = { isStreaming: false, line: null } + diffZone._streamState = { isStreaming: false, line: null } onFinishEdit() }, - ...featureParams + + range: { startLineNumber: startLine, endLineNumber: endLine, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }, }) - - return diffArea + return diffZone } + public addCtrlKZone({ startLine, endLine, uri }: AddCtrlKOpts) { + + const { onFinishEdit } = this._addToHistory(uri) + + const adding: Omit = { + type: 'CtrlKZone', + startLine: startLine, + endLine: endLine, + _URI: uri, + userText: null, + _removeStylesFns: new Set(), + } + const ctrlKZone = this._addDiffArea(adding) + + onFinishEdit() + + this._refreshDiffsInURI(uri) + return ctrlKZone.diffareaid + } - async startStreaming(opts: StartStreamingOpts, userMessage: string) { - const editor = this._editorService.getActiveCodeEditor() - if (!editor) return - const uri = editor.getModel()?.uri - if (!uri) return - // TODO reject all diffs in the diff area - // TODO deselect user's cursor - // TODO convert opts to opts - const addedDiffZone = this._initializeStream(opts, userMessage, uri) + + public startStreaming(opts: StartStreamingOpts) { + const addedDiffZone = this._initializeStream(opts) return addedDiffZone?.diffareaid } - // private _stopIfStreaming(diffZone: DiffZone) { + private _stopIfStreaming(diffZone: DiffZone) { - // } + const streamRequestId = diffZone._streamState.streamRequestIdRef?.current + if (!streamRequestId) + return + + this._llmMessageService.abort(streamRequestId) + + diffZone._streamState = { + isStreaming: false, + streamRequestIdRef: undefined, + line: null + } + + } - - interruptStreaming(diffareaid: string) { + // call this outside undo/redo (it calls undo) + interruptStreaming(diffareaid: number) { const diffArea = this.diffAreaOfId[diffareaid] if (!diffArea) return if (diffArea.type !== 'DiffZone') return - if (!diffArea._sweepState.isStreaming) return - - const streamRequestId = diffArea._sweepState.streamRequestIdRef.current - if (streamRequestId) - this._llmMessageService.abort(streamRequestId) - - // __TODO__ update diffArea streamState here + don't elsewhere - // call undo - __TODO__ make this get called in undo and redo too + if (!diffArea._streamState.isStreaming) return + this._stopIfStreaming(diffArea) this._undoRedoService.undo(diffArea._URI) - } - addCtrlK({ uri, range }: { uri: URI, range: IRange, }) { - - // TODO check if intersects with a current ctrl K, if so focus it - - } - - - // called on void.acceptDiff diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index e3ea47c3..76a1e194 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -4,10 +4,12 @@ import { ServicesAccessor } from '../../../../platform/instantiation/common/inst import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IInlineDiffsService } from './inlineDiffsService.js'; export type QuickEditPropsType = { - quickEditId: number, + diffareaid: number, + onUserUpdateText: (text: string) => void; } export type QuickEdit = { @@ -21,13 +23,21 @@ export type QuickEdit = { export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' registerAction2(class extends Action2 { - constructor() { - super({ id: VOID_CTRL_K_ACTION_ID, title: 'Void: Quick Edit', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyK, weight: KeybindingWeight.BuiltinExtension } }); + constructor( + ) { + super({ + id: VOID_CTRL_K_ACTION_ID, + title: 'Void: Quick Edit', + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyK, + weight: KeybindingWeight.BuiltinExtension, + } + }); } + async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(ICodeEditorService) - const metricsService = accessor.get(IMetricsService) metricsService.capture('User Action', { type: 'Open Ctrl+K' }) @@ -38,11 +48,11 @@ registerAction2(class extends Action2 { const selection = editor.getSelection() if (!selection) return; - // const uri = model.uri - // const startLine = selection.startLineNumber - // const selectedText = model.getValueInRange(selection) - // quickEditService.addZone({ uri, startLine, selectedText, }) + const { startLineNumber: startLine, endLineNumber: endLine } = selection + const uri = model.uri + const inlineDiffsService = accessor.get(IInlineDiffsService) + inlineDiffsService.addCtrlKZone({ startLine, endLine, uri }) } }); diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx index e831ce6c..01a1a22b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx @@ -1,13 +1,18 @@ import React, { FormEvent, useCallback, useRef, useState } from 'react'; -import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState } from '../util/services.js'; +import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js'; import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js'; import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; import { getCmdKey } from '../../../helpers/getCmdKey.js'; import { VoidInputBox } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; -export const CtrlKChat = (props: QuickEditPropsType) => { +export const CtrlKChat = ({ diffareaid, onUserUpdateText }: QuickEditPropsType) => { + + const accessor = useAccessor() + + const inlineDiffsService = accessor.get('IInlineDiffsService') + const inputBoxRef: React.MutableRefObject = useRef(null); @@ -15,11 +20,24 @@ export const CtrlKChat = (props: QuickEditPropsType) => { // state of current message const [instructions, setInstructions] = useState('') // the user's instructions - const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions]) + const onChangeText = useCallback((newStr: string) => { + setInstructions(newStr) + onUserUpdateText(newStr) + }, [setInstructions]) const isDisabled = !instructions.trim() + const currentlyStreamingRef = useRef(undefined) + const onSubmit = useCallback((e: FormEvent) => { - // TODO + currentlyStreamingRef.current = inlineDiffsService.startStreaming({ + featureName: 'Ctrl+K', + diffareaid: diffareaid, + }, instructions) + }, [inlineDiffsService, diffareaid, instructions]) + + const onInterrupt = useCallback(() => { + if (currentlyStreamingRef.current !== undefined) + inlineDiffsService.interruptStreaming(currentlyStreamingRef.current) }, []) return
{ onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { onSubmit(e) + return } }} onSubmit={(e) => { + if (isDisabled) { + // __TODO__ show disabled + return + } console.log('submit!') onSubmit(e) }} @@ -63,6 +86,10 @@ export const CtrlKChat = (props: QuickEditPropsType) => { + +