> = {};
+
+ /**
+ * For each itemId, we store relevant info (the fn to call on the editor, the editorId, the uri, and the current dispose function).
+ */
+ private readonly itemInfoById: Record<
+ string,
+ {
+ editorId: string;
+ uriFsPath: string;
+ fn: (editor: ICodeEditor) => () => void;
+ disposeFn?: () => void;
+ }
+ > = {};
+
+ constructor(
+ @ICodeEditorService private readonly _editorService: ICodeEditorService,
+ ) {
+ super();
+
+ //
+ // Wire up listeners to watch for new editors, removed editors, etc.
+ //
+
+ // Initialize any already-existing editors
+ for (const editor of this._editorService.listCodeEditors()) {
+ this._initializeEditor(editor);
+ }
+
+ // When an editor is added, track it
+ this._register(
+ this._editorService.onCodeEditorAdd((editor) => {
+ this._initializeEditor(editor);
+ })
+ );
+
+ // When an editor is removed, remove all items associated with that editor
+ this._register(
+ this._editorService.onCodeEditorRemove((editor) => {
+ this._removeAllItemsFromEditor(editor);
+ })
+ );
+ }
+
+ /**
+ * Sets up listeners on the provided editor so that:
+ * - If the editor changes models, we remove items and re-mount only if the new model matches.
+ * - If the editor is disposed, we do the needed cleanup.
+ */
+ private _initializeEditor(editor: ICodeEditor) {
+ const editorId = editor.getId();
+
+ //
+ // Listen for model changes
+ //
+ this._register(
+ editor.onDidChangeModel((e) => {
+ this._removeAllItemsFromEditor(editor);
+ if (!e.newModelUrl) {
+ return;
+ }
+ // Re-mount any items that belong to this editor and match the new URI
+ const itemsForEditor = this.itemIdsByEditorId[editorId];
+ if (itemsForEditor) {
+ for (const itemId of itemsForEditor) {
+ const itemInfo = this.itemInfoById[itemId];
+ if (itemInfo && itemInfo.uriFsPath === e.newModelUrl.fsPath) {
+ this._mountItemOnEditor(editor, itemId);
+ }
+ }
+ }
+ })
+ );
+
+ //
+ // When the editor is disposed, remove all items from it
+ //
+ this._register(
+ editor.onDidDispose(() => {
+ this._removeAllItemsFromEditor(editor);
+ })
+ );
+
+ //
+ // If the editor already has a model (e.g. on initial load), try mounting items
+ //
+ const uri = editor.getModel()?.uri;
+ if (!uri) {
+ return;
+ }
+
+ const itemsForEditor = this.itemIdsByEditorId[editorId];
+ if (itemsForEditor) {
+ for (const itemId of itemsForEditor) {
+ const itemInfo = this.itemInfoById[itemId];
+ if (itemInfo && itemInfo.uriFsPath === uri.fsPath) {
+ this._mountItemOnEditor(editor, itemId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Actually calls the item-creation function `fn(editor)` and saves the resulting disposeFn
+ * so we can later clean it up.
+ */
+ private _mountItemOnEditor(editor: ICodeEditor, itemId: string) {
+ const info = this.itemInfoById[itemId];
+ if (!info) {
+ return;
+ }
+ const { fn } = info;
+ const disposeFn = fn(editor);
+ info.disposeFn = disposeFn;
+ }
+
+ /**
+ * Removes a single item from an editor (calling its `disposeFn` if present).
+ */
+ private _removeItemFromEditor(editor: ICodeEditor, itemId: string) {
+ const info = this.itemInfoById[itemId];
+ if (info?.disposeFn) {
+ info.disposeFn();
+ info.disposeFn = undefined;
+ }
+ }
+
+ /**
+ * Removes *all* items from the given editor. Typically called when the editor changes model or is disposed.
+ */
+ private _removeAllItemsFromEditor(editor: ICodeEditor) {
+ const editorId = editor.getId();
+ const itemsForEditor = this.itemIdsByEditorId[editorId];
+ if (!itemsForEditor) {
+ return;
+ }
+
+ for (const itemId of itemsForEditor) {
+ this._removeItemFromEditor(editor, itemId);
+ }
+ }
+
+ /**
+ * Public API: Adds an item to an *individual* editor (determined by editor ID),
+ * but only when that editor is showing the same model (uri.fsPath).
+ */
+ addToEditor(editor: ICodeEditor, fn: () => () => void): string {
+ const uri = editor.getModel()?.uri
+ if (!uri) {
+ throw new Error('No URI on the provided editor or in AddItemInputs.');
+ }
+
+ const editorId = editor.getId();
+
+ // Create an ID for this item
+ const itemId = generateUuid();
+
+ // Record the info
+ this.itemInfoById[itemId] = {
+ editorId,
+ uriFsPath: uri.fsPath,
+ fn,
+ };
+
+ // Add to the editor's known items
+ if (!this.itemIdsByEditorId[editorId]) {
+ this.itemIdsByEditorId[editorId] = new Set();
+ }
+ this.itemIdsByEditorId[editorId].add(itemId);
+
+ // If the editor's current URI matches, mount it now
+ if (editor.getModel()?.uri.fsPath === uri.fsPath) {
+ this._mountItemOnEditor(editor, itemId);
+ }
+
+ return itemId;
+ }
+
+ /**
+ * Public API: Removes an item from the *specific* editor. We look up which editor
+ * had this item and remove it from that editor.
+ */
+ removeFromEditor(itemId: string): void {
+ const info = this.itemInfoById[itemId];
+ if (!info) {
+ // Nothing to remove
+ return;
+ }
+
+ const { editorId } = info;
+
+ // Find the editor in question
+ const editor = this._editorService.listCodeEditors().find(
+ (ed) => ed.getId() === editorId
+ );
+ if (editor) {
+ // Dispose on that editor
+ this._removeItemFromEditor(editor, itemId);
+ }
+
+ // Clean up references
+ this.itemIdsByEditorId[editorId]?.delete(itemId);
+ delete this.itemInfoById[itemId];
+ }
+}
+
+registerSingleton(IConsistentEditorItemService, ConsistentEditorItemService, InstantiationType.Eager);
+
+
diff --git a/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts
new file mode 100644
index 00000000..b4b9d513
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts
@@ -0,0 +1,170 @@
+
+// eg "bash" -> "shell"
+export const nameToVscodeLanguage: { [key: string]: string } = {
+ // Web Technologies
+ 'html': 'html',
+ 'css': 'css',
+ 'scss': 'scss',
+ 'sass': 'scss',
+ 'less': 'less',
+ 'javascript': 'typescript',
+ 'js': 'typescript', // use more general renderer
+ 'jsx': 'typescript',
+ 'typescript': 'typescript',
+ 'ts': 'typescript',
+ 'tsx': 'typescript',
+ 'json': 'json',
+ 'jsonc': 'json',
+
+ // Programming Languages
+ 'python': 'python',
+ 'py': 'python',
+ 'java': 'java',
+ 'cpp': 'cpp',
+ 'c++': 'cpp',
+ 'c': 'c',
+ 'csharp': 'csharp',
+ 'cs': 'csharp',
+ 'c#': 'csharp',
+ 'go': 'go',
+ 'golang': 'go',
+ 'rust': 'rust',
+ 'rs': 'rust',
+ 'ruby': 'ruby',
+ 'rb': 'ruby',
+ 'php': 'php',
+ 'shell': 'shell',
+ 'bash': 'shell',
+ 'sh': 'shell',
+ 'zsh': 'shell',
+
+ // Markup and Config
+ 'markdown': 'markdown',
+ 'md': 'markdown',
+ 'xml': 'xml',
+ 'svg': 'xml',
+ 'yaml': 'yaml',
+ 'yml': 'yaml',
+ 'ini': 'ini',
+ 'toml': 'ini',
+
+ // Database and Query Languages
+ 'sql': 'sql',
+ 'mysql': 'sql',
+ 'postgresql': 'sql',
+ 'graphql': 'graphql',
+ 'gql': 'graphql',
+
+ // Others
+ 'dockerfile': 'dockerfile',
+ 'docker': 'dockerfile',
+ 'makefile': 'makefile',
+ 'plaintext': 'plaintext',
+ 'text': 'plaintext'
+};
+
+
+
+// eg ".ts" -> "typescript"
+const fileExtensionToVscodeLanguage: { [key: string]: string } = {
+ // Web
+ 'html': 'html',
+ 'htm': 'html',
+ 'css': 'css',
+ 'scss': 'scss',
+ 'less': 'less',
+ 'js': 'javascript',
+ 'jsx': 'javascript',
+ 'ts': 'typescript',
+ 'tsx': 'typescript',
+ 'json': 'json',
+ 'jsonc': 'json',
+
+ // Programming Languages
+ 'py': 'python',
+ 'java': 'java',
+ 'cpp': 'cpp',
+ 'cc': 'cpp',
+ 'c': 'c',
+ 'h': 'cpp',
+ 'hpp': 'cpp',
+ 'cs': 'csharp',
+ 'go': 'go',
+ 'rs': 'rust',
+ 'rb': 'ruby',
+ 'php': 'php',
+ 'sh': 'shell',
+ 'bash': 'shell',
+ 'zsh': 'shell',
+
+ // Markup/Config
+ 'md': 'markdown',
+ 'markdown': 'markdown',
+ 'xml': 'xml',
+ 'svg': 'xml',
+ 'yaml': 'yaml',
+ 'yml': 'yaml',
+ 'ini': 'ini',
+ 'toml': 'ini',
+
+ // Other
+ 'sql': 'sql',
+ 'graphql': 'graphql',
+ 'gql': 'graphql',
+ 'dockerfile': 'dockerfile',
+ 'docker': 'dockerfile',
+ 'mk': 'makefile',
+
+ // Config Files and Dot Files
+ 'npmrc': 'ini',
+ 'env': 'ini',
+ 'gitignore': 'ignore',
+ 'dockerignore': 'ignore',
+ 'eslintrc': 'json',
+ 'babelrc': 'json',
+ 'prettierrc': 'json',
+ 'stylelintrc': 'json',
+ 'editorconfig': 'ini',
+ 'htaccess': 'apacheconf',
+ 'conf': 'ini',
+ 'config': 'ini',
+
+ // Package Files
+ 'package': 'json',
+ 'package-lock': 'json',
+ 'gemfile': 'ruby',
+ 'podfile': 'ruby',
+ 'rakefile': 'ruby',
+
+ // Build Systems
+ 'cmake': 'cmake',
+ 'makefile': 'makefile',
+ 'gradle': 'groovy',
+
+ // Shell Scripts
+ 'bashrc': 'shell',
+ 'zshrc': 'shell',
+ 'fish': 'shell',
+
+ // Version Control
+ 'gitconfig': 'ini',
+ 'hgrc': 'ini',
+ 'svnconfig': 'ini',
+
+ // Web Server
+ 'nginx': 'nginx',
+
+ // Misc Config
+ 'properties': 'properties',
+ 'cfg': 'ini',
+ 'reg': 'ini'
+};
+
+
+export function filenameToVscodeLanguage(filename: string): string | undefined {
+
+ const ext = filename.toLowerCase().split('.').pop();
+ if (!ext) return undefined;
+
+ return fileExtensionToVscodeLanguage[ext];
+}
diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts
new file mode 100644
index 00000000..d7e109ae
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts
@@ -0,0 +1,169 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+class SurroundingsRemover {
+ readonly originalS: string
+ i: number
+ j: number
+
+ // string is s[i...j]
+
+ constructor(s: string) {
+ this.originalS = s
+ this.i = 0
+ this.j = s.length - 1
+ }
+ value() {
+ return this.originalS.substring(this.i, this.j + 1)
+ }
+
+ // 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('```')
+ if (!foundCodeBlock) return false
+
+ pm.removeFromStartUntil('\n', true) // language
+
+ const foundCodeBlockEnd = pm.removeSuffix('```')
+ if (!foundCodeBlockEnd) return false
+
+ pm.removeSuffix('\n')
+ return true
+ }
+
+
+ actualRecentlyAdded = (recentlyAddedTextLen: number) => {
+ // aaaaaatextaaaaaa{recentlyAdded}
+ // i ^ j
+ // |
+ // recentyAddedIdx
+ const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1
+ return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
+ }
+
+
+}
+
+
+
+export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => {
+ // Match either:
+ // 1. ```language\n```
+ // 2. ``````
+
+ const pm = new SurroundingsRemover(text)
+
+ pm.removeCodeBlock()
+
+ const s = pm.value()
+ const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
+
+ return [s, actual]
+}
+
+
+
+
+
+// Ollama has its own FIM, we should not use this if we use that
+export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => {
+
+ /* ------------- summary of the regex -------------
+ [optional ` | `` | ```]
+ (match optional_language_name)
+ [optional strings here]
+ [required tag]
+ (match the stuff between mid tags)
+ [optional tag]
+ [optional ` | `` | ```]
+ */
+
+ const pm = new SurroundingsRemover(text)
+
+ pm.removeCodeBlock()
+
+ const foundMid = pm.removePrefix(`<${midTag}>`)
+
+ if (foundMid) {
+ pm.removeSuffix(`${midTag}>`)
+ }
+ const s = pm.value()
+ const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
+
+ return [s, actual]
+
+
+ // // 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]*?)(?:${midTag}>|\`{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
+ // }
+
+}
+
diff --git a/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts
index 8586a3ca..c9235c14 100644
--- a/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts
+++ b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts
@@ -1,7 +1,7 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import { diffLines } from '../react/out/diff/index.js'
diff --git a/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts b/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts
deleted file mode 100644
index fe520e42..00000000
--- a/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
-
-import { isMacintosh } from '../../../../../base/common/platform.js';
-
-// import { OperatingSystem, OS } from '../../../../base/common/platform.js';
-// OS === OperatingSystem.Macintosh
-export function getCmdKey(): string {
- if (isMacintosh) {
- return '⌘';
- } else {
- return 'Ctrl';
- }
-}
-
-
-
-
diff --git a/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts b/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts
deleted file mode 100644
index 3c043344..00000000
--- a/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
-import { IModelService } from '../../../../../editor/common/services/model.js';
-import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
-import { IContextViewService, IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
-import { IFileService } from '../../../../../platform/files/common/files.js';
-import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
-import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
-import { ILLMMessageService } from '../../../../../platform/void/common/llmMessageService.js';
-import { IRefreshModelService } from '../../../../../platform/void/common/refreshModelService.js';
-import { IVoidSettingsService } from '../../../../../platform/void/common/voidSettingsService.js';
-import { IInlineDiffsService } from '../inlineDiffsService.js';
-import { ISidebarStateService } from '../sidebarStateService.js';
-import { IThreadHistoryService } from '../threadHistoryService.js';
-
-export type ReactServicesType = {
- sidebarStateService: ISidebarStateService;
- settingsStateService: IVoidSettingsService;
- threadsStateService: IThreadHistoryService;
- fileService: IFileService;
- modelService: IModelService;
- inlineDiffService: IInlineDiffsService;
- llmMessageService: ILLMMessageService;
- clipboardService: IClipboardService;
- refreshModelService: IRefreshModelService;
-
- themeService: IThemeService,
- hoverService: IHoverService,
-
- contextViewService: IContextViewService;
- contextMenuService: IContextMenuService;
-}
-
-
-export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => {
- return {
- settingsStateService: accessor.get(IVoidSettingsService),
- sidebarStateService: accessor.get(ISidebarStateService),
- threadsStateService: accessor.get(IThreadHistoryService),
- fileService: accessor.get(IFileService),
- modelService: accessor.get(IModelService),
- inlineDiffService: accessor.get(IInlineDiffsService),
- llmMessageService: accessor.get(ILLMMessageService),
- clipboardService: accessor.get(IClipboardService),
- themeService: accessor.get(IThemeService),
- hoverService: accessor.get(IHoverService),
- refreshModelService: accessor.get(IRefreshModelService),
- contextViewService: accessor.get(IContextViewService),
- contextMenuService: accessor.get(IContextMenuService),
- }
-}
-
diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts
new file mode 100644
index 00000000..60e5dc5c
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts
@@ -0,0 +1,10 @@
+import { URI } from '../../../../../base/common/uri'
+import { EndOfLinePreference } from '../../../../../editor/common/model'
+import { IModelService } from '../../../../../editor/common/services/model.js'
+
+// read files from VSCode
+export const VSReadFile = async (modelService: IModelService, uri: URI): Promise => {
+ const model = modelService.getModel(uri)
+ if (!model) return null
+ return model.getValue(EndOfLinePreference.LF)
+}
diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts
index 5d6c0c49..76bcbd8c 100644
--- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts
+++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts
@@ -1,118 +1,224 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
-import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
-import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/browser/editorBrowser.js';
+import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
+import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
// import { throttle } from '../../../../base/common/decorators.js';
-import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js';
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
-import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';
+import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
import { IRange } 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';
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js';
-import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
-import { LineTokens } from '../../../../editor/common/tokens/lineTokens.js';
-import { ILanguageService } from '../../../../editor/common/languages/language.js';
+import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
// import { IModelService } from '../../../../editor/common/services/model.js';
import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
-import { LLMFeatureSelection, ServiceSendLLMMessageParams } from '../../../../platform/void/common/llmMessageTypes.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 { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js'
+import { QuickEditPropsType } from './quickEditActions.js';
+import { errorDetails, 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 { filenameToVscodeLanguage } from './helpers/detectLanguage.js';
+import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
+import { isMacintosh } from '../../../../base/common/platform.js';
+import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
+import { Emitter } from '../../../../base/common/event.js';
+import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
+import { ICommandService } from '../../../../platform/commands/common/commands.js';
-// gets converted to --vscode-void-greenBG, see void.css
+const configOfBG = (color: Color) => {
+ return { dark: color, light: color, hcDark: color, hcLight: color, }
+}
+// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
const greenBG = new Color(new RGBA(155, 185, 85, .3)); // default is RGBA(155, 185, 85, .2)
-registerColor('void.greenBG', {
- dark: greenBG,
- light: greenBG, hcDark: null, hcLight: null
-}, '', true);
+registerColor('void.greenBG', configOfBG(greenBG), '', true);
const redBG = new Color(new RGBA(255, 0, 0, .3)); // default is RGBA(255, 0, 0, .2)
-registerColor('void.redBG', {
- dark: redBG,
- light: redBG, hcDark: null, hcLight: null
-}, '', true);
+registerColor('void.redBG', configOfBG(redBG), '', true);
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
-registerColor('void.sweepBG', {
- dark: sweepBG,
- light: sweepBG, hcDark: null, hcLight: null
-}, '', true);
+registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
+
+const highlightBG = new Color(new RGBA(100, 100, 100, .1));
+registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
-registerColor('void.sweepIdxBG', {
- dark: sweepIdxBG,
- light: sweepIdxBG, hcDark: null, hcLight: null
-}, '', true);
+registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
+const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
+
+ const model = editor.getModel();
+ if (!model) {
+ return 0;
+ }
+
+ // Get the line content, defaulting to empty string if line doesn't exist
+ const lineContent = model.getLineContent(startLine) || '';
+
+ // Find the first non-whitespace character
+ const firstNonWhitespaceIndex = lineContent.search(/\S/);
+
+ // Extract leading whitespace, handling case where line is all whitespace
+ const leadingWhitespace = firstNonWhitespaceIndex === -1
+ ? lineContent
+ : lineContent.slice(0, firstNonWhitespaceIndex);
+
+ // Get font information from editor render options
+ const { tabSize: numSpacesInTab } = model.getFormattingOptions();
+ const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth;
+ const tabWidth = numSpacesInTab * spaceWidth;
+
+ let paddingLeft = 0;
+ for (const char of leadingWhitespace) {
+ if (char === '\t') {
+ paddingLeft += tabWidth
+ } else if (char === ' ') {
+ paddingLeft += spaceWidth;
+ }
+ }
+
+ return paddingLeft;
+};
+
+// similar to ServiceLLM
+export type StartApplyingOpts = {
+ featureName: 'Ctrl+K';
+ diffareaid: number; // id of the CtrlK area (contains text selection)
+ userMessage: string; // user message
+} | {
+ featureName: 'Ctrl+L';
+ userMessage: string;
+} | {
+ featureName: 'Autocomplete';
+ range: IRange;
+ userMessage: string;
+}
+
+export type AddCtrlKOpts = {
+ startLine: number,
+ endLine: number,
+ editor: ICodeEditor,
+}
+
+// // TODO diffArea should be removed if we just discovered it has no more diffs in it
+// for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) {
+// const diffArea = this.diffAreaOfId[diffareaid]
+// if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) {
+// const { onFinishEdit } = this._addToHistory(uri)
+// this._deleteDiffArea(diffArea)
+// onFinishEdit()
+// }
+// }
+
+
export type Diff = {
diffid: number;
diffareaid: number; // the diff area this diff belongs to, "computed"
} & ComputedDiff
+
+
// _ means anything we don't include if we clone it
// DiffArea.originalStartLine is the line in originalCode (not the file)
-type DiffArea = {
+
+type CommonZoneProps = {
diffareaid: number;
- originalCode: string;
startLine: number;
endLine: number;
_URI: URI; // typically we get the URI from model
+
+ _removeStylesFns: Set; // these don't remove diffs or this diffArea, only their styles
+}
+
+type CtrlKZone = {
+ type: 'CtrlKZone';
+ originalCode?: undefined;
+
+ editorId: string; // the editor the input lives on
+
+ _mountInfo: null | {
+ textAreaRef: { current: HTMLTextAreaElement | null }
+ dispose: () => void;
+ refresh: () => void;
+ }
+
+ _linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
+
+} & CommonZoneProps
+
+
+type DiffZone = {
+ type: 'DiffZone',
+ originalCode: string;
_diffOfId: Record; // diffid -> diff in this DiffArea
-} & ({
- _sweepState: {
+ _streamState: {
isStreaming: true;
+ streamRequestIdRef: { current: string | null };
line: number;
} | {
isStreaming: false;
- line: null;
+ streamRequestIdRef?: undefined;
+ line?: undefined;
};
-})
+ editorId?: undefined;
+ linkedStreamingDiffZone?: undefined;
+} & CommonZoneProps
+
+
+
+// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
+type DiffArea = CtrlKZone | DiffZone
const diffAreaSnapshotKeys = [
+ 'type',
'diffareaid',
'originalCode',
'startLine',
'endLine',
+ 'editorId',
+
] as const satisfies (keyof DiffArea)[]
-type DiffAreaSnapshot = Pick
+type DiffAreaSnapshot = Pick
type HistorySnapshot = {
snapshottedDiffAreaOfId: Record;
entireFileCode: string;
-} &
- ({
- type: 'Ctrl+K';
- ctrlKText: string;
- } | {
- type: 'Ctrl+L';
- })
+}
export interface IInlineDiffsService {
readonly _serviceBrand: undefined;
- startStreaming(params: LLMFeatureSelection, str: string): void;
+ startApplying(opts: StartApplyingOpts): number | undefined;
+ interruptStreaming(diffareaid: number): void;
+ addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
+ removeCtrlKZone(opts: { diffareaid: number }): void;
+ // testDiffs(): void;
}
export const IInlineDiffsService = createDecorator('inlineDiffAreasService');
@@ -120,25 +226,18 @@ export const IInlineDiffsService = createDecorator('inlineD
class InlineDiffsService extends Disposable implements IInlineDiffsService {
_serviceBrand: undefined;
- // state of each document
- removeStylesFnsOfUri: Record> = {} // functions that remove the styles of this uri
+ // URI <--> model
diffAreasOfURI: Record> = {}
diffAreaOfId: Record = {};
diffOfId: Record = {}; // redundant with diffArea._diffs
- _diffareaidPool = 0 // each diffarea has an id
- _diffidPool = 0 // each diff has an id
- /*
- Picture of all the data structures:
- () -modelid-> {originalFileStr, Set(diffareaid), state}
- ^ |
- \________________ diffareaid -> diffarea -> diff[]
- ^ |
- \____ diff
- */
+ // only applies to diffZones
+ // streamingDiffZones: Set = new Set()
+ private readonly _onDidChangeStreaming = new Emitter<{ uri: URI; diffareaid: number }>();
+ private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>();
constructor(
@@ -146,8 +245,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
@ICodeEditorService private readonly _editorService: ICodeEditorService,
@IModelService private readonly _modelService: IModelService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
- @ILanguageService private readonly _langService: ILanguageService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
+ @IConsistentItemService private readonly _consistentItemService: IConsistentItemService,
+ @IInstantiationService private readonly _instantiationService: IInstantiationService,
+ @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService,
+ @IMetricsService private readonly _metricsService: IMetricsService,
+ @INotificationService private readonly _notificationService: INotificationService,
+ @ICommandService private readonly _commandService: ICommandService,
) {
super();
@@ -156,165 +260,411 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
if (!(model.uri.fsPath in this.diffAreasOfURI)) {
this.diffAreasOfURI[model.uri.fsPath] = new Set();
}
- if (!(model.uri.fsPath in this.removeStylesFnsOfUri)) {
- this.removeStylesFnsOfUri[model.uri.fsPath] = new Set();
- }
// when the user types, realign diff areas and re-render them
this._register(
model.onDidChangeContent(e => {
// it's as if we just called _write, now all we need to do is realign and refresh
- if (this._weAreWriting) return
+ if (this.weAreWriting) return
const uri = model.uri
- // realign
- for (const change of e.changes) { this._realignAllDiffAreasLines(uri, change.text, change.range) }
- // refresh
- this._refreshDiffsInURI(uri)
+ this._onUserChangeContent(uri, e)
})
)
- }
- // initialize all existing models
- for (let model of this._modelService.getModels()) { initializeModel(model) }
- // initialize whenever a new model mounts
- this._register(this._modelService.onModelAdded(model => initializeModel(model)));
+ // when a stream starts or ends
+ let removeAcceptRejectAllUI: (() => void) | null = null
+ const onChangeUriState = () => {
+ const uri = model.uri
+ const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()]
+ .map(diffareaid => this.diffAreaOfId[diffareaid])
+ .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone')
+ const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming)
+ if (diffZones.length !== 0 && !isStreaming && !removeAcceptRejectAllUI) {
+ removeAcceptRejectAllUI = this._addAcceptRejectUI(uri) ?? null
+ } else {
+ removeAcceptRejectAllUI?.()
+ removeAcceptRejectAllUI = null
+ }
+ }
+ this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() }))
+ this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() }))
+ }
+ // initialize all existing models + initialize when a new model mounts
+ for (let model of this._modelService.getModels()) { initializeModel(model) }
+ this._register(this._modelService.onModelAdded(model => initializeModel(model)));
// this function adds listeners to refresh styles when editor changes tab
let initializeEditor = (editor: ICodeEditor) => {
const uri = editor.getModel()?.uri ?? null
- if (uri) this._refreshDiffsInURI(uri)
-
- // called when the user switches tabs (typically there's only 1 editor on the screen, make sure you understand this)
- this._register(editor.onDidChangeModel((e) => {
- if (e.oldModelUrl) this._refreshDiffsInURI(e.oldModelUrl)
- if (e.newModelUrl) this._refreshDiffsInURI(e.newModelUrl)
- }))
+ if (uri) this._refreshStylesAndDiffsInURI(uri)
}
- // add listeners for all existing editors
+ // add listeners for all existing editors + listen for editor being added
for (let editor of this._editorService.listCodeEditors()) { initializeEditor(editor) }
- // add listeners when an editor is created
- this._register(this._editorService.onCodeEditorAdd(editor => { console.log('ADD EDITOR'); initializeEditor(editor) }))
- this._register(this._editorService.onCodeEditorRemove(editor => { console.log('REMOVE EDITOR'); initializeEditor(editor) }))
+ this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
}
+ private _onUserChangeContent(uri: URI, e: IModelContentChangedEvent) {
+ for (const change of e.changes) {
+ this._realignAllDiffAreasLines(uri, change.text, change.range)
+ }
+ this._refreshStylesAndDiffsInURI(uri)
+ }
+
+ private _onInternalChangeContent(uri: URI, { shouldRealign }: { shouldRealign: false | { newText: string, oldRange: IRange } }) {
+ if (shouldRealign) {
+ const { newText, oldRange } = shouldRealign
+ // console.log('realiging', newText, oldRange)
+ this._realignAllDiffAreasLines(uri, newText, oldRange)
+ }
+ this._refreshStylesAndDiffsInURI(uri)
+
+ }
-
-
-
-
-
-
-
- private _addSweepStylesToURI = (uri: URI, sweepLine: number, endLine: number) => {
-
- const decorationIds: (string | null)[] = []
-
- const model = this._getModel(uri)
+ // highlight the region
+ private _addLineDecoration = (model: ITextModel | null, startLine: number, endLine: number, className: string, options?: Partial) => {
if (model === null) return
+ const id = model.changeDecorations(accessor => accessor.addDecoration(
+ { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER },
+ {
+ className: className,
+ description: className,
+ isWholeLine: true,
+ ...options
+ }))
+ const disposeHighlight = () => {
+ if (id && !model.isDisposed()) model.changeDecorations(accessor => accessor.removeDecoration(id))
+ }
+ return disposeHighlight
+ }
- // sweepLine ... sweepLine
- decorationIds.push(
- model.changeDecorations(accessor => accessor.addDecoration(
- { startLineNumber: sweepLine, startColumn: 1, endLineNumber: sweepLine, endColumn: Number.MAX_SAFE_INTEGER },
- {
- className: 'void-sweepIdxBG',
- description: 'void-sweepIdxBG',
- isWholeLine: true
- }))
- )
- // sweepLine+1 ... endLine
- decorationIds.push(
- model.changeDecorations(accessor => accessor.addDecoration(
- { startLineNumber: sweepLine + 1, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER },
- {
- className: 'void-sweepBG',
- description: 'void-sweepBG',
- isWholeLine: true
- }))
- )
- const disposeSweepStyles = () => {
- for (const id of decorationIds) {
- if (id) model.changeDecorations(accessor => accessor.removeDecoration(id))
+ private _addDiffAreaStylesToURI = (uri: URI) => {
+ const model = this._getModel(uri)
+
+ for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+
+ if (diffArea.type === 'DiffZone') {
+ // add sweep styles to the diffZone
+ if (diffArea._streamState.isStreaming) {
+ // sweepLine ... sweepLine
+ const fn1 = this._addLineDecoration(model, diffArea._streamState.line, diffArea._streamState.line, 'void-sweepIdxBG')
+ // sweepLine+1 ... endLine
+ const fn2 = diffArea._streamState.line + 1 <= diffArea.endLine ?
+ this._addLineDecoration(model, diffArea._streamState.line + 1, diffArea.endLine, 'void-sweepBG')
+ : null
+ diffArea._removeStylesFns.add(() => { fn1?.(); fn2?.(); })
+
+ }
+ }
+
+ else if (diffArea.type === 'CtrlKZone' && diffArea._linkedStreamingDiffZone === null) {
+ // highlight zone's text
+ const fn = this._addLineDecoration(model, diffArea.startLine, diffArea.endLine, 'void-highlightBG')
+ diffArea._removeStylesFns.add(() => fn?.());
}
}
- return disposeSweepStyles
+ }
+
+
+ private _computeDiffsAndAddStylesToURI = (uri: URI) => {
+ const fullFileText = this._readURI(uri) ?? ''
+
+ for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+ if (diffArea.type !== 'DiffZone') continue
+
+ const newDiffAreaCode = fullFileText.split('\n').slice((diffArea.startLine - 1), (diffArea.endLine - 1) + 1).join('\n')
+ const computedDiffs = findDiffs(diffArea.originalCode, newDiffAreaCode)
+ for (let computedDiff of computedDiffs) {
+ if (computedDiff.type === 'deletion') {
+ computedDiff.startLine += diffArea.startLine - 1
+ }
+ if (computedDiff.type === 'edit' || computedDiff.type === 'insertion') {
+ computedDiff.startLine += diffArea.startLine - 1
+ computedDiff.endLine += diffArea.startLine - 1
+ }
+ this._addDiff(computedDiff, diffArea)
+ }
+
+ }
+ }
+
+ private _addAcceptRejectUI(uri: URI) {
+
+ // find all diffzones that aren't streaming
+ const diffZones: DiffZone[] = []
+ for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+ if (diffArea.type !== 'DiffZone') continue
+ if (diffArea._streamState.isStreaming) continue
+ diffZones.push(diffArea)
+ }
+ if (diffZones.length === 0) return
+
+ const consistentItemId = this._consistentItemService.addConsistentItemToURI({
+ uri,
+ fn: (editor) => {
+ const buttonsWidget = new AcceptAllRejectAllWidget({
+ editor,
+ onAcceptAll: () => {
+ this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
+ this._metricsService.capture('Accept All', {})
+ },
+ onRejectAll: () => {
+ this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
+ this._metricsService.capture('Reject All', {})
+ },
+ })
+ return () => { buttonsWidget.dispose() }
+ }
+ })
+
+
+ return () => { this._consistentItemService.removeConsistentItemFromURI(consistentItemId) }
+ }
+
+
+ mostRecentTextOfCtrlKZoneId: Record = {}
+ private _addCtrlKZoneInput = (ctrlKZone: CtrlKZone) => {
+
+ const { editorId } = ctrlKZone
+ const editor = this._editorService.listCodeEditors().find(e => e.getId() === editorId)
+ if (!editor) { return null }
+
+ let zoneId: string | null = null
+ let viewZone_: IViewZone | null = null
+ const textAreaRef: { current: HTMLTextAreaElement | null } = { current: null }
+
+
+ const paddingLeft = getLeadingWhitespacePx(editor, ctrlKZone.startLine)
+
+ const itemId = this._consistentEditorItemService.addToEditor(editor, () => {
+ const domNode = document.createElement('div');
+ domNode.style.zIndex = '1'
+ domNode.style.height = 'auto'
+ domNode.style.paddingLeft = `${paddingLeft}px`
+ const viewZone: IViewZone = {
+ afterLineNumber: ctrlKZone.startLine - 1,
+ domNode: domNode,
+ // heightInPx: 80,
+ suppressMouseDown: false,
+ showInHiddenAreas: true,
+ };
+ viewZone_ = viewZone
+
+ // mount zone
+ editor.changeViewZones(accessor => {
+ zoneId = accessor.addZone(viewZone)
+ })
+
+ // mount react
+ this._instantiationService.invokeFunction(accessor => {
+ mountCtrlK(domNode, accessor, {
+
+ diffareaid: ctrlKZone.diffareaid,
+ initStreamingDiffZoneId: ctrlKZone._linkedStreamingDiffZone,
+
+ textAreaRef: (r) => {
+ textAreaRef.current = r
+ if (!textAreaRef.current) return
+
+ if (!(ctrlKZone.diffareaid in this.mostRecentTextOfCtrlKZoneId)) { // detect first mount this way (a hack)
+ this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = undefined
+ setTimeout(() => textAreaRef.current?.focus(), 100)
+ }
+ },
+ onChangeHeight(height) {
+ if (height === 0) return // the viewZone sets this height to the container if it's out of view, ignore it
+ viewZone.heightInPx = height
+ // re-render with this new height
+ editor.changeViewZones(accessor => {
+ if (zoneId) accessor.layoutZone(zoneId)
+ })
+ },
+ onChangeText: (text) => {
+ this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text;
+ },
+ initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null,
+ } satisfies QuickEditPropsType)
+
+ })
+
+ return () => editor.changeViewZones(accessor => {
+ if (zoneId)
+ accessor.removeZone(zoneId)
+ })
+ })
+
+ return {
+ textAreaRef,
+ refresh: () => editor.changeViewZones(accessor => {
+ if (zoneId && viewZone_) {
+ viewZone_.afterLineNumber = ctrlKZone.startLine - 1
+ accessor.layoutZone(zoneId)
+ }
+ }),
+ dispose: () => {
+ this._consistentEditorItemService.removeFromEditor(itemId)
+ },
+ } satisfies CtrlKZone['_mountInfo']
}
+ private _refreshCtrlKInputs = async (uri: URI) => {
+ for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+ if (diffArea.type !== 'CtrlKZone') continue
+ if (!diffArea._mountInfo) {
+ diffArea._mountInfo = this._addCtrlKZoneInput(diffArea)
+ // console.log('MOUNTED', diffArea.diffareaid)
+ }
+ else {
+ diffArea._mountInfo.refresh()
+ }
+ }
+ }
- private _addDiffStylesToEditor = (editor: ICodeEditor, diff: Diff) => {
+
+ private _addDiffStylesToURI = (uri: URI, diff: Diff) => {
const { type, diffid } = diff
const disposeInThisEditorFns: (() => void)[] = []
- // green decoration and minimap decoration
- editor.changeDecorations(accessor => {
- if (type === 'deletion') return;
+ const model = this._modelService.getModel(uri)
- const greenRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed
- const decorationId = accessor.addDecoration(greenRange, {
- className: 'void-greenBG', // .monaco-editor .line-insert
- description: 'Void added this code',
- isWholeLine: true,
- minimap: {
- color: { id: 'minimapGutter.addedBackground' },
- position: 2
- },
- overviewRuler: {
- color: { id: 'editorOverviewRuler.addedForeground' },
- position: 7
- }
+ // green decoration and minimap decoration
+ if (type !== 'deletion') {
+ const fn = this._addLineDecoration(model, diff.startLine, diff.endLine, 'void-greenBG', {
+ minimap: { color: { id: 'minimapGutter.addedBackground' }, position: 2 },
+ overviewRuler: { color: { id: 'editorOverviewRuler.addedForeground' }, position: 7 }
})
- disposeInThisEditorFns.push(() => { editor.changeDecorations(accessor => { if (decorationId) accessor.removeDecoration(decorationId) }) })
- })
+ disposeInThisEditorFns.push(() => { fn?.() })
+ }
+
// red in a view zone
- editor.changeViewZones(accessor => {
- if (type === 'insertion') return;
+ if (type !== 'insertion') {
+ const consistentZoneId = this._consistentItemService.addConsistentItemToURI({
+ uri,
+ fn: (editor) => {
- const domNode = document.createElement('div');
- domNode.className = 'void-redBG'
+ const domNode = document.createElement('div');
+ domNode.className = 'void-redBG'
- const renderOptions = RenderOptions.fromEditor(editor);
- // applyFontInfo(domNode, renderOptions.fontInfo)
+ const renderOptions = RenderOptions.fromEditor(editor)
- // Compute view-lines based on redText
- const redText = diff.originalCode
- 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 processedText = diff.originalCode.replace(/\t/g, ' '.repeat(renderOptions.tabSize));
- const viewZone: IViewZone = {
- // afterLineNumber: computedDiff.startLine - 1,
- afterLineNumber: type === 'edit' ? diff.endLine : diff.startLine - 1,
- heightInLines: result.heightInLines,
- minWidthInPx: result.minWidthInPx,
- domNode: domNode,
- marginDomNode: document.createElement('div'), // displayed to left
- suppressMouseDown: true,
- };
+ const lines = processedText.split('\n');
- const zoneId = accessor.addZone(viewZone)
- disposeInThisEditorFns.push(() => { editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) })
+ const linesContainer = document.createElement('div');
+ linesContainer.style.fontFamily = renderOptions.fontInfo.fontFamily
+ linesContainer.style.fontSize = `${renderOptions.fontInfo.fontSize}px`
+ linesContainer.style.lineHeight = `${renderOptions.fontInfo.lineHeight}px`
+ // linesContainer.style.tabSize = `${tabWidth}px` // \t
+ linesContainer.style.whiteSpace = 'pre'
+ linesContainer.style.position = 'relative'
+ linesContainer.style.width = '100%'
- });
+ lines.forEach(line => {
+ // div for current line
+ const lineDiv = document.createElement('div');
+ lineDiv.className = 'view-line';
+ lineDiv.style.whiteSpace = 'pre'
+ lineDiv.style.position = 'relative'
+ lineDiv.style.height = `${renderOptions.fontInfo.lineHeight}px`
- // Accept | Reject widget
- const buttonsWidget = new AcceptRejectWidget({
- editor,
- onAccept: () => { this.acceptDiff({ diffid }) },
- onReject: () => { this.rejectDiff({ diffid }) },
- diffid: diffid.toString(),
- startLine: diff.startLine,
- })
- disposeInThisEditorFns.push(() => { buttonsWidget.dispose() })
+ // span (this is just how vscode does it)
+ const span = document.createElement('span');
+ span.textContent = line || '\u00a0';
+ span.style.whiteSpace = 'pre'
+ span.style.display = 'inline-block'
+
+ lineDiv.appendChild(span);
+ linesContainer.appendChild(lineDiv);
+ });
+
+ domNode.appendChild(linesContainer);
+
+ // Calculate height based on number of lines and line height
+ const heightInLines = lines.length;
+ const minWidthInPx = Math.max(...lines.map(line =>
+ Math.ceil(renderOptions.fontInfo.typicalFullwidthCharacterWidth * line.length)
+ ));
+
+ const viewZone: IViewZone = {
+ afterLineNumber: diff.startLine - 1,
+ heightInLines,
+ minWidthInPx,
+ domNode,
+ marginDomNode: document.createElement('div'),
+ suppressMouseDown: false,
+ showInHiddenAreas: false,
+ };
+
+ let zoneId: string | null = null
+ editor.changeViewZones(accessor => { zoneId = accessor.addZone(viewZone) })
+ return () => editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) })
+ },
+ })
+
+ disposeInThisEditorFns.push(() => { this._consistentItemService.removeConsistentItemFromURI(consistentZoneId) })
+
+ }
+
+
+
+ const diffZone = this.diffAreaOfId[diff.diffareaid]
+ if (diffZone.type === 'DiffZone' && !diffZone._streamState.isStreaming) {
+ // Accept | Reject widget
+ const consistentWidgetId = this._consistentItemService.addConsistentItemToURI({
+ uri,
+ fn: (editor) => {
+ let startLine: number
+ let offsetLines: number
+ if (diff.type === 'insertion' || diff.type === 'edit') {
+ startLine = diff.startLine // green start
+ offsetLines = 0
+ }
+ else if (diff.type === 'deletion') {
+ // if diff.startLine is out of bounds
+ if (diff.startLine === 1) {
+ const numRedLines = diff.originalEndLine - diff.originalStartLine + 1
+ startLine = diff.startLine
+ offsetLines = -numRedLines
+ }
+ else {
+ startLine = diff.startLine - 1
+ offsetLines = 1
+ }
+ }
+ else { throw 1 }
+
+ const buttonsWidget = new AcceptRejectWidget({
+ editor,
+ onAccept: () => {
+ this.acceptDiff({ diffid })
+ this._metricsService.capture('Accept Diff', {})
+ },
+ onReject: () => {
+ this.rejectDiff({ diffid })
+ this._metricsService.capture('Reject Diff', {})
+ },
+ diffid: diffid.toString(),
+ startLine,
+ offsetLines
+ })
+ return () => { buttonsWidget.dispose() }
+ }
+ })
+ disposeInThisEditorFns.push(() => { this._consistentItemService.removeConsistentItemFromURI(consistentWidgetId) })
+ }
const disposeInEditor = () => { disposeInThisEditorFns.forEach(f => f()) }
return disposeInEditor;
@@ -330,24 +680,46 @@ 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
}
+ 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) {
+ weAreWriting = false
+ 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 === null) return
- this._weAreWriting = true
- model.applyEdits([{ range, text }]) // applies edits without adding them to undo/redo stack
- this._weAreWriting = false
- this._realignAllDiffAreasLines(uri, text, range)
+ // heuristic check if don't need to make edits
+ const dontNeedToWrite = uriStr === text
+ if (dontNeedToWrite) {
+ // at the end of a write, we still expect to refresh all styles
+ // e.g. sometimes we expect to restore all the decorations even if no edits were made when _writeText is used
+ this._refreshStylesAndDiffsInURI(uri)
+ return
+ }
+
+ // minimal edits so not so flashy
+ // const edits = this.worker.$Void_computeMoreMinimalEdits(uri.toString(), [{ range, text }], false)
+ this.weAreWriting = true
+ model.applyEdits([{ range, text }])
+ this.weAreWriting = false
+
+ this._onInternalChangeContent(uri, { shouldRealign: shouldRealignDiffAreas && { newText: text, oldRange: range } })
+
}
@@ -356,11 +728,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
private _addToHistory(uri: URI) {
const getCurrentSnapshot = (): HistorySnapshot => {
- const diffAreaOfId = this.diffAreaOfId
-
const snapshottedDiffAreaOfId: Record = {}
- for (const diffareaid in diffAreaOfId) {
- const diffArea = diffAreaOfId[diffareaid]
+
+ for (const diffareaid in this.diffAreaOfId) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+
+ if (diffArea._URI.fsPath !== uri.fsPath) continue
+
snapshottedDiffAreaOfId[diffareaid] = structuredClone( // a structured clone must be on a JSON object
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
) as DiffAreaSnapshot
@@ -368,39 +742,61 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
return {
snapshottedDiffAreaOfId,
entireFileCode: this._readURI(uri) ?? '', // the whole file's code
- type: 'Ctrl+L',
}
}
const restoreDiffAreas = (snapshot: HistorySnapshot) => {
+
+ // 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)
+ }
+
+ // delete all diffareas on this uri (clearing their styles)
+ this._deleteAllDiffAreas(uri)
+ this.diffAreasOfURI[uri.fsPath].clear()
+
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = structuredClone(snapshot) // don't want to destroy the snapshot
- // delete all current decorations (diffs, sweep styles) so we don't have any unwanted leftover decorations
- this._clearAllDiffsAndStyles(uri)
-
// restore diffAreaOfId and diffAreasOfModelId
- this.diffAreaOfId = {}
- this.diffAreasOfURI[uri.fsPath].clear()
for (const diffareaid in snapshottedDiffAreaOfId) {
- this.diffAreaOfId[diffareaid] = {
- ...snapshottedDiffAreaOfId[diffareaid],
- _diffOfId: {},
- _URI: uri,
- _sweepState: {
- isStreaming: false,
- line: null,
- },
+
+ const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
+
+ if (snapshottedDiffArea.type === 'DiffZone') {
+ this.diffAreaOfId[diffareaid] = {
+ ...snapshottedDiffArea as DiffAreaSnapshot,
+ type: 'DiffZone',
+ _diffOfId: {},
+ _URI: uri,
+ _streamState: { isStreaming: false }, // when restoring, we will never be streaming
+ _removeStylesFns: new Set(),
+ }
+ }
+ else if (snapshottedDiffArea.type === 'CtrlKZone') {
+ this.diffAreaOfId[diffareaid] = {
+ ...snapshottedDiffArea as DiffAreaSnapshot,
+ _URI: uri,
+ _removeStylesFns: new Set(),
+ _mountInfo: null,
+ _linkedStreamingDiffZone: null, // when restoring, we will never be streaming
+ }
}
this.diffAreasOfURI[uri.fsPath].add(diffareaid)
}
+ this._onDidAddOrDeleteDiffZones.fire({ uri })
// restore file content
const numLines = this._getNumLines(uri)
if (numLines === null) return
- this._writeText(uri, entireModelCode, { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER })
- // restore all the decorations
- this._refreshDiffsInURI(uri)
+
+ this._writeText(uri, entireModelCode,
+ { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER },
+ { shouldRealignDiffAreas: false }
+ )
}
const beforeSnapshot: HistorySnapshot = getCurrentSnapshot()
@@ -424,37 +820,95 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// delete diffOfId and diffArea._diffOfId
private _deleteDiff(diff: Diff) {
const diffArea = this.diffAreaOfId[diff.diffareaid]
+ if (diffArea.type !== 'DiffZone') return
delete diffArea._diffOfId[diff.diffid]
delete this.diffOfId[diff.diffid]
}
- private _deleteDiffs(diffArea: DiffArea) {
- for (const diffid in diffArea._diffOfId) {
- const diff = diffArea._diffOfId[diffid]
+ private _deleteDiffs(diffZone: DiffZone) {
+ for (const diffid in diffZone._diffOfId) {
+ const diff = diffZone._diffOfId[diffid]
this._deleteDiff(diff)
}
}
- private _clearAllDiffsAndStyles(uri: URI) {
- for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) {
- const diffArea = this.diffAreaOfId[diffareaid]
+ private _clearAllDiffAreaEffects(diffArea: DiffArea) {
+ // clear diffZone effects (diffs)
+ if (diffArea.type === 'DiffZone')
this._deleteDiffs(diffArea)
- }
- for (const removeStyleFn of this.removeStylesFnsOfUri[uri.fsPath]) {
- removeStyleFn()
- }
- this.removeStylesFnsOfUri[uri.fsPath].clear()
+
+ diffArea._removeStylesFns.forEach(removeStyles => removeStyles())
+ diffArea._removeStylesFns.clear()
}
+ // clears all Diffs (and their styles) and all styles of DiffAreas, etc
+ private _clearAllEffects(uri: URI) {
+ for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+ this._clearAllDiffAreaEffects(diffArea)
+ }
+ }
+
// delete all diffs, update diffAreaOfId, update diffAreasOfModelId
- private _deleteDiffArea(diffArea: DiffArea) {
- this._deleteDiffs(diffArea)
- delete this.diffAreaOfId[diffArea.diffareaid]
- this.diffAreasOfURI[diffArea._URI.fsPath].delete(diffArea.diffareaid.toString())
+ private _deleteDiffZone(diffZone: DiffZone) {
+ this._clearAllDiffAreaEffects(diffZone)
+ delete this.diffAreaOfId[diffZone.diffareaid]
+ this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString())
+ this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI })
}
+ private _deleteCtrlKZone(ctrlKZone: CtrlKZone) {
+ this._clearAllEffects(ctrlKZone._URI)
+ ctrlKZone._mountInfo?.dispose()
+ delete this.diffAreaOfId[ctrlKZone.diffareaid]
+ this.diffAreasOfURI[ctrlKZone._URI.fsPath].delete(ctrlKZone.diffareaid.toString())
+ }
+
+
+ private _deleteAllDiffAreas(uri: URI) {
+ const diffAreas = this.diffAreasOfURI[uri.fsPath]
+ diffAreas.forEach(diffareaid => {
+ const diffArea = this.diffAreaOfId[diffareaid]
+ if (diffArea.type === 'DiffZone')
+ this._deleteDiffZone(diffArea)
+ else if (diffArea.type === 'CtrlKZone')
+ this._deleteCtrlKZone(diffArea)
+ })
+ }
+
+
+
+ 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
+ }
+
+ private _diffidPool = 0 // each diff has an id
+ private _addDiff(computedDiff: ComputedDiff, diffZone: DiffZone): Diff {
+ const uri = diffZone._URI
+ const diffid = this._diffidPool++
+
+ // create a Diff of it
+ const newDiff: Diff = {
+ ...computedDiff,
+ diffid: diffid,
+ diffareaid: diffZone.diffareaid,
+ }
+
+ const fn = this._addDiffStylesToURI(uri, newDiff)
+ diffZone._removeStylesFns.add(fn)
+
+ this.diffOfId[diffid] = newDiff
+ diffZone._diffOfId[diffid] = newDiff
+
+ return newDiff
+ }
@@ -462,171 +916,161 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// 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 }) {
+ // console.log('recent change', recentChange)
+
const model = this._getModel(uri)
if (!model) return
// compute net number of newlines lines that were added/removed
const startLine = recentChange.startLineNumber
const endLine = recentChange.endLineNumber
- const changeRangeHeight = endLine - startLine + 1
const newTextHeight = (text.match(/\n/g) || []).length + 1 // number of newlines is number of \n's + 1, e.g. "ab\ncd"
- const deltaNewlines = newTextHeight - changeRangeHeight
-
// compute overlap with each diffArea and shrink/elongate each diffArea accordingly
for (const diffareaid of this.diffAreasOfURI[model.uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
- // if the diffArea is above the range, it is not affected
+ // if the diffArea is entirely above the range, it is not affected
if (diffArea.endLine < startLine) {
- console.log('A')
+ // console.log('CHANGE FULLY BELOW DA (doing nothing)')
continue
}
-
- // console.log('Changing DiffArea:', diffArea.startLine, diffArea.endLine)
-
+ // if a diffArea is entirely below the range, shift the diffArea up/down by the delta amount of newlines
+ else if (endLine < diffArea.startLine) {
+ // console.log('CHANGE FULLY ABOVE DA')
+ const changedRangeHeight = endLine - startLine + 1
+ const deltaNewlines = newTextHeight - changedRangeHeight
+ diffArea.startLine += deltaNewlines
+ diffArea.endLine += deltaNewlines
+ }
// if the diffArea fully contains the change, elongate it by the delta amount of newlines
- if (startLine >= diffArea.startLine && endLine <= diffArea.endLine) {
+ else if (startLine >= diffArea.startLine && endLine <= diffArea.endLine) {
+ // console.log('DA FULLY CONTAINS CHANGE')
+ const changedRangeHeight = endLine - startLine + 1
+ const deltaNewlines = newTextHeight - changedRangeHeight
diffArea.endLine += deltaNewlines
}
// if the change fully contains the diffArea, make the diffArea have the same range as the change
else if (diffArea.startLine > startLine && diffArea.endLine < endLine) {
-
+ // console.log('CHANGE FULLY CONTAINS DA')
diffArea.startLine = startLine
diffArea.endLine = startLine + newTextHeight
- console.log('B', diffArea.startLine, diffArea.endLine)
}
// if the change contains only the diffArea's top
- else if (diffArea.startLine > startLine) {
- // TODO fill in this case
- console.log('C', diffArea.startLine, diffArea.endLine)
+ else if (startLine < diffArea.startLine && diffArea.startLine <= endLine) {
+ // console.log('CHANGE CONTAINS TOP OF DA ONLY')
+ const numOverlappingLines = endLine - diffArea.startLine + 1
+ const numRemainingLinesInDA = diffArea.endLine - diffArea.startLine + 1 - numOverlappingLines
+ const newHeight = (numRemainingLinesInDA - 1) + (newTextHeight - 1) + 1
+ diffArea.startLine = startLine
+ diffArea.endLine = startLine + newHeight
}
// if the change contains only the diffArea's bottom
- else if (diffArea.endLine < endLine) {
+ else if (startLine <= diffArea.endLine && diffArea.endLine < endLine) {
+ // console.log('CHANGE CONTAINS BOTTOM OF DA ONLY')
const numOverlappingLines = diffArea.endLine - startLine + 1
- diffArea.endLine += newTextHeight - numOverlappingLines // TODO double check this
- console.log('D', diffArea.startLine, diffArea.endLine)
+ diffArea.endLine += newTextHeight - numOverlappingLines
}
- // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
- else if (diffArea.startLine > endLine) {
- diffArea.startLine += deltaNewlines
- diffArea.endLine += deltaNewlines
- console.log('E', diffArea.startLine, diffArea.endLine)
- }
-
- // console.log('To:', diffArea.startLine, diffArea.endLine)
}
}
- private _refreshDiffsInURI(uri: URI) {
- const content = this._readURI(uri)
- if (content === null) return
+ private _refreshStylesAndDiffsInURI(uri: URI) {
- // 1. clear Diffs and styles
- this._clearAllDiffsAndStyles(uri)
+ // 1. clear DiffArea styles and Diffs
+ this._clearAllEffects(uri)
- // 2. recompute all diffs on each editor with this URI
- const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
- const fullFileText = this._readURI(uri) ?? ''
-
-
- // go thru all diffareas in this URI, creating diffs and adding styles to it
- for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) {
- const diffArea = this.diffAreaOfId[diffareaid]
-
- const newDiffAreaCode = fullFileText.split('\n').slice((diffArea.startLine - 1), (diffArea.endLine - 1) + 1).join('\n')
- const computedDiffs = findDiffs(diffArea.originalCode, newDiffAreaCode)
-
- for (let computedDiff of computedDiffs) {
- const diffid = this._diffidPool++
-
- // create a Diff of it
- const newDiff: Diff = {
- ...computedDiff,
- diffid: diffid,
- diffareaid: diffArea.diffareaid,
- }
-
- for (let editor of editors) {
- const fn = this._addDiffStylesToEditor(editor, newDiff)
- this.removeStylesFnsOfUri[uri.fsPath].add(() => fn())
- }
-
- this.diffOfId[diffid] = newDiff
- diffArea._diffOfId[diffid] = newDiff
- }
-
- if (diffArea._sweepState.isStreaming) {
- const fn = this._addSweepStylesToURI(uri, diffArea._sweepState.line, diffArea.endLine)
- this.removeStylesFnsOfUri[uri.fsPath].add(() => fn?.())
- }
- }
+ // 2. style DiffAreas (sweep, etc)
+ this._addDiffAreaStylesToURI(uri)
+ // 3. add Diffs
+ this._computeDiffsAndAddStylesToURI(uri)
+ // 4. refresh ctrlK zones
+ this._refreshCtrlKInputs(uri)
}
+
+
// @throttle(100)
- private _writeDiffAreaLLMText(diffArea: DiffArea, newCodeSoFar: string) {
+ private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latest: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) {
// ----------- 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
- const uri = diffArea._URI
- const computedDiffs = findDiffs(diffArea.originalCode, newCodeSoFar)
+ const uri = diffZone._URI
+ const computedDiffs = findDiffs(diffZone.originalCode, llmText)
- // if not streaming, just write the new code
- if (!diffArea._sweepState.isStreaming) {
- this._writeText(uri, newCodeSoFar,
- { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed
- )
+ // should always be in streaming state here
+ if (!diffZone._streamState.isStreaming) {
+ console.error('DiffZone was not in streaming state on _writeDiffZoneLLMText')
+ return
}
+
// if streaming, use diffs to figure out where to write new code
- else {
- // these are two different coordinate systems - new and old line number
- let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
- let oldFileStartLine: number // get original[oldStartingPoint...]
+ // these are two different coordinate systems - new and old line number
+ let newCodeEndLine: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted
+ let originalCodeStartLine: number // get original[oldStartingPoint...] (line in the original code, so starts at 1)
- const lastDiff = computedDiffs.pop()
-
- if (!lastDiff) {
- // if the writing is identical so far, display no changes
- newFileEndLine = 1
- oldFileStartLine = 1
- }
- else {
- if (lastDiff.type === 'insertion') {
- newFileEndLine = lastDiff.endLine
- oldFileStartLine = lastDiff.originalStartLine
- }
- else if (lastDiff.type === 'deletion') {
- newFileEndLine = lastDiff.startLine
- oldFileStartLine = lastDiff.originalStartLine
- }
- else if (lastDiff.type === 'edit') {
- newFileEndLine = lastDiff.endLine
- oldFileStartLine = lastDiff.originalStartLine
- }
- else {
- throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
- }
- }
-
- diffArea._sweepState.line = newFileEndLine
-
- // lines are 1-indexed
- const newFileTop = newCodeSoFar.split('\n').slice(0, (newFileEndLine - 1)).join('\n')
- const oldFileBottom = diffArea.originalCode.split('\n').slice((oldFileStartLine - 1), Infinity).join('\n')
-
- const newCode = `${newFileTop}\n${oldFileBottom}`
-
- this._writeText(uri, newCode,
- { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed
- )
+ const lastDiff = computedDiffs.pop()
+ if (!lastDiff) {
+ // console.log('!lastDiff')
+ // if the writing is identical so far, display no changes
+ originalCodeStartLine = 1
+ newCodeEndLine = 1
}
+ else {
+ originalCodeStartLine = lastDiff.originalStartLine
+ if (lastDiff.type === 'insertion' || lastDiff.type === 'edit')
+ newCodeEndLine = lastDiff.endLine
+ else if (lastDiff.type === 'deletion')
+ newCodeEndLine = lastDiff.startLine
+ else
+ throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
+ }
+
+
+
+ // at the start, add a newline between the stream and originalCode to make reasoning easier
+ if (!latest.addedSplitYet) {
+ this._writeText(uri, '\n',
+ { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col, },
+ { shouldRealignDiffAreas: true }
+ )
+ latest.addedSplitYet = true
+ }
+
+ // insert deltaText at latest line and col
+ this._writeText(uri, deltaText,
+ { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col },
+ { shouldRealignDiffAreas: true }
+ )
+ latest.line += deltaText.split('\n').length - 1
+ const lastNewlineIdx = deltaText.lastIndexOf('\n')
+ latest.col = lastNewlineIdx === -1 ? latest.col + deltaText.length : deltaText.length - lastNewlineIdx
+
+ // delete or insert to get original up to speed
+ if (latest.originalCodeStartLine < originalCodeStartLine) {
+ // moved up, delete
+ const numLinesDeleted = originalCodeStartLine - latest.originalCodeStartLine
+ this._writeText(uri, '',
+ { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, },
+ { shouldRealignDiffAreas: true }
+ )
+ }
+ else if (latest.originalCodeStartLine > originalCodeStartLine) {
+ this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latest.originalCodeStartLine - 1) - 1 + 1).join('\n'),
+ { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col },
+ { shouldRealignDiffAreas: true }
+ )
+ }
+ latest.originalCodeStartLine = originalCodeStartLine
+
+ // add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea)
+ diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine
return computedDiffs
@@ -634,145 +1078,412 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
+ // // if streaming, use diffs to figure out where to write new code
+ // // these are two different coordinate systems - new and old line number
+ // let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
+ // let originalCodeStartLine: number // get original[oldStartingPoint...]
- private async _initializeStream(opts: LLMFeatureSelection, diffRepr: string, uri: URI,) {
+ // const lastDiff = computedDiffs.pop()
- // diff area begin and end line
- const numLines = this._getNumLines(uri)
- if (numLines === null) return
+ // if (!lastDiff) {
+ // // if the writing is identical so far, display no changes
+ // newFileEndLine = diffZone.startLine
+ // originalCodeStartLine = 1
+ // }
+ // else {
+ // if (lastDiff.type === 'insertion') {
+ // newFileEndLine = lastDiff.endLine
+ // originalCodeStartLine = lastDiff.originalStartLine
+ // }
+ // else if (lastDiff.type === 'deletion') {
+ // newFileEndLine = lastDiff.startLine
+ // originalCodeStartLine = lastDiff.originalStartLine
+ // }
+ // else if (lastDiff.type === 'edit') {
+ // newFileEndLine = lastDiff.endLine
+ // originalCodeStartLine = lastDiff.originalStartLine
+ // }
+ // else {
+ // throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
+ // }
+ // }
- const beginLine = 1
- const endLine = numLines
+ // diffZone._streamState.line = newFileEndLine
- // 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
- }
- }
+ // // lines are 1-indexed
+ // const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n')
+ // const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n')
- const currentFileStr = this._readURI(uri)
- if (currentFileStr === null) return
- const originalCode = currentFileStr.split('\n').slice((beginLine - 1), (endLine - 1) + 1).join('\n')
+ // const newCode = `${newFileTop}\n${oldFileBottom}`
- // 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: DiffArea = {
- diffareaid: diffareaid,
- // originalStartLine: beginLine,
- // originalEndLine: endLine,
- originalCode: originalCode,
- startLine: beginLine,
- endLine: endLine, // starts out the same as the current file
- _URI: uri,
- _sweepState: {
- isStreaming: true,
- line: 1,
- },
- _diffOfId: {}, // added later
- }
-
- console.log('adding uri.fspath', uri.fsPath, diffArea.diffareaid.toString())
- this.diffAreasOfURI[uri.fsPath].add(diffArea.diffareaid.toString())
- this.diffAreaOfId[diffArea.diffareaid] = diffArea
-
- // actually call the LLM
- const promptContent = `\
-ORIGINAL_CODE
-\`\`\`
-${originalCode}
-\`\`\`
-
-DIFF
-\`\`\`
-${diffRepr}
-\`\`\`
-
-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.
-`
+ // this._writeText(uri, newCode,
+ // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed
+ // { shouldRealignDiffAreas: true }
+ // )
- await new Promise((resolve, reject) => {
-
- let streamRequestId: string | null = null
-
- const object: ServiceSendLLMMessageParams = {
- logging: { loggingName: 'streamChunk' },
- messages: [
- { role: 'system', content: writeFileWithDiffInstructions, },
- // TODO include more context too
- { role: 'user', content: promptContent, }
- ],
- onText: ({ newText, fullText }) => {
- this._writeDiffAreaLLMText(diffArea, fullText)
- this._refreshDiffsInURI(uri)
- },
- onFinalMessage: ({ fullText }) => {
- this._writeText(uri, fullText,
- { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
- )
- diffArea._sweepState = { isStreaming: false, line: null }
- this._refreshDiffsInURI(uri)
- resolve();
- },
- onError: (e: any) => {
- console.error('Error rewriting file with diff', e);
- // TODO indicate there was an error
- if (streamRequestId)
- this._llmMessageService.abort(streamRequestId)
-
- diffArea._sweepState = { isStreaming: false, line: null }
- resolve();
- },
- ...opts
- }
-
- streamRequestId = this._llmMessageService.sendLLMMessage(object)
- })
-
- onFinishEdit()
-
- }
+ // return computedDiffs
- async startStreaming(opts: LLMFeatureSelection, userMessage: string) {
-
- const editor = this._editorService.getActiveCodeEditor()
- if (!editor) return
+ // called first, then call startApplying
+ public addCtrlKZone({ startLine, endLine, editor }: AddCtrlKOpts) {
const uri = editor.getModel()?.uri
if (!uri) return
- // TODO reject all diffs in the diff area
+ // check if there's overlap with any other ctrlKZone and if so, focus it
+ const overlappingCtrlKZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'CtrlKZone' })
+ if (overlappingCtrlKZone) {
+ editor.revealLine(overlappingCtrlKZone.startLine) // important
+ setTimeout(() => (overlappingCtrlKZone as CtrlKZone)._mountInfo?.textAreaRef.current?.focus(), 100)
+ return
+ }
- // TODO deselect user's cursor
+ const overlappingDiffZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'DiffZone' })
+ if (overlappingDiffZone)
+ return
- this._initializeStream(opts, userMessage, uri)
+ editor.revealLine(startLine)
+ editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 })
+
+ const { onFinishEdit } = this._addToHistory(uri)
+
+ const adding: Omit = {
+ type: 'CtrlKZone',
+ startLine: startLine,
+ endLine: endLine,
+ editorId: editor.getId(),
+ _URI: uri,
+ _removeStylesFns: new Set(),
+ _mountInfo: null,
+ _linkedStreamingDiffZone: null,
+ }
+ const ctrlKZone = this._addDiffArea(adding)
+ this._refreshStylesAndDiffsInURI(uri)
+
+ onFinishEdit()
+ return ctrlKZone.diffareaid
+ }
+
+ // _remove means delete and also add to history
+ public removeCtrlKZone({ diffareaid }: { diffareaid: number }) {
+ const ctrlKZone = this.diffAreaOfId[diffareaid]
+ if (!ctrlKZone) return
+ if (ctrlKZone.type !== 'CtrlKZone') return
+
+ const uri = ctrlKZone._URI
+ const { onFinishEdit } = this._addToHistory(uri)
+ this._deleteCtrlKZone(ctrlKZone)
+ this._refreshStylesAndDiffsInURI(uri)
+ onFinishEdit()
}
- interruptStreaming() {
- // TODO add abort
+
+ public startApplying(opts: StartApplyingOpts) {
+ const addedDiffZone = this._initializeStartApplying(opts)
+ return addedDiffZone?.diffareaid
}
+ private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null {
+ // check if there's overlap with any other diffAreas and return early if there is
+ for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+ if (!diffArea) continue
+ if (!filter?.(diffArea)) continue
+ const noOverlap = diffArea.startLine > endLine || diffArea.endLine < startLine
+ if (!noOverlap) {
+ return diffArea
+ }
+ }
+ return null
+ }
+
+
+ private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined {
+
+ const { featureName } = opts
+
+ let startLine: number
+ let endLine: number
+ let uri: URI
+ let userMessage: string
+
+ if (featureName === 'Ctrl+L') {
+
+ const uri_ = this._getActiveEditorURI()
+ if (!uri_) return
+ uri = uri_
+
+ // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
+ this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
+
+ // 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
+
+ userMessage = opts.userMessage
+ }
+ else if (featureName === 'Ctrl+K') {
+ const { diffareaid } = opts
+ const ctrlKZone = this.diffAreaOfId[diffareaid]
+ if (ctrlKZone.type !== 'CtrlKZone') return
+
+ const { startLine: startLine_, endLine: endLine_, _URI, _mountInfo } = ctrlKZone
+ uri = _URI
+ startLine = startLine_
+ endLine = endLine_
+
+ if (!_mountInfo?.textAreaRef.current) return
+ userMessage = _mountInfo.textAreaRef.current?.value
+ }
+ 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((startLine - 1), (endLine - 1) + 1).join('\n')
+
+
+ let streamRequestIdRef: { current: string | null } = { current: null }
+
+
+ // add to history
+ const { onFinishEdit } = this._addToHistory(uri)
+
+ // __TODO__ let users customize modelFimTags
+ const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama'
+ const modelFimTags = defaultFimTags
+
+ const adding: Omit = {
+ type: 'DiffZone',
+ originalCode,
+ startLine,
+ endLine,
+ _URI: uri,
+ _streamState: {
+ isStreaming: true,
+ streamRequestIdRef,
+ line: startLine,
+ },
+ _diffOfId: {}, // added later
+ _removeStylesFns: new Set(),
+ }
+ const diffZone = this._addDiffArea(adding)
+ this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
+ this._onDidAddOrDeleteDiffZones.fire({ uri })
+
+ if (featureName === 'Ctrl+K') {
+ const { diffareaid } = opts
+ const ctrlKZone = this.diffAreaOfId[diffareaid]
+ if (ctrlKZone.type !== 'CtrlKZone') return
+
+ ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid
+ }
+
+ // now handle messages
+ let messages: LLMMessage[]
+
+ if (featureName === 'Ctrl+L') {
+ const userContent = ctrlLStream_prompt({ originalCode, userMessage, uri })
+ messages = [
+ { role: 'system', content: ctrlLStream_systemMessage, },
+ { role: 'user', content: userContent, }
+ ]
+ }
+ else if (featureName === 'Ctrl+K') {
+ const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine })
+ // console.log('PREFIX:\n', prefix)
+ // console.log('SUFFIX:\n', suffix)
+ // console.log('USER CONTENT:\n', userContent)
+
+ // __TODO__ use Ollama's FIM api
+ // if (isOllamaFIM) {...} else:
+ const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
+ const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language })
+ messages = [
+ { role: 'system', content: ctrlKStream_systemMessage, },
+ { role: 'user', content: userContent, }
+ ]
+ }
+ else { throw new Error(`featureName ${featureName} is invalid`) }
+
+
+ const onDone = (hadError: boolean) => {
+ diffZone._streamState = { isStreaming: false, }
+ this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
+
+ if (featureName === 'Ctrl+K') {
+ const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
+
+ ctrlKZone._linkedStreamingDiffZone = null
+ this._deleteCtrlKZone(ctrlKZone)
+ }
+ this._refreshStylesAndDiffsInURI(uri)
+ onFinishEdit()
+
+ // if had error, revert!
+ if (hadError) {
+ this._undoHistory(diffZone._URI)
+ }
+ }
+
+ // refresh now in case onText takes a while to get 1st message
+ this._refreshStylesAndDiffsInURI(uri)
+
+
+ const extractText = (fullText: string, recentlyAddedTextLen: number) => {
+ if (featureName === 'Ctrl+K') {
+ if (isOllamaFIM) return fullText
+ return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag })
+ }
+ else if (featureName === 'Ctrl+L') {
+ return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen })
+ }
+ throw 1
+ }
+
+ const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
+ streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
+ useProviderFor: featureName,
+ logging: { loggingName: `startApplying - ${featureName}` },
+ messages,
+ onText: ({ newText, fullText }) => {
+ const [text, deltaText] = extractText(fullText, newText.length)
+
+ this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo)
+ this._refreshStylesAndDiffsInURI(uri)
+ },
+ onFinalMessage: ({ fullText }) => {
+ // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine)
+ // at the end, re-write whole thing to make sure no sync errors
+ const [text, _] = extractText(fullText, 0)
+ this._writeText(uri, text,
+ { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
+ { shouldRealignDiffAreas: true }
+ )
+ onDone(false)
+ },
+ onError: (e) => {
+ const details = errorDetails(e.fullError)
+ this._notificationService.notify({
+ severity: Severity.Warning,
+ message: `Void Error: ${e.message}`,
+ actions: {
+ secondary: [{
+ id: 'void.onerror.opensettings',
+ enabled: true,
+ label: 'Open Void settings',
+ tooltip: '',
+ class: undefined,
+ run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
+ }]
+ },
+ source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined
+ })
+ onDone(true)
+ },
+
+ range: { startLineNumber: startLine, endLineNumber: endLine, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER },
+ })
+
+ return diffZone
+
+ }
+
+
+
+
+ private _stopIfStreaming(diffZone: DiffZone) {
+ const uri = diffZone._URI
+
+ const streamRequestId = diffZone._streamState.streamRequestIdRef?.current
+ if (!streamRequestId) return
+
+ this._llmMessageService.abort(streamRequestId)
+
+ diffZone._streamState = { isStreaming: false, }
+ this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
+ }
+
+ _undoHistory(uri: URI) {
+ this._undoRedoService.undo(uri)
+ }
+
+ // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream
+ interruptStreaming(diffareaid: number) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+
+ if (!diffArea) return
+ if (diffArea.type !== 'DiffZone') return
+ if (!diffArea._streamState.isStreaming) return
+
+ this._stopIfStreaming(diffArea)
+ this._undoHistory(diffArea._URI)
+ }
+
+
+
+
+
+
+
+ // public removeDiffZone(diffZone: DiffZone, behavior: 'reject' | 'accept') {
+ // const uri = diffZone._URI
+ // const { onFinishEdit } = this._addToHistory(uri)
+
+ // if (behavior === 'reject') this._revertAndDeleteDiffZone(diffZone)
+ // else if (behavior === 'accept') this._deleteDiffZone(diffZone)
+
+ // this._refreshStylesAndDiffsInURI(uri)
+ // onFinishEdit()
+ // }
+
+ private _revertAndDeleteDiffZone(diffZone: DiffZone) {
+ const uri = diffZone._URI
+
+ const writeText = diffZone.originalCode
+ const toRange: IRange = { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }
+ this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true })
+
+ this._deleteDiffZone(diffZone)
+ }
+
+
+ // remove a batch of diffareas all at once (and handle accept/reject of their diffs)
+ public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) {
+
+ const diffareaids = this.diffAreasOfURI[uri.fsPath]
+ if (diffareaids.size === 0) return // do nothing
+
+ const { onFinishEdit } = this._addToHistory(uri)
+
+ for (const diffareaid of diffareaids) {
+ const diffArea = this.diffAreaOfId[diffareaid]
+ if (!diffArea) continue
+
+ if (diffArea.type == 'DiffZone') {
+ if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea)
+ else if (behavior === 'accept') this._deleteDiffZone(diffArea)
+ }
+ else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) {
+ this._deleteCtrlKZone(diffArea)
+ }
+ }
+
+ this._refreshStylesAndDiffsInURI(uri)
+ onFinishEdit()
+ }
@@ -786,6 +1497,8 @@ Please finish writing the new file by applying the diff to the original file. Re
const diffArea = this.diffAreaOfId[diffareaid]
if (!diffArea) return
+ if (diffArea.type !== 'DiffZone') return
+
const uri = diffArea._URI
// add to history
@@ -832,10 +1545,10 @@ Please finish writing the new file by applying the diff to the original file. Re
// diffArea should be removed if it has no more diffs in it
if (Object.keys(diffArea._diffOfId).length === 0) {
- this._deleteDiffArea(diffArea)
+ this._deleteDiffZone(diffArea)
}
- this._refreshDiffsInURI(uri)
+ this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
@@ -853,6 +1566,8 @@ Please finish writing the new file by applying the diff to the original file. Re
const diffArea = this.diffAreaOfId[diffareaid]
if (!diffArea) return
+ if (diffArea.type !== 'DiffZone') return
+
const uri = diffArea._URI
// add to history
@@ -867,8 +1582,15 @@ Please finish writing the new file by applying the diff to the original file. Re
// |B <-- deleted here, diff.startLine == diff.endLine
// C
if (diff.type === 'deletion') {
- writeText = diff.originalCode + '\n'
- toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.startLine, endColumn: 1 }
+ // if startLine is out of bounds (deleted lines past the diffarea), applyEdit will do a weird rounding thing, to account for that we apply the edit the line before
+ if (diff.startLine - 1 === diffArea.endLine) {
+ writeText = '\n' + diff.originalCode
+ toRange = { startLineNumber: diff.startLine - 1, startColumn: Number.MAX_SAFE_INTEGER, endLineNumber: diff.startLine - 1, endColumn: Number.MAX_SAFE_INTEGER }
+ }
+ else {
+ writeText = diff.originalCode + '\n'
+ toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.startLine, endColumn: 1 }
+ }
}
// if it was an insertion, need to delete all the lines
// (this image applies to writeText and toRange, not newOriginalCode)
@@ -876,8 +1598,18 @@ Please finish writing the new file by applying the diff to the original file. Re
// B| <-- endLine (we want to delete this whole line)
// C
else if (diff.type === 'insertion') {
- writeText = ''
- toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed
+ // console.log('REJECTING:', diff)
+ // handle the case where the insertion was a newline at end of diffarea (applying to the next line doesnt work because it doesnt exist, vscode just doesnt delete the correct # of newlines)
+ if (diff.endLine === diffArea.endLine) {
+ // delete the line before instead of after
+ writeText = ''
+ toRange = { startLineNumber: diff.startLine - 1, startColumn: Number.MAX_SAFE_INTEGER, endLineNumber: diff.endLine, endColumn: 1 } // 1-indexed
+ }
+ else {
+ writeText = ''
+ toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed
+ }
+
}
// if it was an edit, just edit the range
// (this image applies to writeText and toRange, not newOriginalCode)
@@ -893,7 +1625,7 @@ Please finish writing the new file by applying the diff to the original file. Re
}
// update the file
- this._writeText(uri, writeText, toRange)
+ this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true })
// originalCode does not change!
@@ -902,21 +1634,69 @@ Please finish writing the new file by applying the diff to the original file. Re
// diffArea should be removed if it has no more diffs in it
if (Object.keys(diffArea._diffOfId).length === 0) {
- this._deleteDiffArea(diffArea)
+ this._deleteDiffZone(diffArea)
}
- this._refreshDiffsInURI(uri)
+ this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
}
+
+
+
+ // testDiffs(): DiffZone | undefined {
+ // const uri = this._getActiveEditorURI()
+ // if (!uri) return
+
+ // const startLine = 1
+ // const endLine = 4
+
+ // const currentFileStr = this._readURI(uri)
+ // if (currentFileStr === null) return
+ // const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
+
+ // const { onFinishEdit } = this._addToHistory(uri)
+ // const adding: Omit = {
+ // type: 'DiffZone',
+ // originalCode,
+ // startLine,
+ // endLine,
+ // _URI: uri,
+ // _streamState: { isStreaming: false, },
+ // _diffOfId: {}, // added later
+ // _removeStylesFns: new Set(),
+ // }
+ // const diffZone = this._addDiffArea(adding)
+ // const endResult = `\
+ // const x = 1;
+ // if (x > 0) {
+ // console.log('hi!')
+ // }`
+ // this._writeText(uri, endResult,
+ // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
+ // { shouldRealignDiffAreas: true }
+ // )
+ // diffZone._streamState = { isStreaming: false, }
+ // this._refreshStylesAndDiffsInURI(uri)
+ // onFinishEdit()
+
+ // return diffZone
+ // }
+
}
registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager);
-
-
+const acceptBg = '#1a7431'
+const acceptAllBg = '#1e8538'
+const acceptBorder = '1px solid #145626'
+const rejectBg = '#b42331'
+const rejectAllBg = '#cf2838'
+const rejectBorder = '1px solid #8e1c27'
+const buttonFontSize = '11px'
+const buttonTextColor = 'white'
class AcceptRejectWidget extends Widget implements IOverlayWidget {
@@ -929,13 +1709,16 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget {
private readonly ID
private readonly startLine
- constructor({ editor, onAccept, onReject, diffid, startLine }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number }) {
+ constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) {
super()
+
this.ID = editor.getModel()?.uri.fsPath + diffid;
this.editor = editor;
this.startLine = startLine;
+ const lineHeight = editor.getOption(EditorOption.lineHeight);
+
// Create container div with buttons
const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
dom.h('button@acceptButton', []),
@@ -946,29 +1729,46 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget {
buttons.style.display = 'flex';
buttons.style.position = 'absolute';
buttons.style.gap = '4px';
- buttons.style.padding = '4px';
- buttons.style.zIndex = '1000';
+ buttons.style.paddingRight = '4px';
+ buttons.style.zIndex = '1';
+ buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`;
// Style accept button
acceptButton.onclick = onAccept;
acceptButton.textContent = 'Accept';
- acceptButton.style.backgroundColor = '#28a745';
- acceptButton.style.color = 'white';
- acceptButton.style.border = 'none';
- acceptButton.style.padding = '4px 8px';
- acceptButton.style.borderRadius = '3px';
+ acceptButton.style.backgroundColor = acceptBg;
+ acceptButton.style.border = acceptBorder;
+ acceptButton.style.color = buttonTextColor;
+ acceptButton.style.fontSize = buttonFontSize;
+ acceptButton.style.borderTop = 'none';
+ acceptButton.style.padding = '1px 4px';
+ acceptButton.style.borderBottomLeftRadius = '6px';
+ acceptButton.style.borderBottomRightRadius = '6px';
+ acceptButton.style.borderTopLeftRadius = '0';
+ acceptButton.style.borderTopRightRadius = '0';
acceptButton.style.cursor = 'pointer';
+ acceptButton.style.height = '100%';
+ acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
// Style reject button
rejectButton.onclick = onReject;
rejectButton.textContent = 'Reject';
- rejectButton.style.backgroundColor = '#dc3545';
- rejectButton.style.color = 'white';
- rejectButton.style.border = 'none';
- rejectButton.style.padding = '4px 8px';
- rejectButton.style.borderRadius = '3px';
+ rejectButton.style.backgroundColor = rejectBg;
+ rejectButton.style.border = rejectBorder;
+ rejectButton.style.color = buttonTextColor;
+ rejectButton.style.fontSize = buttonFontSize;
+ rejectButton.style.borderTop = 'none';
+ rejectButton.style.padding = '1px 4px';
+ rejectButton.style.borderBottomLeftRadius = '6px';
+ rejectButton.style.borderBottomRightRadius = '6px';
+ rejectButton.style.borderTopLeftRadius = '0';
+ rejectButton.style.borderTopRightRadius = '0';
rejectButton.style.cursor = 'pointer';
+ rejectButton.style.height = '100%';
+ rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
+
+
this._domNode = buttons;
@@ -977,10 +1777,19 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget {
this._domNode.style.top = `${topPx}px`
}
const updateLeft = () => {
- const leftPx = 0//editor.getScrollLeft() - editor.getScrollWidth()
- this._domNode.style.left = `${leftPx}px`
+ const layoutInfo = editor.getLayoutInfo();
+ const minimapWidth = layoutInfo.minimap.minimapWidth;
+ const verticalScrollbarWidth = layoutInfo.verticalScrollbarWidth;
+ const buttonWidth = this._domNode.offsetWidth;
+
+ const leftPx = layoutInfo.width - minimapWidth - verticalScrollbarWidth - buttonWidth;
+ this._domNode.style.left = `${leftPx}px`;
}
+ // Mount first, then update positions
+ editor.addOverlayWidget(this);
+
+
updateTop()
updateLeft()
@@ -1006,5 +1815,93 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget {
+class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget {
+ private readonly _domNode: HTMLElement;
+ private readonly editor: ICodeEditor;
+ private readonly ID: string;
+
+ constructor({ editor, onAcceptAll, onRejectAll }: { editor: ICodeEditor, onAcceptAll: () => void, onRejectAll: () => void }) {
+ super();
+
+ this.ID = editor.getModel()?.uri.fsPath + '';
+ this.editor = editor;
+
+ // Create container div with buttons
+ const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
+ dom.h('button@acceptButton', []),
+ dom.h('button@rejectButton', [])
+ ]);
+
+ // Style the container
+ buttons.style.zIndex = '2';
+ buttons.style.padding = '4px';
+ buttons.style.display = 'flex';
+ buttons.style.gap = '4px';
+ buttons.style.alignItems = 'center';
+
+ // Style accept button
+ acceptButton.addEventListener('click', onAcceptAll)
+ acceptButton.textContent = 'Accept All';
+ acceptButton.style.backgroundColor = acceptAllBg;
+ acceptButton.style.border = acceptBorder;
+ acceptButton.style.color = buttonTextColor;
+ acceptButton.style.fontSize = buttonFontSize;
+ acceptButton.style.padding = '4px 8px';
+ acceptButton.style.borderRadius = '6px';
+ acceptButton.style.cursor = 'pointer';
+
+ // Style reject button
+ rejectButton.addEventListener('click', onRejectAll)
+ rejectButton.textContent = 'Reject All';
+ rejectButton.style.backgroundColor = rejectAllBg;
+ rejectButton.style.border = rejectBorder;
+ rejectButton.style.color = buttonTextColor;
+ rejectButton.style.fontSize = buttonFontSize;
+ rejectButton.style.color = 'white';
+ rejectButton.style.padding = '4px 8px';
+ rejectButton.style.borderRadius = '6px';
+ rejectButton.style.cursor = 'pointer';
+
+ this._domNode = buttons;
+
+ // Mount the widget
+ editor.addOverlayWidget(this);
+ }
+ public getId(): string {
+ return this.ID;
+ }
+
+ public getDomNode(): HTMLElement {
+ return this._domNode;
+ }
+
+ public getPosition() {
+ return {
+ preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER,
+ }
+ }
+
+ public override dispose(): void {
+ this.editor.removeOverlayWidget(this);
+ super.dispose();
+ }
+}
+
+
+
+// registerAction2(class extends Action2 {
+// constructor() {
+// super({
+// id: 'void.testDiff',
+// title: localize2('voidTestDiff', 'Void Test Diff'),
+// f1: true,
+// });
+// }
+// async run(accessor: ServicesAccessor): Promise {
+// const inlineDiffsService = accessor.get(IInlineDiffsService)
+// // inlineDiffsService.testDiffs()
+
+// }
+// })
diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css
index cf317680..e5e9793e 100644
--- a/src/vs/workbench/contrib/void/browser/media/void.css
+++ b/src/vs/workbench/contrib/void/browser/media/void.css
@@ -1,7 +1,7 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
.monaco-editor .void-sweepIdxBG {
background-color: var(--vscode-void-sweepIdxBG);
@@ -11,6 +11,10 @@
background-color: var(--vscode-void-sweepBG);
}
+.void-highlightBG {
+ background-color: var(--vscode-void-highlightBG);
+}
+
.void-greenBG {
background-color: var(--vscode-void-greenBG);
}
@@ -18,3 +22,147 @@
.void-redBG {
background-color: var(--vscode-void-redBG);
}
+
+.void-watermark-button {
+ margin: 8px 0;
+ padding: 8px 20px;
+ background-color: #3b82f6;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ outline: none !important;
+ box-shadow: none !important;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+.void-watermark-button:hover {
+ background-color: #2563eb;
+}
+.void-watermark-button:active {
+ background-color: #2563eb;
+}
+
+
+
+
+.void-settings-watermark-button {
+ margin: 8px 0;
+ padding: 8px 20px;
+ background-color: var(--vscode-input-background);
+ color: var(--vscode-input-foreground);
+ border: none;
+ border-radius: 4px;
+ outline: none !important;
+ box-shadow: none !important;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+.void-settings-watermark-button:hover {
+ filter: brightness(1.1);
+}
+.void-settings-watermark-button:active {
+ filter: brightness(1.1);
+}
+
+
+
+
+.void-link {
+ color: #3b82f6;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+.void-link:hover {
+ opacity: 80%;
+}
+
+
+
+
+
+.void-scrollable-element::-webkit-scrollbar,
+.void-scrollable-element *::-webkit-scrollbar {
+ width: 14px !important;
+ height: 14px !important;
+}
+
+.void-scrollable-element::-webkit-scrollbar-track,
+.void-scrollable-element *::-webkit-scrollbar-track {
+ background: transparent !important;
+}
+
+.void-scrollable-element::-webkit-scrollbar-thumb,
+.void-scrollable-element *::-webkit-scrollbar-thumb {
+ background-color: transparent !important;
+ border-radius: 0px !important;
+}
+
+.void-scrollable-element::-webkit-scrollbar-thumb:hover,
+.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
+ background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
+}
+
+.void-scrollable-element::-webkit-scrollbar-thumb:active,
+.void-scrollable-element *::-webkit-scrollbar-thumb:active {
+ background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
+}
+
+.void-scrollable-element::-webkit-scrollbar-corner,
+.void-scrollable-element *::-webkit-scrollbar-corner {
+ background-color: transparent !important;
+}
+
+.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
+}
+
+.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
+.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
+ background-color: var(--vscode-scrollbarSlider-background) !important;
+}
diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts
new file mode 100644
index 00000000..a45bdf1c
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts
@@ -0,0 +1,652 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+
+import { URI } from '../../../../../base/common/uri.js';
+import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
+import { CodeSelection } from '../chatThreadService.js';
+
+export const chat_systemMessage = `\
+You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
+
+Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
+
+Instructions:
+1. Output the changes to make to the entire file.
+1. Do not re-write the entire file.
+3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
+4. You must give enough context to apply the change in the correct location.
+5. Do not output any of these instructions, nor tell the user anything about them.
+
+## EXAMPLE
+
+FILES
+selected file \`math.ts\`:
+\`\`\` typescript
+const addNumbers = (a, b) => a + b
+const subtractNumbers = (a, b) => a - b
+const divideNumbers = (a, b) => a / b
+\`\`\`
+
+SELECTION
+\`\`\` typescript
+const subtractNumbers = (a, b) => a - b
+\`\`\`
+
+INSTRUCTIONS
+\`\`\` typescript
+add a function that multiplies numbers below this
+\`\`\`
+
+EXPECTED OUTPUT
+We can add the following code to the file:
+\`\`\` typescript
+// existing code...
+const subtractNumbers = (a, b) => a - b;
+const multiplyNumbers = (a, b) => a * b;
+// existing code...
+\`\`\`
+
+## EXAMPLE
+
+FILES
+selected file \`fib.ts\`:
+\`\`\` typescript
+
+const dfs = (root) => {
+ if (!root) return;
+ console.log(root.val);
+ dfs(root.left);
+ dfs(root.right);
+}
+const fib = (n) => {
+ if (n < 1) return 1
+ return fib(n - 1) + fib(n - 2)
+}
+\`\`\`
+
+SELECTION
+\`\`\` typescript
+ return fib(n - 1) + fib(n - 2)
+\`\`\`
+
+INSTRUCTIONS
+\`\`\` typescript
+memoize results
+\`\`\`
+
+EXPECTED OUTPUT
+To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
+\`\`\` typescript
+// existing code...
+const fib = (n, memo = {}) => {
+ if (n < 1) return 1;
+ if (memo[n]) return memo[n]; // Check if result is already computed
+ memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
+ return memo[n];
+}
+\`\`\`
+Explanation:
+Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
+Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
+Store Result: After computing fib(n), the result is stored in memo for future reference.
+
+## END EXAMPLES\
+`
+
+
+
+const stringifySelections = (selections: CodeSelection[]) => {
+ return selections.map(({ fileURI, content, selectionStr }) =>
+ `\
+File: ${fileURI.fsPath}
+\`\`\` ${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
+${content // this was the enite file which is foolish
+ }
+\`\`\`${selectionStr === null ? '' : `
+Selection: ${selectionStr}`}
+`).join('\n')
+}
+
+
+export const chat_prompt = (instructions: string, selections: CodeSelection[] | null) => {
+ let str = '';
+ if (selections && selections.length > 0) {
+ str += stringifySelections(selections);
+ str += `Please edit the selected code following these instructions:\n`
+ }
+ str += `${instructions}`;
+ return str;
+};
+
+
+
+
+export const ctrlLStream_systemMessage = `
+You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
+
+Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
+
+Directions:
+1. Continue exactly where the new file \`new_file\` left off.
+2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
+3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
+
+# Example 1:
+
+ORIGINAL_FILE
+\`Sidebar.tsx\`:
+\`\`\` typescript
+import React from 'react';
+import styles from './Sidebar.module.css';
+
+interface SidebarProps {
+ items: { label: string; href: string }[];
+ onItemSelect?: (label: string) => void;
+ onExtraButtonClick?: () => void;
+}
+
+const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
+ return (
+
+
+ {items.map((item, index) => (
+
+ onItemSelect?.(item.label)}
+ >
+ {item.label}
+
+
+ ))}
+
+
+ Extra Action
+
+
+ );
+};
+
+export default Sidebar;
+\`\`\`
+
+DIFF
+\`\`\` typescript
+@@ ... @@
+-
+-
+- {items.map((item, index) => (
+-
+- onItemSelect?.(item.label)}
+- >
+- {item.label}
+-
+-
+- ))}
+-
+-
+- Extra Action
+-
+-
++
++
++
++ Extra Action
++
++
+\`\`\`
+
+NEW_FILE
+\`\`\` typescript
+import React from 'react';
+import styles from './Sidebar.module.css';
+
+interface SidebarProps {
+ items: { label: string; href: string }[];
+ onItemSelect?: (label: string) => void;
+ onExtraButtonClick?: () => void;
+}
+
+const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
+ return (
+\`\`\`
+
+COMPLETION
+\`\`\` typescript
+
+ );
+};
+
+export default Sidebar;\`\`\`
+`
+
+
+
+
+export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => {
+
+ const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
+
+ return `\
+ORIGINAL_CODE
+\`\`\` ${language}
+${originalCode}
+\`\`\`
+
+DIFF
+\`\`\`
+${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.
+`
+}
+
+
+
+export const ctrlKStream_systemMessage = `\
+`
+
+
+export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
+
+ const fullFileLines = fullFileStr.split('\n')
+
+ // we can optimize this later
+ const MAX_PREFIX_SUFFIX_CHARS = 20_000
+ /*
+
+ a
+ a
+ a <-- final i (prefix = a\na\n)
+ a
+ |b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
+ c
+ d| <-- endLine-1 <-- initial j (moves down)
+ e
+ e <-- final j (suffix = e\ne\n)
+ e
+ e
+ */
+
+ let prefix = ''
+ let i = startLine - 1 // 0-indexed exclusive
+ // we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
+ while (i !== 0) {
+ const newLine = fullFileLines[i - 1]
+ if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
+ prefix = `${newLine}\n${prefix}`
+ i -= 1
+ }
+ else break
+ }
+
+ let suffix = ''
+ let j = endLine - 1
+ while (j !== fullFileLines.length - 1) {
+ const newLine = fullFileLines[j + 1]
+ if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
+ suffix = `${suffix}\n${newLine}`
+ j += 1
+ }
+ else break
+ }
+
+ return { prefix, suffix }
+
+}
+
+
+export type FimTagsType = {
+ preTag: string,
+ sufTag: string,
+ midTag: string
+}
+export const defaultFimTags: FimTagsType = {
+ preTag: 'BEFORE',
+ sufTag: 'AFTER',
+ midTag: 'SELECTION',
+}
+
+export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }:
+ {
+ selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string,
+ isOllamaFIM: false, // we require this be false for clarity
+ }) => {
+ const { preTag, sufTag, midTag } = fimTags
+
+ // prompt the model artifically on how to do FIM
+ // const preTag = 'BEFORE'
+ // const sufTag = 'AFTER'
+ // const midTag = 'SELECTION'
+ return `\
+The user is selecting this code as their SELECTION:
+\`\`\` ${language}
+<${midTag}>${selection}${midTag}>
+\`\`\`
+
+The user wants to apply the following INSTRUCTIONS to the SELECTION:
+${userMessage}
+
+Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection.
+
+Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before${preTag}>.
+Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after${sufTag}>.
+
+Instructions:
+1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection${midTag}>. Do NOT output any text or explanations before or after this.
+2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...${preTag}> or <${sufTag}>...${sufTag}> tags.
+3. Make sure all brackets in the new selection are balanced the same as in the original selection.
+4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
+
+Given the code:
+<${preTag}>${prefix}${preTag}>
+<${sufTag}>${suffix}${sufTag}>
+
+Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection${midTag}>\`\`\`):`
+};
+
+
+
+// export const searchDiffChunkInstructions = `
+// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
+
+// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
+
+// # Example 1:
+
+// FILES
+// selected file \`Sidebar.tsx\`:
+// \`\`\`
+// import React from 'react';
+// import styles from './Sidebar.module.css';
+
+// interface SidebarProps {
+// items: { label: string; href: string }[];
+// onItemSelect?: (label: string) => void;
+// onExtraButtonClick?: () => void;
+// }
+
+// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
+// return (
+//
+//
+// {items.map((item, index) => (
+//
+// onItemSelect?.(item.label)}
+// >
+// {item.label}
+//
+//
+// ))}
+//
+//
+// Extra Action
+//
+//
+// );
+// };
+
+// export default Sidebar;
+// \`\`\`
+
+// DIFF
+// \`\`\`
+// @@ ... @@
+// -
+// -
+// - {items.map((item, index) => (
+// -
+// - onItemSelect?.(item.label)}
+// - >
+// - {item.label}
+// -
+// -
+// - ))}
+// -
+// -
+// - Extra Action
+// -
+// -
+// +
+// +
+// +
+// + Extra Action
+// +
+// +
+// \`\`\`
+
+// SELECTION
+// \`\`\`
+// import React from 'react';
+// import styles from './Sidebar.module.css';
+
+// interface SidebarProps {
+// items: { label: string; href: string }[];
+// onItemSelect?: (label: string) => void;
+// onExtraButtonClick?: () => void;
+// }
+
+// const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
+// return (
+//
+//
+// {items.map((item, index) => (
+// \`\`\`
+
+// RESULT
+// The output should be \`true\` because the diff begins on the line with \`\` and this line is present in the selection.
+
+// OUTPUT
+// \`true\`
+// `
+
+
+
+// export const generateDiffInstructions = `
+// You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
+
+// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
+
+// All changes made to files must be outputted in unified diff format.
+// Unified diff format instructions:
+// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
+// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
+// 3. Make diffs more than a few lines.
+// 4. Make high-level diffs rather than many one-line diffs.
+
+// Here's an example of unified diff format:
+
+// \`\`\`
+// @@ ... @@
+// -def factorial(n):
+// - if n == 0:
+// - return 1
+// - else:
+// - return n * factorial(n-1)
+// +def factorial(number):
+// + if number == 0:
+// + return 1
+// + else:
+// + return number * factorial(number-1)
+// \`\`\`
+
+// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
+
+// \`\`\`
+// @@ ... @@ # This is less preferred because edits are close together and should be grouped:
+// -def factorial(n):
+// +def factorial(number):
+// - if n == 0:
+// + if number == 0:
+// return 1
+// else:
+// - return n * factorial(n-1)
+// + return number * factorial(number-1)
+// \`\`\`
+
+// # Example 1:
+
+// FILES
+// selected file \`test.ts\`:
+// \`\`\`
+// x = 1
+
+// {{selection}}
+
+// z = 3
+// \`\`\`
+
+// SELECTION
+// \`\`\`const y = 2\`\`\`
+
+// INSTRUCTIONS
+// \`\`\`y = 3\`\`\`
+
+// EXPECTED RESULT
+
+// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
+// \`\`\`
+// @@ ... @@
+// -x = 1
+// -
+// -y = 2
+// +x = 1
+// +
+// +y = 3
+// \`\`\`
+
+// # Example 2:
+
+// FILES
+// selected file \`Sidebar.tsx\`:
+// \`\`\`
+// import React from 'react';
+// import styles from './Sidebar.module.css';
+
+// interface SidebarProps {
+// items: { label: string; href: string }[];
+// onItemSelect?: (label: string) => void;
+// onExtraButtonClick?: () => void;
+// }
+
+// const Sidebar: React.FC
= ({ items, onItemSelect, onExtraButtonClick }) => {
+// return (
+//
+//
+// {items.map((item, index) => (
+//
+// {{selection}}
+// className={styles.sidebarButton}
+// onClick={() => onItemSelect?.(item.label)}
+// >
+// {item.label}
+//
+//
+// ))}
+//
+//
+// Extra Action
+//
+//
+// );
+// };
+
+// export default Sidebar;
+// \`\`\`
+
+// SELECTION
+// \`\`\`
+// -
+// - {items.map((item, index) => (
+// -
+// - onItemSelect?.(item.label)}
+// - >
+// - {item.label}
+// -
+// -
+// - ))}
+// -
+// -
+// - Extra Action
+// -
+// -
+// +
+// +
+// +
+// + Extra Action
+// +
+// +
+// \`\`\`
+// `;
diff --git a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts b/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts
deleted file mode 100644
index e23ac6f4..00000000
--- a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
-
-import { CodeSelection } from '../threadHistoryService.js';
-
-export const stringifySelections = (selections: CodeSelection[]) => {
-
-
-
- return selections.map(({ fileURI, content, selectionStr }) =>
- `\
-File: ${fileURI.fsPath}
-\`\`\`
-${content // this was the enite file which is foolish
- }
-\`\`\`${selectionStr === null ? '' : `
-Selection: ${selectionStr}`}
-`).join('\n')
-}
-
-
-export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
- let str = '';
- if (selections && selections.length > 0) {
- str += stringifySelections(selections);
- str += `Please edit the selected code following these instructions:\n`
- }
- str += `${instructions}`;
- return str;
-};
diff --git a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts b/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts
deleted file mode 100644
index 157f4292..00000000
--- a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts
+++ /dev/null
@@ -1,438 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
-
-// // used for ctrl+l
-// const partialGenerationInstructions = ``
-
-
-// // used for ctrl+k, autocomplete
-// const fimInstructions = ``
-
-
-// CTRL+K prompt:
-// const promptContent = `Here is the user's original selection:
-// \`\`\`
-// ${selection}
-// \`\`\`
-
-// The user wants to apply the following instructions to the selection:
-// ${instructions}
-
-// Please rewrite the selection following the user's instructions.
-
-// Instructions to follow:
-// 1. Follow the user's instructions
-// 2. You may ONLY CHANGE the selection, and nothing else in the file
-// 3. Make sure all brackets in the new selection are balanced the same was as in the original selection
-// 3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
-
-// Complete the following:
-// \`\`\`
-// ${prefix}
-//
${suffix}
-//
`;
-
-
-
-export const generateDiffInstructions = `
-You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
-
-Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
-
-All changes made to files must be outputted in unified diff format.
-Unified diff format instructions:
-1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
-2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
-3. Make diffs more than a few lines.
-4. Make high-level diffs rather than many one-line diffs.
-
-Here's an example of unified diff format:
-
-\`\`\`
-@@ ... @@
--def factorial(n):
-- if n == 0:
-- return 1
-- else:
-- return n * factorial(n-1)
-+def factorial(number):
-+ if number == 0:
-+ return 1
-+ else:
-+ return number * factorial(number-1)
-\`\`\`
-
-Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
-
-\`\`\`
-@@ ... @@ # This is less preferred because edits are close together and should be grouped:
--def factorial(n):
-+def factorial(number):
-- if n == 0:
-+ if number == 0:
- return 1
- else:
-- return n * factorial(n-1)
-+ return number * factorial(number-1)
-\`\`\`
-
-# Example 1:
-
-FILES
-selected file \`test.ts\`:
-\`\`\`
-x = 1
-
-{{selection}}
-
-z = 3
-\`\`\`
-
-SELECTION
-\`\`\`const y = 2\`\`\`
-
-INSTRUCTIONS
-\`\`\`y = 3\`\`\`
-
-EXPECTED RESULT
-
-We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
-\`\`\`
-@@ ... @@
--x = 1
--
--y = 2
-+x = 1
-+
-+y = 3
-\`\`\`
-
-# Example 2:
-
-FILES
-selected file \`Sidebar.tsx\`:
-\`\`\`
-import React from 'react';
-import styles from './Sidebar.module.css';
-
-interface SidebarProps {
- items: { label: string; href: string }[];
- onItemSelect?: (label: string) => void;
- onExtraButtonClick?: () => void;
-}
-
-const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
- return (
-
-
- {items.map((item, index) => (
-
- {{selection}}
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
-
-
- ))}
-
-
- Extra Action
-
-
- );
-};
-
-export default Sidebar;
-\`\`\`
-
-SELECTION
-\`\`\`
--
-- {items.map((item, index) => (
--
-- onItemSelect?.(item.label)}
-- >
-- {item.label}
--
--
-- ))}
--
--
-- Extra Action
--
--
-+
-+
-+
-+ Extra Action
-+
-+
-\`\`\`
-`;
-
-
-export const searchDiffChunkInstructions = `
-You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
-
-Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
-
-# Example 1:
-
-FILES
-selected file \`Sidebar.tsx\`:
-\`\`\`
-import React from 'react';
-import styles from './Sidebar.module.css';
-
-interface SidebarProps {
- items: { label: string; href: string }[];
- onItemSelect?: (label: string) => void;
- onExtraButtonClick?: () => void;
-}
-
-const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
- return (
-
-
- {items.map((item, index) => (
-
- onItemSelect?.(item.label)}
- >
- {item.label}
-
-
- ))}
-
-
- Extra Action
-
-
- );
-};
-
-export default Sidebar;
-\`\`\`
-
-DIFF
-\`\`\`
-@@ ... @@
--
--
-- {items.map((item, index) => (
--
-- onItemSelect?.(item.label)}
-- >
-- {item.label}
--
--
-- ))}
--
--
-- Extra Action
--
--
-+
-+
-+
-+ Extra Action
-+
-+
-\`\`\`
-
-SELECTION
-\`\`\`
-import React from 'react';
-import styles from './Sidebar.module.css';
-
-interface SidebarProps {
- items: { label: string; href: string }[];
- onItemSelect?: (label: string) => void;
- onExtraButtonClick?: () => void;
-}
-
-const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
- return (
-
-
- {items.map((item, index) => (
-\`\`\`
-
-RESULT
-The output should be \`true\` because the diff begins on the line with \`\` and this line is present in the selection.
-
-OUTPUT
-\`true\`
-`
-
-
-export const writeFileWithDiffInstructions = `
-You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
-
-Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
-
-Directions:
-1. Continue exactly where the new file \`new_file\` left off.
-2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
-3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
-
-# Example 1:
-
-ORIGINAL_FILE
-\`Sidebar.tsx\`:
-\`\`\`
-import React from 'react';
-import styles from './Sidebar.module.css';
-
-interface SidebarProps {
- items: { label: string; href: string }[];
- onItemSelect?: (label: string) => void;
- onExtraButtonClick?: () => void;
-}
-
-const Sidebar: React.FC
= ({ items, onItemSelect, onExtraButtonClick }) => {
- return (
-
-
- {items.map((item, index) => (
-
- onItemSelect?.(item.label)}
- >
- {item.label}
-
-
- ))}
-
-
- Extra Action
-
-
- );
-};
-
-export default Sidebar;
-\`\`\`
-
-DIFF
-\`\`\`
-@@ ... @@
--
--
-- {items.map((item, index) => (
--
-- onItemSelect?.(item.label)}
-- >
-- {item.label}
--
--
-- ))}
--
--
-- Extra Action
--
--
-+
-+
-+
-+ Extra Action
-+
-+
-\`\`\`
-
-NEW_FILE
-\`\`\`
-import React from 'react';
-import styles from './Sidebar.module.css';
-
-interface SidebarProps {
- items: { label: string; href: string }[];
- onItemSelect?: (label: string) => void;
- onExtraButtonClick?: () => void;
-}
-
-const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => {
- return (
-\`\`\`
-
-COMPLETION
-\`\`\`
-
- );
-};
-
-export default Sidebar;\`\`\`
-`
-
-
-
diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts
index fcef7270..1498c8f6 100644
--- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts
+++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts
@@ -1,28 +1,69 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
-import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
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';
+import { roundRangeToLines } from './sidebarActions.js';
+import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
+import { localize2 } from '../../../../nls.js';
+
+
+export type QuickEditPropsType = {
+ diffareaid: number,
+ initStreamingDiffZoneId: number | null,
+ textAreaRef: (ref: HTMLTextAreaElement | null) => void;
+ onChangeHeight: (height: number) => void;
+ onChangeText: (text: string) => void;
+ initText: string | null;
+}
+
+export type QuickEdit = {
+ startLine: number, // 0-indexed
+ beforeCode: string,
+ afterCode?: string,
+ instructions?: string,
+ responseText?: string, // model can produce a text response too
+}
-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,
+ f1: true,
+ title: localize2('voidQuickEditAction', 'Void: Quick Edit'),
+ keybinding: {
+ primary: KeyMod.CtrlCmd | KeyCode.KeyK,
+ weight: KeybindingWeight.VoidExtension,
+ }
+ });
}
+
async run(accessor: ServicesAccessor): Promise {
- console.log('hello111!')
-
- const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
- if (!model)
- return
-
- console.log('hello!')
+ const editorService = accessor.get(ICodeEditorService)
const metricsService = accessor.get(IMetricsService)
- metricsService.capture('User Action', { type: 'Ctrl+K' })
+ metricsService.capture('Ctrl+K', {})
- console.log('bye!')
+ const editor = editorService.getActiveCodeEditor()
+ if (!editor) return;
+ const model = editor.getModel()
+ if (!model) return;
+ const selection = roundRangeToLines(editor.getSelection(), { emptySelectionBehavior: 'line' })
+ if (!selection) return;
+
+
+ const { startLineNumber: startLine, endLineNumber: endLine } = selection
+
+ const inlineDiffsService = accessor.get(IInlineDiffsService)
+ inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
}
});
diff --git a/src/vs/workbench/contrib/void/browser/quickEditStateService.ts b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts
new file mode 100644
index 00000000..62f3823b
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts
@@ -0,0 +1,77 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { Emitter, Event } from '../../../../base/common/event.js';
+import { Disposable } from '../../../../base/common/lifecycle.js';
+import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
+import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
+import { QuickEdit } from './quickEditActions.js';
+
+
+
+// service that manages state
+export type VoidQuickEditState = {
+ quickEditsOfDocument: { [uri: string]: QuickEdit }
+}
+
+export interface IQuickEditStateService {
+ readonly _serviceBrand: undefined;
+
+ readonly state: VoidQuickEditState; // readonly to the user
+ setState(newState: Partial): void;
+ onDidChangeState: Event;
+
+ onDidFocusChat: Event;
+ onDidBlurChat: Event;
+ fireFocusChat(): void;
+ fireBlurChat(): void;
+
+}
+
+export const IQuickEditStateService = createDecorator('voidQuickEditStateService');
+class VoidQuickEditStateService extends Disposable implements IQuickEditStateService {
+ _serviceBrand: undefined;
+
+ static readonly ID = 'voidQuickEditStateService';
+
+ private readonly _onDidChangeState = new Emitter();
+ readonly onDidChangeState: Event = this._onDidChangeState.event;
+
+ private readonly _onFocusChat = new Emitter();
+ readonly onDidFocusChat: Event = this._onFocusChat.event;
+
+ private readonly _onBlurChat = new Emitter();
+ readonly onDidBlurChat: Event = this._onBlurChat.event;
+
+
+ // state
+ state: VoidQuickEditState
+
+ constructor(
+ ) {
+ super()
+
+ // initial state
+ this.state = { quickEditsOfDocument: {} }
+ }
+
+
+ setState(newState: Partial) {
+
+ this.state = { ...this.state, ...newState }
+ this._onDidChangeState.fire()
+ }
+
+ fireFocusChat() {
+ this._onFocusChat.fire()
+ }
+
+ fireBlurChat() {
+ this._onBlurChat.fire()
+ }
+
+}
+
+registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);
diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js
index 118e2eaa..436d10ce 100755
--- a/src/vs/workbench/contrib/void/browser/react/build.js
+++ b/src/vs/workbench/contrib/void/browser/react/build.js
@@ -1,16 +1,86 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import { spawn, execSync } from 'child_process';
+// Added lines below
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+function doesPathExist(filePath) {
+ try {
+ const stats = fs.statSync(filePath);
+
+ return stats.isFile();
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ return false;
+ }
+ throw err;
+ }
+}
+
+/*
+
+This function finds `globalDesiredPath` given `localDesiredPath` and `currentPath`
+
+Diagram:
+
+...basePath/
+└── void/
+ ├── ...currentPath/ (defined globally)
+ └── ...localDesiredPath/ (defined locally)
+
+*/
+function findDesiredPathFromLocalPath(localDesiredPath, currentPath) {
+
+ // walk upwards until currentPath + localDesiredPath exists
+ while (!doesPathExist(path.join(currentPath, localDesiredPath))) {
+ const parentDir = path.dirname(currentPath);
+
+ if (parentDir === currentPath) {
+ return undefined;
+ }
+
+ currentPath = parentDir;
+ }
+
+ // return the `globallyDesiredPath`
+ const globalDesiredPath = path.join(currentPath, localDesiredPath)
+ return globalDesiredPath;
+}
+
+// hack to refresh styles automatically
+function saveStylesFile() {
+ setTimeout(() => {
+ try {
+ const pathToCssFile = findDesiredPathFromLocalPath('./src/vs/workbench/contrib/void/browser/react/src2/styles.css', __dirname);
+
+ if (pathToCssFile === undefined) {
+ console.error('[scope-tailwind] Error finding styles.css');
+ return;
+ }
+
+ // Or re-write with the same content:
+ const content = fs.readFileSync(pathToCssFile, 'utf8');
+ fs.writeFileSync(pathToCssFile, content, 'utf8');
+ console.log('[scope-tailwind] Force-saved styles.css');
+ } catch (err) {
+ console.error('[scope-tailwind] Error saving styles.css:', err);
+ }
+ }, 3000);
+}
const args = process.argv.slice(2);
const isWatch = args.includes('--watch') || args.includes('-w');
if (isWatch) {
// Watch mode
- // Create a watcher for scope-tailwind using nodemon
const scopeTailwindWatcher = spawn('npx', [
'nodemon',
'--watch', 'src',
@@ -19,15 +89,17 @@ if (isWatch) {
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
]);
- // Create a watcher for tsup in watch mode
const tsupWatcher = spawn('npx', [
'tsup',
'--watch'
]);
- // Handle scope-tailwind watcher output
scopeTailwindWatcher.stdout.on('data', (data) => {
console.log(`[scope-tailwind] ${data}`);
+ // If the output mentions "styles.css", trigger the save:
+ if (data.toString().includes('styles.css')) {
+ saveStylesFile();
+ }
});
scopeTailwindWatcher.stderr.on('data', (data) => {
diff --git a/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx
index 18584d7b..31fee155 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx
@@ -1,7 +1,7 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import { diffLines, Change } from 'diff';
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 b2c83e66..43abd5b1 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
@@ -1,46 +1,30 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
-import React, { ReactNode } from "react"
-import SyntaxHighlighter from "react-syntax-highlighter";
-import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
+import React from 'react';
+
+import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
-export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
+export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => {
+ const isSingleLine = !codeEditorProps.initValue.includes('\n')
- const customStyle = {
- ...atomOneDarkReasonable,
- 'code[class*="language-"]': {
- ...atomOneDarkReasonable['code[class*="language-"]'],
- background: "none",
- },
- }
-
- return (<>
-
-
- {buttonsOnHover === null ? null : (
-
- )}
-
-
-
- {text}
-
+ return (
+ <>
+
+ {buttonsOnHover === null ? null : (
+
+ )}
+
-
- >
+ >
)
}
-
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 0977a18a..79429e20 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
@@ -1,12 +1,13 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import React, { JSX, useCallback, useEffect, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
-import { useService } from '../util/services.js'
+import { useAccessor } from '../util/services.js'
+import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
enum CopyButtonState {
@@ -17,14 +18,16 @@ enum CopyButtonState {
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
-const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
+const CodeButtonsOnHover = ({ text }: { text: string }) => {
+ const accessor = useAccessor()
+
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
- const inlineDiffService = useService('inlineDiffService')
-
- const clipboardService = useService('clipboardService')
-
+ const inlineDiffService = accessor.get('IInlineDiffsService')
+ const clipboardService = accessor.get('IClipboardService')
+ const metricsService = accessor.get('IMetricsService')
useEffect(() => {
+
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
@@ -36,29 +39,52 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
clipboardService.writeText(text)
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
+ metricsService.capture('Copy Code', { length: text.length }) // capture the length only
+
}, [text, clipboardService])
+ const onApply = useCallback(() => {
+ inlineDiffService.startApplying({
+ featureName: 'Ctrl+L',
+ userMessage: text,
+ })
+ metricsService.capture('Apply Code', { length: text.length }) // capture the length only
+ }, [inlineDiffService])
+
+ const isSingleLine = !text.includes('\n')
+
return <>
{copyButtonState}
{
-
- inlineDiffService.startStreaming({ featureName: 'Ctrl+L' }, text)
- }}
+ // btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
+ className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
+ onClick={onApply}
>
Apply
>
}
+export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
+ return
+ {children}
+
+}
-const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
+const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
@@ -69,53 +95,68 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "code") {
return
}
+ initValue={t.text}
+ language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language
+ buttonsOnHover={
}
/>
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
- return
{t.text}
+ const headingClasses: { [h: string]: string } = {
+ h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
+ h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
+ h3: "text-2xl font-semibold mt-6 mb-4",
+ h4: "text-xl font-semibold mt-6 mb-4",
+ h5: "text-lg font-semibold mt-6 mb-4",
+ h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
+ }
+ return
{t.text}
}
if (t.type === "table") {
return (
-
-
-
- {t.header.map((cell: any, index: number) => (
-
- {cell.raw}
-
- ))}
-
-
-
- {t.rows.map((row: any[], rowIndex: number) => (
-
- {row.map((cell: any, cellIndex: number) => (
-
+
+
+
+ {t.header.map((cell: any, index: number) => (
+
{cell.raw}
-
+
))}
- ))}
-
-
+
+
+ {t.rows.map((row: any[], rowIndex: number) => (
+
+ {row.map((cell: any, cellIndex: number) => (
+
+ {cell.raw}
+
+ ))}
+
+ ))}
+
+
+
)
}
if (t.type === "hr") {
- return
+ return
}
if (t.type === "blockquote") {
- return {t.text}
+ return {t.text}
}
if (t.type === "list") {
@@ -123,14 +164,16 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
return (
{t.items.map((item, index) => (
-
+
{item.task && (
-
+
)}
-
+
+
+
))}
@@ -145,13 +188,12 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
>
if (nested)
return contents
- return {contents}
+ return {contents}
}
- // don't actually render tags, just render strings of them
if (t.type === "html") {
return (
-
+
{``}
{t.raw}
{``}
@@ -169,30 +211,40 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "link") {
return (
-
+ { window.open(t.href) }}
+ href={t.href}
+ title={t.title ?? undefined}
+ >
{t.text}
)
}
if (t.type === "image") {
- return
+ return
}
if (t.type === "strong") {
- return {t.text}
+ return {t.text}
}
if (t.type === "em") {
- return {t.text}
+ return {t.text}
}
// inline code
if (t.type === "codespan") {
return (
-
+
{t.text}
-
+
)
}
@@ -202,24 +254,24 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
// strikethrough
if (t.type === "del") {
- return {t.text}
+ return {t.text}
}
// default
return (
-
-
Unknown type:
+
+ Unknown type:
{t.raw}
)
}
-export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
+export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
-
+
))}
>
)
diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx
new file mode 100644
index 00000000..53c68998
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx
@@ -0,0 +1,23 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import React, { useEffect, useState } from 'react'
+import { useIsDark, useSidebarState } from '../util/services.js'
+import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
+import { QuickEditChat } from './QuickEditChat.js'
+import { QuickEditPropsType } from '../../../quickEditActions.js'
+
+export const QuickEdit = (props: QuickEditPropsType) => {
+
+ const isDark = useIsDark()
+
+ return
+
+
+
+
+
+
+}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx
new file mode 100644
index 00000000..80988038
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx
@@ -0,0 +1,190 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
+import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
+import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
+import { QuickEditPropsType } from '../../../quickEditActions.js';
+import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
+import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
+import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
+import { useRefState } from '../util/helpers.js';
+import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
+
+export const QuickEditChat = ({
+ diffareaid,
+ initStreamingDiffZoneId,
+ onChangeHeight,
+ onChangeText: onChangeText_,
+ textAreaRef: textAreaRef_,
+ initText
+}: QuickEditPropsType) => {
+
+ const accessor = useAccessor()
+ const inlineDiffsService = accessor.get('IInlineDiffsService')
+ const sizerRef = useRef
(null)
+ const textAreaRef = useRef(null)
+ const textAreaFnsRef = useRef(null)
+
+ useEffect(() => {
+ const inputContainer = sizerRef.current
+ if (!inputContainer) return;
+ // only observing 1 element
+ let resizeObserver: ResizeObserver | undefined
+ resizeObserver = new ResizeObserver((entries) => {
+ const height = entries[0].borderBoxSize[0].blockSize
+ onChangeHeight(height)
+ })
+ resizeObserver.observe(inputContainer);
+ return () => { resizeObserver?.disconnect(); };
+ }, [onChangeHeight]);
+
+
+ // state of current message
+ const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
+ const isDisabled = instructionsAreEmpty
+
+ const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId)
+ const isStreaming = currStreamingDiffZoneRef.current !== null
+
+ const onSubmit = useCallback((e: FormEvent) => {
+ if (isDisabled) return
+ if (currStreamingDiffZoneRef.current !== null) return
+ textAreaFnsRef.current?.disable()
+
+ const instructions = textAreaRef.current?.value ?? ''
+ const id = inlineDiffsService.startApplying({
+ featureName: 'Ctrl+K',
+ diffareaid: diffareaid,
+ userMessage: instructions,
+ })
+ setCurrentlyStreamingDiffZone(id ?? null)
+ }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
+
+ const onInterrupt = useCallback(() => {
+ if (currStreamingDiffZoneRef.current === null) return
+ inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current)
+ setCurrentlyStreamingDiffZone(null)
+ textAreaFnsRef.current?.enable()
+ }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService])
+
+
+ const onX = useCallback(() => {
+ onInterrupt()
+ inlineDiffsService.removeCtrlKZone({ diffareaid })
+ }, [inlineDiffsService, diffareaid])
+
+ useScrollbarStyles(sizerRef)
+
+ const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
+
+ return
+
+
+}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx
new file mode 100644
index 00000000..301c0f24
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx
@@ -0,0 +1,12 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { mountFnGenerator } from '../util/mountFnGenerator.js'
+import { QuickEdit } from './QuickEdit.js'
+
+
+export const mountCtrlK = mountFnGenerator(QuickEdit)
+
+
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx
index a095f6fd..2ccc54b2 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx
@@ -1,7 +1,7 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx
index 714fbf26..84fe410a 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx
@@ -1,14 +1,15 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
+import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
export const ErrorDisplay = ({
- message,
+ message:message_,
fullError,
onDismiss,
showDismiss,
@@ -20,54 +21,46 @@ export const ErrorDisplay = ({
}) => {
const [isExpanded, setIsExpanded] = useState(false);
- let details: string | null = null;
+ const details = errorDetails(fullError)
- if (fullError === null) {
- details = null
- }
- else if (typeof fullError === 'object') {
- details = JSON.stringify(fullError, null, 2)
- }
- else if (typeof fullError === 'string') {
- details = null
- }
+ const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_
return (
{/* Header */}
-
-
-
-
-
+
+
+
+
+
{/* eg Error */}
Error
-
+
{/* eg Something went wrong */}
{message}
-
+
{details && (
- setIsExpanded(!isExpanded)}
>
{isExpanded ? (
-
+
) : (
-
+
)}
)}
{showDismiss && onDismiss && (
-
-
+
)}
@@ -75,10 +68,10 @@ export const ErrorDisplay = ({
{/* Expandable Details */}
{isExpanded && details && (
-
+
-
Full Error:
-
{details}
+
Full Error:
+
{details}
)}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx
index 55fa83b7..839a6679 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx
@@ -1,7 +1,7 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { mountFnGenerator } from '../util/mountFnGenerator.js'
@@ -13,17 +13,26 @@ import { useIsDark, useSidebarState } from '../util/services.js';
// import { SidebarChat } from './SidebarChat.js';
import '../styles.css'
-import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
- const { isHistoryOpen, currentTab: tab } = sidebarState
+ const { currentTab: tab } = sidebarState
- const isDark = useIsDark()
- return
-
+ // const isDark = useIsDark()
+ return
+
{/*
{
const tabs = ['chat', 'settings', 'threadSelector']
@@ -31,11 +40,11 @@ export const Sidebar = ({ className }: { className: string }) => {
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab} */}
-
*/}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
index e5ff34c9..75e4579e 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
@@ -1,31 +1,38 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
-import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react';
+import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
-import { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js';
-import { generateDiffInstructions } from '../../../prompt/systemPrompts.js';
-import { userInstructionsStr } from '../../../prompt/stringifySelections.js';
-import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js';
+import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
+import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
-import { IModelService } from '../../../../../../../editor/common/services/model.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
-import { getCmdKey } from '../../../helpers/getCmdKey.js'
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
-import { VoidInputBox } from '../util/inputs.js';
-import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
+import { TextAreaFns, VoidCodeEditorProps, VoidInputBox2 } from '../util/inputs.js';
+import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js';
+import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
+import { ISidebarStateService } from '../../../sidebarStateService.js';
+import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
+import { IModelService } from '../../../../../../../editor/common/services/model.js';
+import { SidebarThreadSelector } from './SidebarThreadSelector.js';
+import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
+import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
+import { ArrowBigLeftDash, CopyX, Delete, FileX2, SquareX, X } from 'lucide-react';
+import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
+import { Pencil } from 'lucide-react'
+import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
-const IconX = ({ size, className = '' }: { size: number, className?: string }) => {
+export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => {
return (
{
return (
@@ -60,10 +67,9 @@ const IconArrowUp = ({ size, className = '' }: { size: number, className?: strin
fill="black"
fillRule="evenodd"
clipRule="evenodd"
- d="M15.1918 8.90615C15.6381 8.45983 16.3618 8.45983 16.8081 8.90615L21.9509 14.049C22.3972 14.4953 22.3972 15.2189 21.9509 15.6652C21.5046 16.1116 20.781 16.1116 20.3347 15.6652L17.1428 12.4734V22.2857C17.1428 22.9169 16.6311 23.4286 15.9999 23.4286C15.3688 23.4286 14.8571 22.9169 14.8571 22.2857V12.4734L11.6652 15.6652C11.2189 16.1116 10.4953 16.1116 10.049 15.6652C9.60265 15.2189 9.60265 14.4953 10.049 14.049L15.1918 8.90615Z"
+ d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
>
-
);
};
@@ -86,9 +92,121 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string
};
-const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => {
+export const IconWarning = ({ size, className = '' }: { size: number, className?: string }) => {
+ return (
+
+
+
+ );
+};
+
+
+export const IconLoading = ({ className = '' }: { className?: string }) => {
+
+ const [loadingText, setLoadingText] = useState('.');
+
+ useEffect(() => {
+ let intervalId;
+
+ // Function to handle the animation
+ const toggleLoadingText = () => {
+ if (loadingText === '...') {
+ setLoadingText('.');
+ } else {
+ setLoadingText(loadingText + '.');
+ }
+ };
+
+ // Start the animation loop
+ intervalId = setInterval(toggleLoadingText, 300);
+
+ // Cleanup function to clear the interval when component unmounts
+ return () => clearInterval(intervalId);
+ }, [loadingText, setLoadingText]);
+
+ return {loadingText}
;
+
+}
+
+const useResizeObserver = () => {
+ const ref = useRef(null);
+ const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
+
+ useEffect(() => {
+ if (ref.current) {
+ const resizeObserver = new ResizeObserver((entries) => {
+ if (entries.length > 0) {
+ const entry = entries[0];
+ setDimensions({
+ height: entry.contentRect.height,
+ width: entry.contentRect.width
+ });
+ }
+ });
+
+ resizeObserver.observe(ref.current);
+
+ return () => {
+ if (ref.current)
+ resizeObserver.unobserve(ref.current);
+ };
+ }
+ }, []);
+
+ return [ref, dimensions] as const;
+};
+
+
+
+
+type ButtonProps = ButtonHTMLAttributes
+const DEFAULT_BUTTON_SIZE = 22;
+export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => {
+
+ return
+
+
+}
+
+export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes) => {
+
+ return
+
+
+}
+
+
+const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject }) => {
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
- const divRef = useRef(null);
+
+ const divRef = scrollContainerRef
const scrollToBottom = () => {
if (divRef.current) {
@@ -133,13 +251,6 @@ const ScrollToBottomContainer = ({ children, className, style }: { children: Rea
};
-// read files from VSCode
-const VSReadFile = async (modelService: IModelService, uri: URI): Promise => {
- const model = modelService.getModel(uri)
- if (!model) return null
- return model.getValue(EndOfLinePreference.LF)
-}
-
const getBasename = (pathStr: string) => {
// 'unixify' path
@@ -149,349 +260,439 @@ const getBasename = (pathStr: string) => {
}
export const SelectedFiles = (
- { type, selections, setStaging }:
- | { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined }
- | { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) }
+ { type, selections, setSelections, showProspectiveSelections }:
+ | { type: 'past', selections: CodeSelection[]; setSelections?: undefined, showProspectiveSelections?: undefined }
+ | { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void), showProspectiveSelections?: boolean }
) => {
// index -> isOpened
const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? [])
+ // state for tracking hover on clear all button
+ const [isClearHovered, setIsClearHovered] = useState(false)
+
+ const accessor = useAccessor()
+ const commandService = accessor.get('ICommandService')
+
+ // state for tracking prospective files
+ const { currentUri } = useUriState()
+ const [recentUris, setRecentUris] = useState([])
+ const maxRecentUris = 10
+ const maxProspectiveFiles = 3
+ useEffect(() => { // handle recent files
+ if (!currentUri) return
+ setRecentUris(prev => {
+ const withoutCurrent = prev.filter(uri => uri.fsPath !== currentUri.fsPath) // remove duplicates
+ const withCurrent = [currentUri, ...withoutCurrent]
+ return withCurrent.slice(0, maxRecentUris)
+ })
+ }, [currentUri])
+ let prospectiveSelections: CodeStagingSelection[] = []
+ if (type === 'staging' && showProspectiveSelections) { // handle prospective files
+ // add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet
+ prospectiveSelections = recentUris
+ .filter(uri => !selections.find(s => s.range === null && s.fileURI.fsPath === uri.fsPath))
+ .slice(0, maxProspectiveFiles)
+ .map(uri => ({
+ type: 'File',
+ fileURI: uri,
+ selectionStr: null,
+ range: null,
+ }))
+ }
+
+ const allSelections = [...selections, ...prospectiveSelections]
+
+ if (allSelections.length === 0) {
+ return null
+ }
+
return (
- !!selections && selections.length !== 0 && (
-
- {selections.map((selection, i) => {
+
- const showSelectionText = selection.selectionStr && selectionIsOpened[i]
+ {allSelections.map((selection, i) => {
- return (
-
- {/* selection summary */}
-
selections.length - 1
+
+ const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
+
+ const selectionHTML = (
+ {/* selection summary */}
+
+
{
+ ${isThisSelectionProspective ? 'bg-void-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
+ text-xs text-nowrap
+ border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
+ transition-all duration-150`}
+ onClick={() => {
+ if (isThisSelectionProspective) { // add prospective selection to selections
+ if (type !== 'staging') return; // (never)
+ setSelections([...selections, selection as CodeStagingSelection])
+
+ } else if (isThisSelectionAFile) { // open files
+ commandService.executeCommand('vscode.open', selection.fileURI, {
+ preview: true,
+ // preserveFocus: false,
+ });
+ } else { // show text
setSelectionIsOpened(s => {
const newS = [...s]
newS[i] = !newS[i]
return newS
});
- }}
- >
-
- {/* file name */}
- {getBasename(selection.fileURI.fsPath)}
- {/* selection range */}
- {selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
-
-
- {/* type of selection */}
- {selection.selectionStr !== null ? 'Selection' : 'File'}
-
- {/* X button */}
- {type === 'staging' && // hoveredIdx === i
- {
- e.stopPropagation();
- if (type !== 'staging') return;
- setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
- setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
- }}
- >
-
-
}
-
- {/* selection text */}
- {showSelectionText &&
-
-
-
- }
+ }}
+ >
+
+ {/* file name */}
+ {getBasename(selection.fileURI.fsPath)}
+ {/* selection range */}
+ {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
+
+
+ {/* X button */}
+ {type === 'staging' && !isThisSelectionProspective &&
+
{
+ e.stopPropagation(); // don't open/close selection
+ if (type !== 'staging') return;
+ setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
+ setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
+ }}
+ >
+
+ }
+
+
- )
- })}
-
- )
+
+ {/* clear all selections button */}
+ {type !== 'staging' || selections.length === 0 || i !== selections.length - 1
+ ? null
+ :
+
setIsClearHovered(true)}
+ onMouseLeave={() => setIsClearHovered(false)}
+ >
+ { setSelections([]) }}
+ />
+
+
+ }
+
+ {/* selection text */}
+ {isThisSelectionOpened &&
+
{
+ e.stopPropagation(); // don't focus input box
+ }}
+ >
+
+
+ }
+
)
+
+ return
+ {selections.length > 0 && i === selections.length &&
+
// divider between `selections` and `prospectiveSelections`
+ }
+ {selectionHTML}
+
+
+ })}
+
+
+
+
)
}
-const ChatBubble = ({ chatMessage }: {
- chatMessage: ChatMessage
-}) => {
+
+const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => {
+
+ return
+
+ {children}
+ {isLoading && }
+
+
+ {/* edit button */}
+ {/* {role === 'user' &&
+
{ setIsEditMode(v => !v); }}
+ />
+ } */}
+
+}
+
+
+const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
const role = chatMessage.role
- if (!chatMessage.displayContent)
+ // edit mode state
+ const [isEditMode, setIsEditMode] = useState(false)
+
+
+ if (!chatMessage.content) { // don't show if empty
return null
+ }
let chatbubbleContents: React.ReactNode
if (role === 'user') {
chatbubbleContents = <>
-
+
{chatMessage.displayContent}
+
+ {/* {!isEditMode ? chatMessage.displayContent : <>>} */}
+ {/* edit mode content */}
+ {/* TODO this should be the same input box as in the Sidebar */}
+ {/*
*/}
+
>
}
else if (role === 'assistant') {
- chatbubbleContents =
// sectionsHTML
+ chatbubbleContents =
}
- return
-
- {chatbubbleContents}
-
-
+ return
+ {chatbubbleContents}
+
}
-
export const SidebarChat = () => {
- const inputBoxRef: React.MutableRefObject
= useRef(null);
+ const textAreaRef = useRef(null)
+ const textAreaFnsRef = useRef(null)
- const modelService = useService('modelService')
+ const accessor = useAccessor()
+ // const modelService = accessor.get('IModelService')
+ const commandService = accessor.get('ICommandService')
// ----- HIGHER STATE -----
// sidebar state
- const sidebarStateService = useService('sidebarStateService')
+ const sidebarStateService = accessor.get('ISidebarStateService')
useEffect(() => {
const disposables: IDisposable[] = []
disposables.push(
- sidebarStateService.onDidFocusChat(() => { inputBoxRef.current?.focus() }),
- sidebarStateService.onDidBlurChat(() => { inputBoxRef.current?.blur() })
+ sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
+ sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
)
return () => disposables.forEach(d => d.dispose())
- }, [sidebarStateService, inputBoxRef])
+ }, [sidebarStateService, textAreaRef])
+
+ const { isHistoryOpen } = useSidebarState()
// threads state
- const threadsState = useThreadsState()
- const threadsStateService = useService('threadsStateService')
+ const chatThreadsState = useChatThreadsState()
+ const chatThreadsService = accessor.get('IChatThreadService')
+
+ const currentThread = chatThreadsService.getCurrentThread()
+ const previousMessages = currentThread?.messages ?? []
+ const selections = chatThreadsState.currentStagingSelections
+
+ // stream state
+ const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
+ const isStreaming = !!currThreadStreamState?.streamingToken
+ const latestError = currThreadStreamState?.error
+ const messageSoFar = currThreadStreamState?.messageSoFar
// ----- SIDEBAR CHAT state (local) -----
- // state of chat
- const [messageStream, setMessageStream] = useState(null)
- const [isLoading, setIsLoading] = useState(false)
- const latestRequestIdRef = useRef(null)
-
- const [latestError, setLatestError] = useState[0] | null>(null)
-
- const llmMessageService = useService('llmMessageService')
-
// state of current message
- const [instructions, setInstructions] = useState('') // the user's instructions
- const isDisabled = !instructions.trim()
- const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
- const [sidebarHeight, setSidebarHeight] = useState(0)
- const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
+ const initVal = ''
+ const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
+ const isDisabled = instructionsAreEmpty
+
+ const [sidebarRef, sidebarDimensions] = useResizeObserver()
+ const [formRef, formDimensions] = useResizeObserver()
+ const [historyRef, historyDimensions] = useResizeObserver()
+
+ useScrollbarStyles(sidebarRef)
- const onSubmit = async (e: FormEvent) => {
+ const onSubmit = async () => {
- e.preventDefault()
if (isDisabled) return
- if (isLoading) return
-
-
-
- const currSelns = threadsStateService.state._currentStagingSelections ?? []
- const selections = !currSelns ? null : await Promise.all(
- currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
- ).then(
- (files) => files.filter(file => file.content !== null) as CodeSelection[]
- )
-
-
- // // TODO don't save files to the thread history
- // const selectedSnippets = currSelns.filter(sel => sel.selectionStr !== null)
- // const selectedFiles = await Promise.all( // do not add these to the context history
- // currSelns.filter(sel => sel.selectionStr === null)
- // .map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
- // ).then(
- // (files) => files.filter(file => file.content !== null) as CodeSelection[]
- // )
- // const contextToSendToLLM = ''
- // const contextToAddToHistory = ''
-
-
- // add system message to chat history
- const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
- threadsStateService.addMessageToCurrentThread(systemPromptElt)
-
- // add user's message to chat history
- const userHistoryElt: ChatMessage = { role: 'user', content: userInstructionsStr(instructions, selections), displayContent: instructions, selections: selections }
- threadsStateService.addMessageToCurrentThread(userHistoryElt)
-
- const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
+ if (isStreaming) return
// send message to LLM
- setIsLoading(true) // must come before message is sent so onError will work
- setLatestError(null)
- if (inputBoxRef.current) {
- inputBoxRef.current.value = ''; // this triggers onDidChangeText
- inputBoxRef.current.blur();
- }
+ const userMessage = textAreaRef.current?.value ?? ''
+ await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
- const object: ServiceSendLLMMessageParams = {
- logging: { loggingName: 'Chat' },
- messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content || '(null)' })),],
- onText: ({ newText, fullText }) => setMessageStream(fullText),
- onFinalMessage: ({ fullText: content }) => {
- console.log('chat: running final message')
-
- // add assistant's message to chat history, and clear selection
- const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
- threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
- setMessageStream(null)
- setIsLoading(false)
- },
- onError: ({ message, fullError }) => {
- console.log('chat: running error', message, fullError)
-
- // add assistant's message to chat history, and clear selection
- let content = messageStream ?? ''; // just use the current content
- const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null, }
- threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
-
- setMessageStream('')
- setIsLoading(false)
-
- setLatestError({ message, fullError })
- },
- featureName: 'Ctrl+L',
-
- }
-
- const latestRequestId = llmMessageService.sendLLMMessage(object)
- latestRequestIdRef.current = latestRequestId
-
- threadsStateService.setStaging([]) // clear staging
+ chatThreadsService.setStaging([]) // clear staging
+ textAreaFnsRef.current?.setValue('')
+ textAreaRef.current?.focus() // focus input after submit
}
const onAbort = () => {
- // abort the LLM call
- if (latestRequestIdRef.current)
- llmMessageService.abort(latestRequestIdRef.current)
-
- // if messageStream was not empty, add it to the history
- const llmContent = messageStream ?? ''
- const assistantHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream || null, }
- threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
-
- setMessageStream('')
- setIsLoading(false)
-
+ const threadId = currentThread.id
+ chatThreadsService.cancelStreaming(threadId)
}
- const currentThread = threadsStateService.getCurrentThread(threadsState)
-
- const selections = threadsState._currentStagingSelections
-
- const previousMessages = currentThread?.messages ?? []
-
// const [_test_messages, _set_test_messages] = useState([])
+ const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel()
+
+ // scroll to top on thread switch
+ const scrollContainerRef = useRef(null)
+ useEffect(() => {
+ if (isHistoryOpen)
+ scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
+ }, [isHistoryOpen, currentThread.id])
+
return { if (ref) { setSidebarHeight(ref.clientHeight); } }}
+ ref={sidebarRef}
className={`w-full h-full`}
>
+ {/* thread selector */}
+
+
+
+
+ {/* previous messages + current stream */}
{/* previous messages */}
- {previousMessages.map((message, i) => )}
+ {previousMessages.map((message, i) =>
+
+ )}
{/* message stream */}
-
+
- {/* {_test_messages.map((_, i) => div {i}
)}
- {`totalHeight: ${sidebarHeight - formHeight - 30}`}
- {`sidebarHeight: ${sidebarHeight}`}
- {`formHeight: ${formHeight}`}
- { _set_test_messages(d => [...d, 'asdasdsadasd']) }}>add div */}
+
+ {/* error message */}
+ {latestError === undefined ? null :
+
+ { chatThreadsService.dismissStreamError(currentThread.id) }}
+ showDismiss={true}
+ />
+
+ { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
+
+ }
{/* input box */}
0 ? 'absolute bottom-0' : ''}`}
+ className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
>
-
}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
index 6a2b1943..cca66c58 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
@@ -1,10 +1,12 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
import React from "react";
-import { useService, useThreadsState } from '../util/services.js';
+import { useAccessor, useChatThreadsState } from '../util/services.js';
+import { ISidebarStateService } from '../../../sidebarStateService.js';
+import { IconX } from './SidebarChat.js';
const truncate = (s: string) => {
@@ -17,73 +19,101 @@ const truncate = (s: string) => {
export const SidebarThreadSelector = () => {
- const threadsState = useThreadsState()
- const threadsStateService = useService('threadsStateService')
- const sidebarStateService = useService('sidebarStateService')
+ const threadsState = useChatThreadsState()
+
+ const accessor = useAccessor()
+ const chatThreadsService = accessor.get('IChatThreadService')
+ const sidebarStateService = accessor.get('ISidebarStateService')
const { allThreads } = threadsState
// sorted by most recent to least recent
- const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
+ const sortedThreadIds = Object.keys(allThreads ?? {})
+ .sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
+ .filter(threadId => allThreads![threadId].messages.length !== 0)
return (
-
+
- {/* X button at top right */}
-
-
sidebarStateService.setState({ isHistoryOpen: false })}>
-
-
-
+
+ {/* title */}
+
{`History`}
+ {/* X button at top right */}
+ sidebarStateService.setState({ isHistoryOpen: false })}
+ >
+
{/* a list of all the past threads */}
-
- {sortedThreadIds.map((threadId) => {
- if (!allThreads)
- return <>Error: Threads not found.>
- const pastThread = allThreads[threadId]
+
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/index.tsx
index a174f0ad..64143bfd 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/index.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/index.tsx
@@ -1,3 +1,8 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { Sidebar } from './Sidebar.js'
diff --git a/src/vs/workbench/contrib/void/browser/react/src/styles.css b/src/vs/workbench/contrib/void/browser/react/src/styles.css
index 2ceabda6..59197583 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/styles.css
+++ b/src/vs/workbench/contrib/void/browser/react/src/styles.css
@@ -1,21 +1,35 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
@tailwind base;
@tailwind components;
@tailwind utilities;
-@layer components {
- .select-ellipsis select {
- text-overflow: ellipsis;
- white-space: nowrap;
- padding-right: 24px;
- }
+.select-child-restyle select {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 24px;
}
+* {
+ outline: none !important;
+}
+
+
+
+.inherit-bg-all-restyle > * {
+ background-color: inherit !important;
+}
+
+
+.bg-editor-style-override {
+ --vscode-sideBar-background: var(--vscode-editor-background);
+}
+
+
/* html {
font-size: var(--vscode-font-size);
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx
new file mode 100644
index 00000000..32ab1902
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx
@@ -0,0 +1,19 @@
+import { useCallback, useRef, useState } from 'react'
+
+
+
+type ReturnType = [
+ { readonly current: T },
+ (t: T) => void
+]
+
+// use this if state might be too slow to catch
+export const useRefState = (initVal: T): ReturnType => {
+ const [_, _setState] = useState(false)
+ const ref = useRef(initVal)
+ const setState = useCallback((newVal: T) => {
+ _setState(n => !n) // call rerender
+ ref.current = newVal
+ }, [])
+ return [ref, setState]
+}
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 70be6772..c1421f66 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
@@ -1,21 +1,32 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Glass Devtools, Inc. All rights reserved.
- * Void Editor additions licensed under the AGPL 3.0 License.
- *--------------------------------------------------------------------------------------------*/
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
-import React, { useCallback, useEffect, useRef } from 'react';
-import { useService } from '../util/services.js';
+import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
-import { defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
+import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
+import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js';
+
+import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
+import { useAccessor } from './services.js';
+import { ITextModel } from '../../../../../../../editor/common/model.js';
+import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
+import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
+// type guard
+const isConstructor = (f: any)
+ : f is { new(...params: any[]): any } => {
+ return !!f.prototype && f.prototype.constructor === f;
+}
export const WidgetComponent = ({ ctor, propsFn, dispose, onCreateInstance, children, className }
: {
- ctor: { new(...params: CtorParams): Instance },
- propsFn: (container: HTMLDivElement) => CtorParams,
+ ctor: { new(...params: CtorParams): Instance } | ((container: HTMLDivElement) => Instance),
+ propsFn: (container: HTMLDivElement) => CtorParams, // unused if fn
onCreateInstance: (instance: Instance) => IDisposable[],
dispose: (instance: Instance) => void,
children?: React.ReactNode,
@@ -25,7 +36,7 @@ export const WidgetComponent = ({ ctor, prop
const containerRef = useRef(null);
useEffect(() => {
- const instance = new ctor(...propsFn(containerRef.current!));
+ const instance = isConstructor(ctor) ? new ctor(...propsFn(containerRef.current!)) : ctor(containerRef.current!)
const disposables = onCreateInstance(instance);
return () => {
disposables.forEach(d => d.dispose());
@@ -37,8 +48,105 @@ export const WidgetComponent = ({ ctor, prop
}
+export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
+type InputBox2Props = {
+ initValue?: string | null;
+ placeholder: string;
+ multiline: boolean;
+ fnsRef?: { current: null | TextAreaFns };
+ className?: string;
+ onChangeText?: (value: string) => void;
+ onKeyDown?: (e: React.KeyboardEvent) => void;
+ onChangeHeight?: (newHeight: number) => void;
+}
+export const VoidInputBox2 = forwardRef(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
-export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: {
+ // mirrors whatever is in ref
+ const textAreaRef = useRef(null)
+ const [isEnabled, setEnabled] = useState(true)
+
+ const adjustHeight = useCallback(() => {
+ const r = textAreaRef.current
+ if (!r) return
+
+ r.style.height = 'auto' // set to auto to reset height, then set to new height
+
+ if (r.scrollHeight === 0) return requestAnimationFrame(adjustHeight)
+ const h = r.scrollHeight
+ const newHeight = Math.min(h + 1, 500) // plus one to avoid scrollbar appearing when it shouldn't
+ r.style.height = `${newHeight}px`
+ }, []);
+
+
+
+ const fns: TextAreaFns = useMemo(() => ({
+ setValue: (val) => {
+ const r = textAreaRef.current
+ if (!r) return
+ r.value = val
+ onChangeText?.(r.value)
+ adjustHeight()
+ },
+ enable: () => { setEnabled(true) },
+ disable: () => { setEnabled(false) },
+ }), [onChangeText, adjustHeight])
+
+
+
+ useEffect(() => {
+ if (initValue)
+ fns.setValue(initValue)
+ }, [initValue])
+
+
+
+
+ return (
+