diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..6b10e2d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ + + +1/11/24 - release + + +Added + +- delete the Void extension, and move entirely inside the VS Code codebase. + +- model selection + +- model fetching with .list() + + +- We switched from the MIT License to to the Apache 2.0 License. +- New diff algorithm for computing and streaming diffs. + +- Streaming a change doesn't jitter the syntax highlighter + +- Ctrl+K added! + + + diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index cf275f37..c27d0b2d 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -3,30 +3,119 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +class SurroundingsRemover { + readonly originalS: string + i: number + j: number -// modelWasTrainedOnFIM should be false here -export const extractCodeFromFIM = ({ text, midTag, modelWasTrainedOnFIM }: { text: string, midTag: string, modelWasTrainedOnFIM: false }) => { + // string is s[i...j] - /* desired matches -` -`` -``` -< -

-

 a
-
 a 
-
 a 
< -
 a 
a
a a + constructor(s: string) { + this.originalS = s + this.i = 0 + this.j = s.length - 1 + } + value() { + return this.originalS.substring(this.i, this.j + 1) + } -
 a 
 ->
-	*/
+	// returns whether it removed the whole prefix
+	removePrefix = (prefix: string): boolean => {
+		let offset = 0
+		console.log('prefix', prefix, Math.min(this.j, prefix.length - 1))
+		while (this.i <= this.j && offset <= prefix.length - 1) {
+			if (this.originalS.charAt(this.i) !== prefix.charAt(offset))
+				break
+			offset += 1
+			this.i += 1
+		}
+		return offset === prefix.length
+	}
 
