improvements but might still be buggy

This commit is contained in:
Andrew Pareles 2024-12-23 02:43:19 -08:00
parent 98a22d2eb6
commit 80a87ff3c6
3 changed files with 245 additions and 231 deletions

View file

@ -0,0 +1,176 @@
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
// lets you add a "consistent" item to a Model (aka URI),
// instead of just to a single editor
type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); }
export interface IConsistentItemService {
readonly _serviceBrand: undefined;
addConsistentItemToURI(inputs: AddItemInputs): string;
removeConsistentItemFromURI(consistentItemId: string): void;
}
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
export class ConsistentItemService extends Disposable {
readonly _serviceBrand: undefined
// the items that are attached to each URI, completely independent from current state of editors
private readonly consistentItemIdsOfURI: Record<string, Set<string> | undefined> = {}
private readonly infoOfConsistentItemId: Record<string, AddItemInputs> = {}
// current state of items on each editor, and the fns to call to remove them
private readonly itemIdsOfEditorId: Record<string, Set<string> | undefined> = {}
private readonly consistentItemIdOfItemId: Record<string, string> = {}
private readonly disposeFnOfItemId: Record<string, () => void> = {}
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
const removeItemsFromEditor = (editor: ICodeEditor) => {
const editorId = editor.getId()
for (const itemId of this.itemIdsOfEditorId[editorId] ?? [])
this._removeItemFromEditor(editor, itemId)
}
// put items on the editor, based on the consistent items for that URI
const putItemsOnEditor = (editor: ICodeEditor, uri: URI | null) => {
if (!uri) return
for (const consistentItemId of this.consistentItemIdsOfURI[uri.fsPath] ?? [])
this._putItemOnEditor(editor, consistentItemId)
}
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(
editor.onDidChangeModel(e => {
removeItemsFromEditor(editor)
putItemsOnEditor(editor, e.newModelUrl)
})
)
}
const addDisposeListener = (editor: ICodeEditor) => {
this._register(editor.onDidDispose(() => {
// anything on the editor has been disposed already
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? [])
delete this.disposeFnOfItemId[itemId]
}))
}
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
addDisposeListener(editor)
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
}
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// when an editor is deleted, remove its items
this._register(this._editorService.onCodeEditorRemove(editor => {
removeItemsFromEditor(editor)
}))
}
_putItemOnEditor(editor: ICodeEditor, consistentItemId: string) {
const { fn } = this.infoOfConsistentItemId[consistentItemId]
// add item
const dispose = fn(editor)
const itemId = generateUuid()
const editorId = editor.getId()
if (!(editorId in this.itemIdsOfEditorId))
this.itemIdsOfEditorId[editorId] = new Set()
this.itemIdsOfEditorId[editorId]!.add(itemId)
this.consistentItemIdOfItemId[itemId] = consistentItemId
this.disposeFnOfItemId[itemId] = () => {
// console.log('calling remove for', itemId)
dispose?.()
}
}
_removeItemFromEditor(editor: ICodeEditor, itemId: string) {
const editorId = editor.getId()
this.itemIdsOfEditorId[editorId]?.delete(itemId)
this.disposeFnOfItemId[itemId]?.()
delete this.disposeFnOfItemId[itemId]
delete this.consistentItemIdOfItemId[itemId]
}
consistentItemIdPool = 0
addConsistentItemToURI({ uri, fn }: AddItemInputs) {
const consistentItemId = (this.consistentItemIdPool++) + ''
if (!(uri.fsPath in this.consistentItemIdsOfURI))
this.consistentItemIdsOfURI[uri.fsPath] = new Set()
this.consistentItemIdsOfURI[uri.fsPath]!.add(consistentItemId)
this.infoOfConsistentItemId[consistentItemId] = { fn, uri }
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
for (const editor of editors)
this._putItemOnEditor(editor, consistentItemId)
return consistentItemId
}
removeConsistentItemFromURI(consistentItemId: string) {
if (!(consistentItemId in this.infoOfConsistentItemId))
return
const { uri } = this.infoOfConsistentItemId[consistentItemId]
const editors = this._editorService.listCodeEditors().filter(e => e.getModel()?.uri.fsPath === uri.fsPath)
for (const editor of editors) {
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) {
if (this.consistentItemIdOfItemId[itemId] === consistentItemId)
this._removeItemFromEditor(editor, itemId)
}
}
// clear
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
delete this.infoOfConsistentItemId[consistentItemId]
}
}
registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager);

