From 028cbd132491fdb457d4dfe06bce7de342aa63d8 Mon Sep 17 00:00:00 2001 From: netcon Date: Wed, 23 Jun 2021 15:13:54 +0800 Subject: [PATCH] feat: support for finding symbol references with souregraph (#329) --- .../src/interfaces/sourcegraph/definition.ts | 95 +----------- .../src/interfaces/sourcegraph/position.ts | 109 ++++++++++++++ .../src/interfaces/sourcegraph/reference.ts | 137 ++++++++++++++++++ .../github1s/src/providers/hoverProvider.ts | 8 +- extensions/github1s/src/providers/index.ts | 19 ++- .../src/providers/referenceProvider.ts | 70 +++++++++ 6 files changed, 333 insertions(+), 105 deletions(-) create mode 100644 extensions/github1s/src/interfaces/sourcegraph/position.ts create mode 100644 extensions/github1s/src/interfaces/sourcegraph/reference.ts create mode 100644 extensions/github1s/src/providers/referenceProvider.ts diff --git a/extensions/github1s/src/interfaces/sourcegraph/definition.ts b/extensions/github1s/src/interfaces/sourcegraph/definition.ts index c36f0c6a7..2e70332de 100644 --- a/extensions/github1s/src/interfaces/sourcegraph/definition.ts +++ b/extensions/github1s/src/interfaces/sourcegraph/definition.ts @@ -4,11 +4,8 @@ */ import { gql } from '@apollo/client/core'; -import { - escapeRegexp, - sourcegraphClient, - getRepoRefQueryString, -} from './common'; +import { sourcegraphClient } from './common'; +import { getSymbolPositions } from './position'; export interface SymbolDefinition { precise: boolean; @@ -108,85 +105,6 @@ const getLSIFDefinitions = async ( }); }; -const searchDefinitionsQuery = gql` - query($query: String!) { - search(query: $query) { - results { - results { - ... on FileMatch { - symbols { - name - kind - location { - resource { - path - repository { - name - } - commit { - oid - } - } - range { - start { - line - character - } - end { - line - character - } - } - } - } - } - } - } - } - } -`; - -// get symbol definitions base on search -export const getSearchDefinitions = async ( - owner: string, - repo: string, - ref: string, - symbol: string -): Promise => { - const repoRefString = getRepoRefQueryString(owner, repo, ref); - const optionsString = [ - 'context:global', - 'type:symbol', - 'patternType:regexp', - 'case:yes', - ].join(' '); - const patternString = `^${escapeRegexp(symbol)}$`; - const query = [repoRefString, optionsString, patternString].join(' '); - const response = await sourcegraphClient.query({ - query: searchDefinitionsQuery, - variables: { query }, - }); - - const resultSymbols = response?.data?.search?.results?.results?.flatMap( - (item) => item.symbols - ); - return (resultSymbols || []).map((symbol) => { - const { resource, range } = symbol.location; - const [owner, repo] = resource.repository.name - .split('/') - .filter(Boolean) - .slice(-2); - return { - precise: false, - owner, - repo, - ref: resource.commit.oid, - path: `/${resource.path}`, - range, - }; - }); -}; - export const getSymbolDefinitions = ( owner: string, repo: string, @@ -208,17 +126,12 @@ export const getSymbolDefinitions = ( line, character ); - const searchDefinitionsPromise = getSearchDefinitions( - owner, - repo, - ref, - symbol - ); + const searchDefinitionsPromise = getSymbolPositions(owner, repo, ref, symbol); return LSIFDefinitionsPromise.then((LSIFDefinitions) => { if (LSIFDefinitions.length) { return LSIFDefinitions; } - return searchDefinitionsPromise; + return searchDefinitionsPromise as Promise; }); }; diff --git a/extensions/github1s/src/interfaces/sourcegraph/position.ts b/extensions/github1s/src/interfaces/sourcegraph/position.ts new file mode 100644 index 000000000..a568f5eba --- /dev/null +++ b/extensions/github1s/src/interfaces/sourcegraph/position.ts @@ -0,0 +1,109 @@ +/** + * @file Sourcegraph api for searching symbol positions + * @author netcon + */ + +import { gql } from '@apollo/client/core'; +import { + escapeRegexp, + sourcegraphClient, + getRepoRefQueryString, +} from './common'; + +export interface SymbolPosition { + precise: boolean; + owner: string; + repo: string; + ref: string; + path: string; + range: { + start: { + line: number; + character: number; + }; + end: { + line: number; + character: number; + }; + }; +} + +const searchPositionsQuery = gql` + query($query: String!) { + search(query: $query) { + results { + results { + ... on FileMatch { + symbols { + name + kind + location { + resource { + path + repository { + name + } + commit { + oid + } + } + range { + start { + line + character + } + end { + line + character + } + } + } + } + } + } + } + } + } +`; + +// get symbol position information base on search, +// used by definition, reference and hover +export const getSymbolPositions = async ( + owner: string, + repo: string, + ref: string, + symbol: string +): Promise => { + const repoRefString = getRepoRefQueryString(owner, repo, ref); + const optionsString = [ + 'context:global', + 'type:symbol', + 'patternType:regexp', + 'case:yes', + ].join(' '); + const patternString = `^${escapeRegexp(symbol)}$`; + const query = [repoRefString, optionsString, patternString].join(' '); + const response = await sourcegraphClient.query({ + query: searchPositionsQuery, + variables: { query }, + }); + + const resultSymbols = response?.data?.search?.results?.results?.flatMap( + (item) => item.symbols + ); + return (resultSymbols || []).map((symbol) => { + const { resource, range } = symbol.location; + const [owner, repo] = resource.repository.name + .split('/') + .filter(Boolean) + .slice(-2); + return { + precise: false, + owner, + repo, + ref: resource.commit.oid, + path: `/${resource.path}`, + range, + }; + }); +}; diff --git a/extensions/github1s/src/interfaces/sourcegraph/reference.ts b/extensions/github1s/src/interfaces/sourcegraph/reference.ts new file mode 100644 index 000000000..cf9054aa7 --- /dev/null +++ b/extensions/github1s/src/interfaces/sourcegraph/reference.ts @@ -0,0 +1,137 @@ +/** + * @file Sourcegraph reference api + * @author netcon + */ + +import { gql } from '@apollo/client/core'; +import { sourcegraphClient } from './common'; +import { getSymbolPositions } from './position'; + +export interface SymbolReference { + precise: boolean; + owner: string; + repo: string; + ref: string; + path: string; + range: { + start: { + line: number; + character: number; + }; + end: { + line: number; + character: number; + }; + }; +} + +const LSIFReferencesQuery = gql` + query( + $repository: String! + $ref: String! + $path: String! + $line: Int! + $character: Int! + ) { + repository(name: $repository) { + commit(rev: $ref) { + blob(path: $path) { + lsif { + references(line: $line, character: $character) { + nodes { + resource { + path + repository { + name + } + commit { + oid + } + } + range { + start { + line + character + } + end { + line + character + } + } + } + } + } + } + } + } + } +`; + +// find references with Sourcegraph LSIF +// https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence +const getLSIFReferences = async ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number +): Promise => { + const response = await sourcegraphClient.query({ + query: LSIFReferencesQuery, + variables: { + repository: `github.com/${owner}/${repo}`, + ref, + path: path.slice(1), + line, + character, + }, + }); + const referenceNodes = + response?.data?.repository?.commit?.blob?.lsif?.references?.nodes; + return (referenceNodes || []).map(({ resource, range }) => { + const [owner, repo] = resource.repository.name + .split('/') + .filter(Boolean) + .slice(-2); + return { + precise: true, + owner, + repo, + ref: resource.commit.oid, + path: `/${resource.path}`, + range, + }; + }); +}; + +export const getSymbolReferences = ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number, + symbol: string +): Promise => { + // if failed to find references from LSIF, + // fallback to search-based references, using + // two promise instead of `await` to request in + // parallel for getting result as soon as possible + const LSIFReferencesPromise = getLSIFReferences( + owner, + repo, + ref, + path, + line, + character + ); + const searchReferencesPromise = getSymbolPositions(owner, repo, ref, symbol); + + return LSIFReferencesPromise.then((LSIFReferences) => { + if (LSIFReferences.length) { + return LSIFReferences; + } + return searchReferencesPromise as Promise; + }); +}; diff --git a/extensions/github1s/src/providers/hoverProvider.ts b/extensions/github1s/src/providers/hoverProvider.ts index 2f37bbd4f..63d25c4b5 100644 --- a/extensions/github1s/src/providers/hoverProvider.ts +++ b/extensions/github1s/src/providers/hoverProvider.ts @@ -6,21 +6,21 @@ import * as vscode from 'vscode'; import router from '@/router'; import { getSymbolHover, SymbolHover } from '@/interfaces/sourcegraph/hover'; -import { getSearchDefinitions } from '@/interfaces/sourcegraph/definition'; +import { getSymbolPositions } from '@/interfaces/sourcegraph/position'; import { getSourcegraphUrl } from '@/helpers/urls'; const getSemanticMarkdownSuffix = (sourcegraphUrl: String) => ` --- -[SEMANTIC](https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence) result provide by [Sourcegraph](${sourcegraphUrl}) +[Semantic](https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence) result provided by [Sourcegraph](${sourcegraphUrl}) `; const getSearchBasedMarkdownSuffix = (sourcegraphUrl: String) => ` --- -[SEARCH-BASED](https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence) result provide by [Sourcegraph](${sourcegraphUrl}) +[Search-based](https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence) result provided by [Sourcegraph](${sourcegraphUrl}) `; export class GitHub1sHoverProvider implements vscode.HoverProvider { @@ -32,7 +32,7 @@ export class GitHub1sHoverProvider implements vscode.HoverProvider { ): Promise { const authority = document.uri.authority || (await router.getAuthority()); const [owner, repo, ref] = authority.split('+').filter(Boolean); - const definitions = await getSearchDefinitions(owner, repo, ref, symbol); + const definitions = await getSymbolPositions(owner, repo, ref, symbol); if (!definitions.length) { return null; diff --git a/extensions/github1s/src/providers/index.ts b/extensions/github1s/src/providers/index.ts index 5c04fa9f6..99b954028 100644 --- a/extensions/github1s/src/providers/index.ts +++ b/extensions/github1s/src/providers/index.ts @@ -12,6 +12,7 @@ import { GitHub1sSubmoduleDecorationProvider } from './submoduleDecorationProvid import { GitHub1sChangedFileDecorationProvider } from './changedFileDecorationProvider'; import { GitHub1sSourceControlDecorationProvider } from './sourceControlDecorationProvider'; import { GitHub1sDefinitionProvider } from './definitionProvider'; +import { GitHub1sReferenceProvider } from './referenceProvider'; import { GitHub1sHoverProvider } from './hoverProvider'; export const fileSystemProvider = new GitHub1sFileSystemProvider(); @@ -25,6 +26,7 @@ export const submoduleDecorationProvider = new GitHub1sSubmoduleDecorationProvid export const changedFileDecorationProvider = new GitHub1sChangedFileDecorationProvider(); export const sourceControlDecorationProvider = new GitHub1sSourceControlDecorationProvider(); export const definitionProvider = new GitHub1sDefinitionProvider(); +export const referenceProvider = new GitHub1sReferenceProvider(); export const hoverProvider = new GitHub1sHoverProvider(); export const EMPTY_FILE_SCHEME = 'github1s-empty-file'; @@ -39,10 +41,7 @@ export const registerVSCodeProviders = () => { vscode.workspace.registerFileSystemProvider( GitHub1sFileSystemProvider.scheme, fileSystemProvider, - { - isCaseSensitive: true, - isReadonly: true, - } + { isCaseSensitive: true, isReadonly: true } ), vscode.workspace.registerFileSearchProvider( GitHub1sFileSearchProvider.scheme, @@ -59,15 +58,15 @@ export const registerVSCodeProviders = () => { ), vscode.languages.registerDefinitionProvider( - { - scheme: GitHub1sDefinitionProvider.scheme, - }, + { scheme: GitHub1sDefinitionProvider.scheme }, definitionProvider ), + vscode.languages.registerReferenceProvider( + { scheme: GitHub1sReferenceProvider.scheme }, + referenceProvider + ), vscode.languages.registerHoverProvider( - { - scheme: GitHub1sHoverProvider.scheme, - }, + { scheme: GitHub1sHoverProvider.scheme }, hoverProvider ), diff --git a/extensions/github1s/src/providers/referenceProvider.ts b/extensions/github1s/src/providers/referenceProvider.ts new file mode 100644 index 000000000..f98e4c4e8 --- /dev/null +++ b/extensions/github1s/src/providers/referenceProvider.ts @@ -0,0 +1,70 @@ +/** + * @file ReferenceProvider + * @author netcon + */ + +import * as vscode from 'vscode'; +import router from '@/router'; +import { getSymbolReferences } from '@/interfaces/sourcegraph/reference'; +import { showSourcegraphSymbolMessage } from '@/messages'; +import { GitHub1sFileSystemProvider } from './fileSystemProvider'; + +export class GitHub1sReferenceProvider implements vscode.ReferenceProvider { + static scheme = 'github1s'; + + async provideReferences( + document: vscode.TextDocument, + position: vscode.Position, + _context: vscode.ReferenceContext, + _token: vscode.CancellationToken + ): Promise { + const symbolRange = document.getWordRangeAtPosition(position); + const symbol = symbolRange ? document.getText(symbolRange) : ''; + + if (!symbol) { + return; + } + + const authority = document.uri.authority || (await router.getAuthority()); + const [owner, repo, ref] = authority.split('+').filter(Boolean); + const path = document.uri.path; + const { line, character } = position; + + const symbolReferences = await getSymbolReferences( + owner, + repo, + ref, + path, + line, + character, + symbol + ); + + if (symbolReferences.length) { + showSourcegraphSymbolMessage(owner, repo, ref, path, line, character); + } + + return symbolReferences.map((repoReference) => { + const isSameRepo = + repoReference.owner === owner && repoReference.repo === repo; + // if the reference target and the searched symbol is in the same + // repository, just replace the `document.uri.path` with targetPath + // (so that the target file will open with expanding the file explorer) + const uri = isSameRepo + ? document.uri.with({ path: repoReference.path }) + : vscode.Uri.parse('').with({ + scheme: GitHub1sFileSystemProvider.scheme, + authority: `${owner}+${repo}+${ref}`, + path: repoReference.path, + }); + const { start, end } = repoReference.range; + return { + uri, + range: new vscode.Range( + new vscode.Position(start.line, start.character), + new vscode.Position(end.line, end.character) + ), + }; + }); + } +}