diff --git a/vscode/src/completions/context/context-mixer.test.ts b/vscode/src/completions/context/context-mixer.test.ts index b6739b1da5ce..9b79e3731c02 100644 --- a/vscode/src/completions/context/context-mixer.test.ts +++ b/vscode/src/completions/context/context-mixer.test.ts @@ -1,8 +1,9 @@ -import { beforeAll, describe, expect, it, vi } from 'vitest' +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { type AutocompleteContextSnippet, CODY_IGNORE_URI_PATH, + contextFiltersProvider, ignores, isCodyIgnoredFile, testFileUri, @@ -17,6 +18,8 @@ import { Utils } from 'vscode-uri' import { ContextMixer } from './context-mixer' import type { ContextStrategyFactory } from './context-strategy' +import type * as vscode from 'vscode' + function createMockStrategy(resultSets: AutocompleteContextSnippet[][]): ContextStrategyFactory { const retrievers = [] for (const [index, set] of resultSets.entries()) { @@ -55,6 +58,10 @@ const defaultOptions = { } describe('ContextMixer', () => { + beforeEach(() => { + vi.spyOn(contextFiltersProvider, 'isUriIgnored').mockResolvedValue(false) + }) + describe('with no retriever', () => { it('returns empty result if no retrievers', async () => { const mixer = new ContextMixer(createMockStrategy([])) @@ -91,7 +98,6 @@ describe('ContextMixer', () => { ]) ) const { context, logSummary } = await mixer.getContext(defaultOptions) - expect(normalize(context)).toEqual([ { fileName: 'foo.ts', @@ -225,7 +231,7 @@ describe('ContextMixer', () => { }) }) - describe('retrived context is filtered by .cody/ignore', () => { + describe('retrieved context is filtered by .cody/ignore', () => { const workspaceRoot = testFileUri('') beforeAll(() => { ignores.setActiveState(true) @@ -287,6 +293,62 @@ describe('ContextMixer', () => { } }) }) + + describe('retrieved context is filtered by context filters', () => { + beforeAll(() => { + vi.spyOn(contextFiltersProvider, 'isUriIgnored').mockImplementation( + async (uri: vscode.Uri) => { + if (uri.path.includes('foo.ts')) { + return true + } + return false + } + ) + }) + it('mixes results are filtered', async () => { + const mixer = new ContextMixer( + createMockStrategy([ + [ + { + uri: testFileUri('foo.ts'), + content: 'function foo1() {}', + startLine: 0, + endLine: 0, + }, + { + uri: testFileUri('foo/bar.ts'), + content: 'function bar1() {}', + startLine: 0, + endLine: 0, + }, + ], + [ + { + uri: testFileUri('test/foo.ts'), + content: 'function foo3() {}', + startLine: 10, + endLine: 10, + }, + { + uri: testFileUri('foo.ts'), + content: 'function foo1() {}\nfunction foo2() {}', + startLine: 0, + endLine: 1, + }, + { + uri: testFileUri('example/bar.ts'), + content: 'function bar1() {}\nfunction bar2() {}', + startLine: 0, + endLine: 1, + }, + ], + ]) + ) + const { context } = await mixer.getContext(defaultOptions) + const contextFiles = normalize(context) + expect(contextFiles.map(c => c.fileName)).toEqual(['bar.ts', 'bar.ts']) + }) + }) }) }) diff --git a/vscode/src/completions/context/context-mixer.ts b/vscode/src/completions/context/context-mixer.ts index 46debbcbed62..270e1f0b55ca 100644 --- a/vscode/src/completions/context/context-mixer.ts +++ b/vscode/src/completions/context/context-mixer.ts @@ -3,6 +3,7 @@ import type * as vscode from 'vscode' import { type AutocompleteContextSnippet, type DocumentContext, + contextFiltersProvider, isCodyIgnoredFile, wrapInActiveSpan, } from '@sourcegraph/cody-shared' @@ -95,7 +96,8 @@ export class ContextMixer implements vscode.Disposable { }, }) ) - const filteredSnippets = allSnippets.filter(snippet => !isCodyIgnoredFile(snippet.uri)) + + const filteredSnippets = await filter(allSnippets) return { identifier: retriever.identifier, @@ -177,3 +179,19 @@ export class ContextMixer implements vscode.Disposable { this.strategyFactory.dispose() } } + +async function filter(snippets: AutocompleteContextSnippet[]): Promise { + return ( + await Promise.all( + snippets.map(async snippet => { + if (isCodyIgnoredFile(snippet.uri)) { + return null + } + if (await contextFiltersProvider.isUriIgnored(snippet.uri)) { + return null + } + return snippet + }) + ) + ).filter((snippet): snippet is AutocompleteContextSnippet => snippet !== null) +} diff --git a/vscode/src/completions/inline-completion-item-provider.test.ts b/vscode/src/completions/inline-completion-item-provider.test.ts index f9e19b6a0750..69faffe19bae 100644 --- a/vscode/src/completions/inline-completion-item-provider.test.ts +++ b/vscode/src/completions/inline-completion-item-provider.test.ts @@ -6,6 +6,7 @@ import { type AuthStatus, type GraphQLAPIClientConfig, RateLimitError, + contextFiltersProvider, graphqlClient, } from '@sourcegraph/cody-shared' @@ -90,6 +91,9 @@ describe('InlineCompletionItemProvider', () => { beforeAll(async () => { await initCompletionProviderConfig({}) }) + beforeEach(() => { + vi.spyOn(contextFiltersProvider, 'isUriIgnored').mockResolvedValue(false) + }) it('returns results that span the whole line', async () => { const { document, position } = documentAndPosition('const foo = █', 'typescript') @@ -225,6 +229,20 @@ describe('InlineCompletionItemProvider', () => { expect(fn.mock.calls.map(call => call[0].lastCandidate?.result.items)).toEqual([[item]]) }) + it('no-ops on files that are ignored by the context filter policy', async () => { + vi.spyOn(contextFiltersProvider, 'isUriIgnored').mockResolvedValueOnce(true) + const { document, position } = documentAndPosition('const foo = █', 'typescript') + const fn = vi.fn() + const provider = new MockableInlineCompletionItemProvider(fn) + const completions = await provider.provideInlineCompletionItems( + document, + position, + DUMMY_CONTEXT + ) + expect(completions).toBe(null) + expect(fn).not.toHaveBeenCalled() + }) + describe('onboarding', () => { // Set up local storage backed by an object. Local storage is used to // track whether a completion was accepted for the first time. diff --git a/vscode/src/completions/inline-completion-item-provider.ts b/vscode/src/completions/inline-completion-item-provider.ts index bd229858fd07..8b68c74bc1f1 100644 --- a/vscode/src/completions/inline-completion-item-provider.ts +++ b/vscode/src/completions/inline-completion-item-provider.ts @@ -5,6 +5,7 @@ import { ConfigFeaturesSingleton, FeatureFlag, RateLimitError, + contextFiltersProvider, isCodyIgnoredFile, wrapInActiveSpan, } from '@sourcegraph/cody-shared' @@ -202,10 +203,16 @@ export class InlineCompletionItemProvider ): Promise { // Do not create item for files that are on the cody ignore list if (isCodyIgnoredFile(document.uri)) { + logIgnored(document.uri, 'cody-ignore') return null } return wrapInActiveSpan('autocomplete.provideInlineCompletionItems', async span => { + if (await contextFiltersProvider.isUriIgnored(document.uri)) { + logIgnored(document.uri, 'context-filter') + return null + } + // Update the last request const lastCompletionRequest = this.lastCompletionRequest const completionRequest: CompletionRequest = { @@ -794,3 +801,16 @@ function onlyCompletionWidgetSelectionChanged( return prevSelectedCompletionInfo.text !== nextSelectedCompletionInfo.text } + +let lasIgnoredUriLogged: string | undefined = undefined +function logIgnored(uri: vscode.Uri, reason: 'cody-ignore' | 'context-filter') { + const string = uri.toString() + if (lasIgnoredUriLogged === string) { + return + } + lasIgnoredUriLogged = string + logDebug( + 'CodyCompletionProvider:ignored', + 'Cody is disabled in file ' + uri.toString() + ' (' + reason + ')' + ) +}