-
Notifications
You must be signed in to change notification settings - Fork 13
Add LSIF support #124
Add LSIF support #124
Changes from all commits
8c08143
0ecedc9
f273112
37a83ec
c421389
3fc92b2
6ab7d8d
9fb7a55
8d5a9c5
0bd3c1d
fd2625c
f1dde1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| node_modules/ | ||
| .git/ | ||
| coverage/ | ||
| .cache/ | ||
| lib/ | ||
| .nyc_output/ | ||
| coverage/ | ||
| dist/ | ||
| package.json | ||
| node_modules/ | ||
| temp/ | ||
| lib/ | ||
| yarn-error.log | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,44 +1,2 @@ | ||
| import * as sourcegraph from 'sourcegraph' | ||
| import { Handler, HandlerArgs, documentSelector } from './handler' | ||
|
|
||
| export { Handler, HandlerArgs, registerFeedbackButton } from './handler' | ||
|
|
||
| // No-op for Sourcegraph versions prior to 3.0-preview | ||
| const DUMMY_CTX = { subscriptions: { add: (_unsubscribable: any) => void 0 } } | ||
|
|
||
| export function activateBasicCodeIntel( | ||
| args: HandlerArgs | ||
| ): (ctx: sourcegraph.ExtensionContext) => void { | ||
| return function activate( | ||
| ctx: sourcegraph.ExtensionContext = DUMMY_CTX | ||
| ): void { | ||
| const h = new Handler({ ...args, sourcegraph }) | ||
|
|
||
| sourcegraph.internal.updateContext({ isImprecise: true }) | ||
|
|
||
| ctx.subscriptions.add( | ||
| sourcegraph.languages.registerHoverProvider( | ||
| documentSelector(h.fileExts), | ||
| { | ||
| provideHover: (doc, pos) => h.hover(doc, pos), | ||
| } | ||
| ) | ||
| ) | ||
| ctx.subscriptions.add( | ||
| sourcegraph.languages.registerDefinitionProvider( | ||
| documentSelector(h.fileExts), | ||
| { | ||
| provideDefinition: (doc, pos) => h.definition(doc, pos), | ||
| } | ||
| ) | ||
| ) | ||
| ctx.subscriptions.add( | ||
| sourcegraph.languages.registerReferenceProvider( | ||
| documentSelector(h.fileExts), | ||
| { | ||
| provideReferences: (doc, pos) => h.references(doc, pos), | ||
| } | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
| export { Handler, HandlerArgs } from './handler' | ||
| export { initLSIF } from './lsif' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| import * as sourcegraph from 'sourcegraph' | ||
| import * as LSP from 'vscode-languageserver-types' | ||
| import { convertLocations, convertHover } from './lsp-conversion' | ||
| import { queryGraphQL } from './api' | ||
|
|
||
| function repositoryFromDoc(doc: sourcegraph.TextDocument): string { | ||
| const url = new URL(doc.uri) | ||
| return url.hostname + url.pathname | ||
| } | ||
|
|
||
| function commitFromDoc(doc: sourcegraph.TextDocument): string { | ||
| const url = new URL(doc.uri) | ||
| return url.search.slice(1) | ||
| } | ||
|
|
||
| function pathFromDoc(doc: sourcegraph.TextDocument): string { | ||
| const url = new URL(doc.uri) | ||
| return url.hash.slice(1) | ||
| } | ||
|
|
||
| function setPath(doc: sourcegraph.TextDocument, path: string): string { | ||
| const url = new URL(doc.uri) | ||
| url.hash = path | ||
| return url.href | ||
| } | ||
|
|
||
| async function queryLSIF({ | ||
| doc, | ||
| method, | ||
| path, | ||
| position, | ||
| }: { | ||
| doc: sourcegraph.TextDocument | ||
| method: string | ||
| path: string | ||
| position: LSP.Position | ||
| }): Promise<any> { | ||
| const url = new URL( | ||
| '.api/lsif/request', | ||
| sourcegraph.internal.sourcegraphURL | ||
| ) | ||
| url.searchParams.set('repository', repositoryFromDoc(doc)) | ||
| url.searchParams.set('commit', commitFromDoc(doc)) | ||
|
|
||
| const response = await fetch(url.href, { | ||
| method: 'POST', | ||
| headers: new Headers({ | ||
| 'content-type': 'application/json', | ||
| 'x-requested-with': 'Sourcegraph LSIF extension', | ||
chrismwendt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }), | ||
| body: JSON.stringify({ | ||
| method, | ||
| path, | ||
| position, | ||
| }), | ||
| }) | ||
| if (!response.ok) { | ||
| throw new Error(`LSIF /request returned ${response.statusText}`) | ||
| } | ||
| return await response.json() | ||
| } | ||
|
|
||
| export const mkIsLSIFAvailable = (lsifDocs: Map<string, Promise<boolean>>) => ( | ||
| doc: sourcegraph.TextDocument, | ||
| pos: sourcegraph.Position | ||
| ): Promise<boolean> => { | ||
chrismwendt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!sourcegraph.configuration.get().get('codeIntel.lsif')) { | ||
| return Promise.resolve(false) | ||
| } | ||
|
|
||
| if (lsifDocs.has(doc.uri)) { | ||
| return lsifDocs.get(doc.uri)! | ||
| } | ||
|
|
||
| const url = new URL('.api/lsif/exists', sourcegraph.internal.sourcegraphURL) | ||
| url.searchParams.set('repository', repositoryFromDoc(doc)) | ||
| url.searchParams.set('commit', commitFromDoc(doc)) | ||
| url.searchParams.set('file', pathFromDoc(doc)) | ||
|
|
||
| const hasLSIFPromise = (async () => { | ||
| try { | ||
| // Prevent leaking the name of a private repository to | ||
| // Sourcegraph.com by relying on the Sourcegraph extension host's | ||
| // private repository detection, which will throw an error when | ||
| // making a GraphQL request. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part of the code is not clear to me.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Private repo names would have been leaked by a browser extension running on private GitHub.com repos. There's already code in the extension host to detect if the page is a private repo. This logic triggers that code to get run at the expense of an extra GraphQL request. Do you have any ideas to improve this? Maybe the extension API could expose |
||
| await queryGraphQL({ | ||
| query: `query { currentUser { id } }`, | ||
| vars: {}, | ||
| sourcegraph, | ||
| }) | ||
| } catch (e) { | ||
| return false | ||
| } | ||
| const response = await fetch(url.href, { | ||
| method: 'POST', | ||
| headers: new Headers({ | ||
| 'x-requested-with': 'Sourcegraph LSIF extension', | ||
| }), | ||
| }) | ||
| if (!response.ok) { | ||
| throw new Error(`LSIF /exists returned ${response.statusText}`) | ||
| } | ||
| return await response.json() | ||
| })() | ||
|
|
||
| lsifDocs.set(doc.uri, hasLSIFPromise) | ||
|
|
||
| return hasLSIFPromise | ||
| } | ||
|
|
||
| async function hover( | ||
| doc: sourcegraph.TextDocument, | ||
| position: sourcegraph.Position | ||
| ): Promise<sourcegraph.Hover | null> { | ||
| const hover: LSP.Hover | null = await queryLSIF({ | ||
| doc, | ||
| method: 'hover', | ||
| path: pathFromDoc(doc), | ||
| position, | ||
| }) | ||
| if (!hover) { | ||
| return null | ||
| } | ||
| return convertHover(sourcegraph, hover) | ||
| } | ||
|
|
||
| async function definition( | ||
| doc: sourcegraph.TextDocument, | ||
| position: sourcegraph.Position | ||
| ): Promise<sourcegraph.Definition | null> { | ||
| const body: LSP.Location | LSP.Location[] | null = await queryLSIF({ | ||
| doc, | ||
| method: 'definitions', | ||
| path: pathFromDoc(doc), | ||
| position, | ||
| }) | ||
| if (!body) { | ||
| return null | ||
| } | ||
| const locations = Array.isArray(body) ? body : [body] | ||
| return convertLocations( | ||
| sourcegraph, | ||
| locations.map((definition: LSP.Location) => ({ | ||
| ...definition, | ||
| uri: setPath(doc, definition.uri), | ||
| })) | ||
| ) | ||
| } | ||
|
|
||
| async function references( | ||
| doc: sourcegraph.TextDocument, | ||
| position: sourcegraph.Position | ||
| ): Promise<sourcegraph.Location[] | null> { | ||
| const locations: LSP.Location[] | null = await queryLSIF({ | ||
| doc, | ||
| method: 'references', | ||
| path: pathFromDoc(doc), | ||
| position, | ||
| }) | ||
| if (!locations) { | ||
| return [] | ||
| } | ||
| return convertLocations( | ||
| sourcegraph, | ||
| locations.map((reference: LSP.Location) => ({ | ||
| ...reference, | ||
| uri: setPath(doc, reference.uri), | ||
| })) | ||
| ) | ||
| } | ||
|
|
||
| export type Maybe<T> = { value: T } | undefined | ||
chrismwendt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export const wrapMaybe = <A extends any[], R>( | ||
| f: (...args: A) => Promise<R> | ||
| ) => async (...args: A): Promise<Maybe<R>> => { | ||
| const r = await f(...args) | ||
| return r !== undefined ? { value: r } : undefined | ||
| } | ||
|
|
||
| export function asyncWhen<A extends any[], R>( | ||
| asyncPredicate: (...args: A) => Promise<boolean> | ||
| ): (f: (...args: A) => Promise<R>) => (...args: A) => Promise<Maybe<R>> { | ||
| return f => async (...args) => | ||
| (await asyncPredicate(...args)) | ||
| ? { value: await f(...args) } | ||
| : undefined | ||
| } | ||
|
|
||
| export function when<A extends any[], R>( | ||
| predicate: (...args: A) => boolean | ||
| ): (f: (...args: A) => Promise<R>) => (...args: A) => Promise<Maybe<R>> { | ||
| return f => async (...args) => | ||
| predicate(...args) ? { value: await f(...args) } : undefined | ||
| } | ||
|
|
||
| export const asyncFirst = <A extends any[], R>( | ||
| fs: ((...args: A) => Promise<Maybe<R>>)[], | ||
| defaultR: R | ||
| ) => async (...args: A): Promise<R> => { | ||
| for (const f of fs) { | ||
| const r = await f(...args) | ||
| if (r !== undefined) { | ||
| return r.value | ||
| } | ||
| } | ||
| return defaultR | ||
| } | ||
chrismwendt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export function initLSIF() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could it be that there is no TSLint setup in this repo? Because this function doesn't have a return type annotation, which would be rejected by our TSLint config (and also would have made it a lot easier to understand this code, as a reviewer)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WIP on tslint in #125 |
||
| const lsifDocs = new Map<string, Promise<boolean>>() | ||
|
|
||
| const isLSIFAvailable = mkIsLSIFAvailable(lsifDocs) | ||
|
|
||
| return { | ||
| hover: asyncWhen< | ||
| [sourcegraph.TextDocument, sourcegraph.Position], | ||
| sourcegraph.Hover | null | ||
| >(isLSIFAvailable)(hover), | ||
| definition: asyncWhen< | ||
| [sourcegraph.TextDocument, sourcegraph.Position], | ||
| sourcegraph.Definition | null | ||
| >(isLSIFAvailable)(definition), | ||
| references: asyncWhen< | ||
| [sourcegraph.TextDocument, sourcegraph.Position], | ||
| sourcegraph.Location[] | null | ||
| >(isLSIFAvailable)(references), | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // Copied from @sourcegraph/lsp-client because adding it as a dependency makes | ||
| // providers in index.ts lose type safety. | ||
|
|
||
| import * as sourcegraph from 'sourcegraph' | ||
| import * as LSP from 'vscode-languageserver-types' | ||
|
|
||
| export const convertPosition = ( | ||
| sourcegraph: typeof import('sourcegraph'), | ||
| position: LSP.Position | ||
| ): sourcegraph.Position => | ||
| new sourcegraph.Position(position.line, position.character) | ||
|
|
||
| export const convertRange = ( | ||
| sourcegraph: typeof import('sourcegraph'), | ||
| range: LSP.Range | ||
| ): sourcegraph.Range => | ||
| new sourcegraph.Range( | ||
| convertPosition(sourcegraph, range.start), | ||
| convertPosition(sourcegraph, range.end) | ||
| ) | ||
|
|
||
| export function convertHover( | ||
| sourcegraph: typeof import('sourcegraph'), | ||
| hover: LSP.Hover | null | ||
| ): sourcegraph.Hover | null { | ||
| if (!hover) { | ||
| return null | ||
| } | ||
| const contents = Array.isArray(hover.contents) | ||
| ? hover.contents | ||
| : [hover.contents] | ||
| return { | ||
| range: hover.range && convertRange(sourcegraph, hover.range), | ||
| contents: { | ||
| kind: sourcegraph.MarkupKind.Markdown, | ||
| value: contents | ||
| .map(content => { | ||
| if (LSP.MarkupContent.is(content)) { | ||
| // Assume it's markdown. To be correct, markdown would need to be escaped for non-markdown kinds. | ||
| return content.value | ||
| } | ||
| if (typeof content === 'string') { | ||
| return content | ||
| } | ||
| if (!content.value) { | ||
| return '' | ||
| } | ||
| return ( | ||
| '```' + | ||
| content.language + | ||
| '\n' + | ||
| content.value + | ||
| '\n```' | ||
| ) | ||
| }) | ||
| .filter(str => !!str.trim()) | ||
| .join('\n\n---\n\n'), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| export const convertLocation = ( | ||
| sourcegraph: typeof import('sourcegraph'), | ||
| location: LSP.Location | ||
| ): sourcegraph.Location => ({ | ||
| uri: new sourcegraph.URI(location.uri), | ||
| range: convertRange(sourcegraph, location.range), | ||
| }) | ||
|
|
||
| export function convertLocations( | ||
| sourcegraph: typeof import('sourcegraph'), | ||
| locationOrLocations: LSP.Location | LSP.Location[] | null | ||
| ): sourcegraph.Location[] | null { | ||
| if (!locationOrLocations) { | ||
| return null | ||
| } | ||
| const locations = Array.isArray(locationOrLocations) | ||
| ? locationOrLocations | ||
| : [locationOrLocations] | ||
| return locations.map(location => convertLocation(sourcegraph, location)) | ||
| } | ||
chrismwendt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.