diff --git a/src/services/continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts b/src/services/continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts index fb30f83c4cb..8f128e552b7 100644 --- a/src/services/continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts +++ b/src/services/continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts @@ -14,6 +14,7 @@ export class RecentlyVisitedRangesService { private maxRecentFiles = 3 private maxSnippetsPerFile = 3 private isEnabled = true + private disposable: vscode.Disposable | undefined constructor(private readonly ide: IDE) { this.cache = new LRUCache>({ @@ -32,7 +33,7 @@ export class RecentlyVisitedRangesService { this.numSurroundingLines = recentlyVisitedRangesNumSurroundingLines } - vscode.window.onDidChangeTextEditorSelection(this.cacheCurrentSelectionContext) + this.disposable = vscode.window.onDidChangeTextEditorSelection(this.cacheCurrentSelectionContext) } private cacheCurrentSelectionContext = async (event: vscode.TextEditorSelectionChangeEvent) => { @@ -105,4 +106,9 @@ export class RecentlyVisitedRangesService { .sort((a, b) => b.timestamp - a.timestamp) .map(({ timestamp: _timestamp, ...snippet }) => snippet) } + + public dispose(): void { + this.disposable?.dispose() + this.cache.clear() + } } diff --git a/src/services/continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited.ts b/src/services/continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited.ts index 71731c9813c..d7ebdc03027 100644 --- a/src/services/continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited.ts +++ b/src/services/continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited.ts @@ -21,9 +21,11 @@ export class RecentlyEditedTracker { private recentlyEditedDocuments: VsCodeRecentlyEditedDocument[] = [] private static maxRecentlyEditedDocuments = 10 + private disposable: vscode.Disposable | undefined + private cleanupInterval: NodeJS.Timeout | undefined constructor(private ide: IDE) { - vscode.workspace.onDidChangeTextDocument((event) => { + this.disposable = vscode.workspace.onDidChangeTextDocument((event) => { event.contentChanges.forEach((change) => { const editedRange = { uri: event.document.uri, @@ -39,7 +41,7 @@ export class RecentlyEditedTracker { this.insertDocument(event.document.uri) }) - setInterval(() => { + this.cleanupInterval = setInterval(() => { this.removeOldEntries() }, 1000 * 15) } @@ -128,4 +130,13 @@ export class RecentlyEditedTracker { } }) } + + public dispose(): void { + this.disposable?.dispose() + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval) + } + this.recentlyEditedRanges = [] + this.recentlyEditedDocuments = [] + } } diff --git a/src/services/ghost/GhostServiceManager.ts b/src/services/ghost/GhostServiceManager.ts index 0c30be50ffc..31dc63d4f9b 100644 --- a/src/services/ghost/GhostServiceManager.ts +++ b/src/services/ghost/GhostServiceManager.ts @@ -6,6 +6,7 @@ import { GhostModel } from "./GhostModel" import { GhostStatusBar } from "./GhostStatusBar" import { GhostCodeActionProvider } from "./GhostCodeActionProvider" import { GhostInlineCompletionProvider } from "./classic-auto-complete/GhostInlineCompletionProvider" +import { GhostContextProvider } from "./classic-auto-complete/GhostContextProvider" //import { NewAutocompleteProvider } from "./new-auto-complete/NewAutocompleteProvider" import { GhostServiceSettings, TelemetryEventName } from "@roo-code/types" import { ContextProxy } from "../../core/config/ContextProxy" @@ -24,6 +25,7 @@ export class GhostServiceManager { private providerSettingsManager: ProviderSettingsManager private settings: GhostServiceSettings | null = null private ghostContext: GhostContext + private ghostContextProvider: GhostContextProvider private taskId: string | null = null private isProcessing: boolean = false @@ -50,6 +52,7 @@ export class GhostServiceManager { this.providerSettingsManager = new ProviderSettingsManager(context) this.model = new GhostModel() this.ghostContext = new GhostContext(this.documentStore) + this.ghostContextProvider = new GhostContextProvider(context) // Register the providers this.codeActionProvider = new GhostCodeActionProvider() @@ -58,6 +61,7 @@ export class GhostServiceManager { this.updateCostTracking.bind(this), this.ghostContext, () => this.settings, + this.ghostContextProvider, ) // Register document event handlers @@ -457,6 +461,8 @@ export class GhostServiceManager { this.inlineCompletionProviderDisposable = null } + // Dispose inline completion provider resources + this.inlineCompletionProvider.dispose() // Dispose new autocomplete provider if it exists //if (this.newAutocompleteProvider) { // this.newAutocompleteProvider.dispose() diff --git a/src/services/ghost/__tests__/GhostRecentOperations.spec.ts b/src/services/ghost/__tests__/GhostRecentOperations.spec.ts deleted file mode 100644 index 0fc54369f48..00000000000 --- a/src/services/ghost/__tests__/GhostRecentOperations.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from "vitest" -import * as vscode from "vscode" -import { GhostContext } from "../GhostContext" -import { GhostDocumentStore } from "../GhostDocumentStore" -import { HoleFiller } from "../classic-auto-complete/HoleFiller" -import { GhostSuggestionContext, contextToAutocompleteInput } from "../types" -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://", ""), - }), - }, - workspace: { - asRelativePath: vi.fn().mockImplementation((uri) => { - if (typeof uri === "string") { - return uri.replace("file:///", "") - } - return uri.toString().replace("file:///", "") - }), - textDocuments: [], // Mock textDocuments as an empty array - }, - window: { - activeTextEditor: null, - }, - languages: { - getDiagnostics: vi.fn().mockReturnValue([]), // Mock getDiagnostics to return empty array - }, - Position: class { - constructor( - public line: number, - public character: number, - ) {} - }, - Range: class { - constructor( - public start: any, - public end: any, - ) {} - }, - DiagnosticSeverity: { - Error: 0, - Warning: 1, - Information: 2, - Hint: 3, - }, -})) - -// Mock diff - using importOriginal as recommended in the error message -vi.mock("diff", async (importOriginal) => { - // Create a mock module with the functions we need - return { - createPatch: vi.fn().mockImplementation((filePath, oldContent, newContent) => { - return `--- a/${filePath}\n+++ b/${filePath}\n@@ -1,1 +1,1 @@\n-${oldContent}\n+${newContent}` - }), - structuredPatch: vi.fn().mockImplementation((oldFileName, newFileName, oldContent, newContent) => { - return { - hunks: [ - { - oldStart: 1, - oldLines: 1, - newStart: 1, - newLines: 1, - lines: [`-${oldContent}`, `+${newContent}`], - }, - ], - } - }), - parsePatch: vi.fn().mockReturnValue([]), - } -}) - -describe("GhostRecentOperations", () => { - let documentStore: GhostDocumentStore - let context: GhostContext - let holeFiller: HoleFiller - let mockDocument: MockTextDocument - - beforeEach(() => { - documentStore = new GhostDocumentStore() - context = new GhostContext(documentStore) - holeFiller = new HoleFiller() - - // Create a mock document - const uri = vscode.Uri.parse("file:///test-file.ts") - mockDocument = new MockTextDocument(uri, "test-content") - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - it("should include recent operations in the prompt when available", async () => { - // Store initial document version - await documentStore.storeDocument({ document: mockDocument, bypassDebounce: true }) - - // Update document content and store again - mockDocument.updateContent("test-content-updated") - await documentStore.storeDocument({ document: mockDocument, bypassDebounce: true }) - - // Create a suggestion context - const suggestionContext: GhostSuggestionContext = { - document: mockDocument, - } - - // Generate context with recent operations - const enrichedContext = await context.generate(suggestionContext) - - // Verify that recent operations were added to the context - expect(enrichedContext.recentOperations).toBeDefined() - expect(enrichedContext.recentOperations?.length).toBeGreaterThan(0) - - const autocompleteInput = contextToAutocompleteInput(enrichedContext) - - // Generate prompt - const prefix = enrichedContext.document.getText() - const suffix = "" - const languageId = enrichedContext.document.languageId - const { userPrompt } = holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId) - - // Verify that the prompt includes the recent operations section - // The strategy system uses "" XML format - expect(userPrompt).toContain("") - }) - - it("should not include recent operations in the prompt when not available", async () => { - // Create a suggestion context without storing document history - const suggestionContext: GhostSuggestionContext = { - document: mockDocument, - } - - // Generate context - const enrichedContext = await context.generate(suggestionContext) - - const autocompleteInput = contextToAutocompleteInput(enrichedContext) - - // Generate prompt - const prefix = enrichedContext.document.getText() - const suffix = "" - const languageId = enrichedContext.document.languageId - const { userPrompt } = holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId) - - // Verify that the prompt does not include recent operations section - // The current document content will still be in the prompt, so we should only check - // that the "**Recent Changes (Diff):**" section is not present - expect(userPrompt.includes("**Recent Changes (Diff):**")).toBe(false) - }) -}) diff --git a/src/services/ghost/classic-auto-complete/GhostContextProvider.ts b/src/services/ghost/classic-auto-complete/GhostContextProvider.ts new file mode 100644 index 00000000000..244822e0eb7 --- /dev/null +++ b/src/services/ghost/classic-auto-complete/GhostContextProvider.ts @@ -0,0 +1,89 @@ +import * as vscode from "vscode" +import { ContextRetrievalService } from "../../continuedev/core/autocomplete/context/ContextRetrievalService" +import { VsCodeIde } from "../../continuedev/core/vscode-test-harness/src/VSCodeIde" +import { AutocompleteInput } from "../types" +import { AutocompleteSnippetType } from "../../continuedev/core/autocomplete/snippets/types" +import { HelperVars } from "../../continuedev/core/autocomplete/util/HelperVars" +import { getAllSnippetsWithoutRace } from "../../continuedev/core/autocomplete/snippets/getAllSnippets" +import { getDefinitionsFromLsp } from "../../continuedev/core/vscode-test-harness/src/autocomplete/lsp" +import { DEFAULT_AUTOCOMPLETE_OPTS } from "../../continuedev/core/util/parameters" +import { getSnippets } from "../../continuedev/core/autocomplete/templating/filtering" +import { formatSnippets } from "../../continuedev/core/autocomplete/templating/formatting" + +function convertToContinuedevInput(autocompleteInput: AutocompleteInput) { + return { + ...autocompleteInput, + recentlyVisitedRanges: autocompleteInput.recentlyVisitedRanges.map((range) => ({ + ...range, + type: AutocompleteSnippetType.Code, + })), + } +} + +export class GhostContextProvider { + private contextService: ContextRetrievalService + private ide: VsCodeIde + + constructor(context: vscode.ExtensionContext) { + this.ide = new VsCodeIde(context) + this.contextService = new ContextRetrievalService(this.ide) + } + + /** + * Get the IDE instance for use by tracking services + */ + public getIde(): VsCodeIde { + return this.ide + } + + /** + * Get context snippets for the current autocomplete request + * Returns comment-based formatted context that can be added to prompts + */ + async getFormattedContext(autocompleteInput: AutocompleteInput, filepath: string): Promise { + try { + // Convert filepath to URI if it's not already one + const filepathUri = filepath.startsWith("file://") ? filepath : vscode.Uri.file(filepath).toString() + + // Initialize import definitions cache + await this.contextService.initializeForFile(filepathUri) + + const continuedevInput = convertToContinuedevInput(autocompleteInput) + + // Create helper with URI filepath + const helperInput = { + ...continuedevInput, + filepath: filepathUri, + } + + const helper = await HelperVars.create(helperInput as any, DEFAULT_AUTOCOMPLETE_OPTS, "codestral", this.ide) + + const snippetPayload = await getAllSnippetsWithoutRace({ + helper, + ide: this.ide, + getDefinitionsFromLsp, + contextRetrievalService: this.contextService, + }) + + const filteredSnippets = getSnippets(helper, snippetPayload) + + // Convert all snippet filepaths to URIs + const snippetsWithUris = filteredSnippets.map((snippet: any) => ({ + ...snippet, + filepath: snippet.filepath?.startsWith("file://") + ? snippet.filepath + : vscode.Uri.file(snippet.filepath).toString(), + })) + + const workspaceDirs = await this.ide.getWorkspaceDirs() + const formattedContext = formatSnippets(helper, snippetsWithUris, workspaceDirs) + + console.log("[GhostContextProvider] - formattedContext:", formattedContext) + + return formattedContext + } catch (error) { + console.warn("Failed to get formatted context:", error) + return "" + } + } +} diff --git a/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts b/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts index 94c287f0c26..f9b965eec82 100644 --- a/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts +++ b/src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts @@ -1,9 +1,12 @@ import * as vscode from "vscode" import { extractPrefixSuffix, GhostSuggestionContext, contextToAutocompleteInput } from "../types" +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" import type { GhostServiceSettings } from "@roo-code/types" import { refuseUselessSuggestion } from "./uselessSuggestionFilter" @@ -76,18 +79,30 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte private costTrackingCallback: CostTrackingCallback private ghostContext: GhostContext private getSettings: () => GhostServiceSettings | null + private recentlyVisitedRangesService: RecentlyVisitedRangesService + private recentlyEditedTracker: RecentlyEditedTracker 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() + this.holeFiller = new HoleFiller(contextProvider) + + // Get IDE from context provider if available + const ide = contextProvider?.getIde() + if (ide) { + this.recentlyVisitedRangesService = new RecentlyVisitedRangesService(ide) + this.recentlyEditedTracker = new RecentlyEditedTracker(ide) + } else { + throw new Error("GhostContextProvider with IDE is required for tracking services") + } } public updateSuggestions(fillInAtCursor: FillInAtCursorSuggestion): void { @@ -120,7 +135,9 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte public async getFromLLM(context: GhostSuggestionContext, model: GhostModel): Promise { this.isRequestCancelled = false - const autocompleteInput = contextToAutocompleteInput(context) + const recentlyVisitedRanges = this.recentlyVisitedRangesService.getSnippets() + const recentlyEditedRanges = await this.recentlyEditedTracker.getRecentlyEditedRanges() + const autocompleteInput = contextToAutocompleteInput(context, recentlyVisitedRanges, recentlyEditedRanges) const position = context.range?.start ?? context.document.positionAt(0) const { prefix, suffix } = extractPrefixSuffix(context.document, position) @@ -140,7 +157,12 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte } } - const { systemPrompt, userPrompt } = this.holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId) + const { systemPrompt, userPrompt } = await this.holeFiller.getPrompts( + autocompleteInput, + prefix, + suffix, + languageId, + ) if (this.isRequestCancelled) { return { @@ -203,13 +225,15 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte } } - /** - * Cancel any ongoing LLM request - */ public cancelRequest(): void { this.isRequestCancelled = true } + public dispose(): void { + this.recentlyVisitedRangesService.dispose() + this.recentlyEditedTracker.dispose() + } + public async provideInlineCompletionItems( document: vscode.TextDocument, position: vscode.Position, diff --git a/src/services/ghost/classic-auto-complete/HoleFiller.ts b/src/services/ghost/classic-auto-complete/HoleFiller.ts index 1e2c431cf7a..46416bab2b2 100644 --- a/src/services/ghost/classic-auto-complete/HoleFiller.ts +++ b/src/services/ghost/classic-auto-complete/HoleFiller.ts @@ -1,5 +1,5 @@ import { AutocompleteInput } from "../types" -import type { TextDocument, Range } from "vscode" +import { GhostContextProvider } from "./GhostContextProvider" /** * Special marker used to indicate where completions should occur in the document @@ -15,8 +15,10 @@ export interface FillInAtCursorSuggestion { export function getBaseSystemInstructions(): string { return `You are a HOLE FILLER. You are provided with a file containing holes, formatted as '{{FILL_HERE}}'. Your TASK is to complete with a string to replace this hole with, inside a XML tag, including context-aware indentation, if needed. All completions MUST be truthful, accurate, well-written and correct. -## Context Tags -: file language | : recent changes | : code with {{FILL_HERE}} +## Context Format +: file language +: contains commented reference code (// Path: file.ts) followed by code with {{FILL_HERE}} +Comments provide context from related files, recent edits, imports, etc. ## EXAMPLE QUERY: @@ -103,17 +105,6 @@ function hypothenuse(a, b) { ` } -export function addCursorMarker(document: TextDocument, range?: Range): string { - if (!range) return document.getText() - - const fullText = document.getText() - const cursorOffset = document.offsetAt(range.start) - const beforeCursor = fullText.substring(0, cursorOffset) - const afterCursor = fullText.substring(cursorOffset) - - return `${beforeCursor}${CURSOR_MARKER}${afterCursor}` -} - /** * Parse the response - only handles responses with tags * Returns a FillInAtCursorSuggestion with the extracted text, or an empty string if nothing found @@ -140,18 +131,21 @@ export function parseGhostResponse(fullResponse: string, prefix: string, suffix: } export class HoleFiller { - getPrompts( + constructor(private contextProvider?: GhostContextProvider) {} + + async getPrompts( autocompleteInput: AutocompleteInput, prefix: string, suffix: string, languageId: string, - ): { + ): Promise<{ systemPrompt: string userPrompt: string - } { + }> { + const userPrompt = await this.getUserPrompt(autocompleteInput, prefix, suffix, languageId) return { systemPrompt: this.getSystemInstructions(), - userPrompt: this.getUserPrompt(autocompleteInput, prefix, suffix, languageId), + userPrompt, } } @@ -166,22 +160,32 @@ Provide a subtle, non-intrusive completion after a typing pause. } /** - * Build minimal prompt for auto-trigger + * Build minimal prompt for auto-trigger with optional context */ - getUserPrompt(autocompleteInput: AutocompleteInput, prefix: string, suffix: string, languageId: string): string { + async getUserPrompt( + autocompleteInput: AutocompleteInput, + prefix: string, + suffix: string, + languageId: string, + ): Promise { let prompt = `${languageId}\n\n` - if (autocompleteInput.recentlyEditedRanges && autocompleteInput.recentlyEditedRanges.length > 0) { - prompt += "\n" - autocompleteInput.recentlyEditedRanges.forEach((range, index) => { - const description = `Edited ${range.filepath} at line ${range.range.start.line}` - prompt += `${index + 1}. ${description}\n` - }) - prompt += "\n\n" + // Get comment-wrapped context (includes all context types with token-based filtering) + let formattedContext = "" + if (this.contextProvider && autocompleteInput.filepath) { + try { + formattedContext = await this.contextProvider.getFormattedContext( + autocompleteInput, + autocompleteInput.filepath, + ) + } catch (error) { + console.warn("Failed to get formatted context:", error) + } } + // Context and code go together in QUERY (comments provide context for the code) prompt += ` -${prefix}{{FILL_HERE}}${suffix} +${formattedContext}${prefix}{{FILL_HERE}}${suffix} TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now. diff --git a/src/services/ghost/classic-auto-complete/__tests__/GhostContextProvider.test.ts b/src/services/ghost/classic-auto-complete/__tests__/GhostContextProvider.test.ts new file mode 100644 index 00000000000..bb57df8ef0e --- /dev/null +++ b/src/services/ghost/classic-auto-complete/__tests__/GhostContextProvider.test.ts @@ -0,0 +1,195 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import { GhostContextProvider } from "../GhostContextProvider" +import { AutocompleteInput } from "../../types" +import { AutocompleteSnippetType } from "../../../continuedev/core/autocomplete/snippets/types" +import * as vscode from "vscode" +import crypto from "crypto" + +vi.mock("vscode", () => ({ + Uri: { + parse: (uriString: string) => ({ + toString: () => uriString, + fsPath: uriString.replace("file://", ""), + }), + file: (path: string) => ({ + toString: () => `file://${path}`, + fsPath: path, + }), + }, + workspace: { + textDocuments: [], + workspaceFolders: [], + }, + window: { + activeTextEditor: null, + }, +})) + +vi.mock("../../../continuedev/core/autocomplete/context/ContextRetrievalService", () => ({ + ContextRetrievalService: vi.fn().mockImplementation(() => ({ + initializeForFile: vi.fn().mockResolvedValue(undefined), + })), +})) + +vi.mock("../../../continuedev/core/vscode-test-harness/src/VSCodeIde", () => ({ + VsCodeIde: vi.fn().mockImplementation(() => ({ + getWorkspaceDirs: vi.fn().mockResolvedValue(["file:///workspace"]), + })), +})) + +vi.mock("../../../continuedev/core/autocomplete/util/HelperVars", () => ({ + HelperVars: { + create: vi.fn().mockResolvedValue({ + filepath: "file:///test.ts", + lang: { name: "typescript", singleLineComment: "//" }, + }), + }, +})) + +vi.mock("../../../continuedev/core/autocomplete/snippets/getAllSnippets", () => ({ + getAllSnippetsWithoutRace: vi.fn().mockResolvedValue({ + recentlyOpenedFileSnippets: [], + importDefinitionSnippets: [], + rootPathSnippets: [], + recentlyEditedRangeSnippets: [], + recentlyVisitedRangesSnippets: [], + diffSnippets: [], + clipboardSnippets: [], + ideSnippets: [], + staticSnippet: [], + }), +})) + +vi.mock("../../../continuedev/core/autocomplete/templating/filtering", () => ({ + getSnippets: vi + .fn() + .mockImplementation((_helper, payload) => [ + ...payload.recentlyOpenedFileSnippets, + ...payload.importDefinitionSnippets, + ]), +})) + +vi.mock("../../../continuedev/core/autocomplete/templating/formatting", () => ({ + formatSnippets: vi.fn().mockImplementation((helper, snippets) => { + if (snippets.length === 0) return "" + const comment = helper.lang.singleLineComment + return snippets.map((s: any) => `${comment} Path: ${s.filepath}\n${s.content}`).join("\n") + }), +})) + +function createAutocompleteInput(filepath: string = "/test.ts"): AutocompleteInput { + return { + isUntitledFile: false, + completionId: crypto.randomUUID(), + filepath, + pos: { line: 0, character: 0 }, + recentlyVisitedRanges: [], + recentlyEditedRanges: [], + } +} + +describe("GhostContextProvider", () => { + let contextProvider: GhostContextProvider + let mockContext: vscode.ExtensionContext + + beforeEach(() => { + vi.clearAllMocks() + mockContext = { + subscriptions: [], + globalState: { + get: vi.fn(), + update: vi.fn(), + }, + } as any + + contextProvider = new GhostContextProvider(mockContext) + }) + + describe("getFormattedContext", () => { + it("should return empty string when no snippets available", async () => { + const input = createAutocompleteInput("/test.ts") + const formatted = await contextProvider.getFormattedContext(input, "/test.ts") + + expect(formatted).toBe("") + }) + + it("should return formatted context when snippets are available", async () => { + const { getAllSnippetsWithoutRace } = await import( + "../../../continuedev/core/autocomplete/snippets/getAllSnippets" + ) + + ;(getAllSnippetsWithoutRace as any).mockResolvedValueOnce({ + recentlyOpenedFileSnippets: [ + { + filepath: "/recent.ts", + content: "const recent = 1;", + type: AutocompleteSnippetType.Code, + }, + ], + importDefinitionSnippets: [], + rootPathSnippets: [], + recentlyEditedRangeSnippets: [], + recentlyVisitedRangesSnippets: [], + diffSnippets: [], + clipboardSnippets: [], + ideSnippets: [], + staticSnippet: [], + }) + + const input = createAutocompleteInput("/test.ts") + const formatted = await contextProvider.getFormattedContext(input, "/test.ts") + + const expected = "// Path: file:///recent.ts\nconst recent = 1;" + expect(formatted).toBe(expected) + }) + + it("should format multiple snippets correctly", async () => { + const { getAllSnippetsWithoutRace } = await import( + "../../../continuedev/core/autocomplete/snippets/getAllSnippets" + ) + + ;(getAllSnippetsWithoutRace as any).mockResolvedValueOnce({ + recentlyOpenedFileSnippets: [ + { + filepath: "/file1.ts", + content: "const first = 1;", + type: AutocompleteSnippetType.Code, + }, + ], + importDefinitionSnippets: [ + { + filepath: "/file2.ts", + content: "const second = 2;", + type: AutocompleteSnippetType.Code, + }, + ], + rootPathSnippets: [], + recentlyEditedRangeSnippets: [], + recentlyVisitedRangesSnippets: [], + diffSnippets: [], + clipboardSnippets: [], + ideSnippets: [], + staticSnippet: [], + }) + + const input = createAutocompleteInput("/test.ts") + const formatted = await contextProvider.getFormattedContext(input, "/test.ts") + + const expected = "// Path: file:///file1.ts\nconst first = 1;\n// Path: file:///file2.ts\nconst second = 2;" + expect(formatted).toBe(expected) + }) + + it("should handle errors gracefully and return empty string", async () => { + const { getAllSnippetsWithoutRace } = await import( + "../../../continuedev/core/autocomplete/snippets/getAllSnippets" + ) + + ;(getAllSnippetsWithoutRace as any).mockRejectedValueOnce(new Error("Test error")) + + const input = createAutocompleteInput("/test.ts") + const formatted = await contextProvider.getFormattedContext(input, "/test.ts") + + expect(formatted).toBe("") + }) + }) +}) 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 239fd1f81fd..f29538b06b3 100644 --- a/src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts +++ b/src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts @@ -9,7 +9,7 @@ import { MockTextDocument } from "../../../mocking/MockTextDocument" import { GhostModel } from "../../GhostModel" import { GhostContext } from "../../GhostContext" -// Mock vscode InlineCompletionTriggerKind enum +// Mock vscode InlineCompletionTriggerKind enum and event listeners vi.mock("vscode", async () => { const actual = await vi.importActual("vscode") return { @@ -18,6 +18,14 @@ vi.mock("vscode", async () => { Invoke: 0, Automatic: 1, }, + window: { + ...actual.window, + onDidChangeTextEditorSelection: vi.fn(() => ({ dispose: vi.fn() })), + }, + workspace: { + ...actual.workspace, + onDidChangeTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + }, } }) @@ -288,6 +296,7 @@ describe("GhostInlineCompletionProvider", () => { let mockCostTrackingCallback: CostTrackingCallback let mockGhostContext: GhostContext let mockSettings: { enableAutoTrigger: boolean } | null + let mockContextProvider: any beforeEach(() => { mockDocument = new MockTextDocument(vscode.Uri.file("/test.ts"), "const x = 1\nconst y = 2") @@ -299,6 +308,20 @@ describe("GhostInlineCompletionProvider", () => { mockToken = {} as vscode.CancellationToken mockSettings = { enableAutoTrigger: true } + // Create mock IDE for tracking services + const mockIde = { + getWorkspaceDirs: vi.fn().mockResolvedValue([]), + getOpenFiles: vi.fn().mockResolvedValue([]), + readFile: vi.fn().mockResolvedValue(""), + // Add other methods as needed by RecentlyVisitedRangesService and RecentlyEditedTracker + } + + // Create mock context provider with IDE + mockContextProvider = { + getIde: vi.fn().mockReturnValue(mockIde), + getFormattedContext: vi.fn().mockResolvedValue(""), + } + // Create mock dependencies mockModel = { generateResponse: vi.fn().mockResolvedValue({ @@ -323,6 +346,7 @@ describe("GhostInlineCompletionProvider", () => { mockCostTrackingCallback, mockGhostContext, () => mockSettings, + mockContextProvider, ) }) diff --git a/src/services/ghost/classic-auto-complete/__tests__/HoleFiller.test.ts b/src/services/ghost/classic-auto-complete/__tests__/HoleFiller.test.ts index 8d388e39c39..cfdb1fc6dba 100644 --- a/src/services/ghost/classic-auto-complete/__tests__/HoleFiller.test.ts +++ b/src/services/ghost/classic-auto-complete/__tests__/HoleFiller.test.ts @@ -26,8 +26,8 @@ describe("HoleFiller", () => { }) describe("getPrompts", () => { - it("should generate prompts with QUERY/FILL_HERE format", () => { - const { systemPrompt, userPrompt } = holeFiller.getPrompts( + it("should generate prompts with QUERY/FILL_HERE format", async () => { + const { systemPrompt, userPrompt } = await holeFiller.getPrompts( createAutocompleteInput("/test.ts", 0, 13), "const x = 1;\n", "", @@ -36,86 +36,59 @@ describe("HoleFiller", () => { // Verify system prompt contains auto-trigger keywords expect(systemPrompt).toContain("Auto-Completion") - expect(systemPrompt).toContain("non-intrusive") + expect(systemPrompt).toContain("Context Format") - // Verify user prompt uses QUERY/FILL_HERE format - expect(userPrompt).toContain("") - expect(userPrompt).toContain("{{FILL_HERE}}") - expect(userPrompt).toContain("") - expect(userPrompt).toContain("COMPLETION") - }) + const expected = `typescript - it("should document context tags in system prompt", () => { - const { systemPrompt } = holeFiller.getPrompts( - createAutocompleteInput("/test.ts", 0, 13), - "const x = 1;\n", - "", - "typescript", - ) + +const x = 1; +{{FILL_HERE}} + - // Verify system prompt documents the XML tags - expect(systemPrompt).toContain("Context Tags") - expect(systemPrompt).toContain("") - expect(systemPrompt).toContain("") - expect(systemPrompt).toContain("") - }) - - it("should include language ID in prompt with XML tags", () => { - const { userPrompt } = holeFiller.getPrompts( - createAutocompleteInput("/test.ts", 0, 13), - "const x = 1;\n", - "", - "typescript", - ) +TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now. +Return the COMPLETION tags` - expect(userPrompt).toContain("typescript") + expect(userPrompt).toBe(expected) }) - it("should include recently edited ranges in prompt with XML tags", () => { - const input = createAutocompleteInput("/test.ts", 5, 0) - input.recentlyEditedRanges = [ - { - filepath: "/test.ts", - range: { start: { line: 2, character: 0 }, end: { line: 3, character: 0 } }, - timestamp: Date.now(), - lines: ["function sum(a, b) {"], - symbols: new Set(["sum"]), + it("should include comment-wrapped context when provider is set", async () => { + const mockContextProvider = { + getFormattedContext: async () => { + // Simulate comment-wrapped format + return `// Path: utils.ts +// export function sum(a: number, b: number) { +// return a + b +// } +// Path: app.ts +` }, - ] + } as any - const { userPrompt } = holeFiller.getPrompts(input, "const x = 1;\n", "", "typescript") - - expect(userPrompt).toContain("") - expect(userPrompt).toContain("") - expect(userPrompt).toContain("Edited /test.ts at line 2") - }) - - it("should handle empty recently edited ranges", () => { - const { userPrompt } = holeFiller.getPrompts( - createAutocompleteInput("/test.ts", 0, 13), - "const x = 1;\n", - "", + const holeFillerWithContext = new HoleFiller(mockContextProvider) + const { userPrompt } = await holeFillerWithContext.getPrompts( + createAutocompleteInput("/app.ts", 5, 0), + "function calculate() {\n ", + "\n}", "typescript", ) - expect(userPrompt).not.toContain("") - expect(userPrompt).toContain("typescript") - }) + const expected = `typescript - it("should handle comments in code", () => { - const { systemPrompt, userPrompt } = holeFiller.getPrompts( - createAutocompleteInput("/test.ts", 1, 0), - "// TODO: implement sum function\n", - "", - "typescript", - ) + +// Path: utils.ts +// export function sum(a: number, b: number) { +// return a + b +// } +// Path: app.ts +function calculate() { + {{FILL_HERE}} +} + - // Should use same prompt format - expect(systemPrompt).toContain("Auto-Completion") - expect(userPrompt).toContain("") - expect(userPrompt).toContain("{{FILL_HERE}}") - expect(userPrompt).toContain("") - expect(userPrompt).toContain("COMPLETION") +TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now. +Return the COMPLETION tags` + + expect(userPrompt).toBe(expected) }) }) diff --git a/src/services/ghost/types.ts b/src/services/ghost/types.ts index 43dd191eebc..3ba8c64ae21 100644 --- a/src/services/ghost/types.ts +++ b/src/services/ghost/types.ts @@ -1,4 +1,6 @@ import * as vscode from "vscode" +import type { AutocompleteCodeSnippet as ContinuedevAutocompleteCodeSnippet } 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 @@ -115,7 +117,8 @@ export interface RecentlyEditedRange extends RangeInFile { * Code snippet for autocomplete context * Duplicated from continuedev/core to avoid coupling */ -export interface AutocompleteCodeSnippet extends RangeInFile { +export interface AutocompleteCodeSnippet extends Partial { + filepath: string content: string score?: number } @@ -220,12 +223,16 @@ export function vscodeRangeToRange(range: vscode.Range): Range { /** * Convert GhostSuggestionContext to AutocompleteInput */ -export function contextToAutocompleteInput(context: GhostSuggestionContext): AutocompleteInput { +export function contextToAutocompleteInput( + context: GhostSuggestionContext, + recentlyVisitedRanges: ContinuedevAutocompleteCodeSnippet[] = [], + recentlyEditedRanges: ContinuedevRecentlyEditedRange[] = [], +): AutocompleteInput { const position = context.range?.start ?? context.document.positionAt(0) const { prefix, suffix } = extractPrefixSuffix(context.document, position) - // Convert recent operations to recently edited ranges - const recentlyEditedRanges: RecentlyEditedRange[] = + // Merge recently edited ranges from context operations with tracked ranges + const contextEditedRanges: RecentlyEditedRange[] = context.recentOperations?.map((op) => { const range: Range = op.lineRange ? { @@ -246,13 +253,16 @@ export function contextToAutocompleteInput(context: GhostSuggestionContext): Aut } }) ?? [] + // 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: [], // Not tracked in current Ghost implementation - recentlyEditedRanges, + recentlyVisitedRanges, // Populated from GhostRecentlyVisitedRangesService + recentlyEditedRanges: allRecentlyEditedRanges, // Populated from GhostRecentlyEditedTracker manuallyPassFileContents: undefined, manuallyPassPrefix: prefix, } diff --git a/src/test-llm-autocompletion/strategy-tester.ts b/src/test-llm-autocompletion/strategy-tester.ts index aaa2ad74b0e..81b22ef07c1 100644 --- a/src/test-llm-autocompletion/strategy-tester.ts +++ b/src/test-llm-autocompletion/strategy-tester.ts @@ -124,7 +124,12 @@ export class StrategyTester { recentlyEditedRanges: [], } - const { systemPrompt, userPrompt } = this.holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId) + const { systemPrompt, userPrompt } = await this.holeFiller.getPrompts( + autocompleteInput, + prefix, + suffix, + languageId, + ) const response = await this.llmClient.sendPrompt(systemPrompt, userPrompt)