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

Add search features API #95

Merged
merged 17 commits into from
Oct 26, 2018
Merged
Show file tree
Hide file tree
Changes from 6 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
42 changes: 42 additions & 0 deletions src/client/api/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { from, Observable, Subscription } from 'rxjs'
import { ExtSearchFeaturesAPI } from 'src/extension/api/search'
import { Connection } from 'src/protocol/jsonrpc2/connection'
import { createProxyAndHandleRequests } from '../../common/proxy'
import { FeatureProviderRegistry } from '../providers/registry'
import { TransformQuerySignature } from '../providers/search'
import { SubscriptionMap } from './common'

export interface SearchFeaturesAPI {
$registerQueryTransformProvider(id: number): void
$unregister(id: number): void
}

export class SearchFeatures implements SearchFeaturesAPI {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can just call it Search, not SearchFeatures. LanguageFeatures is called that because just "Language" alone would be confusing. That problem does not exist for search. (Update all instances of "searchFeatures" too.)

private subscriptions = new Subscription()
private registrations = new SubscriptionMap()
private proxy: ExtSearchFeaturesAPI

constructor(connection: Connection, private searchRegistry: FeatureProviderRegistry<{}, TransformQuerySignature>) {
this.subscriptions.add(this.registrations)

this.proxy = createProxyAndHandleRequests('searchFeatures', connection, this)
}

public $registerQueryTransformProvider(id: number): void {
this.registrations.add(
id,
this.searchRegistry.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()
}
}
2 changes: 2 additions & 0 deletions src/client/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ClientConfiguration } from './api/configuration'
import { ClientContext } from './api/context'
import { ClientDocuments } from './api/documents'
import { ClientLanguageFeatures } from './api/languageFeatures'
import { SearchFeatures } from './api/search'
import { ClientWindows } from './api/windows'
import { applyContextUpdate, EMPTY_CONTEXT } from './context/context'
import { EMPTY_ENVIRONMENT, Environment } from './environment'
Expand Down Expand Up @@ -268,6 +269,7 @@ export class Controller<X extends Extension, C extends ConfigurationCascade> imp
this.registries.textDocumentReferences
)
)
subscription.add(new SearchFeatures(client, this.registries.search))
subscription.add(new ClientCommands(client, this.registries.commands))
}

Expand Down
33 changes: 33 additions & 0 deletions src/client/providers/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Observable } from 'rxjs'
import { 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. */
export class SearchTransformProviderRegistry extends FeatureProviderRegistry<{}, TransformQuerySignature> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a search transform, a query transform, or what? Be consistent.

I think that "query transformer" is the best term to use. So QueryTransformerRegistry, registerQueryTransformer, etc.

public transformQuery(query: string): Observable<string | null | undefined> {
return transformQuery(this.providers, query)
}
}

/**
* Returns an observable that emits the first provider's transformed query whenever any of the last-emitted set of providers emits
* a query.
*
* Most callers should use SearchTransformProviderRegistry's getHover method, which uses the registered search
* 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[0](query)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be chained? Something like provider[0](query).then(q1 => provider[1](q1)).then(...)?

})
)
}
2 changes: 2 additions & 0 deletions src/client/registries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ContributionRegistry } from './providers/contribution'
import { TextDocumentDecorationProviderRegistry } from './providers/decoration'
import { TextDocumentHoverProviderRegistry } from './providers/hover'
import { TextDocumentLocationProviderRegistry, TextDocumentReferencesProviderRegistry } from './providers/location'
import { SearchTransformProviderRegistry } from './providers/search'

/**
* Registries is a container for all provider registries.
Expand All @@ -25,4 +26,5 @@ export class Registries<X extends Extension, C extends ConfigurationCascade> {
public readonly textDocumentTypeDefinition = new TextDocumentLocationProviderRegistry()
public readonly textDocumentHover = new TextDocumentHoverProviderRegistry()
public readonly textDocumentDecoration = new TextDocumentDecorationProviderRegistry()
public readonly search = new SearchTransformProviderRegistry()
}
1 change: 1 addition & 0 deletions src/common/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function createProxy(call: (name: string, args: any[]) => any): any {
if (!target[name] && name[0] === '$') {
target[name] = (...args: any[]) => call(name, args)
}

return target[name]
},
})
Expand Down
26 changes: 26 additions & 0 deletions src/extension/api/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Unsubscribable } from 'rxjs'
import { QueryTransformProvider } from 'sourcegraph'
import { SearchFeaturesAPI } from 'src/client/api/search'
import { ProviderMap } from './common'

/** @internal */
export interface ExtSearchFeaturesAPI {
$transformQuery: (id: number, query: string) => Promise<string | null | undefined>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, I think you should only accept Promise<string>, not Promise<string | null | undefined>. I'm not sure what null and undefined mean without checking docs or impl, and it's not clear that there is any benefit to allowing those return types.

}

