From 8c081434c3d9382d0c4da470d4da12f1769b36c9 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 02:14:21 -0600 Subject: [PATCH 01/12] LSIF --- package/package.json | 3 +- package/src/index.ts | 149 +++++++++++++++++++++++++++++++++- package/src/lsp-conversion.ts | 59 ++++++++++++++ package/yarn.lock | 5 ++ template/src/extension.ts | 51 +----------- 5 files changed, 216 insertions(+), 51 deletions(-) create mode 100644 package/src/lsp-conversion.ts diff --git a/package/package.json b/package/package.json index 73645e26b..821e4b72e 100644 --- a/package/package.json +++ b/package/package.json @@ -60,7 +60,8 @@ "dependencies": { "lodash": "^4.17.11", "rxjs": "^6.3.3", - "sourcegraph": "^23.0.0" + "sourcegraph": "^23.0.0", + "vscode-languageserver-types": "^3.14.0" }, "sideEffects": false } diff --git a/package/src/index.ts b/package/src/index.ts index f43c64259..3cd6feb5c 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -1,5 +1,7 @@ import * as sourcegraph from 'sourcegraph' import { Handler, HandlerArgs, documentSelector } from './handler' +import * as LSP from 'vscode-languageserver-types' +import { convertLocations, convertHover } from './lsp-conversion'; export { Handler, HandlerArgs, registerFeedbackButton } from './handler' @@ -20,7 +22,7 @@ export function activateBasicCodeIntel( sourcegraph.languages.registerHoverProvider( documentSelector(h.fileExts), { - provideHover: (doc, pos) => h.hover(doc, pos), + provideHover: async (doc, pos) => await hasLSIF(doc) ? await lsif.provideHover(doc, pos) : await h.hover(doc, pos), } ) ) @@ -28,7 +30,7 @@ export function activateBasicCodeIntel( sourcegraph.languages.registerDefinitionProvider( documentSelector(h.fileExts), { - provideDefinition: (doc, pos) => h.definition(doc, pos), + provideDefinition: async (doc, pos) => await hasLSIF(doc) ? await lsif.provideDefinition(doc, pos) : await h.definition(doc, pos), } ) ) @@ -36,9 +38,150 @@ export function activateBasicCodeIntel( sourcegraph.languages.registerReferenceProvider( documentSelector(h.fileExts), { - provideReferences: (doc, pos) => h.references(doc, pos), + provideReferences: async (doc, pos) => await hasLSIF(doc) ? await lsif.provideReferences(doc, pos) : await h.references(doc, pos), } ) ) } } + +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 send({ + doc, + method, + path, + position, +}: { + doc: sourcegraph.TextDocument + method: string + path: string + position: LSP.Position +}): Promise { + 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', + }), + body: JSON.stringify({ + method, + path, + position, + }), + }) + if (!response.ok) { + throw new Error(`LSIF /request returned ${response.statusText}`) + } + return await response.json() +} + +const lsifDocs = new Map>() + +async function hasLSIF(doc: sourcegraph.TextDocument): Promise { + 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 () => { + 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 +} + +const lsif = { + provideHover: async (doc: sourcegraph.TextDocument, position: sourcegraph.Position): Promise => { + console.log('lsifhover') + const hover: LSP.Hover | null = await send({ + doc, + method: 'hover', + path: pathFromDoc(doc), + position, + }) + if (!hover) { + return null + } + return convertHover(sourcegraph, hover) + }, + + provideDefinition: async (doc: sourcegraph.TextDocument, position: sourcegraph.Position): Promise => { + const body: LSP.Location | LSP.Location[] | null = await send({ + 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), + })) + ) + }, + provideReferences: async (doc: sourcegraph.TextDocument, position: sourcegraph.Position): Promise => { + const locations: LSP.Location[] | null = await send({ + doc, + method: 'references', + path: pathFromDoc(doc), + position, + }) + if (!locations) { + return [] + } + return convertLocations( + sourcegraph, + locations.map((reference: LSP.Location) => ({ + ...reference, + uri: setPath(doc, reference.uri), + })) + ) + }, +} diff --git a/package/src/lsp-conversion.ts b/package/src/lsp-conversion.ts new file mode 100644 index 000000000..30b44f1ae --- /dev/null +++ b/package/src/lsp-conversion.ts @@ -0,0 +1,59 @@ +// 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)) +} diff --git a/package/yarn.lock b/package/yarn.lock index f78ae5864..441e90357 100644 --- a/package/yarn.lock +++ b/package/yarn.lock @@ -2297,6 +2297,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vscode-languageserver-types@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" + integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" diff --git a/template/src/extension.ts b/template/src/extension.ts index 3d35381e7..acf93ac25 100644 --- a/template/src/extension.ts +++ b/template/src/extension.ts @@ -1,7 +1,6 @@ -import { Handler, HandlerArgs, registerFeedbackButton } from '../../package/lib' +import { activateBasicCodeIntel } from '../../package/lib' import * as sourcegraph from 'sourcegraph' import { languageSpecs } from '../../languages' -import { documentSelector } from '../../package/lib/handler' const DUMMY_CTX = { subscriptions: { add: (_unsubscribable: any) => void 0 } } @@ -11,56 +10,14 @@ export function activate(ctx: sourcegraph.ExtensionContext = DUMMY_CTX): void { if (languageID === 'all') { for (const languageSpec of languageSpecs) { - activateWithArgs(ctx, { ...languageSpec.handlerArgs, sourcegraph }) + activateBasicCodeIntel({ ...languageSpec.handlerArgs, sourcegraph })(ctx) } } else { // TODO consider Record - activateWithArgs(ctx, { + activateBasicCodeIntel({ ...languageSpecs.find(l => l.handlerArgs.languageID === languageID)! .handlerArgs, sourcegraph, - }) + })(ctx) } } - -function activateWithArgs( - ctx: sourcegraph.ExtensionContext, - args: HandlerArgs -): void { - const h = new Handler({ ...args, sourcegraph }) - - sourcegraph.internal.updateContext({ isImprecise: true }) - - ctx.subscriptions.add( - registerFeedbackButton({ - languageID: args.languageID, - sourcegraph, - isPrecise: false, - }) - ) - - 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), - } - ) - ) -} From 0ecedc980c38662339a8d31c7e2be6f0109bdb28 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 02:19:10 -0600 Subject: [PATCH 02/12] prettier --- package/src/index.ts | 60 ++++++++++++++++------------------- package/src/lsp-conversion.ts | 11 +++++-- template/src/extension.ts | 3 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package/src/index.ts b/package/src/index.ts index 3cd6feb5c..806953965 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -1,46 +1,36 @@ import * as sourcegraph from 'sourcegraph' import { Handler, HandlerArgs, documentSelector } from './handler' import * as LSP from 'vscode-languageserver-types' -import { convertLocations, convertHover } from './lsp-conversion'; +import { convertLocations, convertHover } from './lsp-conversion' 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 { +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: async (doc, pos) => await hasLSIF(doc) ? await lsif.provideHover(doc, pos) : await h.hover(doc, pos), - } - ) + sourcegraph.languages.registerHoverProvider(documentSelector(h.fileExts), { + provideHover: async (doc, pos) => + (await hasLSIF(doc)) ? await lsif.provideHover(doc, pos) : await h.hover(doc, pos), + }) ) ctx.subscriptions.add( - sourcegraph.languages.registerDefinitionProvider( - documentSelector(h.fileExts), - { - provideDefinition: async (doc, pos) => await hasLSIF(doc) ? await lsif.provideDefinition(doc, pos) : await h.definition(doc, pos), - } - ) + sourcegraph.languages.registerDefinitionProvider(documentSelector(h.fileExts), { + provideDefinition: async (doc, pos) => + (await hasLSIF(doc)) ? await lsif.provideDefinition(doc, pos) : await h.definition(doc, pos), + }) ) ctx.subscriptions.add( - sourcegraph.languages.registerReferenceProvider( - documentSelector(h.fileExts), - { - provideReferences: async (doc, pos) => await hasLSIF(doc) ? await lsif.provideReferences(doc, pos) : await h.references(doc, pos), - } - ) + sourcegraph.languages.registerReferenceProvider(documentSelector(h.fileExts), { + provideReferences: async (doc, pos) => + (await hasLSIF(doc)) ? await lsif.provideReferences(doc, pos) : await h.references(doc, pos), + }) ) } } @@ -77,10 +67,7 @@ async function send({ path: string position: LSP.Position }): Promise { - const url = new URL( - '.api/lsif/request', - sourcegraph.internal.sourcegraphURL - ) + const url = new URL('.api/lsif/request', sourcegraph.internal.sourcegraphURL) url.searchParams.set('repository', repositoryFromDoc(doc)) url.searchParams.set('commit', commitFromDoc(doc)) @@ -133,7 +120,10 @@ async function hasLSIF(doc: sourcegraph.TextDocument): Promise { } const lsif = { - provideHover: async (doc: sourcegraph.TextDocument, position: sourcegraph.Position): Promise => { + provideHover: async ( + doc: sourcegraph.TextDocument, + position: sourcegraph.Position + ): Promise => { console.log('lsifhover') const hover: LSP.Hover | null = await send({ doc, @@ -147,7 +137,10 @@ const lsif = { return convertHover(sourcegraph, hover) }, - provideDefinition: async (doc: sourcegraph.TextDocument, position: sourcegraph.Position): Promise => { + provideDefinition: async ( + doc: sourcegraph.TextDocument, + position: sourcegraph.Position + ): Promise => { const body: LSP.Location | LSP.Location[] | null = await send({ doc, method: 'definitions', @@ -166,7 +159,10 @@ const lsif = { })) ) }, - provideReferences: async (doc: sourcegraph.TextDocument, position: sourcegraph.Position): Promise => { + provideReferences: async ( + doc: sourcegraph.TextDocument, + position: sourcegraph.Position + ): Promise => { const locations: LSP.Location[] | null = await send({ doc, method: 'references', diff --git a/package/src/lsp-conversion.ts b/package/src/lsp-conversion.ts index 30b44f1ae..fea3a61af 100644 --- a/package/src/lsp-conversion.ts +++ b/package/src/lsp-conversion.ts @@ -4,13 +4,18 @@ 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 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 { +export function convertHover( + sourcegraph: typeof import('sourcegraph'), + hover: LSP.Hover | null +): sourcegraph.Hover | null { if (!hover) { return null } diff --git a/template/src/extension.ts b/template/src/extension.ts index acf93ac25..d9cfa7b6f 100644 --- a/template/src/extension.ts +++ b/template/src/extension.ts @@ -15,8 +15,7 @@ export function activate(ctx: sourcegraph.ExtensionContext = DUMMY_CTX): void { } else { // TODO consider Record activateBasicCodeIntel({ - ...languageSpecs.find(l => l.handlerArgs.languageID === languageID)! - .handlerArgs, + ...languageSpecs.find(l => l.handlerArgs.languageID === languageID)!.handlerArgs, sourcegraph, })(ctx) } From f2731128671ccb14f8be981bbee3f5cdf1786cc1 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 02:24:31 -0600 Subject: [PATCH 03/12] prettierignore --- .prettierignore | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.prettierignore b/.prettierignore index d54d28b4f..968036cce 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,8 @@ -node_modules/ -.git/ -coverage/ .cache/ -lib/ +.nyc_output/ +coverage/ dist/ -package.json +node_modules/ +temp/ +lib/ +yarn-error.log From 37a83ec1c9c58393fd84f59c7ac50c83459389cb Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 02:25:04 -0600 Subject: [PATCH 04/12] prettier --- package/src/index.ts | 52 +++++++++++++++++++++++++---------- package/src/lsp-conversion.ts | 29 +++++++++++++++---- template/src/extension.ts | 8 ++++-- 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/package/src/index.ts b/package/src/index.ts index 806953965..376ae9098 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -8,29 +8,48 @@ 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 { +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: async (doc, pos) => - (await hasLSIF(doc)) ? await lsif.provideHover(doc, pos) : await h.hover(doc, pos), - }) + sourcegraph.languages.registerHoverProvider( + documentSelector(h.fileExts), + { + provideHover: async (doc, pos) => + (await hasLSIF(doc)) + ? await lsif.provideHover(doc, pos) + : await h.hover(doc, pos), + } + ) ) ctx.subscriptions.add( - sourcegraph.languages.registerDefinitionProvider(documentSelector(h.fileExts), { - provideDefinition: async (doc, pos) => - (await hasLSIF(doc)) ? await lsif.provideDefinition(doc, pos) : await h.definition(doc, pos), - }) + sourcegraph.languages.registerDefinitionProvider( + documentSelector(h.fileExts), + { + provideDefinition: async (doc, pos) => + (await hasLSIF(doc)) + ? await lsif.provideDefinition(doc, pos) + : await h.definition(doc, pos), + } + ) ) ctx.subscriptions.add( - sourcegraph.languages.registerReferenceProvider(documentSelector(h.fileExts), { - provideReferences: async (doc, pos) => - (await hasLSIF(doc)) ? await lsif.provideReferences(doc, pos) : await h.references(doc, pos), - }) + sourcegraph.languages.registerReferenceProvider( + documentSelector(h.fileExts), + { + provideReferences: async (doc, pos) => + (await hasLSIF(doc)) + ? await lsif.provideReferences(doc, pos) + : await h.references(doc, pos), + } + ) ) } } @@ -67,7 +86,10 @@ async function send({ path: string position: LSP.Position }): Promise { - const url = new URL('.api/lsif/request', sourcegraph.internal.sourcegraphURL) + const url = new URL( + '.api/lsif/request', + sourcegraph.internal.sourcegraphURL + ) url.searchParams.set('repository', repositoryFromDoc(doc)) url.searchParams.set('commit', commitFromDoc(doc)) diff --git a/package/src/lsp-conversion.ts b/package/src/lsp-conversion.ts index fea3a61af..4e94b4579 100644 --- a/package/src/lsp-conversion.ts +++ b/package/src/lsp-conversion.ts @@ -7,10 +7,17 @@ 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) +): 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 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'), @@ -19,7 +26,9 @@ export function convertHover( if (!hover) { return null } - const contents = Array.isArray(hover.contents) ? hover.contents : [hover.contents] + const contents = Array.isArray(hover.contents) + ? hover.contents + : [hover.contents] return { range: hover.range && convertRange(sourcegraph, hover.range), contents: { @@ -36,7 +45,13 @@ export function convertHover( if (!content.value) { return '' } - return '```' + content.language + '\n' + content.value + '\n```' + return ( + '```' + + content.language + + '\n' + + content.value + + '\n```' + ) }) .filter(str => !!str.trim()) .join('\n\n---\n\n'), @@ -59,6 +74,8 @@ export function convertLocations( if (!locationOrLocations) { return null } - const locations = Array.isArray(locationOrLocations) ? locationOrLocations : [locationOrLocations] + const locations = Array.isArray(locationOrLocations) + ? locationOrLocations + : [locationOrLocations] return locations.map(location => convertLocation(sourcegraph, location)) } diff --git a/template/src/extension.ts b/template/src/extension.ts index d9cfa7b6f..527083e4e 100644 --- a/template/src/extension.ts +++ b/template/src/extension.ts @@ -10,12 +10,16 @@ export function activate(ctx: sourcegraph.ExtensionContext = DUMMY_CTX): void { if (languageID === 'all') { for (const languageSpec of languageSpecs) { - activateBasicCodeIntel({ ...languageSpec.handlerArgs, sourcegraph })(ctx) + activateBasicCodeIntel({ + ...languageSpec.handlerArgs, + sourcegraph, + })(ctx) } } else { // TODO consider Record activateBasicCodeIntel({ - ...languageSpecs.find(l => l.handlerArgs.languageID === languageID)!.handlerArgs, + ...languageSpecs.find(l => l.handlerArgs.languageID === languageID)! + .handlerArgs, sourcegraph, })(ctx) } From c4213891390cabeb8780be62205443e1c23e9d01 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 02:27:42 -0600 Subject: [PATCH 05/12] Add toggle codeIntel.lsif --- package/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package/src/index.ts b/package/src/index.ts index 376ae9098..994999162 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -114,6 +114,10 @@ async function send({ const lsifDocs = new Map>() async function hasLSIF(doc: sourcegraph.TextDocument): Promise { + if (!sourcegraph.configuration.get().get('codeIntel.lsif')) { + return false + } + if (lsifDocs.has(doc.uri)) { return lsifDocs.get(doc.uri)! } From 3fc92b279410be5cb1c60f5ada8ef4c7c8a9a291 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 02:29:53 -0600 Subject: [PATCH 06/12] 6.0.19 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index 821e4b72e..be08274ad 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "@sourcegraph/basic-code-intel", - "version": "6.0.18", + "version": "6.0.19", "description": "Common library for providing basic code intelligence in Sourcegraph extensions", "repository": { "type": "git", From 6ab7d8d7716bebd91dd9ed1a471096fdc502ceb0 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 14:01:11 -0600 Subject: [PATCH 07/12] s/send/queryLSIF/ --- package/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package/src/index.ts b/package/src/index.ts index 994999162..0a6bf0803 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -75,7 +75,7 @@ function setPath(doc: sourcegraph.TextDocument, path: string): string { return url.href } -async function send({ +async function queryLSIF({ doc, method, path, @@ -151,7 +151,7 @@ const lsif = { position: sourcegraph.Position ): Promise => { console.log('lsifhover') - const hover: LSP.Hover | null = await send({ + const hover: LSP.Hover | null = await queryLSIF({ doc, method: 'hover', path: pathFromDoc(doc), @@ -167,7 +167,7 @@ const lsif = { doc: sourcegraph.TextDocument, position: sourcegraph.Position ): Promise => { - const body: LSP.Location | LSP.Location[] | null = await send({ + const body: LSP.Location | LSP.Location[] | null = await queryLSIF({ doc, method: 'definitions', path: pathFromDoc(doc), @@ -189,7 +189,7 @@ const lsif = { doc: sourcegraph.TextDocument, position: sourcegraph.Position ): Promise => { - const locations: LSP.Location[] | null = await send({ + const locations: LSP.Location[] | null = await queryLSIF({ doc, method: 'references', path: pathFromDoc(doc), From 9fb7a55fd85795a5048d2b6a775a388d6533b779 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 14:50:23 -0600 Subject: [PATCH 08/12] Check for private repository --- package/src/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/package/src/index.ts b/package/src/index.ts index 0a6bf0803..1b8f7c9e5 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -2,6 +2,7 @@ import * as sourcegraph from 'sourcegraph' import { Handler, HandlerArgs, documentSelector } from './handler' import * as LSP from 'vscode-languageserver-types' import { convertLocations, convertHover } from './lsp-conversion' +import { queryGraphQL } from './api' export { Handler, HandlerArgs, registerFeedbackButton } from './handler' @@ -128,6 +129,19 @@ async function hasLSIF(doc: sourcegraph.TextDocument): Promise { 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. + await queryGraphQL({ + query: `query { currentUser { id } }`, + vars: {}, + sourcegraph, + }) + } catch (e) { + return false + } const response = await fetch(url.href, { method: 'POST', headers: new Headers({ From 8d5a9c551db7bfaeefeed556eb5c321db187daa0 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 21:07:10 -0600 Subject: [PATCH 09/12] fileExts is required --- package/src/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/src/handler.ts b/package/src/handler.ts index 28e2ecd39..591d8b68f 100644 --- a/package/src/handler.ts +++ b/package/src/handler.ts @@ -565,7 +565,7 @@ export interface HandlerArgs { /** * The part of the filename after the `.` (e.g. `cpp` in `main.cpp`). */ - fileExts?: string[] + fileExts: string[] /** * Regex that matches lines between a definition and the docstring that * should be ignored. Java example: `/^\s*@/` for annotations. From 0bd3c1d7fd69b904989a0c850ba944768fc10173 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 21:07:54 -0600 Subject: [PATCH 10/12] Big refactor --- package/src/index.ts | 225 +------------------------------------ package/src/lsif.ts | 229 ++++++++++++++++++++++++++++++++++++++ template/src/extension.ts | 17 ++- 3 files changed, 238 insertions(+), 233 deletions(-) create mode 100644 package/src/lsif.ts diff --git a/package/src/index.ts b/package/src/index.ts index 1b8f7c9e5..11a913a09 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -1,223 +1,2 @@ -import * as sourcegraph from 'sourcegraph' -import { Handler, HandlerArgs, documentSelector } from './handler' -import * as LSP from 'vscode-languageserver-types' -import { convertLocations, convertHover } from './lsp-conversion' -import { queryGraphQL } from './api' - -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: async (doc, pos) => - (await hasLSIF(doc)) - ? await lsif.provideHover(doc, pos) - : await h.hover(doc, pos), - } - ) - ) - ctx.subscriptions.add( - sourcegraph.languages.registerDefinitionProvider( - documentSelector(h.fileExts), - { - provideDefinition: async (doc, pos) => - (await hasLSIF(doc)) - ? await lsif.provideDefinition(doc, pos) - : await h.definition(doc, pos), - } - ) - ) - ctx.subscriptions.add( - sourcegraph.languages.registerReferenceProvider( - documentSelector(h.fileExts), - { - provideReferences: async (doc, pos) => - (await hasLSIF(doc)) - ? await lsif.provideReferences(doc, pos) - : await h.references(doc, pos), - } - ) - ) - } -} - -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 { - 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', - }), - body: JSON.stringify({ - method, - path, - position, - }), - }) - if (!response.ok) { - throw new Error(`LSIF /request returned ${response.statusText}`) - } - return await response.json() -} - -const lsifDocs = new Map>() - -async function hasLSIF(doc: sourcegraph.TextDocument): Promise { - if (!sourcegraph.configuration.get().get('codeIntel.lsif')) { - return 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. - 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 -} - -const lsif = { - provideHover: async ( - doc: sourcegraph.TextDocument, - position: sourcegraph.Position - ): Promise => { - console.log('lsifhover') - const hover: LSP.Hover | null = await queryLSIF({ - doc, - method: 'hover', - path: pathFromDoc(doc), - position, - }) - if (!hover) { - return null - } - return convertHover(sourcegraph, hover) - }, - - provideDefinition: async ( - doc: sourcegraph.TextDocument, - position: sourcegraph.Position - ): Promise => { - 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), - })) - ) - }, - provideReferences: async ( - doc: sourcegraph.TextDocument, - position: sourcegraph.Position - ): Promise => { - 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 { Handler, HandlerArgs } from './handler' +export { initLSIF } from './lsif' diff --git a/package/src/lsif.ts b/package/src/lsif.ts new file mode 100644 index 000000000..5f04965f7 --- /dev/null +++ b/package/src/lsif.ts @@ -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 { + 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', + }), + 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>) => ( + doc: sourcegraph.TextDocument, + pos: sourcegraph.Position +): Promise => { + 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. + 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 { + 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 { + 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 { + 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 = { value: T } | undefined + +export const wrapMaybe = ( + f: (...args: A) => Promise +) => async (...args: A): Promise> => { + const r = await f(...args) + return r !== undefined ? { value: r } : undefined +} + +export function asyncWhen( + asyncPredicate: (...args: A) => Promise +): (f: (...args: A) => Promise) => (...args: A) => Promise> { + return f => async (...args) => + (await asyncPredicate(...args)) + ? { value: await f(...args) } + : undefined +} + +export function when( + predicate: (...args: A) => boolean +): (f: (...args: A) => Promise) => (...args: A) => Promise> { + return f => async (...args) => + predicate(...args) ? { value: await f(...args) } : undefined +} + +export const asyncFirst = ( + fs: ((...args: A) => Promise>)[], + defaultR: R +) => async (...args: A): Promise => { + for (const f of fs) { + const r = await f(...args) + if (r !== undefined) { + return r.value + } + } + return defaultR +} + +export function initLSIF() { + const lsifDocs = new Map>() + + 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), + } +} diff --git a/template/src/extension.ts b/template/src/extension.ts index 527083e4e..9d073f2ec 100644 --- a/template/src/extension.ts +++ b/template/src/extension.ts @@ -1,4 +1,4 @@ -import { activateBasicCodeIntel } from '../../package/lib' +import { Handler, initLSIF } from '../../package/lib' import * as sourcegraph from 'sourcegraph' import { languageSpecs } from '../../languages' @@ -8,19 +8,16 @@ export function activate(ctx: sourcegraph.ExtensionContext = DUMMY_CTX): void { // This is set to an individual language ID by the generator script. const languageID = 'all' - if (languageID === 'all') { - for (const languageSpec of languageSpecs) { + for (const languageSpec of languageID === 'all' + ? languageSpecs + : [languageSpecs.find(l => l.handlerArgs.languageID === languageID)!]) { + if (sourcegraph.configuration.get().get('codeIntel.lsif')) { + initLSIF() + } else { activateBasicCodeIntel({ ...languageSpec.handlerArgs, sourcegraph, })(ctx) } - } else { - // TODO consider Record - activateBasicCodeIntel({ - ...languageSpecs.find(l => l.handlerArgs.languageID === languageID)! - .handlerArgs, - sourcegraph, - })(ctx) } } From fd2625caf2291310b17b00ec7131436c7f90beeb Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 21:20:40 -0600 Subject: [PATCH 11/12] v7.0.0 --- package/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/package.json b/package/package.json index be08274ad..15c1f3e4e 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "@sourcegraph/basic-code-intel", - "version": "6.0.19", + "version": "7.0.0", "description": "Common library for providing basic code intelligence in Sourcegraph extensions", "repository": { "type": "git", From f1dde1ff761b2f96ec38c37be834fa98346e9638 Mon Sep 17 00:00:00 2001 From: Chris Wendt Date: Fri, 19 Jul 2019 22:25:25 -0600 Subject: [PATCH 12/12] . --- template/src/extension.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/template/src/extension.ts b/template/src/extension.ts index 9d073f2ec..0aab41b51 100644 --- a/template/src/extension.ts +++ b/template/src/extension.ts @@ -14,10 +14,26 @@ export function activate(ctx: sourcegraph.ExtensionContext = DUMMY_CTX): void { if (sourcegraph.configuration.get().get('codeIntel.lsif')) { initLSIF() } else { - activateBasicCodeIntel({ + const handler = new Handler({ ...languageSpec.handlerArgs, sourcegraph, - })(ctx) + }) + const goFiles = [{ pattern: '*.go' }] + ctx.subscriptions.add( + sourcegraph.languages.registerHoverProvider(goFiles, { + provideHover: handler.hover.bind(handler), + }) + ) + ctx.subscriptions.add( + sourcegraph.languages.registerDefinitionProvider(goFiles, { + provideDefinition: handler.definition.bind(handler), + }) + ) + ctx.subscriptions.add( + sourcegraph.languages.registerReferenceProvider(goFiles, { + provideReferences: handler.references.bind(handler), + }) + ) } } }