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

Commit 322d416

Browse files
authored
feat: add method to get locations with extension ID (#45)
1 parent 163a8ee commit 322d416

14 files changed

+158
-14
lines changed

src/client/client.test.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,15 @@ describe('Client', () => {
8080

8181
// Request registration.
8282
client.handleRegistrationRequest({
83-
registrations: [{ id: 'a', method: 'm', registerOptions: { a: 1 }, overwriteExisting: true }],
83+
registrations: [
84+
{ id: 'a', method: 'm', registerOptions: { a: 1, extensionID: '' }, overwriteExisting: true },
85+
],
8486
})
8587
assert.deepStrictEqual(registerCalls, [
86-
{ message: { method: 'm' }, data: { id: 'a', registerOptions: { a: 1 }, overwriteExisting: true } },
88+
{
89+
message: { method: 'm' },
90+
data: { id: 'a', registerOptions: { a: 1, extensionID: '' }, overwriteExisting: true },
91+
},
8792
] as typeof registerCalls)
8893

8994
// Request unregistration.

src/client/client.ts

+3
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,9 @@ export class Client implements Unsubscribable {
447447
if (!options.documentSelector && this.options.documentSelector) {
448448
options.documentSelector = this.options.documentSelector
449449
}
450+
if (!options.extensionID) {
451+
options.extensionID = this.id
452+
}
450453
const data: RegistrationData<any> = {
451454
id: registration.id,
452455
registerOptions: options,

src/client/features/common.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class TextDocumentFeature extends AbstractTextDocumentFeature<TextDocumentRegist
2020
protected registerProvider(): Unsubscribable {
2121
return { unsubscribe: () => void 0 }
2222
}
23+
protected validateRegistrationOptions(data: any): TextDocumentRegistrationOptions {
24+
return data
25+
}
2326
public fillClientCapabilities(): void {
2427
/* noop */
2528
}
@@ -28,7 +31,7 @@ class TextDocumentFeature extends AbstractTextDocumentFeature<TextDocumentRegist
2831
}
2932
}
3033

31-
const FIXTURE_REGISTER_OPTIONS: TextDocumentRegistrationOptions = { documentSelector: ['*'] }
34+
const FIXTURE_REGISTER_OPTIONS: TextDocumentRegistrationOptions = { documentSelector: ['*'], extensionID: 'test' }
3235

3336
describe('TextDocumentFeature', () => {
3437
describe('dynamic registration', () => {

src/client/features/common.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,20 @@ export abstract class Feature<O> implements DynamicFeature<O> {
6565

6666
public abstract get messages(): RPCMessageType
6767

68-
public register(message: RPCMessageType, data: RegistrationData<O>): void {
68+
/**
69+
* validateRegistrationOptions validates a proposed options object. This is used by the register
70+
* method to verify registration options. At a minimum, the options
71+
* object should conform to O (the options type of a Feature subclass).
72+
*
73+
* Implementors of this method should return the original options object unmodified if it is
74+
* valid and throw an error if it is invalid (i.e., does not satisfy the type).
75+
*
76+
* @param options the options object to validate
77+
*/
78+
protected abstract validateRegistrationOptions(options: any): O
79+
80+
public register(message: RPCMessageType, data: RegistrationData<any>): void {
81+
const registerOptions = this.validateRegistrationOptions(data.registerOptions)
6982
if (message.method !== this.messages.method) {
7083
throw new Error(
7184
`Register called on wrong feature. Requested ${message.method} but reached feature ${
@@ -76,7 +89,7 @@ export abstract class Feature<O> implements DynamicFeature<O> {
7689
if (this.subscriptionsByID.has(data.id)) {
7790
throw new Error(`registration already exists with ID ${data.id}`)
7891
}
79-
this.subscriptionsByID.set(data.id, this.registerProvider(data.registerOptions))
92+
this.subscriptionsByID.set(data.id, this.registerProvider(registerOptions))
8093
}
8194

8295
protected abstract registerProvider(options: O): Unsubscribable

src/client/features/decoration.ts

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ export class TextDocumentDecorationFeature extends Feature<undefined> {
4444
)
4545
}
4646

47+
protected validateRegistrationOptions(data: any): undefined {
48+
if (data) {
49+
throw new Error('TextDocumentDecorationFeature registration options should be undefined')
50+
}
51+
return data
52+
}
53+
4754
private getDecorationsSubject(
4855
textDocument: TextDocumentIdentifier,
4956
value?: TextDocumentDecoration[] | null

src/client/features/hover.test.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const create = (): {
1010
registry: TextDocumentHoverProviderRegistry
1111
feature: TextDocumentHoverFeature
1212
} => {
13-
const client = { options: {} } as Client
13+
const client = { options: {}, id: 'test' } as Client
1414
const registry = new TextDocumentHoverProviderRegistry()
1515
const feature = new TextDocumentHoverFeature(client, registry)
1616
return { client, registry, feature }
@@ -30,7 +30,10 @@ describe('TextDocumentHoverFeature', () => {
3030
describe('registration', () => {
3131
it('supports dynamic registration and unregistration', () => {
3232
const { registry, feature } = create()
33-
feature.register(feature.messages, { id: 'a', registerOptions: { documentSelector: ['*'] } })
33+
feature.register(feature.messages, {
34+
id: 'a',
35+
registerOptions: { documentSelector: ['*'], extensionID: 'test' },
36+
})
3437
assert.strictEqual(registry.providersSnapshot.length, 1)
3538
feature.unregister('a')
3639
assert.strictEqual(registry.providersSnapshot.length, 0)

src/client/features/hover.ts

+8
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,12 @@ export class TextDocumentHoverFeature extends Feature<TextDocumentRegistrationOp
3737
from(this.client.sendRequest(HoverRequest.type, params))
3838
)
3939
}
40+
41+
protected validateRegistrationOptions(data: any): TextDocumentRegistrationOptions {
42+
const options: TextDocumentRegistrationOptions = data
43+
if (!options.extensionID) {
44+
throw new Error('extensionID should be non-empty in registration options')
45+
}
46+
return options
47+
}
4048
}

src/client/features/location.test.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const create = <P extends TextDocumentPositionParams, F extends TextDocumentLoca
1818
registry: TextDocumentLocationProviderRegistry<P>
1919
feature: F
2020
} => {
21-
const client = { options: {} } as Client
21+
const client = { options: {}, id: 'test' } as Client
2222
const registry = new RegistryClass()
2323
const feature = new FeatureClass(client, registry)
2424
return { client, registry, feature }
@@ -39,7 +39,10 @@ describe('TextDocumentLocationFeature', () => {
3939
},
4040
TextDocumentLocationProviderRegistry
4141
)
42-
feature.register(feature.messages, { id: 'a', registerOptions: { documentSelector: ['*'] } })
42+
feature.register(feature.messages, {
43+
id: 'a',
44+
registerOptions: { documentSelector: ['*'], extensionID: 'test' },
45+
})
4346
assert.strictEqual(registry.providersSnapshot.length, 1)
4447
feature.unregister('a')
4548
assert.strictEqual(registry.providersSnapshot.length, 0)

src/client/features/location.ts

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ export abstract class TextDocumentLocationFeature<
4242
(params: P): Observable<L | L[] | null> => from(this.client.sendRequest(this.messages, params))
4343
)
4444
}
45+
46+
protected validateRegistrationOptions(data: any): TextDocumentRegistrationOptions {
47+
const options: TextDocumentRegistrationOptions = data
48+
if (!options.extensionID) {
49+
throw new Error('extensionID should be non-empty in registration options')
50+
}
51+
return options
52+
}
4553
}
4654

4755
/**

src/client/features/textDocument.test.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('TextDocumentNotificationFeature', () => {
4444
}
4545
}
4646

47-
const FIXTURE_REGISTER_OPTIONS: TextDocumentRegistrationOptions = { documentSelector: ['*'] }
47+
const FIXTURE_REGISTER_OPTIONS: TextDocumentRegistrationOptions = { documentSelector: ['*'], extensionID: 'test' }
4848

4949
describe('registration', () => {
5050
it('supports dynamic registration and unregistration', () => {
@@ -101,7 +101,10 @@ describe('TextDocumentDidOpenFeature', () => {
101101
describe('when a text document is opened', () => {
102102
it('sends a textDocument/didOpen notification to the server', done => {
103103
const { client, environment, feature } = create()
104-
feature.register(feature.messages, { id: 'a', registerOptions: { documentSelector: ['l'] } })
104+
feature.register(feature.messages, {
105+
id: 'a',
106+
registerOptions: { documentSelector: ['l'], extensionID: 'id' },
107+
})
105108

106109
const textDocument: TextDocumentItem = {
107110
uri: 'file:///f',
@@ -161,7 +164,10 @@ describe('TextDocumentDidCloseFeature', () => {
161164
describe('when a text document is opened and then closed', () => {
162165
it('sends a textDocument/didClose notification to the server', done => {
163166
const { client, environment, feature } = create()
164-
feature.register(feature.messages, { id: 'a', registerOptions: { documentSelector: ['l'] } })
167+
feature.register(feature.messages, {
168+
id: 'a',
169+
registerOptions: { documentSelector: ['l'], extensionID: 'test' },
170+
})
165171

166172
const textDocument: TextDocumentItem = {
167173
uri: 'file:///f',

src/environment/providers/location.test.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import * as assert from 'assert'
22
import { of } from 'rxjs'
33
import { TestScheduler } from 'rxjs/testing'
44
import { Location, Position, Range } from 'vscode-languageserver-types'
5-
import { getLocation, getLocations, ProvideTextDocumentLocationSignature } from './location'
5+
import {
6+
getLocation,
7+
getLocations,
8+
getLocationsWithExtensionID,
9+
ProvideTextDocumentLocationSignature,
10+
} from './location'
611
import { FIXTURE } from './registry.test'
712

813
const scheduler = () => new TestScheduler((a, b) => assert.deepStrictEqual(a, b))
@@ -182,3 +187,31 @@ describe('getLocations', () => {
182187
})
183188
))
184189
})
190+
191+
describe('getLocationsWithExtensionID', () => {
192+
it('wraps single result in array', () =>
193+
scheduler().run(({ cold, expectObservable }) => {
194+
const res = getLocationsWithExtensionID(
195+
cold<{ extensionID: string; provider: ProvideTextDocumentLocationSignature }[]>('-a-|', {
196+
a: [{ extensionID: 'test', provider: () => of(FIXTURE_LOCATION) }],
197+
}),
198+
FIXTURE.TextDocumentPositionParams
199+
)
200+
expectObservable(res).toBe('-a-|', {
201+
a: [{ extensionID: 'test', location: FIXTURE_LOCATION }],
202+
})
203+
}))
204+
it('preserves array results', () =>
205+
scheduler().run(({ cold, expectObservable }) =>
206+
expectObservable(
207+
getLocationsWithExtensionID(
208+
cold<{ extensionID: string; provider: ProvideTextDocumentLocationSignature }[]>('-a-|', {
209+
a: [{ extensionID: 'test', provider: () => of(FIXTURE_LOCATIONS) }],
210+
}),
211+
FIXTURE.TextDocumentPositionParams
212+
)
213+
).toBe('-a-|', {
214+
a: FIXTURE_LOCATIONS.map(l => ({ extensionID: 'test', location: l })),
215+
})
216+
))
217+
})

src/environment/providers/location.ts

+47
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { combineLatest, from, Observable } from 'rxjs'
22
import { map, switchMap } from 'rxjs/operators'
33
import { Location } from 'vscode-languageserver-types'
44
import { ReferenceParams, TextDocumentPositionParams, TextDocumentRegistrationOptions } from '../../protocol'
5+
import { compact, flatten } from '../../util'
56
import { FeatureProviderRegistry } from './registry'
67
import { flattenAndCompact } from './util'
78

@@ -22,6 +23,24 @@ export class TextDocumentLocationProviderRegistry<
2223
public getLocation(params: P): Observable<L | L[] | null> {
2324
return getLocation<P, L>(this.providers, params)
2425
}
26+
27+
public getLocationsWithExtensionID(params: P): Observable<{ extensionID: string; location: L }[] | null> {
28+
return getLocationsWithExtensionID<P, L>(this.providersWithID, params)
29+
}
30+
31+
/**
32+
* List of providers with their associated extension ID
33+
*/
34+
public readonly providersWithID: Observable<
35+
{ extensionID: string; provider: ProvideTextDocumentLocationSignature<P, L> }[]
36+
> = this.entries.pipe(
37+
map(providers =>
38+
providers.map(({ provider, registrationOptions }) => ({
39+
extensionID: registrationOptions.extensionID,
40+
provider,
41+
}))
42+
)
43+
)
2544
}
2645

2746
/**
@@ -62,6 +81,29 @@ export function getLocations<
6281
)
6382
}
6483

84+
/**
85+
* Like getLocations, but includes the ID of the extension that provided each location result
86+
*/
87+
export function getLocationsWithExtensionID<
88+
P extends TextDocumentPositionParams = TextDocumentPositionParams,
89+
L extends Location = Location
90+
>(
91+
providersWithID: Observable<{ extensionID: string; provider: ProvideTextDocumentLocationSignature<P, L> }[]>,
92+
params: P
93+
): Observable<{ extensionID: string; location: L }[]> {
94+
return providersWithID.pipe(
95+
switchMap(providersWithID =>
96+
combineLatest(
97+
providersWithID.map(({ provider, extensionID }) =>
98+
provider(params).pipe(
99+
map(r => flattenAndCompactNonNull([r]).map(l => ({ extensionID, location: l })))
100+
)
101+
)
102+
).pipe(map(flattenAndCompactNonNull))
103+
)
104+
)
105+
}
106+
65107
/**
66108
* Provides reference results from all extensions.
67109
*
@@ -76,3 +118,8 @@ export class TextDocumentReferencesProviderRegistry extends TextDocumentLocation
76118
return getLocations(this.providers, params)
77119
}
78120
}
121+
122+
/** Flattens and compacts the argument. If it is null or if the result is empty, it returns null. */
123+
function flattenAndCompactNonNull<T>(value: (T | T[] | null)[] | null): T[] {
124+
return value ? flatten(compact(value)) : []
125+
}

src/environment/providers/registry.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface Entry<O, P> {
99

1010
/** Base class for provider registries for features. */
1111
export abstract class FeatureProviderRegistry<O, P> {
12-
private entries = new BehaviorSubject<Entry<O, P>[]>([])
12+
protected entries = new BehaviorSubject<Entry<O, P>[]>([])
1313

1414
public constructor(initialEntries?: Entry<O, P>[]) {
1515
if (initialEntries) {

src/protocol/textDocument.ts

+5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ export interface TextDocumentRegistrationOptions {
7777
* the document selector provided on the client side will be used.
7878
*/
7979
documentSelector: DocumentSelector | null
80+
81+
/**
82+
* ID of the extension that registers the provider.
83+
*/
84+
extensionID: string
8085
}
8186

8287
export interface TextDocumentSyncOptions {

0 commit comments

Comments
 (0)