/** @internal */
export class ExtSearchFeatures implements ExtSearchFeaturesAPI {
private registrations = new ProviderMap<QueryTransformProvider>(id => this.proxy.$unregister(id))
constructor(private proxy: SearchFeaturesAPI) {}

public registerQueryTransformProvider(provider: QueryTransformProvider): Unsubscribable {
const { id, subscription } = this.registrations.add(provider)
this.proxy.$registerQueryTransformProvider(id)
return subscription
}

public $transformQuery(id: number, query: string): Promise<string | null | undefined> {
const provider = this.registrations.get<QueryTransformProvider>(id)
return Promise.resolve(provider.transformQuery(query))
}
}
8 changes: 8 additions & 0 deletions src/extension/extensionHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { ExtSearchFeatures } from './api/search'
import { ExtWindows } from './api/windows'
import { Location } from './types/location'
import { Position } from './types/position'
Expand Down Expand Up @@ -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 ExtSearchFeatures(proxy('searchFeatures'))
handleRequests(connection, 'searchFeatures', searchFeatures)

const commands = new ExtCommands(proxy('commands'))
handleRequests(connection, 'commands', commands)

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

search: {
registerQueryTransformProvider: provider => searchFeatures.registerQueryTransformProvider(provider),
},

commands: {
registerCommand: (command, callback) => commands.registerCommand({ command, callback }),
executeCommand: (command, ...args) => commands.executeCommand(command, args),
Expand Down
8 changes: 8 additions & 0 deletions src/sourcegraph.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,14 @@ declare module 'sourcegraph' {
): ProviderResult<Location[]>
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should just call it QueryTransformer not QueryTransformerProvider. I struggled with this in other cases but here's my thinking:

  • The goal of naming is to communicate what something is.
  • Using the "Provider" suffix is generally good for things like this because it communicates that this fits the "provider" pattern. Not in the strict Erich Gamma Design Patterns sense, but just in the "this is like hover providers, definition providers, etc., in that it can be registered/unregistered and provides data given some parameters".
  • But when there is a word that is similar to "provider", is more accurate, is short, and is the term you'd use anywhere else in code describing this, then you should use that word instead.
  • In this case, that word is "Transformer". QueryTransformer and QueryTransformerProvider basically mean the same thing; in the latter, "transformer" and "provider" basically mean the same thing, so it's weird to have them both.
  • Looking at vscode.d.ts, they have many things that follow the guidelines I laid out instead of always using the "Provider" suffix: registerUriHandler, registerWebviewPanelSerializer, etc. Those are way better than registerUriHandlerProvider or registerUriProvider, or registerWebviewPanelSerializationProvider or registerWebviewPanelSerializerProvider.

export interface QueryTransformProvider {
transformQuery(query: string): ProviderResult<string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote 2 comments above about:

  • not special-casing null return values
  • using Promise<string> instead of Promise<string | null | undefined>

I see why you included null and undefined now, because ProviderResult adds them. But I think ProviderResult is only useful for things like hover providers, not for transform funcs. So you should just make this return string | Promise<string>.

The reason why ProviderResult is useful for hover/definition/reference/etc. providers is that they need to be able to return null to say "I have no answer for this query". But in transformQuery, you can say "I have no transformations to apply to this query" by just returning the original query input argument.

Another related reason you should use string | Promise<string> instead of ProviderResult<string> here is that it adds confusion if there are 3 things that all mean the same thing ("do not transform this query"):

  • query => query
  • query => null
  • query => undefined

There is no need for the 2nd and 3rd ways of expressing this.

}

export namespace search {
export function registerQueryTransformProvider(provider: QueryTransformProvider): Unsubscribable
}

export namespace languages {
export function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Unsubscribable

Expand Down