Skip to content
This repository was archived by the owner on Mar 18, 2024. It is now read-only.

Activation refactor #198

Merged
merged 16 commits into from
Jan 31, 2020
2 changes: 1 addition & 1 deletion languages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HandlerArgs, CommentStyle } from './package/lib/handler'
import { HandlerArgs, CommentStyle } from './package/src/index'
const path = require('path-browserify')

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
Expand Down
21 changes: 21 additions & 0 deletions package/src/abort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface AbortError extends Error {
name: 'AbortError'
}

/**
* Creates an Error with name "AbortError"
*/
export const createAbortError = (): AbortError =>
Object.assign(new Error('Aborted'), { name: 'AbortError' as const })

/**
* Returns true if the given value is an AbortError
*/
export const isAbortError = (err: any): err is AbortError =>
typeof err === 'object' && err !== null && err.name === 'AbortError'

export function throwIfAbortError(err: unknown): void {
if (isAbortError(err)) {
throw err
}
}
258 changes: 258 additions & 0 deletions package/src/activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import * as sourcegraph from 'sourcegraph'
import { HandlerArgs, Handler } from './search/handler'
import { initLSIF } from './lsif/activation'
import { impreciseBadge } from './badges'
import { shareReplay } from 'rxjs/operators'
import { Observable, Observer } from 'rxjs'
import { createAbortError } from './abort'
import { LSPProviders } from './lsp/providers'
import { LSIFProviders } from './lsif/providers'
import { SearchProviders } from './search/providers'

export function activateCodeIntel(
ctx: sourcegraph.ExtensionContext,
selector: sourcegraph.DocumentSelector,
handlerArgs: HandlerArgs,
lspProviders?: LSPProviders
): void {
const lsifProviders = initLSIF()
const searchProviders = new Handler(handlerArgs)

ctx.subscriptions.add(
sourcegraph.languages.registerDefinitionProvider(
selector,
createDefinitionProvider(
lsifProviders,
searchProviders,
lspProviders
)
)
)
ctx.subscriptions.add(
sourcegraph.languages.registerReferenceProvider(
selector,
createReferencesProvider(
lsifProviders,
searchProviders,
lspProviders
)
)
)
ctx.subscriptions.add(
sourcegraph.languages.registerHoverProvider(
selector,
createHoverProvider(lsifProviders, searchProviders, lspProviders)
)
)
}

function createDefinitionProvider(
lsifProviders: LSIFProviders,
searchProviders: SearchProviders,
lspProviders?: LSPProviders
): sourcegraph.DefinitionProvider {
async function* provideDefinition(
doc: sourcegraph.TextDocument,
pos: sourcegraph.Position
): AsyncGenerator<sourcegraph.Definition | undefined, void, undefined> {
const lsifResult = await lsifProviders.definition(doc, pos)
if (lsifResult) {
yield lsifResult
return
}

if (lspProviders) {
yield* lspProviders.definition(doc, pos)
return
}

let searchResult = await searchProviders.definition(doc, pos)
if (!searchResult) {
yield undefined
return
}

if (!Array.isArray(searchResult)) {
const badged = { ...searchResult, badge: impreciseBadge }
yield badged
return
}

yield searchResult.map(v => ({ ...v, badge: impreciseBadge }))
}

return {
provideDefinition: wrap(areProviderParamsEqual, provideDefinition),
}
}

function createReferencesProvider(
lsifProviders: LSIFProviders,
searchProviders: SearchProviders,
lspProviders?: LSPProviders
): sourcegraph.ReferenceProvider {
// Gets an opaque value that is the same for all locations
// within a file but different from other files.
const file = (loc: sourcegraph.Location) =>
`${loc.uri.host} ${loc.uri.pathname} ${loc.uri.hash}`

async function* provideReferences(
doc: sourcegraph.TextDocument,
pos: sourcegraph.Position,
ctx: sourcegraph.ReferenceContext
): AsyncGenerator<sourcegraph.Location[] | null, void, undefined> {
Copy link
Contributor Author

@efritz efritz Jan 27, 2020

Choose a reason for hiding this comment

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

Note: this logic isn't great but it's what currently exists for Go and Typescript. We should favor fast LSIF results first, then LSP results, then search results (if LSP is provided). This is possible now that we've updated our providers to be AsyncGenerators.

if (lspProviders) {
yield* lspProviders.references(doc, pos, ctx)
return
}

// Get and extract LSIF results
const lsifResult = await lsifProviders.references(doc, pos)
const lsifReferences = lsifResult || []
const lsifFiles = new Set(lsifReferences.map(file))

// Unconditionally get search references and append them with
// precise results because LSIF data might be sparse. Remove any
// search-based result that occurs in a file with an LSIF result.
const searchResults = (
(await searchProviders.references(doc, pos)) || []
).filter(fuzzyRef => !lsifFiles.has(file(fuzzyRef)))

yield [
...lsifReferences,
...searchResults.map(v => ({
...v,
badge: impreciseBadge,
})),
]
}

return {
provideReferences: wrap(
areProviderParamsContextEqual,
provideReferences
),
}
}

