Skip to content
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Commit 3c629da

Browse files
authored
feat: add search query transformer extensibility point (#95)
* adds a new search namespace * adds query transformer extensibility point
1 parent 1f34d21 commit 3c629da

File tree

9 files changed

+265
-0
lines changed

9 files changed

+265
-0
lines changed

src/client/api/search.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { from, Observable, Subscription } from 'rxjs'
2+
import { ExtSearch } from 'src/extension/api/search'
3+
import { Connection } from 'src/protocol/jsonrpc2/connection'
4+
import { createProxyAndHandleRequests } from '../../common/proxy'
5+
import { TransformQuerySignature } from '../providers/queryTransformer'
6+
import { FeatureProviderRegistry } from '../providers/registry'
7+
import { SubscriptionMap } from './common'
8+
9+
/** @internal */
10+
export interface SearchAPI {
11+
$registerQueryTransformer(id: number): void
12+
$unregister(id: number): void
13+
}
14+
15+
/** @internal */
16+
export class Search implements SearchAPI {
17+
private subscriptions = new Subscription()
18+
private registrations = new SubscriptionMap()
19+
private proxy: ExtSearch
20+
21+
constructor(
22+
connection: Connection,
23+
private queryTransformerRegistry: FeatureProviderRegistry<{}, TransformQuerySignature>
24+
) {
25+
this.subscriptions.add(this.registrations)
26+
27+
this.proxy = createProxyAndHandleRequests('search', connection, this)
28+
}
29+
30+
public $registerQueryTransformer(id: number): void {
31+
this.registrations.add(
32+
id,
33+
this.queryTransformerRegistry.registerProvider(
34+
{},
35+
(query: string): Observable<string> => from(this.proxy.$transformQuery(id, query))
36+
)
37+
)
38+
}
39+
40+
public $unregister(id: number): void {
41+
this.registrations.remove(id)
42+
}
43+
44+
public unsubscribe(): void {
45+
this.subscriptions.unsubscribe()
46+
}
47+
}

src/client/controller.ts

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ClientConfiguration } from './api/configuration'
1919
import { ClientContext } from './api/context'
2020
import { ClientDocuments } from './api/documents'
2121
import { ClientLanguageFeatures } from './api/languageFeatures'
22+
import { Search } from './api/search'
2223
import { ClientWindows } from './api/windows'
2324
import { applyContextUpdate, EMPTY_CONTEXT } from './context/context'
2425
import { EMPTY_ENVIRONMENT, Environment } from './environment'
@@ -268,6 +269,7 @@ export class Controller<X extends Extension, C extends ConfigurationCascade> imp
268269
this.registries.textDocumentReferences
269270
)
270271
)
272+
subscription.add(new Search(client, this.registries.queryTransformer))
271273
subscription.add(new ClientCommands(client, this.registries.commands))
272274
}
273275

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as assert from 'assert'
2+
import { of } from 'rxjs'
3+
import { TestScheduler } from 'rxjs/testing'
4+
import { transformQuery, TransformQuerySignature } from './queryTransformer'
5+
6+
const scheduler = () => new TestScheduler((a, b) => assert.deepStrictEqual(a, b))
7+
8+
const FIXTURE_INPUT = 'foo'
9+
const FIXTURE_RESULT = 'bar'
10+
const FIXTURE_RESULT_TWO = 'qux'
11+
const FIXTURE_RESULT_MERGED = 'foo bar qux'
12+
13+
describe('transformQuery', () => {
14+
describe('0 providers', () => {
15+
it('returns original query', () =>
16+
scheduler().run(({ cold, expectObservable }) =>
17+
expectObservable(
18+
transformQuery(cold<TransformQuerySignature[]>('-a-|', { a: [] }), FIXTURE_INPUT)
19+
).toBe('-a-|', {
20+
a: FIXTURE_INPUT,
21+
})
22+
))
23+
})
24+
25+
describe('1 provider', () => {
26+
it('returns result from provider', () =>
27+
scheduler().run(({ cold, expectObservable }) =>
28+
expectObservable(
29+
transformQuery(
30+
cold<TransformQuerySignature[]>('-a-|', {
31+
a: [q => of(FIXTURE_RESULT)],
32+
}),
33+
FIXTURE_INPUT
34+
)
35+
).toBe('-a-|', { a: FIXTURE_RESULT })
36+
))
37+
})
38+
39+
describe('2 providers', () => {
40+
it('returns a single query transformed by both providers', () =>
41+
scheduler().run(({ cold, expectObservable }) =>
42+
expectObservable(
43+
transformQuery(
44+
cold<TransformQuerySignature[]>('-a-|', {
45+
a: [q => of(`${q} ${FIXTURE_RESULT}`), q => of(`${q} ${FIXTURE_RESULT_TWO}`)],
46+
}),
47+
FIXTURE_INPUT
48+
)
49+
).toBe('-a-|', { a: FIXTURE_RESULT_MERGED })
50+
))
51+
})
52+
53+
describe('Multiple emissions', () => {
54+
it('returns stream of results', () =>
55+
scheduler().run(({ cold, expectObservable }) =>
56+
expectObservable(
57+
transformQuery(
58+
cold<TransformQuerySignature[]>('-a-b-|', {
59+
a: [q => of(`${q} ${FIXTURE_RESULT}`)],
60+
b: [q => of(`${q} ${FIXTURE_RESULT_TWO}`)],
61+
}),
62+
FIXTURE_INPUT
63+
)
64+
).toBe('-a-b-|', {
65+
a: `${FIXTURE_INPUT} ${FIXTURE_RESULT}`,
66+
b: `${FIXTURE_INPUT} ${FIXTURE_RESULT_TWO}`,
67+
})
68+
))
69+
})
70+
})
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Observable, of } from 'rxjs'
2+
import { flatMap, map, switchMap } from 'rxjs/operators'
3+
import { FeatureProviderRegistry } from './registry'
4+
5+
export type TransformQuerySignature = (query: string) => Observable<string>
6+
7+
/** Transforms search queries using registered query transformers from extensions. */
8+
export class QueryTransformerRegistry extends FeatureProviderRegistry<{}, TransformQuerySignature> {
9+
public transformQuery(query: string): Observable<string> {
10+
return transformQuery(this.providers, query)
11+
}
12+
}
13+
14+
/**
15+
* Returns an observable that emits a query transformed by all providers whenever any of the last-emitted set of providers emits
16+
* a query.
17+
*
18+
* Most callers should use QueryTransformerRegistry's transformQuery method, which uses the registered query transformers
19+
*
20+
*/
21+
export function transformQuery(providers: Observable<TransformQuerySignature[]>, query: string): Observable<string> {
22+
return providers.pipe(
23+
switchMap(providers => {
24+
if (providers.length === 0) {
25+
return [query]
26+
}
27+
return providers.reduce(
28+
(currentQuery, transformQuery) =>
29+
currentQuery.pipe(flatMap(q => transformQuery(q).pipe(map(transformedQuery => transformedQuery)))),
30+
of(query)
31+
)
32+
})
33+
)
34+
}

