Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Commit 3c629da

Browse filesBrowse 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
Copy full SHA for 3c629da

9 files changed

+265Lines changed: 265 additions & 0 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/client/api/search.ts‎

Copy file name to clipboard
+47Lines changed: 47 additions & 0 deletions
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+
}
Collapse file

‎src/client/controller.ts‎

Copy file name to clipboardExpand all lines: src/client/controller.ts
+2Lines changed: 2 additions & 0 deletions
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

Collapse file
+70Lines changed: 70 additions & 0 deletions
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+
})
Collapse file
+34Lines changed: 34 additions & 0 deletions
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+
}
Collapse file

‎src/client/registries.ts‎

Copy file name to clipboardExpand all lines: src/client/registries.ts
+2Lines changed: 2 additions & 0 deletions
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
}
Collapse file

‎src/extension/api/search.ts‎

Copy file name to clipboard
+24Lines changed: 24 additions & 0 deletions
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+
}
Collapse file

‎src/extension/extensionHost.ts‎

Copy file name to clipboardExpand all lines: src/extension/extensionHost.ts
+8Lines changed: 8 additions & 0 deletions
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),
Collapse file
+46Lines changed: 46 additions & 0 deletions
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+
})
Collapse file

‎src/sourcegraph.d.ts‎

Copy file name to clipboardExpand all lines: src/sourcegraph.d.ts
+32Lines changed: 32 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.