View file

@ -1,178 +0,0 @@
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { ICodeEditor, IViewZone } from '../../../../../editor/browser/editorBrowser.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
// lets you add a zone to a Model (aka URI), instead of just to a single editor
export interface IZoneStyleService {
readonly _serviceBrand: undefined;
addConsistentZoneToURI(uri: URI, iZoneFn: (editor: ICodeEditor) => IViewZone, iOther?: (editor: ICodeEditor) => (() => void)): string;
removeConsistentZoneFromURI(consistentZoneId: string): void;
}
export const IZoneStyleService = createDecorator<IZoneStyleService>('zoneStyleService');
export class ZoneStyleService extends Disposable {
readonly _serviceBrand: undefined
// the zones that are attached to each URI, completely independent from current state of editors
private readonly consistentZoneIdsOfURI: Record<string, Set<string> | undefined> = {}
private readonly infoOfConsistentZoneId: Record<string, {
uri: URI
iZoneFn: (editor: ICodeEditor) => IViewZone,
iOther?: (editor: ICodeEditor) => (() => void),
}> = {}
// listener disposables
private readonly disposablesOfEditorId: Record<string, Set<IDisposable> | undefined> = {}
// current state of zones on each editor, and the fns to call to remove them. A zone is the actual zone plus whatever iOther you put on it.
private readonly zoneIdsOfEditorId: Record<string, Set<string> | undefined> = {}
private readonly removeFnOfZoneId: Record<string, () => void> = {}
private readonly consistentZoneIdOfZoneId: Record<string, string> = {}
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
const removeZonesFromEditor = (editor: ICodeEditor) => {
const editorId = editor.getId()
for (const zoneId of this.zoneIdsOfEditorId[editorId] ?? [])
this._removeZoneIdFromEditor(editor, zoneId)
}
// put zones on the editor, based on the consistentZones for that URI
const putZonesOnEditor = (editor: ICodeEditor, uri: URI | null) => {
if (!uri) return
for (const consistentZoneId of this.consistentZoneIdsOfURI[uri.fsPath] ?? [])
this._putZoneOnEditor(editor, consistentZoneId)
}
const addTabSwitchListeners = (editor: ICodeEditor) => {
const editorId = editor.getId()
if (!(editorId in this.disposablesOfEditorId))
this.disposablesOfEditorId[editorId] = new Set()
this.disposablesOfEditorId[editorId]!.add(
editor.onDidChangeModel(e => {
removeZonesFromEditor(editor)
putZonesOnEditor(editor, e.newModelUrl)
})
)
}
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
putZonesOnEditor(editor, editor.getModel()?.uri ?? null)
}
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// when an editor is deleted, remove its zones and call any disposables it has
this._register(this._editorService.onCodeEditorRemove(editor => {
const editorId = editor.getId()
removeZonesFromEditor(editor)
for (const d of this.disposablesOfEditorId[editorId] ?? [])
d.dispose()
delete this.disposablesOfEditorId[editorId]
}))
}
_putZoneOnEditor(editor: ICodeEditor, consistentZoneId: string) {
const { iZoneFn, iOther } = this.infoOfConsistentZoneId[consistentZoneId]
editor.changeViewZones(accessor => {
// add zone + other
const zoneId = accessor.addZone(iZoneFn(editor))
const rmFn = iOther?.(editor)
const editorId = editor.getId()
if (!(editorId in this.zoneIdsOfEditorId))
this.zoneIdsOfEditorId[editorId] = new Set()
this.zoneIdsOfEditorId[editorId]!.add(zoneId)
// fn that describes how to remove zone + other
this.removeFnOfZoneId[zoneId] = () => {
editor.changeViewZones(accessor => accessor.removeZone(zoneId))
rmFn?.()
}
this.consistentZoneIdOfZoneId[zoneId] = consistentZoneId
})
}
_removeZoneIdFromEditor(editor: ICodeEditor, zoneId: string) {
const editorId = editor.getId()
this.zoneIdsOfEditorId[editorId]?.delete(zoneId)
this.removeFnOfZoneId[zoneId]?.()
delete this.removeFnOfZoneId[zoneId]
delete this.consistentZoneIdOfZoneId[zoneId]
}
addConsistentZoneToURI(uri: URI, iZoneFn: (editor: ICodeEditor) => IViewZone, iOther?: (editor: ICodeEditor) => (() => void)) {
const consistentZoneId = generateUuid()
this.infoOfConsistentZoneId[consistentZoneId] = { iZoneFn, iOther, uri }
if (!(uri.fsPath in this.consistentZoneIdsOfURI))
this.consistentZoneIdsOfURI[uri.fsPath] = new Set()
this.consistentZoneIdsOfURI[uri.fsPath]!.add(consistentZoneId)
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
for (const editor of editors)
this._putZoneOnEditor(editor, consistentZoneId)
return consistentZoneId
}
removeConsistentZoneFromURI(consistentZoneId: string) {
if (!(consistentZoneId in this.infoOfConsistentZoneId))
return
const { uri } = this.infoOfConsistentZoneId[consistentZoneId]
const editors = this._editorService.listCodeEditors().filter(e => e.getModel()?.uri.fsPath === uri.fsPath)
for (const editor of editors) {
for (const zoneId of this.zoneIdsOfEditorId[editor.getId()] ?? []) {
if (this.consistentZoneIdOfZoneId[zoneId] === consistentZoneId)
this._removeZoneIdFromEditor(editor, zoneId)
}
}
// clear
this.consistentZoneIdsOfURI[uri.fsPath]?.delete(consistentZoneId)
delete this.infoOfConsistentZoneId[consistentZoneId]
}
}
registerSingleton(IZoneStyleService, ZoneStyleService, InstantiationType.Eager);