function createHoverProvider(
lsifProviders: LSIFProviders,
searchProviders: SearchProviders,
lspProviders?: LSPProviders
): sourcegraph.HoverProvider {
async function* provideHover(
doc: sourcegraph.TextDocument,
pos: sourcegraph.Position
): AsyncGenerator<
sourcegraph.Badged<sourcegraph.Hover> | null | undefined,
void,
undefined
> {
const lsifResult = await lsifProviders.hover(doc, pos)
if (lsifResult) {
yield lsifResult
return
}

if (lspProviders) {
yield* lspProviders.hover(doc, pos)
return
}

const searchResult = await searchProviders.hover(doc, pos)
if (!searchResult) {
yield undefined
return
}

yield { ...searchResult, badge: impreciseBadge }
}

return {
provideHover: wrap(areProviderParamsEqual, provideHover),
}
}

//
//
//
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The following code was lifted from the typescript extension.


const wrap = <P extends any[], R>(
compare: (a: P, b: P) => boolean,
fn: (...args: P) => AsyncGenerator<R, void, void>
): ((...args: P) => Observable<R>) =>
memoizePrevious(compare, (...args) =>
observableFromAsyncGenerator(() => fn(...args)).pipe(shareReplay(1))
)

const areProviderParamsEqual = (
[doc1, pos1]: [sourcegraph.TextDocument, sourcegraph.Position],
[doc2, pos2]: [sourcegraph.TextDocument, sourcegraph.Position]
): boolean => doc1.uri === doc2.uri && pos1.isEqual(pos2)

const areProviderParamsContextEqual = (
[doc1, pos1]: [
sourcegraph.TextDocument,
sourcegraph.Position,
sourcegraph.ReferenceContext
],
[doc2, pos2]: [
sourcegraph.TextDocument,
sourcegraph.Position,
sourcegraph.ReferenceContext
]
): boolean => areProviderParamsEqual([doc1, pos1], [doc2, pos2])

const observableFromAsyncGenerator = <T>(
generator: () => AsyncGenerator<T, unknown, void>
): Observable<T> =>
new Observable((observer: Observer<T>) => {
const iterator = generator()
let unsubscribed = false
let iteratorDone = false
function next(): void {
iterator.next().then(
result => {
if (unsubscribed) {
return
}
if (result.done) {
iteratorDone = true
observer.complete()
} else {
observer.next(result.value)
next()
}
},
err => {
observer.error(err)
}
)
}
next()
return () => {
unsubscribed = true
if (!iteratorDone && iterator.throw) {
iterator.throw(createAbortError()).catch(() => {
// ignore
})
}
}
})

/** Workaround for https://github.com/sourcegraph/sourcegraph/issues/1321 */
function memoizePrevious<P extends any[], R>(
compare: (a: P, b: P) => boolean,
fn: (...args: P) => R
): (...args: P) => R {
let previousResult: R
let previousArgs: P
return (...args) => {
if (previousArgs && compare(previousArgs, args)) {
return previousResult
}
previousArgs = args
previousResult = fn(...args)
return previousResult
}
}
22 changes: 21 additions & 1 deletion package/src/memoizeAsync.ts → package/src/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
// TODO(sqs): this will never release the memory of the cached responses; use an LRU cache or similar.
export const queryGraphQL = memoizeAsync(
async ({
query,
vars,
sourcegraph,
}: {
query: string
vars: { [name: string]: any }
sourcegraph: typeof import('sourcegraph')
}): Promise<any> => {
return sourcegraph.commands.executeCommand<any>(
'queryGraphQL',
query,
vars
)
},
arg => JSON.stringify({ query: arg.query, vars: arg.vars })
)

/**
* Creates a function that memoizes the async result of func.
* If the promise rejects, the value will not be cached.
*
* @param resolver If resolver provided, it determines the cache key for storing the result based on
* the first argument provided to the memoized function.
*/
export function memoizeAsync<P extends {}, T>(
function memoizeAsync<P extends {}, T>(
func: (params: P) => Promise<T>,
resolver?: (params: P) => string
): (params: P, force?: boolean) => Promise<T> {
Expand Down
25 changes: 9 additions & 16 deletions package/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
export { impreciseBadge } from './badges'
export { Handler, HandlerArgs } from './handler'
export { activateCodeIntel } from './activation'
export { HandlerArgs } from './search/handler'
export { CommentStyle, BlockCommentStyle } from './search/comments'
export { LSPProviders } from './lsp/providers'
export {
initLSIF,
asyncFirst,
asyncWhen,
when,
wrapMaybe,
Maybe,
MaybeProviders,
noopMaybeProviders,
mkIsLSIFAvailable,
hover,
definition,
references,
Providers,
} from './lsif'
AbortError,
createAbortError,
isAbortError,
throwIfAbortError,
} from './abort'
Loading