-
Notifications
You must be signed in to change notification settings - Fork 2
Add search features API #95
Changes from 11 commits
efa285d
5e4fe5b
de15667
f5fd2be
e10a9fc
25ca64b
c76d18c
8a1c7aa
39590b9
4847f2b
13e53ba
4bbef15
c112fa7
61756bc
f1326e3
480071b
4c57cae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { from, Observable, Subscription } from 'rxjs' | ||
import { ExtSearch } from 'src/extension/api/search' | ||
import { Connection } from 'src/protocol/jsonrpc2/connection' | ||
import { createProxyAndHandleRequests } from '../../common/proxy' | ||
import { TransformQuerySignature } from '../providers/queryTransformer' | ||
import { FeatureProviderRegistry } from '../providers/registry' | ||
import { SubscriptionMap } from './common' | ||
|
||
export interface SearchAPI { | ||
$registerQueryTransformer(id: number): void | ||
$unregister(id: number): void | ||
} | ||
|
||
export class Search implements SearchAPI { | ||
private subscriptions = new Subscription() | ||
private registrations = new SubscriptionMap() | ||
private proxy: ExtSearch | ||
|
||
constructor( | ||
connection: Connection, | ||
private queryTransformerRegistry: FeatureProviderRegistry<{}, TransformQuerySignature> | ||
) { | ||
this.subscriptions.add(this.registrations) | ||
|
||
this.proxy = createProxyAndHandleRequests('searchFeatures', connection, this) | ||
} | ||
|
||
public $registerQueryTransformer(id: number): void { | ||
this.registrations.add( | ||
id, | ||
this.queryTransformerRegistry.registerProvider( | ||
{}, | ||
(query: string): Observable<string | null | undefined> => from(this.proxy.$transformQuery(id, query)) | ||
) | ||
) | ||
} | ||
|
||
public $unregister(id: number): void { | ||
this.registrations.remove(id) | ||
} | ||
|
||
public unsubscribe(): void { | ||
this.subscriptions.unsubscribe() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import * as assert from 'assert' | ||
import { of } from 'rxjs' | ||
import { TestScheduler } from 'rxjs/testing' | ||
import { transformQuery, TransformQuerySignature } from './queryTransformer' | ||
|
||
const scheduler = () => new TestScheduler((a, b) => assert.deepStrictEqual(a, b)) | ||
|
||
const FIXTURE_INPUT = 'foo' | ||
const FIXTURE_RESULT_MERGED = 'foo bar' | ||
const FIXTURE_RESULT = 'bar' | ||
|
||
describe('transformQuery', () => { | ||
describe('0 providers', () => { | ||
it('returns original query', () => | ||
scheduler().run(({ cold, expectObservable }) => | ||
expectObservable( | ||
transformQuery(cold<TransformQuerySignature[]>('-a-|', { a: [] }), FIXTURE_INPUT) | ||
).toBe('-a-|', { | ||
a: FIXTURE_INPUT, | ||
}) | ||
)) | ||
}) | ||
|
||
describe('1 provider', () => { | ||
it('returns result from provider', () => | ||
scheduler().run(({ cold, expectObservable }) => | ||
expectObservable( | ||
transformQuery( | ||
cold<TransformQuerySignature[]>('-a-|', { | ||
a: [q => of(FIXTURE_RESULT)], | ||
}), | ||
FIXTURE_INPUT | ||
) | ||
).toBe('-a-|', { a: FIXTURE_RESULT }) | ||
)) | ||
}) | ||
|
||
describe('2 providers', () => { | ||
it('returns a single query transformed by both providers', () => | ||
scheduler().run(({ cold, expectObservable }) => | ||
expectObservable( | ||
transformQuery( | ||
cold<TransformQuerySignature[]>('-a-|', { | ||
a: [q => of(q), q => of(`${q} ${FIXTURE_RESULT}`)], | ||
}), | ||
FIXTURE_INPUT | ||
) | ||
).toBe('-a-|', { a: FIXTURE_RESULT_MERGED }) | ||
)) | ||
}) | ||
|
||
describe('Multiple emissions', () => { | ||
it('returns stream of results', () => | ||
scheduler().run(({ cold, expectObservable }) => | ||
expectObservable( | ||
transformQuery( | ||
cold<TransformQuerySignature[]>('-a-b-|', { | ||
a: [q => of(q)], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the first query transform is just the identity func, then this doesn't really test the behavior of "all query transform funcs are applied". Instead you should test something like:
and test that the result is the original string with (I think the current test would pass even if your logic was like |
||
b: [q => of(FIXTURE_RESULT)], | ||
}), | ||
FIXTURE_INPUT | ||
) | ||
).toBe('-a-b-|', { a: FIXTURE_INPUT, b: FIXTURE_RESULT }) | ||
)) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { Observable, of } from 'rxjs' | ||
import { flatMap, map, switchMap } from 'rxjs/operators' | ||
import { FeatureProviderRegistry } from './registry' | ||
|
||
export type TransformQuerySignature = (query: string) => Observable<string | null | undefined> | ||
|
||
/** Provides transformed queries from the first search extension. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inaccurate now: it is not just the first search extension, it calls all of them. Also, there is no such concept as a "search extension". An extension can have any number of query transformers. The relevant concept here is "query transformers". So:
|
||
export class QueryTransformerRegistry extends FeatureProviderRegistry<{}, TransformQuerySignature> { | ||
public transformQuery(query: string): Observable<string | null | undefined> { | ||
return transformQuery(this.providers, query) | ||
} | ||
} | ||
|
||
/** | ||
* Returns an observable that emits a query transformed by all providers whenever any of the last-emitted set of providers emits | ||
* a query. | ||
* | ||
* Most callers should use QueryTransformerRegistry's transformQuery method, which uses the registered search | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The term "search provider" is inaccurate; there is no such concept (or it is an imprecise way to refer to query transformers). It should be "query transformers". |
||
* providers. | ||
*/ | ||
export function transformQuery( | ||
providers: Observable<TransformQuerySignature[]>, | ||
query: string | ||
): Observable<string | null | undefined> { | ||
return providers.pipe( | ||
switchMap(providers => { | ||
if (providers.length === 0) { | ||
return [query] | ||
} | ||
return providers.reduce( | ||
(currentQuery, transformQuery) => | ||
currentQuery.pipe( | ||
flatMap(q => transformQuery(q).pipe(map(transformedQuery => transformedQuery || q))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the desired behavior?
I suspect you don't intend to consider the empty string as meaning "use the original input string". But the empty string is falsey. So I would do:
Or, really, why special-case null at all? If a query transformer wants to not make any modifications to the string, it can just return it unmodified. That is actually what I'd recommend. |
||
), | ||
of(query) | ||
) | ||
}) | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Unsubscribable } from 'rxjs' | ||
import { QueryTransformerProvider } from 'sourcegraph' | ||
import { SearchAPI } from 'src/client/api/search' | ||
import { ProviderMap } from './common' | ||
|
||
/** @internal */ | ||
export interface ExtSearchAPI { | ||
$transformQuery: (id: number, query: string) => Promise<string | null | undefined> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned above, I think you should only accept |
||
} | ||
|
||
/** @internal */ | ||
export class ExtSearch implements ExtSearchAPI { | ||
private registrations = new ProviderMap<QueryTransformerProvider>(id => this.proxy.$unregister(id)) | ||
constructor(private proxy: SearchAPI) {} | ||
|
||
public registerQueryTransformer(provider: QueryTransformerProvider): Unsubscribable { | ||
const { id, subscription } = this.registrations.add(provider) | ||
this.proxy.$registerQueryTransformer(id) | ||
return subscription | ||
} | ||
|
||
public $transformQuery(id: number, query: string): Promise<string | null | undefined> { | ||
const provider = this.registrations.get<QueryTransformerProvider>(id) | ||
return Promise.resolve(provider.transformQuery(query)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import { ExtConfiguration } from './api/configuration' | |
import { ExtContext } from './api/context' | ||
import { ExtDocuments } from './api/documents' | ||
import { ExtLanguageFeatures } from './api/languageFeatures' | ||
import { ExtSearch } from './api/search' | ||
import { ExtWindows } from './api/windows' | ||
import { Location } from './types/location' | ||
import { Position } from './types/position' | ||
|
@@ -82,6 +83,9 @@ function createExtensionHandle(initData: InitData, connection: Connection): type | |
const languageFeatures = new ExtLanguageFeatures(proxy('languageFeatures'), documents) | ||
handleRequests(connection, 'languageFeatures', languageFeatures) | ||
|
||
const searchFeatures = new ExtSearch(proxy('searchFeatures')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You call it "search" everywhere else, not "searchFeatures". So for consistency, replace |
||
handleRequests(connection, 'searchFeatures', searchFeatures) | ||
|
||
const commands = new ExtCommands(proxy('commands')) | ||
handleRequests(connection, 'commands', commands) | ||
|
||
|
@@ -129,6 +133,10 @@ function createExtensionHandle(initData: InitData, connection: Connection): type | |
languageFeatures.registerReferenceProvider(selector, provider), | ||
}, | ||
|
||
search: { | ||
registerQueryTransformer: provider => searchFeatures.registerQueryTransformer(provider), | ||
}, | ||
|
||
commands: { | ||
registerCommand: (command, callback) => commands.registerCommand({ command, callback }), | ||
executeCommand: (command, ...args) => commands.executeCommand(command, args), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import * as assert from 'assert' | ||
import { take } from 'rxjs/operators' | ||
import { integrationTestContext } from './helpers.test' | ||
|
||
describe('search (integration)', () => { | ||
it('registers a query transformer', async () => { | ||
const { clientController, extensionHost, ready } = await integrationTestContext() | ||
|
||
// Register the provider and call it | ||
const unsubscribe = extensionHost.search.registerQueryTransformer({ transformQuery: () => 'bar' }) | ||
await ready | ||
assert.deepStrictEqual( | ||
await clientController.registries.queryTransformer | ||
.transformQuery('foo') | ||
.pipe(take(1)) | ||
.toPromise(), | ||
'bar' | ||
) | ||
|
||
// Unregister the provider and ensure it's removed | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for consistency, add a period after a line comment if it's a full sentence |
||
unsubscribe.unsubscribe() | ||
assert.deepStrictEqual( | ||
await clientController.registries.queryTransformer | ||
.transformQuery('foo') | ||
.pipe(take(1)) | ||
.toPromise(), | ||
'foo' | ||
) | ||
}) | ||
|
||
it('supports multiple query transformers', async () => { | ||
const { clientController, extensionHost, ready } = await integrationTestContext() | ||
|
||
// Register the provider and call it | ||
extensionHost.search.registerQueryTransformer({ transformQuery: q => `${q} bar` }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, but |
||
extensionHost.search.registerQueryTransformer({ transformQuery: q => `${q} baz` }) | ||
await ready | ||
assert.deepStrictEqual( | ||
await clientController.registries.queryTransformer | ||
.transformQuery('foo') | ||
.pipe(take(1)) | ||
.toPromise(), | ||
'foo bar baz' | ||
) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -717,6 +717,14 @@ declare module 'sourcegraph' { | |
): ProviderResult<Location[]> | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should just call it
|
||
export interface QueryTransformerProvider { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add tsdoc docstrings to:
|
||
transformQuery(query: string): ProviderResult<string> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wrote 2 comments above about:
I see why you included null and undefined now, because The reason why Another related reason you should use
There is no need for the 2nd and 3rd ways of expressing this. |
||
} | ||
|
||
export namespace search { | ||
export function registerQueryTransformer(provider: QueryTransformerProvider): Unsubscribable | ||
} | ||
|
||
export namespace languages { | ||
export function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Unsubscribable | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
/** @internal */
tsdoc directives as in https://sourcegraph.com/github.com/sourcegraph/sourcegraph-extension-api/-/blob/src/client/api/configuration.ts#L8:1 and other files because this type is part of the internal API (so we want to communicate that to readers and to tools like doc generators).