View file

@ -11,7 +11,6 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
// import { throttle } from '../../../../base/common/decorators.js';
import { inlineDiff_systemMessage } from './prompt/prompts.js';
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
import { IRange } from '../../../../editor/common/core/range.js';
@ -28,6 +27,8 @@ 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 { IConsistentItemService } from './helperServices/consistentItemService.js';
import { inlineDiff_systemMessage } from './prompt/prompts.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
@ -132,6 +133,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
@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 _zoneStyleService: IConsistentItemService,
) {
super();
@ -150,36 +152,33 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// it's as if we just called _write, now all we need to do is realign and refresh
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)
})
)
}
// initialize all existing models
// initialize all existing models + initialize when a new model mounts
for (let model of this._modelService.getModels()) { initializeModel(model) }
// initialize whenever a new model mounts
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)
// this isn't relevant anymore because consistentItemService takes care of it
// called when the user switches tabs (typically there's only 1 editor on the screen, it switches between models, make sure you understand this)
this._register(editor.onDidChangeModel((e) => {
if (e.oldModelUrl) this._refreshDiffsInURI(e.oldModelUrl)
if (e.newModelUrl) this._refreshDiffsInURI(e.newModelUrl)
}))
// this._register(editor.onDidChangeModel((e) => {
// if (e.newModelUrl) this._refreshDiffsInURI(e.newModelUrl)
// if (e.oldModelUrl) this._clearAllDiffsAndStyles(e.oldModelUrl)
// }))
}
// 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) }))
// this._register(this._editorService.onCodeEditorRemove(editor => { console.log('REMOVE EDITOR'); initializeEditor(editor) }))
}
@ -198,7 +197,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
...options
}))
const disposeHighlight = () => {
if (id) model.changeDecorations(accessor => accessor.removeDecoration(id))
if (id && !model.isDisposed()) model.changeDecorations(accessor => accessor.removeDecoration(id))
}
return disposeHighlight
}
@ -227,14 +226,16 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
}
private _addDiffStylesToEditor = (editor: ICodeEditor, diff: Diff) => {
private _addDiffStylesToURI = (uri: URI, diff: Diff) => {
const { type, diffid } = diff
const disposeInThisEditorFns: (() => void)[] = []
const model = this._modelService.getModel(uri)
// green decoration and minimap decoration
if (type !== 'deletion') {
const fn = this._addLineDecoration(editor.getModel(), diff.startLine, diff.endLine, 'void-greenBG', {
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 }
})
@ -244,47 +245,65 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// red in a view zone
if (type !== 'insertion') {
editor.changeViewZones(accessor => {
const consistentZoneId = this._zoneStyleService.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);
// applyFontInfo(domNode, renderOptions.fontInfo)
// 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);
// 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 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 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 zoneId = accessor.addZone(viewZone)
disposeInThisEditorFns.push(() => { editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) })
let zoneId: string | null = null
editor.changeViewZones(accessor => { zoneId = accessor.addZone(viewZone) })
return () => editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) })
},
})
console.log('added (Z)', consistentZoneId)
disposeInThisEditorFns.push(() => { console.log('removing (Z)', consistentZoneId); this._zoneStyleService.removeConsistentItemFromURI(consistentZoneId) })
});
}
// Accept | Reject widget
const buttonsWidget = new AcceptRejectWidget({
editor,
onAccept: () => { this.acceptDiff({ diffid }) },
onReject: () => { this.rejectDiff({ diffid }) },
diffid: diffid.toString(),
startLine: diff.startLine,
const consistentWidgetId = this._zoneStyleService.addConsistentItemToURI({
uri,
fn: (editor) => {
const buttonsWidget = new AcceptRejectWidget({
editor,
onAccept: () => { this.acceptDiff({ diffid }) },
onReject: () => { this.rejectDiff({ diffid }) },
diffid: diffid.toString(),
startLine: diff.startLine,
})
return () => { buttonsWidget.dispose() }
}
})
disposeInThisEditorFns.push(() => { buttonsWidget.dispose() })
console.log('added (O)', consistentWidgetId)
disposeInThisEditorFns.push(() => { console.log('removing (O)', consistentWidgetId); this._zoneStyleService.removeConsistentItemFromURI(consistentWidgetId) })
const disposeInEditor = () => { disposeInThisEditorFns.forEach(f => f()) }
return disposeInEditor;
@ -499,7 +518,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this._clearAllDiffsAndStyles(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) ?? ''
@ -520,10 +538,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
diffareaid: diffArea.diffareaid,
}
for (let editor of editors) {
const fn = this._addDiffStylesToEditor(editor, newDiff)
this.removeStylesFnsOfURI[uri.fsPath].add(() => fn())
}
const fn = this._addDiffStylesToURI(uri, newDiff)
this.removeStylesFnsOfURI[uri.fsPath].add(fn)
this.diffOfId[diffid] = newDiff
diffArea._diffOfId[diffid] = newDiff
@ -651,7 +667,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
_diffOfId: {}, // added later
}
console.log('adding uri.fspath', uri.fsPath, diffArea.diffareaid.toString())
// console.log('adding uri.fspath', uri.fsPath, diffArea.diffareaid.toString())
this.diffAreasOfURI[uri.fsPath].add(diffArea.diffareaid.toString())
this.diffAreaOfId[diffArea.diffareaid] = diffArea