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

Add search features API #95

merged 17 commits into from
Oct 26, 2018

Conversation

attfarhan
Copy link

This adds the first part of the new search features API. It currently allows extensions to transform user queries.

@codecov
Copy link

codecov bot commented Oct 24, 2018

Codecov Report

Merging #95 into master will increase coverage by 0.33%.
The diff coverage is 95.83%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #95      +/-   ##
==========================================
+ Coverage   78.07%   78.41%   +0.33%     
==========================================
  Files          61       64       +3     
  Lines        2472     2520      +48     
  Branches      521      522       +1     
==========================================
+ Hits         1930     1976      +46     
- Misses        542      544       +2
Impacted Files Coverage Δ
src/client/providers/queryTransformer.ts 100% <100%> (ø)
src/client/registries.ts 100% <100%> (ø) ⬆️
src/extension/api/search.ts 100% <100%> (ø)
src/extension/extensionHost.ts 88.23% <100%> (+0.73%) ⬆️
src/client/controller.ts 86.2% <100%> (+0.24%) ⬆️
src/client/api/search.ts 87.5% <87.5%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update fd0d000...4c57cae. Read the comment docs.

Copy link

@chrismwendt chrismwendt left a comment

Choose a reason for hiding this comment

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

Can you provide a small example of what this looks like when used in a Sourcegraph extension?

Make sure the naming here won't clash if/when we add providers for search results like JIRA or GitHub issues.

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(...)?

@attfarhan
Copy link
Author

@chrismwendt here's an example extension: https://github.com/attfarhan/search-extension

Perhaps we could change the naming from SearchProvider to QueryTransformProvider or something similar to be more descriptive/accurate. WDYT?

@chrismwendt
Copy link

QueryTransformProvider sounds good 👍

Copy link
Member

@sqs sqs left a comment

Choose a reason for hiding this comment

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

Add tests for all newly added code. There are existing tests for similar things to everything you've done, so you can start with those.

$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.)

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.

@attfarhan
Copy link
Author

@chrismwendt @sqs PTAL

Copy link
Member

@sqs sqs left a comment

Choose a reason for hiding this comment

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

This looks fantastic! Nice job. I added a lot of comments about very picky details.

import { FeatureProviderRegistry } from '../providers/registry'
import { SubscriptionMap } from './common'

export interface SearchAPI {
Copy link
Member

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).

expectObservable(
transformQuery(
cold<TransformQuerySignature[]>('-a-b-|', {
a: [q => of(q)],
Copy link
Member

Choose a reason for hiding this comment

The 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:

  • 1st query transform func appends a to the string
  • 2nd query transform func appends b to the string

and test that the result is the original string with ab appended.

(I think the current test would pass even if your logic was like this.providers[1](query), which would obviously be incorrect and would not be caught by the test.)


export type TransformQuerySignature = (query: string) => Observable<string | null | undefined>

/** Provides transformed queries from the first search extension. */
Copy link
Member

Choose a reason for hiding this comment

The 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:

/** Transforms search queries using registered query transformers from extensions. */

* 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
Copy link
Member

Choose a reason for hiding this comment

The 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".

return providers.reduce(
(currentQuery, transformQuery) =>
currentQuery.pipe(
flatMap(q => transformQuery(q).pipe(map(transformedQuery => transformedQuery || q)))
Copy link
Member

Choose a reason for hiding this comment

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

Is this the desired behavior?

  • If a query transformer returns null or the empty string, use the original input string.
  • Otherwise, use the original input string.

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:

transformedQuery => transformedQuery === null ? q : transformedQuery

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.

'bar'
)

// Unregister the provider and ensure it's removed
Copy link
Member

Choose a reason for hiding this comment

The 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

const { clientController, extensionHost, ready } = await integrationTestContext()

// Register the provider and call it
extensionHost.search.registerQueryTransformer({ transformQuery: q => `${q} bar` })
Copy link
Member

Choose a reason for hiding this comment

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

Minor, but bar and baz look almost the same; it's not easy to visually spot that they are distinct, given that everything else on the 2 lines is the same. I would recommend using foo, qux, etc., here on one of the lines so it's easier to spot that they are different.

@@ -717,6 +717,14 @@ declare module 'sourcegraph' {
): ProviderResult<Location[]>
}

export interface QueryTransformerProvider {
Copy link
Member

Choose a reason for hiding this comment

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

Add tsdoc docstrings to:

  • interface QueryTransformerProvider
  • QueryTransformerProvider#transformQuery
  • namespace search
  • function registerQueryTransformer

@@ -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.

@@ -717,6 +717,14 @@ declare module 'sourcegraph' {
): ProviderResult<Location[]>
}

export interface QueryTransformerProvider {
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 interface QueryTransformer {
/**
* Transforms a search query into another, valid query. If there are no transformations to be made
Copy link
Member

Choose a reason for hiding this comment

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

I'd replace "the original query is returned" with ", it should return the original query"

* Transforms a search query into another, valid query. If there are no transformations to be made
* the original query is returned.
*
* @param query A search query provided by a client.
Copy link
Member

Choose a reason for hiding this comment

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

just "A search query" is fine, no need to say "provided by a client" (not sure what that adds)

/**
* Registers a query transformer.
*
* Multiple transformers can be registered. In that case, all transformers will be executed
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 it is more idiomatic to say a transformation is applied, not executed. So replace "executed" with "applied".

Also, the order in which multiple transformers are applied is undefined (i.e., the extension API makes no guarantee). So I'd state that.

@attfarhan attfarhan merged commit 3c629da into master Oct 26, 2018
@attfarhan attfarhan deleted the fa/search-provider branch October 26, 2018 22:10
@sourcegraph-bot
Copy link

🎉 This PR is included in version 18.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants