Skip to content
This repository was archived by the owner on Mar 18, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 6 additions & 5 deletions .prettierignore
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
5 changes: 3 additions & 2 deletions package/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sourcegraph/basic-code-intel",
"version": "6.0.18",
"version": "7.0.0",
"description": "Common library for providing basic code intelligence in Sourcegraph extensions",
"repository": {
"type": "git",
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion package/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 2 additions & 44 deletions package/src/index.ts
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'
229 changes: 229 additions & 0 deletions package/src/lsif.ts
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',
}),
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> => {
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

This part of the code is not clear to me.

  • How would we leak private repo names? Wouldn't we always contact the private instance?
  • Why would the extension host throw an error on any GraphQL request to the private instance? How would it even intercept GraphQL requests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 isPrivateRepository?

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

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
}

export function initLSIF() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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),
}
}
81 changes: 81 additions & 0 deletions package/src/lsp-conversion.ts
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))
}
Loading