+	// // removes suffix from right to left
+	removeSuffix = (suffix: string): boolean => {
+		// e.g. suffix = 
, the string is 
hi

= 1; len -= 1) { + if (s.endsWith(suffix.substring(0, len))) { // the end of the string equals a prefix + this.j -= len + return len === suffix.length + } + } + return false + } + // removeSuffix = (suffix: string): boolean => { + // let offset = 0 + + // while (this.j >= Math.max(this.i, 0)) { + // if (this.originalS.charAt(this.j) !== suffix.charAt(suffix.length - 1 - offset)) + // break + // offset += 1 + // this.j -= 1 + // } + // return offset === suffix.length + // } + + removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => { + const index = this.originalS.indexOf(until, this.i) + + if (index === -1) { + this.i = this.j + 1 + return false + } + console.log('index', index, until.length) + + if (alsoRemoveUntilStr) + this.i = index + until.length + else + this.i = index + + return true + } + + + removeCodeBlock = () => { + const pm = this + const foundCodeBlock = pm.removePrefix('```') + console.log('A', this.i, this.j) + if (!foundCodeBlock) return false + + pm.removeFromStartUntil('\n', true) // language + console.log('B', this.i, this.j) + + const foundCodeBlockEnd = pm.removeSuffix('```') + if (!foundCodeBlockEnd) return false + + console.log('C', this.i, this.j) + pm.removeSuffix('\n') + return true + } + + +} + + + +export const extractCodeFromRegular = (text: string): string => { + // Match either: + // 1. ```language\n``` + // 2. `````` + + const pm = new SurroundingsRemover(text) + + pm.removeCodeBlock() + + const s = pm.value() + return s +} + + + + + +// Ollama has its own FIM, we should not use this if we use that +export const extractCodeFromFIM = ({ text, midTag }: { text: string, midTag: string }): string => { /* ------------- summary of the regex ------------- [optional ` | `` | ```] @@ -38,35 +127,40 @@ export const extractCodeFromFIM = ({ text, midTag, modelWasTrainedOnFIM }: { tex [optional ` | `` | ```] */ - // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?([\s\S]*?)(?:<\/MID>|`{1,3}|$)/; - const regex = new RegExp( - `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:|\`{1,3}|$)`, - '' - ); - const match = text.match(regex); - if (match) { - const [_, languageName, codeBetweenMidTags] = match; - return [languageName, codeBetweenMidTags] as const + const pm = new SurroundingsRemover(text) + + console.log('ORIGIINAL CODE', text) + + pm.removeCodeBlock() + + console.log('D', pm.i, pm.j) + + + const foundMid = pm.removePrefix(`<${midTag}>`) + console.log('E', midTag, pm.i, pm.j) + + if (foundMid) { + pm.removeSuffix(``) + console.log('F', pm.i, pm.j) - } else { - return [undefined, extractCodeFromRegular(text)] as const } + const s = pm.value() + return s + + + // // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?([\s\S]*?)(?:<\/MID>|`{1,3}|$)/; + // const regex = new RegExp( + // `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:|\`{1,3}|$)`, + // '' + // ); + // const match = text.match(regex); + // if (match) { + // const [_, languageName, codeBetweenMidTags] = match; + // return [languageName, codeBetweenMidTags] as const + + // } else { + // return [undefined, extractCodeFromRegular(text)] as const + // } } - - -export const extractCodeFromRegular = (result: string) => { - // Match either: - // 1. ```language\n``` - // 2. `````` - - const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/); - - if (!match) { - return result; - } - - // Return whichever group matched (non-empty) - return match[1] ?? match[2] ?? result; -} diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index c5207eb0..090de24a 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -13,7 +13,7 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit // import { throttle } from '../../../../base/common/decorators.js'; import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; -import { IRange } from '../../../../editor/common/core/range.js'; +import { IRange, Range } from '../../../../editor/common/core/range.js'; import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { Color, RGBA } from '../../../../base/common/color.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -29,7 +29,6 @@ import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; import { ctrlKStream_prefixAndSuffix, ctrlKStream_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage, defaultFimTags } from './prompt/prompts.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; -import { IPosition } from '../../../../editor/common/core/position.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -38,6 +37,9 @@ import { LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js' import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; +import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; +import { InlineDecorationType } from '../../../../editor/common/viewModel.js'; +import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -200,6 +202,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService, @IMetricsService private readonly _metricsService: IMetricsService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, ) { super(); @@ -451,7 +454,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const lines = redText.split('\n'); const lineTokens = lines.map(line => LineTokens.createFromTextAndMetadata([{ text: line, metadata: 0 }], this._langService.languageIdCodec)); const source = new LineSource(lineTokens, lines.map(() => null), false, false) - const result = renderLines(source, renderOptions, [], domNode); + const result = renderLines(source, renderOptions, [ + { // add dummy so it doesn't highlight in red + range: Range.lift({ startLineNumber: 1, startColumn: 1, endLineNumber: Number.MAX_SAFE_INTEGER, endColumn: Number.MAX_SAFE_INTEGER }), + inlineClassName: '', + type: InlineDecorationType.Regular + } + ], domNode); const viewZone: IViewZone = { // afterLineNumber: computedDiff.startLine - 1, @@ -514,8 +523,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } return model } - private _readURI(uri: URI): string | null { - return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + private _readURI(uri: URI, range?: IRange): string | null { + if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null } private _getNumLines(uri: URI): number | null { return this._getModel(uri)?.getLineCount() ?? null @@ -529,13 +539,19 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } weAreWriting = false - private _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { + private async _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { const model = this._getModel(uri) if (!model) return + const uriStr = this._readURI(uri, range) + if (!uriStr) return - this.weAreWriting = true - model.applyEdits([{ range, text }]) // applies edits without adding them to undo/redo stack - this.weAreWriting = false + // minimal edits so not so flashy + const edits = await this._editorWorkerService.computeMoreMinimalEdits(uri, [{ range, text }]) + if (edits) { + this.weAreWriting = true + model.applyEdits(edits) + this.weAreWriting = false + } this._onInternalChangeContent(uri, { shouldRealign: shouldRealignDiffAreas && { newText: text, oldRange: range } }) } @@ -813,7 +829,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeDiffZoneLLMText(diffZone: DiffZone, llmText: string, latestCurrentFileEnd: IPosition, newPosition: IPosition) { + private _writeDiffZoneLLMText(diffZone: DiffZone, llmText: string) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out @@ -1055,8 +1071,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const { onFinishEdit } = this._addToHistory(uri) - // __TODO__ ctrl+K should use Ollama's FIM method. Also, modelWasTrainedOnFIM should not be a thing - const modelWasTrainedOnFIM = featureName === 'Ctrl+K' ? false : false + // __TODO__ ctrl+K should use Ollama's FIM method. + const ollamaStyleFIM = false const modelFimTags = defaultFimTags const adding: Omit = { @@ -1087,7 +1103,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } else if (featureName === 'Ctrl+K') { const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, modelWasTrainedOnFIM, fimTags: modelFimTags, uri }) + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, ollamaStyleFIM, fimTags: modelFimTags, language }) console.log('PREFIX:\n', prefix) console.log('SUFFIX:\n', suffix) console.log('USER CONTENT:\n', userContent) @@ -1099,9 +1116,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } else { throw new Error(`featureName ${featureName} is invalid`) } - // __TODO__ make these only move forward - const latestCurrentFileEnd: IPosition = { lineNumber: 1, column: 1 } - const latestOriginalFileStart: IPosition = { lineNumber: 1, column: 1 } const onDone = () => { diffZone._streamState = { isStreaming: false, } @@ -1121,8 +1135,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const extractText = (fullText: string) => { if (featureName === 'Ctrl+K') { - const [_, textSoFar] = extractCodeFromFIM({ text: fullText, midTag: modelFimTags.midTag, modelWasTrainedOnFIM }) - return textSoFar + if (ollamaStyleFIM) return fullText + return extractCodeFromFIM({ text: fullText, midTag: modelFimTags.midTag }) } else if (featureName === 'Ctrl+L') { return extractCodeFromRegular(fullText) @@ -1135,7 +1149,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { logging: { loggingName: `startApplying - ${featureName}` }, messages, onText: ({ newText, fullText }) => { - this._writeDiffZoneLLMText(diffZone, extractText(fullText), latestCurrentFileEnd, latestOriginalFileStart) + this._writeDiffZoneLLMText(diffZone, extractText(fullText)) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: ({ fullText }) => { diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index bc42b581..ca692569 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -341,12 +341,13 @@ export const defaultFimTags: FimTagsType = { midTag: 'SELECTION', } -export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, modelWasTrainedOnFIM, fimTags, uri }: { selection: string, prefix: string, suffix: string, userMessage: string, modelWasTrainedOnFIM: boolean, fimTags: FimTagsType, uri: URI }) => { +export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, ollamaStyleFIM, language }: + { selection: string, prefix: string, suffix: string, userMessage: string, ollamaStyleFIM: boolean, fimTags: FimTagsType, language: string }) => { const { preTag, sufTag, midTag } = fimTags - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - if (modelWasTrainedOnFIM) { + + if (ollamaStyleFIM) { // const preTag = 'PRE' // const sufTag = 'SUF' // const midTag = 'MID' diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index eb5805a7..a18bcbce 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { ReactNode } from "react" +import { ReactNode } from 'react' import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 4147c225..c9fcb230 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -88,7 +88,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested? if (t.type === "code") { return } /> } diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 333451f4..8e1e9db4 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useId, useRef, useState } from 'react'; import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'; @@ -12,9 +12,7 @@ import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js' import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js' import { useAccessor } from './services.js'; -import { ScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; -import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'; -import { createPortal } from 'react-dom'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; // type guard @@ -327,6 +325,7 @@ export const VoidCustomSelectBox = ({ {/* Select Button */}