diff --git a/src/package.json b/src/package.json index 9eed181fe4c..6f6751cba95 100644 --- a/src/package.json +++ b/src/package.json @@ -434,11 +434,6 @@ "mac": "cmd+shift+a", "when": "true" }, - { - "command": "kilo-code.ghost.cancelRequest", - "key": "escape", - "when": "editorTextFocus && !editorTabMovesFocus && !inSnippetMode && kilocode.ghost.isProcessing" - }, { "command": "kilo-code.ghost.cancelSuggestions", "key": "escape", diff --git a/src/services/ghost/GhostContext.ts b/src/services/ghost/GhostContext.ts deleted file mode 100644 index 9ab96b0a3b1..00000000000 --- a/src/services/ghost/GhostContext.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as vscode from "vscode" -import { GhostSuggestionContext } from "./types" -import { GhostDocumentStore } from "./GhostDocumentStore" - -export class GhostContext { - private documentStore: GhostDocumentStore - - constructor(documentStore: GhostDocumentStore) { - this.documentStore = documentStore - } - - private addRecentOperations(context: GhostSuggestionContext): GhostSuggestionContext { - if (!context.document) { - return context - } - const recentOperations = this.documentStore.getRecentOperations(context.document) - if (recentOperations) { - context.recentOperations = recentOperations - } - return context - } - - private addEditor(context: GhostSuggestionContext): GhostSuggestionContext { - const editor = vscode.window.activeTextEditor - if (editor) { - context.editor = editor - } - return context - } - - private addOpenFiles(context: GhostSuggestionContext): GhostSuggestionContext { - const openFiles = vscode.workspace.textDocuments.filter((doc) => doc.uri.scheme === "file") - context.openFiles = openFiles - return context - } - - private addRange(context: GhostSuggestionContext): GhostSuggestionContext { - if (!context.range && context.editor) { - context.range = context.editor.selection - } - return context - } - - private addDiagnostics(context: GhostSuggestionContext): GhostSuggestionContext { - if (!context.document) { - return context - } - const diagnostics = vscode.languages.getDiagnostics(context.document.uri) - if (diagnostics && diagnostics.length > 0) { - context.diagnostics = diagnostics - } - return context - } - - public async generate(initialContext: GhostSuggestionContext): Promise { - let context = initialContext - context = this.addEditor(context) - context = this.addOpenFiles(context) - context = this.addRange(context) - context = this.addRecentOperations(context) - context = this.addDiagnostics(context) - return context - } -} diff --git a/src/services/ghost/GhostDocumentStore.ts b/src/services/ghost/GhostDocumentStore.ts deleted file mode 100644 index 7bcb6a2cfd3..00000000000 --- a/src/services/ghost/GhostDocumentStore.ts +++ /dev/null @@ -1,461 +0,0 @@ -import * as vscode from "vscode" -import * as path from "path" -import { structuredPatch } from "diff" -import { GhostDocumentStoreItem, UserAction, UserActionType } from "./types" - -export const GHOST_DOCUMENT_STORE_LIMITS = { - MAX_DOCUMENTS: 50, // Limit the number of documents to keep - MAX_HISTORY_PER_DOCUMENT: 50, // Limit the number of snapshots per document to keep -} as const - -export class GhostDocumentStore { - private debounceTimers: Map = new Map() - private historyLimit: number = 3 // Limit the number of snapshots to keep - private documentStore: Map = new Map() - private parserInitialized: boolean = false - - /** - * Store a document in the document store - * @param document The document to store - * @param bypassDebounce Whether to bypass the debounce mechanism and store immediately - */ - public async storeDocument({ - document, - bypassDebounce = false, - }: { - document: vscode.TextDocument - bypassDebounce?: boolean - }): Promise { - const uri = document.uri.toString() - const debounceWait = 500 // 500ms delay - - // Function to perform the actual document storage - const performStorage = async () => { - if (!this.documentStore.has(uri)) { - this.documentStore.set(uri, { - uri, - document, - history: [], - }) - } - - const item = this.documentStore.get(uri)! - item.document = document // Update the document reference - item.history.push(document.getText()) - if (item.history.length > this.historyLimit) { - item.history.shift() // Remove the oldest snapshot if we exceed the limit - } - - // Once executed, remove the timer from the map. - this.debounceTimers.delete(uri) - } - - // If bypassDebounce is true, execute storage immediately - if (bypassDebounce) { - await performStorage() - return - } - - // Otherwise, use the debounce mechanism - // Clear any existing timer for this specific document to reset the debounce period. - if (this.debounceTimers.has(uri)) { - clearTimeout(this.debounceTimers.get(uri)!) - } - - // Set a new timer to execute the storage logic after the specified delay. - const timer = setTimeout(performStorage, debounceWait) - - // Store the new timer ID, associating it with the document's URI. - this.debounceTimers.set(uri, timer) - } - - /** - * Get a document from the store - * @param documentUri The URI of the document - * @returns The document store item or undefined if not found - */ - public getDocument(documentUri: vscode.Uri): GhostDocumentStoreItem | undefined { - const uri = documentUri.toString() - return this.documentStore.get(uri) - } - - /** - * Remove a document completely from the store - * @param documentUri The URI of the document to remove - */ - public removeDocument(documentUri: vscode.Uri): void { - const uri = documentUri.toString() - - // Clear any debounce timer for this document - if (this.debounceTimers.has(uri)) { - clearTimeout(this.debounceTimers.get(uri)!) - this.debounceTimers.delete(uri) - } - - // Remove the document from the store - this.documentStore.delete(uri) - } - - /** - * Analyzes a single pair of document versions to extract meaningful changes - * @param oldContent Previous version of the document - * @param newContent Current version of the document - * @param filePath Path to the document - * @returns A collection of user actions representing the changes - */ - private analyzeDocumentChanges(oldContent: string, newContent: string, filePath: string): UserAction[] { - // Generate a structured diff between the two versions - const patch = structuredPatch(filePath, filePath, oldContent, newContent, "", "") - const actions: UserAction[] = [] - - // Process each hunk in the patch - for (const hunk of patch.hunks) { - // Track consecutive additions and deletions to group them - let consecutiveAdditions: string[] = [] - let consecutiveDeletions: string[] = [] - let currentLineNumber = hunk.newStart - - // Process each line in the hunk - for (const line of hunk.lines) { - const operationType = line.charAt(0) - const content = line.substring(1) - - switch (operationType) { - case "+": - // Addition - consecutiveAdditions.push(content) - currentLineNumber++ - break - case "-": - // Deletion - consecutiveDeletions.push(content) - break - default: - // Context line - process any pending additions/deletions - this.processConsecutiveChanges( - consecutiveAdditions, - consecutiveDeletions, - currentLineNumber - consecutiveAdditions.length, - actions, - filePath, - ) - consecutiveAdditions = [] - consecutiveDeletions = [] - currentLineNumber++ - break - } - } - - // Process any remaining additions/deletions at the end of the hunk - this.processConsecutiveChanges( - consecutiveAdditions, - consecutiveDeletions, - currentLineNumber - consecutiveAdditions.length, - actions, - filePath, - ) - } - - return actions - } - - /** - * Process consecutive additions and deletions to determine the type of change - * @param additions Lines that were added - * @param deletions Lines that were deleted - * @param lineNumber Starting line number - * @param actions Array to add the resulting actions to - * @param filePath Path to the file being analyzed - */ - private processConsecutiveChanges( - additions: string[], - deletions: string[], - lineNumber: number, - actions: UserAction[], - filePath: string, - ): void { - if (additions.length === 0 && deletions.length === 0) { - return // No changes to process - } - - if (additions.length > 0 && deletions.length === 0) { - // Pure addition - actions.push(this.createAdditionAction(additions, lineNumber, filePath)) - } else if (additions.length === 0 && deletions.length > 0) { - // Pure deletion - actions.push(this.createDeletionAction(deletions, lineNumber, filePath)) - } else { - // Check if this is a complete replacement (deletion followed by unrelated addition) - const addedText = additions.join("\n").trim() - const deletedText = deletions.join("\n").trim() - - // If the deleted text contains a variable declaration and the added text doesn't contain it, - // it's likely a deletion rather than a modification - const varMatch = deletedText.match(/(?:const|let|var|private|public|protected)\s+(\w+)/) - if (varMatch && !addedText.includes(varMatch[1])) { - // This is a deletion followed by an unrelated addition - actions.push(this.createDeletionAction(deletions, lineNumber, filePath)) - if (addedText.length > 0) { - actions.push(this.createAdditionAction(additions, lineNumber, filePath)) - } - } else { - // Regular modification (both additions and deletions) - actions.push(this.createModificationAction(additions, deletions, lineNumber, filePath)) - } - } - } - - /** - * Creates an action representing code addition - * @param addedLines The lines that were added - * @param lineNumber The starting line number - * @param filePath Path to the file - * @returns A UserAction representing the addition - */ - private createAdditionAction(addedLines: string[], lineNumber: number, filePath: string): UserAction { - const joinedLines = addedLines.join("\n") - - // Try to identify what was added - let description = "Added code" - let affectedSymbol = undefined - let scope = undefined - - // Check for function/method definition - const functionMatch = joinedLines.match(/(?:function|method|def)\s+(\w+)\s*\(/) - if (functionMatch) { - description = `Added function '${functionMatch[1]}'` - affectedSymbol = functionMatch[1] - } - - // Check for class definition - const classMatch = joinedLines.match(/class\s+(\w+)/) - if (classMatch) { - description = `Added class '${classMatch[1]}'` - affectedSymbol = classMatch[1] - } - - // Check for variable declaration - const varMatch = joinedLines.match(/(?:const|let|var|private|public|protected)\s+(\w+)/) - if (varMatch) { - description = `Added variable '${varMatch[1]}'` - affectedSymbol = varMatch[1] - } - - // If we couldn't identify a specific construct, try to be more descriptive - if (description === "Added code") { - if (joinedLines.includes("if") || joinedLines.includes("else")) { - description = "Added conditional logic" - } else if (joinedLines.includes("for") || joinedLines.includes("while")) { - description = "Added loop" - } else if (joinedLines.includes("try") || joinedLines.includes("catch")) { - description = "Added error handling" - } else if (joinedLines.includes("import") || joinedLines.includes("require")) { - description = "Added import statement" - } else if (joinedLines.trim().startsWith("//") || joinedLines.trim().startsWith("/*")) { - description = "Added comment" - } - } - - return { - type: UserActionType.ADDITION, - description, - lineRange: { - start: lineNumber, - end: lineNumber + addedLines.length - 1, - }, - affectedSymbol, - scope, - content: joinedLines, - } - } - - /** - * Creates an action representing code deletion - * @param deletedLines The lines that were deleted - * @param lineNumber The starting line number - * @param filePath Path to the file - * @returns A UserAction representing the deletion - */ - private createDeletionAction(deletedLines: string[], lineNumber: number, filePath: string): UserAction { - const joinedLines = deletedLines.join("\n") - - // Try to identify what was deleted - let description = "Deleted code" - let affectedSymbol = undefined - let scope = undefined - - // Check for function/method definition - const functionMatch = joinedLines.match(/(?:function|method|def)\s+(\w+)\s*\(/) - if (functionMatch) { - description = `Deleted function '${functionMatch[1]}'` - affectedSymbol = functionMatch[1] - } - - // Check for class definition - const classMatch = joinedLines.match(/class\s+(\w+)/) - if (classMatch) { - description = `Deleted class '${classMatch[1]}'` - affectedSymbol = classMatch[1] - } - - // Check for variable declaration - const varMatch = joinedLines.match(/(?:const|let|var|private|public|protected)\s+(\w+)/) - if (varMatch) { - description = `Deleted variable '${varMatch[1]}'` - affectedSymbol = varMatch[1] - } - - // If we couldn't identify a specific construct, try to be more descriptive - if (description === "Deleted code") { - if (joinedLines.includes("if") || joinedLines.includes("else")) { - description = "Deleted conditional logic" - } else if (joinedLines.includes("for") || joinedLines.includes("while")) { - description = "Deleted loop" - } else if (joinedLines.includes("try") || joinedLines.includes("catch")) { - description = "Deleted error handling" - } else if (joinedLines.includes("import") || joinedLines.includes("require")) { - description = "Deleted import statement" - } else if (joinedLines.trim().startsWith("//") || joinedLines.trim().startsWith("/*")) { - description = "Deleted comment" - } - } - - return { - type: UserActionType.DELETION, - description, - lineRange: { - start: lineNumber, - end: lineNumber + deletedLines.length - 1, - }, - affectedSymbol, - scope, - content: joinedLines, - } - } - - /** - * Creates an action representing code modification - * @param addedLines The lines that were added - * @param deletedLines The lines that were deleted - * @param lineNumber The starting line number - * @param filePath Path to the file - * @returns A UserAction representing the modification - */ - private createModificationAction( - addedLines: string[], - deletedLines: string[], - lineNumber: number, - filePath: string, - ): UserAction { - const addedText = addedLines.join("\n") - const deletedText = deletedLines.join("\n") - - // Try to identify what was modified - let description = "Modified code" - let actionType = UserActionType.MODIFICATION - let affectedSymbol = undefined - let scope = undefined - - // Check if this is a rename operation - const deletedSymbolMatch = deletedText.match(/\b(\w+)\b/) - const addedSymbolMatch = addedText.match(/\b(\w+)\b/) - - if ( - deletedSymbolMatch && - addedSymbolMatch && - deletedText.replace(deletedSymbolMatch[1], addedSymbolMatch[1]) === addedText - ) { - description = `Renamed '${deletedSymbolMatch[1]}' to '${addedSymbolMatch[1]}'` - actionType = UserActionType.REFACTOR - affectedSymbol = `${deletedSymbolMatch[1]} → ${addedSymbolMatch[1]}` - } - - // Check if this is just a formatting change - const isFormatChange = this.isFormattingChange(addedText, deletedText) - if (isFormatChange) { - description = "Reformatted code" - actionType = UserActionType.FORMAT - } - - // Check for condition changes in if statements - const deletedConditionMatch = deletedText.match(/if\s*\((.*?)\)/) - const addedConditionMatch = addedText.match(/if\s*\((.*?)\)/) - if (deletedConditionMatch && addedConditionMatch) { - description = "Modified condition in if statement" - } - - // Check for changes in function parameters - const deletedParamsMatch = deletedText.match(/\(\s*(.*?)\s*\)/) - const addedParamsMatch = addedText.match(/\(\s*(.*?)\s*\)/) - if (deletedParamsMatch && addedParamsMatch && deletedParamsMatch[1] !== addedParamsMatch[1]) { - description = "Modified function parameters" - } - - return { - type: actionType, - description, - lineRange: { - start: lineNumber, - end: lineNumber + Math.max(addedLines.length, deletedLines.length) - 1, - }, - affectedSymbol, - scope, - content: addedText, // For modifications, show the new content - } - } - - /** - * Determines if changes are purely formatting (whitespace, indentation) - * @param addedText The text that was added - * @param deletedText The text that was deleted - * @returns True if the changes appear to be formatting only - */ - private isFormattingChange(addedText: string, deletedText: string): boolean { - // Remove all whitespace and compare - const normalizedAdded = addedText.replace(/\s+/g, "") - const normalizedDeleted = deletedText.replace(/\s+/g, "") - return normalizedAdded === normalizedDeleted - } - - /** - * Get the last 10 operations performed by the user on a document as meaningful actions - * @param document The document to get operations for - * @returns A collection of user action groups representing meaningful changes - */ - public getRecentOperations(document: vscode.TextDocument): UserAction[] { - if (!document) { - return [] - } - - const uri = document.uri.toString() - const item = this.getDocument(document.uri) - - if (!item || item.history.length < 2) { - return [] - } - - // Get the last 10 versions (or fewer if not available) - const historyLimit = 2 - const startIdx = Math.max(0, item.history.length - historyLimit) - const recentHistory = item.history.slice(startIdx) - - // If we have at least 2 versions, analyze the changes - if (recentHistory.length >= 2) { - const filePath = vscode.workspace.asRelativePath(document.uri) - const allActions: UserAction[] = [] - - // Analyze changes between consecutive versions - for (let i = 0; i < recentHistory.length - 1; i++) { - const oldContent = recentHistory[i] - const newContent = recentHistory[i + 1] - - const actions = this.analyzeDocumentChanges(oldContent, newContent, filePath) - allActions.push(...actions) - } - - return allActions - } - - return [] - } -} diff --git a/src/services/ghost/GhostServiceManager.ts b/src/services/ghost/GhostServiceManager.ts index 5aa67b7bed8..903ebac1159 100644 --- a/src/services/ghost/GhostServiceManager.ts +++ b/src/services/ghost/GhostServiceManager.ts @@ -1,7 +1,6 @@ import crypto from "crypto" import * as vscode from "vscode" import { t } from "../../i18n" -import { GhostDocumentStore } from "./GhostDocumentStore" import { GhostModel } from "./GhostModel" import { GhostStatusBar } from "./GhostStatusBar" import { GhostCodeActionProvider } from "./GhostCodeActionProvider" @@ -10,23 +9,19 @@ import { GhostContextProvider } from "./classic-auto-complete/GhostContextProvid //import { NewAutocompleteProvider } from "./new-auto-complete/NewAutocompleteProvider" import { GhostServiceSettings, TelemetryEventName } from "@roo-code/types" import { ContextProxy } from "../../core/config/ContextProxy" -import { GhostContext } from "./GhostContext" import { TelemetryService } from "@roo-code/telemetry" import { ClineProvider } from "../../core/webview/ClineProvider" import { RooIgnoreController } from "../../core/ignore/RooIgnoreController" export class GhostServiceManager { private static instance: GhostServiceManager | null = null - private readonly documentStore: GhostDocumentStore private readonly model: GhostModel private readonly cline: ClineProvider private readonly context: vscode.ExtensionContext private settings: GhostServiceSettings | null = null - private readonly ghostContext: GhostContext private readonly ghostContextProvider: GhostContextProvider private taskId: string | null = null - private isProcessing: boolean = false // Status bar integration private statusBar: GhostStatusBar | null = null @@ -46,9 +41,7 @@ export class GhostServiceManager { this.cline = cline // Register Internal Components - this.documentStore = new GhostDocumentStore() this.model = new GhostModel() - this.ghostContext = new GhostContext(this.documentStore) this.ghostContextProvider = new GhostContextProvider(context, this.model) // Register the providers @@ -56,19 +49,10 @@ export class GhostServiceManager { this.inlineCompletionProvider = new GhostInlineCompletionProvider( this.model, this.updateCostTracking.bind(this), - this.ghostContext, () => this.settings, this.ghostContextProvider, ) - // Register document event handlers - vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, context.subscriptions) - vscode.workspace.onDidOpenTextDocument(this.onDidOpenTextDocument, this, context.subscriptions) - vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, context.subscriptions) - vscode.workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, context.subscriptions) - vscode.window.onDidChangeTextEditorSelection(this.onDidChangeTextEditorSelection, this, context.subscriptions) - vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, context.subscriptions) - void this.load() } @@ -152,13 +136,6 @@ export class GhostServiceManager { } // VsCode Event Handlers - private onDidCloseTextDocument(document: vscode.TextDocument): void { - if (document.uri.scheme !== "file") { - return - } - this.documentStore.removeDocument(document.uri) - } - private initializeIgnoreController() { if (!this.ignoreController) { this.ignoreController = (async () => { @@ -178,69 +155,6 @@ export class GhostServiceManager { } } - private onDidChangeWorkspaceFolders() { - this.disposeIgnoreController() - } - - private async onDidOpenTextDocument(document: vscode.TextDocument): Promise { - if (document.uri.scheme !== "file") { - return - } - await this.documentStore.storeDocument({ - document, - }) - } - - private async onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent): Promise { - if (event.document.uri.scheme !== "file") { - return - } - - // Filter out undo/redo operations - if (event.reason !== undefined) { - return - } - - if (event.contentChanges.length === 0) { - return - } - - // Heuristic to filter out bulk changes (git operations, external edits) - const isBulkChange = event.contentChanges.some((change) => change.rangeLength > 100 || change.text.length > 100) - if (isBulkChange) { - return - } - - // Heuristic to filter out changes far from cursor (likely external or LLM edits) - const editor = vscode.window.activeTextEditor - if (!editor || editor.document !== event.document) { - return - } - - const cursorPos = editor.selection.active - const isNearCursor = event.contentChanges.some((change) => { - const distance = Math.abs(cursorPos.line - change.range.start.line) - return distance <= 2 - }) - if (!isNearCursor) { - return - } - - await this.documentStore.storeDocument({ document: event.document }) - } - - private async onDidChangeTextEditorSelection(event: vscode.TextEditorSelectionChangeEvent): Promise { - // No longer needed - gutter animation removed - } - - private async onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { - if (!editor) { - return - } - // Update global context when switching editors - await this.updateGlobalContext() - } - private async hasAccess(document: vscode.TextDocument) { return document.isUntitled || (await this.initializeIgnoreController()).validateAccess(document.fileName) } @@ -313,7 +227,6 @@ export class GhostServiceManager { } private async updateGlobalContext() { - await vscode.commands.executeCommand("setContext", "kilocode.ghost.isProcessing", this.isProcessing) await vscode.commands.executeCommand( "setContext", "kilocode.ghost.enableQuickInlineTaskKeybinding", @@ -406,13 +319,7 @@ export class GhostServiceManager { } } - private stopProcessing() { - this.isProcessing = false - this.updateGlobalContext() - } - public cancelRequest() { - this.stopProcessing() // Check which provider is active and cancel appropriately const useNewAutocomplete = this.settings?.useNewAutocomplete ?? false if (useNewAutocomplete) { diff --git a/src/services/ghost/__tests__/GhostDocumentStore.spec.ts b/src/services/ghost/__tests__/GhostDocumentStore.spec.ts deleted file mode 100644 index a6ca81c05de..00000000000 --- a/src/services/ghost/__tests__/GhostDocumentStore.spec.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from "vitest" -import * as vscode from "vscode" -import { GhostDocumentStore } from "../GhostDocumentStore" -import { MockTextDocument } from "../../mocking/MockTextDocument" - -// Mock vscode -vi.mock("vscode", () => ({ - Uri: { - parse: (uriString: string) => ({ - toString: () => uriString, - fsPath: uriString.replace("file://", ""), - scheme: "file", - path: uriString.replace("file://", ""), - }), - }, - Position: class { - constructor( - public line: number, - public character: number, - ) {} - }, - Range: class { - constructor( - public start: any, - public end: any, - ) {} - }, -})) - -describe("GhostDocumentStore", () => { - let documentStore: GhostDocumentStore - let mockDocument: MockTextDocument - - beforeEach(() => { - // Create a custom implementation of GhostDocumentStore with mocked behavior - const mockDocumentMap = new Map() - - // Set a history limit for testing - const historyLimit = 10 - - documentStore = { - historyLimit, - storeDocument: vi - .fn() - .mockImplementation( - async ({ - document, - bypassDebounce = false, - }: { - document: vscode.TextDocument - bypassDebounce?: boolean - }) => { - const uri = document.uri.toString() - if (!mockDocumentMap.has(uri)) { - mockDocumentMap.set(uri, { - uri, - document, - history: [], - }) - } - - const item = mockDocumentMap.get(uri) - item.document = document - item.history.push(document.getText()) - - // Limit history array length - if (item.history.length > historyLimit) { - item.history = item.history.slice(-historyLimit) - } - }, - ), - - getDocument: vi.fn().mockImplementation((documentUri: vscode.Uri) => { - const uri = documentUri.toString() - return mockDocumentMap.get(uri) - }), - - getRecentOperations: vi.fn().mockImplementation((document: vscode.TextDocument): string => { - if (!document) { - return "" - } - - const uri = document.uri.toString() - const item = mockDocumentMap.get(uri) - - if (!item || item.history.length < 2) { - return "" - } - - // Get the last 10 versions (or fewer if not available) - const historyLimit = 10 - const startIdx = Math.max(0, item.history.length - historyLimit) - const recentHistory = item.history.slice(startIdx) - - // If we have at least 2 versions, compare the oldest with the newest - if (recentHistory.length >= 2) { - const oldContent = recentHistory[0] - const newContent = recentHistory[recentHistory.length - 1] - - // Use the mocked createPatch function - const { createPatch } = require("diff") - const filePath = "test-file-path" // Mock the file path - return createPatch(filePath, oldContent, newContent, "Previous version", "Current version") - } - - return "" - }), - } as unknown as GhostDocumentStore - - const uri = vscode.Uri.parse("file:///test.js") - mockDocument = new MockTextDocument(uri, "function test() { return true; }") - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("storeDocument", () => { - it("should store a document and its history", async () => { - await documentStore.storeDocument({ - document: mockDocument, - bypassDebounce: true, - }) - - const storedItem = documentStore.getDocument(mockDocument.uri) - expect(storedItem).toBeDefined() - expect(storedItem?.document).toBe(mockDocument) - expect(storedItem?.history).toHaveLength(1) - expect(storedItem?.history[0]).toBe("function test() { return true; }") - }) - - it("should limit history to historyLimit", async () => { - // Access private property for testing - const historyLimit = (documentStore as any).historyLimit - - // Store document multiple times with different content - for (let i = 0; i < historyLimit + 5; i++) { - mockDocument.updateContent(`function test${i}() { return true; }`) - await documentStore.storeDocument({ - document: mockDocument, - bypassDebounce: true, - }) - } - - const storedItem = documentStore.getDocument(mockDocument.uri) - expect(storedItem?.history).toHaveLength(historyLimit) - // First entries should be removed - expect(storedItem?.history[0]).toBe(`function test${5}() { return true; }`) - }) - }) - - describe("getRecentOperations", () => { - it("should return empty string for document not in store", () => { - const unknownUri = vscode.Uri.parse("file:///unknown.js") - const unknownDoc = new MockTextDocument(unknownUri, "") - - const operations = documentStore.getRecentOperations(unknownDoc) - expect(operations).toBe("") - }) - - it("should return empty string if history has less than 2 entries", async () => { - await documentStore.storeDocument({ - document: mockDocument, - bypassDebounce: true, - }) - - const operations = documentStore.getRecentOperations(mockDocument) - expect(operations).toBe("") - }) - - it("should return diff between oldest and newest versions", async () => { - // Store document multiple times with different content - await documentStore.storeDocument({ - document: mockDocument, - bypassDebounce: true, - }) - - mockDocument.updateContent("function updated() { return false; }") - await documentStore.storeDocument({ - document: mockDocument, - bypassDebounce: true, - }) - - const operations = documentStore.getRecentOperations(mockDocument) - // Instead of expecting a mocked output, verify that the diff contains the expected content - expect(operations).toContain("function test() { return true; }") - expect(operations).toContain("function updated() { return false; }") - expect(operations).toContain("@@ -1,1 +1,1 @@") // Check for diff markers - }) - - it("should handle undefined document", () => { - const operations = documentStore.getRecentOperations(undefined as any) - expect(operations).toBe("") - }) - }) -}) diff --git a/src/services/ghost/__tests__/GhostServiceManager.spec.ts b/src/services/ghost/__tests__/GhostServiceManager.spec.ts index 0ab8a7a2e08..add58182ff2 100644 --- a/src/services/ghost/__tests__/GhostServiceManager.spec.ts +++ b/src/services/ghost/__tests__/GhostServiceManager.spec.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest" import { MockWorkspace } from "./MockWorkspace" import * as vscode from "vscode" import { parseGhostResponse } from "../classic-auto-complete/HoleFiller" -import { GhostSuggestionContext, extractPrefixSuffix } from "../types" +import { extractPrefixSuffix } from "../types" vi.mock("vscode", () => ({ Uri: { @@ -80,7 +80,7 @@ describe("GhostServiceManager", () => { }) }) - // Helper function to set up test document and context + // Helper function to set up test document async function setupTestDocument(filename: string, content: string) { const testUri = vscode.Uri.parse(`file://${filename}`) mockWorkspace.addDocument(testUri, content) @@ -91,33 +91,28 @@ describe("GhostServiceManager", () => { const mockDocument = await mockWorkspace.openTextDocument(testUri) ;(mockDocument as any).uri = testUri - const context: GhostSuggestionContext = { - document: mockDocument, - openFiles: [mockDocument], - } - - return { testUri, context, mockDocument } + return { testUri, mockDocument } } describe("Error Handling", () => { it("should handle empty responses", async () => { const initialContent = `console.log('test');` - const { context } = await setupTestDocument("empty.js", initialContent) + const { mockDocument } = await setupTestDocument("empty.js", initialContent) // Test empty response - const position = context.range?.start ?? context.document.positionAt(0) - const { prefix, suffix } = extractPrefixSuffix(context.document, position) + const position = new vscode.Position(0, 0) + const { prefix, suffix } = extractPrefixSuffix(mockDocument, position) const result = parseGhostResponse("", prefix, suffix) expect(result.text).toBe("") }) it("should handle invalid COMPLETION format", async () => { const initialContent = `console.log('test');` - const { context } = await setupTestDocument("invalid.js", initialContent) + const { mockDocument } = await setupTestDocument("invalid.js", initialContent) const invalidCOMPLETION = "This is not a valid COMPLETION format" - const position = context.range?.start ?? context.document.positionAt(0) - const { prefix, suffix } = extractPrefixSuffix(context.document, position) + const position = new vscode.Position(0, 0) + const { prefix, suffix } = extractPrefixSuffix(mockDocument, position) const result = parseGhostResponse(invalidCOMPLETION, prefix, suffix) expect(result.text).toBe("") }) @@ -126,17 +121,11 @@ describe("GhostServiceManager", () => { const initialContent = `console.log('test');` const { mockDocument } = await setupTestDocument("missing.js", initialContent) - // Create context without the file in openFiles - const context: GhostSuggestionContext = { - document: mockDocument, - openFiles: [], - } - const completionResponse = `// Added comment console.log('test');` - const position = context.range?.start ?? context.document.positionAt(0) - const { prefix, suffix } = extractPrefixSuffix(context.document, position) + const position = new vscode.Position(0, 0) + const { prefix, suffix } = extractPrefixSuffix(mockDocument, position) const result = parseGhostResponse(completionResponse, prefix, suffix) expect(result.text).toBe("// Added comment\nconsole.log('test');") }) diff --git a/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts b/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts index b15dcd2d61a..019326aab73 100644 --- a/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts +++ b/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts @@ -3,7 +3,6 @@ import { extractPrefixSuffix, GhostSuggestionContext, contextToAutocompleteInput import { GhostContextProvider } from "./GhostContextProvider" import { parseGhostResponse, HoleFiller, FillInAtCursorSuggestion } from "./HoleFiller" import { GhostModel } from "../GhostModel" -import { GhostContext } from "../GhostContext" import { ApiStreamChunk } from "../../../api/transform/stream" import { RecentlyVisitedRangesService } from "../../continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService" import { RecentlyEditedTracker } from "../../continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited" @@ -77,7 +76,6 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte private isRequestCancelled: boolean = false private model: GhostModel private costTrackingCallback: CostTrackingCallback - private ghostContext: GhostContext private getSettings: () => GhostServiceSettings | null private recentlyVisitedRangesService: RecentlyVisitedRangesService private recentlyEditedTracker: RecentlyEditedTracker @@ -85,13 +83,11 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte constructor( model: GhostModel, costTrackingCallback: CostTrackingCallback, - ghostContext: GhostContext, getSettings: () => GhostServiceSettings | null, contextProvider?: GhostContextProvider, ) { this.model = model this.costTrackingCallback = costTrackingCallback - this.ghostContext = ghostContext this.getSettings = getSettings this.holeFiller = new HoleFiller(contextProvider) @@ -128,23 +124,14 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte /** * Retrieve suggestions from LLM - * @param context - The suggestion context + * @param context - The suggestion context (should already include recentlyVisitedRanges and recentlyEditedRanges) * @param model - The Ghost model to use for generation * @returns LLM retrieval result with suggestions and usage info */ public async getFromLLM(context: GhostSuggestionContext, model: GhostModel): Promise { this.isRequestCancelled = false - const recentlyVisitedRanges = this.recentlyVisitedRangesService.getSnippets() - const recentlyEditedRanges = await this.recentlyEditedTracker.getRecentlyEditedRanges() - - const enrichedContext: GhostSuggestionContext = { - ...context, - recentlyVisitedRanges, - recentlyEditedRanges, - } - - const autocompleteInput = contextToAutocompleteInput(enrichedContext) + const autocompleteInput = contextToAutocompleteInput(context) const position = context.range?.start ?? context.document.positionAt(0) const { prefix, suffix } = extractPrefixSuffix(context.document, position) @@ -280,15 +267,20 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte } // No cached suggestion available - invoke LLM - if (this.model && this.ghostContext) { + if (this.model) { + // Build complete context with all tracking data + const recentlyVisitedRanges = this.recentlyVisitedRangesService.getSnippets() + const recentlyEditedRanges = await this.recentlyEditedTracker.getRecentlyEditedRanges() + const context: GhostSuggestionContext = { document, range: new vscode.Range(position, position), + recentlyVisitedRanges, + recentlyEditedRanges, } - const fullContext = await this.ghostContext.generate(context) try { - const result = await this.getFromLLM(fullContext, this.model) + const result = await this.getFromLLM(context, this.model) if (this.costTrackingCallback && result.cost > 0) { this.costTrackingCallback( diff --git a/src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts b/src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts index f29538b06b3..a8fd9f3631d 100644 --- a/src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts +++ b/src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts @@ -7,7 +7,6 @@ import { import { FillInAtCursorSuggestion } from "../HoleFiller" import { MockTextDocument } from "../../../mocking/MockTextDocument" import { GhostModel } from "../../GhostModel" -import { GhostContext } from "../../GhostContext" // Mock vscode InlineCompletionTriggerKind enum and event listeners vi.mock("vscode", async () => { @@ -294,7 +293,6 @@ describe("GhostInlineCompletionProvider", () => { let mockToken: vscode.CancellationToken let mockModel: GhostModel let mockCostTrackingCallback: CostTrackingCallback - let mockGhostContext: GhostContext let mockSettings: { enableAutoTrigger: boolean } | null let mockContextProvider: any @@ -333,18 +331,10 @@ describe("GhostInlineCompletionProvider", () => { }), } as unknown as GhostModel mockCostTrackingCallback = vi.fn() as CostTrackingCallback - mockGhostContext = { - generate: vi.fn().mockImplementation(async (ctx) => ({ - ...ctx, - document: ctx.document, - range: ctx.range, - })), - } as unknown as GhostContext provider = new GhostInlineCompletionProvider( mockModel, mockCostTrackingCallback, - mockGhostContext, () => mockSettings, mockContextProvider, ) diff --git a/src/services/ghost/types.ts b/src/services/ghost/types.ts index f93f0689678..e1dfdcfb492 100644 --- a/src/services/ghost/types.ts +++ b/src/services/ghost/types.ts @@ -1,50 +1,9 @@ import * as vscode from "vscode" import type { AutocompleteCodeSnippet } from "../continuedev/core/autocomplete/snippets/types" -import type { RecentlyEditedRange as ContinuedevRecentlyEditedRange } from "../continuedev/core/autocomplete/util/types" - -/** - * Represents the type of user action performed on a document - */ -export enum UserActionType { - ADDITION = "ADDITION", // Added new code - DELETION = "DELETION", // Removed existing code - MODIFICATION = "MODIFICATION", // Changed existing code - REFACTOR = "REFACTOR", // Renamed or moved code - FORMAT = "FORMAT", // Changed formatting without semantic changes -} - -/** - * Represents a meaningful user action performed on a document - */ -export interface UserAction { - type: UserActionType - description: string - lineRange?: { - start: number - end: number - } - affectedSymbol?: string // Function/variable/class name if applicable - scope?: string // Function/class/namespace containing the change - timestamp?: number // When the action occurred - content?: string // The actual content that was added, deleted, or modified -} - -export interface GhostDocumentStoreItem { - uri: string - document: vscode.TextDocument - history: string[] - lastParsedVersion?: number - recentActions?: UserAction[] -} export interface GhostSuggestionContext { document: vscode.TextDocument - editor?: vscode.TextEditor - openFiles?: vscode.TextDocument[] range?: vscode.Range | vscode.Selection - userInput?: string - recentOperations?: UserAction[] // Stores meaningful user actions instead of raw diff - diagnostics?: vscode.Diagnostic[] // Document diagnostics (errors, warnings, etc.) recentlyVisitedRanges?: AutocompleteCodeSnippet[] // Recently visited code snippets for context recentlyEditedRanges?: RecentlyEditedRange[] // Recently edited ranges for context } @@ -208,16 +167,6 @@ export function vscodePositionToPosition(pos: vscode.Position): Position { } } -/** - * Convert VSCode Range to our Range type - */ -export function vscodeRangeToRange(range: vscode.Range): Range { - return { - start: vscodePositionToPosition(range.start), - end: vscodePositionToPosition(range.end), - } -} - /** * Convert GhostSuggestionContext to AutocompleteInput */ @@ -229,38 +178,13 @@ export function contextToAutocompleteInput(context: GhostSuggestionContext): Aut const recentlyVisitedRanges = context.recentlyVisitedRanges ?? [] const recentlyEditedRanges = context.recentlyEditedRanges ?? [] - // Merge recently edited ranges from context operations with tracked ranges - const contextEditedRanges: RecentlyEditedRange[] = - context.recentOperations?.map((op) => { - const range: Range = op.lineRange - ? { - start: { line: op.lineRange.start, character: 0 }, - end: { line: op.lineRange.end, character: 0 }, - } - : { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 }, - } - - return { - filepath: context.document.uri.fsPath, - range, - timestamp: op.timestamp ?? Date.now(), - lines: op.content ? op.content.split("\n") : [], - symbols: new Set(op.affectedSymbol ? [op.affectedSymbol] : []), - } - }) ?? [] - - // Combine tracked recently edited ranges with context operations - const allRecentlyEditedRanges = [...recentlyEditedRanges, ...contextEditedRanges] - return { isUntitledFile: context.document.isUntitled, completionId: crypto.randomUUID(), filepath: context.document.uri.fsPath, pos: vscodePositionToPosition(position), recentlyVisitedRanges, - recentlyEditedRanges: allRecentlyEditedRanges, + recentlyEditedRanges, manuallyPassFileContents: undefined, manuallyPassPrefix: prefix, } diff --git a/src/test-llm-autocompletion/strategy-tester.ts b/src/test-llm-autocompletion/strategy-tester.ts index 2b945bee010..9848188a542 100644 --- a/src/test-llm-autocompletion/strategy-tester.ts +++ b/src/test-llm-autocompletion/strategy-tester.ts @@ -51,10 +51,6 @@ export class StrategyTester { return { document: document as any, range: range as any, - recentOperations: [], - diagnostics: [], - openFiles: [], - userInput: undefined, } }