Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete: Implement context filter #3897

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions vscode/src/completions/context/context-mixer.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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()) {
Expand Down Expand Up @@ -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([]))
Expand Down Expand Up @@ -91,7 +98,6 @@ describe('ContextMixer', () => {
])
)
const { context, logSummary } = await mixer.getContext(defaultOptions)

expect(normalize(context)).toEqual([
{
fileName: 'foo.ts',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'])
})
})
})
})

Expand Down
20 changes: 19 additions & 1 deletion vscode/src/completions/context/context-mixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type * as vscode from 'vscode'
import {
type AutocompleteContextSnippet,
type DocumentContext,
contextFiltersProvider,
isCodyIgnoredFile,
wrapInActiveSpan,
} from '@sourcegraph/cody-shared'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -177,3 +179,19 @@ export class ContextMixer implements vscode.Disposable {
this.strategyFactory.dispose()
}
}

async function filter(snippets: AutocompleteContextSnippet[]): Promise<AutocompleteContextSnippet[]> {
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)
}
18 changes: 18 additions & 0 deletions vscode/src/completions/inline-completion-item-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type AuthStatus,
type GraphQLAPIClientConfig,
RateLimitError,
contextFiltersProvider,
graphqlClient,
} from '@sourcegraph/cody-shared'

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions vscode/src/completions/inline-completion-item-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ConfigFeaturesSingleton,
FeatureFlag,
RateLimitError,
contextFiltersProvider,
isCodyIgnoredFile,
wrapInActiveSpan,
} from '@sourcegraph/cody-shared'
Expand Down Expand Up @@ -202,10 +203,16 @@ export class InlineCompletionItemProvider
): Promise<AutocompleteResult | null> {
// 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 = {
Expand Down Expand Up @@ -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 + ')'
)
}
Loading