From 37200dacbdde3c52e028759cb1bd665142ec20b3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 13 Jan 2025 21:48:04 -0800 Subject: [PATCH] add metrics + streaming works pretty well! --- src/vs/platform/void/common/metricsService.ts | 1 + .../void/electron-main/metricsMainService.ts | 12 +- .../void/browser/autocompleteService.ts | 3 +- .../browser/helpers/extractCodeFromResult.ts | 34 +++--- .../void/browser/inlineDiffsService.ts | 114 ++++++++++++------ .../contrib/void/browser/prompt/prompts.ts | 2 +- .../contrib/void/browser/quickEditActions.ts | 3 + 7 files changed, 114 insertions(+), 55 deletions(-) diff --git a/src/vs/platform/void/common/metricsService.ts b/src/vs/platform/void/common/metricsService.ts index 3d185669..7002af45 100644 --- a/src/vs/platform/void/common/metricsService.ts +++ b/src/vs/platform/void/common/metricsService.ts @@ -25,6 +25,7 @@ export class MetricsService implements IMetricsService { constructor( @IMainProcessService mainProcessService: IMainProcessService // (only usable on client side) ) { + // creates an IPC proxy to use metricsMainService.ts this.metricsService = ProxyChannel.toService(mainProcessService.getChannel('void-channel-metrics')); } diff --git a/src/vs/platform/void/electron-main/metricsMainService.ts b/src/vs/platform/void/electron-main/metricsMainService.ts index 31ca1252..e1811e4c 100644 --- a/src/vs/platform/void/electron-main/metricsMainService.ts +++ b/src/vs/platform/void/electron-main/metricsMainService.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------*/ import { Disposable } from '../../../base/common/lifecycle.js'; +import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { IProductService } from '../../product/common/productService.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IMetricsService } from '../common/metricsService.js'; @@ -24,14 +26,20 @@ export class MetricsMainService extends Disposable implements IMetricsService { readonly client: PostHog constructor( - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IProductService private readonly _productService: IProductService ) { super() this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', }) const { devDeviceId, firstSessionDate, machineId } = this._telemetryService + this._distinctId = devDeviceId - this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } }) + + const { commit, version } = this._productService + const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null + + this.client.identify({ distinctId: this._distinctId, properties: { firstSessionDate, machineId, commit, version, os } }) console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId })) } diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 1400d429..f034c947 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -652,7 +652,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // newAutocompletion.abortRef = { current: () => { } } newAutocompletion.status = 'finished' // newAutocompletion.promise = undefined - newAutocompletion.insertText = postprocessResult(extractCodeFromRegular(fullText)) + const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 }) + newAutocompletion.insertText = postprocessResult(text) resolve(newAutocompletion.insertText) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index af883788..d7e109ae 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -22,7 +22,7 @@ class SurroundingsRemover { // 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)) + // 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 @@ -64,7 +64,7 @@ class SurroundingsRemover { this.i = this.j + 1 return false } - console.log('index', index, until.length) + // console.log('index', index, until.length) if (alsoRemoveUntilStr) this.i = index + until.length @@ -90,11 +90,21 @@ class SurroundingsRemover { } + 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: string): string => { +export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => { // Match either: // 1. ```language\n``` // 2. `````` @@ -104,7 +114,9 @@ export const extractCodeFromRegular = (text: string): string => { pm.removeCodeBlock() const s = pm.value() - return s + const actual = pm.actualRecentlyAdded(recentlyAddedTextLen) + + return [s, actual] } @@ -112,7 +124,7 @@ export const extractCodeFromRegular = (text: string): string => { // Ollama has its own FIM, we should not use this if we use that -export const extractCodeFromFIM = ({ text, midTag }: { text: string, midTag: string }): string => { +export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => { /* ------------- summary of the regex ------------- [optional ` | `` | ```] @@ -126,23 +138,17 @@ export const extractCodeFromFIM = ({ text, midTag }: { text: string, midTag: str const pm = new SurroundingsRemover(text) - console.log('ORIGIINAL CODE', text) - pm.removeCodeBlock() - console.log('D', pm.i, pm.j) - - const foundMid = pm.removePrefix(`<${midTag}>`) - console.log('E', midTag, pm.i, pm.j) if (foundMid) { pm.removeSuffix(``) - console.log('F', pm.i, pm.j) - } const s = pm.value() - return s + 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}|$)/; diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 3105cbd0..29fcb56c 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -38,7 +38,6 @@ import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCod import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; import { InlineDecorationType } from '../../../../editor/common/viewModel.js'; import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; -import { BaseEditorSimpleWorker } from '../../../../editor/common/services/editorSimpleWorker.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; // import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -130,6 +129,8 @@ type CtrlKZone = { refresh: () => void; } + _linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here + } & CommonZoneProps @@ -292,13 +293,15 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // sweepLine ... sweepLine const fn1 = this._addLineDecoration(model, diffArea._streamState.line, diffArea._streamState.line, 'void-sweepIdxBG') // sweepLine+1 ... endLine - const fn2 = this._addLineDecoration(model, diffArea._streamState.line + 1, diffArea.endLine, 'void-sweepBG') + 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') { + 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?.()); @@ -478,7 +481,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { minWidthInPx: result.minWidthInPx, domNode: domNode, marginDomNode: document.createElement('div'), // displayed to left - suppressMouseDown: true, + suppressMouseDown: false, showInHiddenAreas: true, }; @@ -548,24 +551,30 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } weAreWriting = false - worker = new BaseEditorSimpleWorker() private async _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { const model = this._getModel(uri) if (!model) return const uriStr = this._readURI(uri, range) - if (!uriStr) return + if (uriStr === null) return - // minimal edits so not so flashy - // __TODO__ THIS IS NOT INSIDE A WORKER, SO IT MIGHT BE SLOW, we should instead just do an optimal write ourselves - const edits = this.worker.$Void_computeMoreMinimalEdits(uri.toString(), [{ range, text }], false) - if (edits) { - this.weAreWriting = true - model.applyEdits(edits) - this.weAreWriting = false + // 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 } }) + } @@ -627,6 +636,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _URI: uri, _removeStylesFns: new Set(), _mountInfo: null, + _linkedStreamingDiffZone: null, } } this.diffAreasOfURI[uri.fsPath].add(diffareaid) @@ -636,16 +646,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const numLines = this._getNumLines(uri) if (numLines === null) return - const hasWriteChange = this._readURI(uri) !== entireModelCode // a heuristic check - if (hasWriteChange) - this._writeText(uri, entireModelCode, - { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, - { shouldRealignDiffAreas: false } - ) - else { - // restore all the decorations - this._refreshStylesAndDiffsInURI(uri) - } + + this._writeText(uri, entireModelCode, + { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, + { shouldRealignDiffAreas: false } + ) } const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() @@ -844,7 +849,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeDiffZoneLLMText(diffZone: DiffZone, llmText: 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 @@ -881,17 +886,41 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - // lines are 1-indexed - const newCodeTop = llmText.split('\n').slice(0, (newCodeEndLine - 1) + 1).join('\n') - const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1) + 1, Infinity).join('\n') - // oriignalCode[1 + line...Infinity]. Must make sure 1 + line < originalCode.length. This is another way to check: - const newCode = (newCodeTop && oldFileBottom) ? `${newCodeTop}\n${oldFileBottom}` : (oldFileBottom || newCodeTop) - this._writeText(uri, newCode, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed + // insert 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 + + if (!latest.addedSplitYet) { + this._writeText(uri, '', + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col, }, + { shouldRealignDiffAreas: true } + ) + latest.addedSplitYet = true + } + + // 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((latest.originalCodeStartLine - 1), (originalCodeStartLine - 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 @@ -980,6 +1009,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _URI: uri, _removeStylesFns: new Set(), _mountInfo: null, + _linkedStreamingDiffZone: null, } const ctrlKZone = this._addDiffArea(adding) this._refreshStylesAndDiffsInURI(uri) @@ -1054,7 +1084,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } else if (featureName === 'Ctrl+K') { const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return @@ -1101,7 +1130,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) + 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') { @@ -1147,29 +1183,33 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { this._refreshStylesAndDiffsInURI(uri) - const extractText = (fullText: string) => { + const extractText = (fullText: string, recentlyAddedTextLen: number) => { if (featureName === 'Ctrl+K') { if (ollamaStyleFIM) return fullText - return extractCodeFromFIM({ text: fullText, midTag: modelFimTags.midTag }) + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) } else if (featureName === 'Ctrl+L') { - return extractCodeFromRegular(fullText) + return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) } throw 1 } + const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ featureName, logging: { loggingName: `startApplying - ${featureName}` }, messages, onText: ({ newText, fullText }) => { - this._writeDiffZoneLLMText(diffZone, extractText(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 - this._writeText(uri, extractText(fullText), + 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 } ) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index ca692569..1d769964 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -381,7 +381,7 @@ Note that the SELECTION has code that comes before it. This code is indicated wi 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 give any explanation before or after this. ONLY output this format, nothing more. +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. diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index a4b8a16c..8818191d 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -60,9 +60,12 @@ registerAction2(class extends Action2 { const { startLineNumber: startLine, endLineNumber: endLine } = selection + // deselect - clear selection editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 }) + editor.revealLine(startLine) // important + const inlineDiffsService = accessor.get(IInlineDiffsService) inlineDiffsService.addCtrlKZone({ startLine, endLine, editor }) }