src/client/registries.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ContributionRegistry } from './providers/contribution'
77
import { TextDocumentDecorationProviderRegistry } from './providers/decoration'
88
import { TextDocumentHoverProviderRegistry } from './providers/hover'
99
import { TextDocumentLocationProviderRegistry, TextDocumentReferencesProviderRegistry } from './providers/location'
10+
import { QueryTransformerRegistry } from './providers/queryTransformer'
1011

1112
/**
1213
* Registries is a container for all provider registries.
@@ -25,4 +26,5 @@ export class Registries<X extends Extension, C extends ConfigurationCascade> {
2526
public readonly textDocumentTypeDefinition = new TextDocumentLocationProviderRegistry()
2627
public readonly textDocumentHover = new TextDocumentHoverProviderRegistry()
2728
public readonly textDocumentDecoration = new TextDocumentDecorationProviderRegistry()
29+
public readonly queryTransformer = new QueryTransformerRegistry()
2830
}

src/extension/api/search.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Unsubscribable } from 'rxjs'
2+
import { QueryTransformer } from 'sourcegraph'
3+
import { SearchAPI } from 'src/client/api/search'
4+
import { ProviderMap } from './common'
5+
6+
export interface ExtSearchAPI {
7+
$transformQuery: (id: number, query: string) => Promise<string>
8+
}
9+
10+
export class ExtSearch implements ExtSearchAPI {
11+
private registrations = new ProviderMap<QueryTransformer>(id => this.proxy.$unregister(id))
12+
constructor(private proxy: SearchAPI) {}
13+
14+
public registerQueryTransformer(provider: QueryTransformer): Unsubscribable {
15+
const { id, subscription } = this.registrations.add(provider)
16+
this.proxy.$registerQueryTransformer(id)
17+
return subscription
18+
}
19+
20+
public $transformQuery(id: number, query: string): Promise<string> {
21+
const provider = this.registrations.get<QueryTransformer>(id)
22+
return Promise.resolve(provider.transformQuery(query))
23+
}
24+
}

src/extension/extensionHost.ts

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ExtConfiguration } from './api/configuration'
88
import { ExtContext } from './api/context'
99
import { ExtDocuments } from './api/documents'
1010
import { ExtLanguageFeatures } from './api/languageFeatures'
11+
import { ExtSearch } from './api/search'
1112
import { ExtWindows } from './api/windows'
1213
import { Location } from './types/location'
1314
import { Position } from './types/position'
@@ -82,6 +83,9 @@ function createExtensionHandle(initData: InitData, connection: Connection): type
8283
const languageFeatures = new ExtLanguageFeatures(proxy('languageFeatures'), documents)
8384
handleRequests(connection, 'languageFeatures', languageFeatures)
8485

86+
const search = new ExtSearch(proxy('search'))
87+
handleRequests(connection, 'search', search)
88+
8589
const commands = new ExtCommands(proxy('commands'))
8690
handleRequests(connection, 'commands', commands)
8791

@@ -129,6 +133,10 @@ function createExtensionHandle(initData: InitData, connection: Connection): type
129133
languageFeatures.registerReferenceProvider(selector, provider),
130134
},
131135

136+
search: {
137+
registerQueryTransformer: provider => search.registerQueryTransformer(provider),
138+
},
139+
132140
commands: {
133141
registerCommand: (command, callback) => commands.registerCommand({ command, callback }),
134142
executeCommand: (command, ...args) => commands.executeCommand(command, args),

src/integration-test/search.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as assert from 'assert'
2+
import { take } from 'rxjs/operators'
3+
import { integrationTestContext } from './helpers.test'
4+
5+
describe('search (integration)', () => {
6+
it('registers a query transformer', async () => {
7+
const { clientController, extensionHost, ready } = await integrationTestContext()
8+
9+
// Register the provider and call it
10+
const unsubscribe = extensionHost.search.registerQueryTransformer({ transformQuery: () => 'bar' })
11+
await ready
12+
assert.deepStrictEqual(
13+
await clientController.registries.queryTransformer
14+
.transformQuery('foo')
15+
.pipe(take(1))
16+
.toPromise(),
17+
'bar'
18+
)
19+
20+
// Unregister the provider and ensure it's removed.
21+
unsubscribe.unsubscribe()
22+
assert.deepStrictEqual(
23+
await clientController.registries.queryTransformer
24+
.transformQuery('foo')
25+
.pipe(take(1))
26+
.toPromise(),
27+
'foo'
28+
)
29+
})
30+
31+
it('supports multiple query transformers', async () => {
32+
const { clientController, extensionHost, ready } = await integrationTestContext()
33+
34+
// Register the provider and call it
35+
extensionHost.search.registerQueryTransformer({ transformQuery: q => `${q} bar` })
36+
extensionHost.search.registerQueryTransformer({ transformQuery: q => `${q} qux` })
37+
await ready
38+
assert.deepStrictEqual(
39+
await clientController.registries.queryTransformer
40+
.transformQuery('foo')
41+
.pipe(take(1))
42+
.toPromise(),
43+
'foo bar qux'
44+
)
45+
})
46+
})

src/sourcegraph.d.ts

+32
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,38 @@ declare module 'sourcegraph' {
784784
): Unsubscribable
785785
}
786786

787+
/**
788+
* A query transformer alters a user's search query before executing a search.
789+
*
790+
* Query transformers allow extensions to define new search query operators and syntax, for example,
791+
* by matching strings in a query (e.g. `go.imports:`) and replacing them with a regular expression or string.
792+
*/
793+
export interface QueryTransformer {
794+
/**
795+
* Transforms a search query into another, valid query. If there are no transformations to be made
796+
* the original query is returned.
797+
*
798+
* @param query A search query.
799+
*/
800+
transformQuery(query: string): string | Promise<string>
801+
}
802+
803+
/**
804+
* API for extensions to augment search functionality.
805+
*/
806+
export namespace search {
807+
/**
808+
* Registers a query transformer.
809+
*
810+
* Multiple transformers can be registered. In that case, all transformations will be applied
811+
* and the result is a single query that has been altered by all transformers. The order in
812+
* which transfomers are applied is not defined.
813+
*
814+
* @param provider A query transformer.
815+
*/
816+
export function registerQueryTransformer(provider: QueryTransformer): Unsubscribable
817+
}
818+
787819
/**
788820
* Commands are functions that are implemented and registered by extensions. Extensions can invoke any command
789821
* (including commands registered by other extensions). The extension can also define contributions (in

0 commit comments

Comments
 (0)