diff --git a/client/web/src/codeintel/ReferencesPanelQueries.ts b/client/web/src/codeintel/ReferencesPanelQueries.ts index cc18b7c8b90..c6fb8c7b2b5 100644 --- a/client/web/src/codeintel/ReferencesPanelQueries.ts +++ b/client/web/src/codeintel/ReferencesPanelQueries.ts @@ -239,6 +239,18 @@ export const CODE_INTEL_SEARCH_QUERY = gql` } ` +export const LOCAL_CODE_INTEL_QUERY = gql` + query LocalCodeIntel($repository: String!, $commit: String!, $path: String!) { + repository(name: $repository) { + commit(rev: $commit) { + blob(path: $path) { + localCodeIntel + } + } + } + } +` + export const RESOLVE_REPO_REVISION_BLOB_QUERY = gql` fragment RepoRevisionBlobFields on Repository { id diff --git a/client/web/src/codeintel/location.ts b/client/web/src/codeintel/location.ts index 72dc243f0f6..eb9a9bf96f7 100644 --- a/client/web/src/codeintel/location.ts +++ b/client/web/src/codeintel/location.ts @@ -44,11 +44,13 @@ export const buildSearchBasedLocation = (node: Result): Location => ({ commitID: node.rev, content: node.content, url: node.url, - lines: node.content.split(/\r?\n/), + lines: split(node.content), precise: false, range: node.range, }) +export const split = (content: string): string[] => content.split(/\r?\n/) + export const buildPreciseLocation = (node: LocationFields): Location => { const location: Location = { content: node.resource.content, diff --git a/client/web/src/codeintel/useCodeIntel.ts b/client/web/src/codeintel/useCodeIntel.ts index 8cae869f0ce..2374409bff2 100644 --- a/client/web/src/codeintel/useCodeIntel.ts +++ b/client/web/src/codeintel/useCodeIntel.ts @@ -161,6 +161,10 @@ export const useCodeIntel = ({ path: variables.path, filter: variables.filter ?? undefined, searchToken, + position: { + line: variables.line, + character: variables.character, + }, fileContent, spec, isFork, diff --git a/client/web/src/codeintel/useSearchBasedCodeIntel.ts b/client/web/src/codeintel/useSearchBasedCodeIntel.ts index 3326de92846..9d84d5fd5a7 100644 --- a/client/web/src/codeintel/useSearchBasedCodeIntel.ts +++ b/client/web/src/codeintel/useSearchBasedCodeIntel.ts @@ -1,16 +1,20 @@ import { useCallback, useState } from 'react' -import { flatten } from 'lodash' +import stringify from 'fast-json-stable-stringify' +import { flatten, sortBy } from 'lodash' +import LRU from 'lru-cache' import { createAggregateError, ErrorLike } from '@sourcegraph/common' +import { Range as ExtensionRange, Position as ExtensionPosition } from '@sourcegraph/extension-api-types' import { getDocumentNode } from '@sourcegraph/http-client' +import { toPrettyBlobURL } from '@sourcegraph/shared/src/util/url' import { getWebGraphQLClient } from '../backend/graphql' import { CodeIntelSearchVariables } from '../graphql-operations' import { LanguageSpec } from './language-specs/languagespec' -import { Location, buildSearchBasedLocation } from './location' -import { CODE_INTEL_SEARCH_QUERY } from './ReferencesPanelQueries' +import { Location, buildSearchBasedLocation, split } from './location' +import { CODE_INTEL_SEARCH_QUERY, LOCAL_CODE_INTEL_QUERY } from './ReferencesPanelQueries' import { definitionQuery, isExternalPrivateSymbol, @@ -38,6 +42,7 @@ interface UseSearchBasedCodeIntelOptions { commit: string path: string + position: ExtensionPosition searchToken: string fileContent: string @@ -110,6 +115,8 @@ export async function searchBasedReferences({ commit, searchToken, path, + position, + fileContent, spec, getSetting, filter, @@ -117,6 +124,29 @@ export async function searchBasedReferences({ const filterReferences = (results: Location[]): Location[] => filter ? results.filter(location => location.file.includes(filter)) : results + const symbol = await findSymbol({ repository: repo, commit, path, row: position.line, column: position.character }) + if (symbol?.refs) { + return symbol.refs.map(reference => ({ + repo, + file: path, + content: fileContent, + commitID: commit, + range: rangeToExtensionRange(reference), + url: toPrettyBlobURL({ + filePath: path, + revision: commit, + repoName: repo, + commitID: commit, + position: { + line: reference.row + 1, + character: reference.column + 1, + }, + }), + lines: split(fileContent), + precise: false, + })) + } + const queryTerms = referencesQuery({ searchToken, path, fileExts: spec.fileExts }) const queryArguments = { repo, @@ -158,10 +188,36 @@ export async function searchBasedDefinitions({ searchToken, fileContent, path, + position, spec, getSetting, filter, }: UseSearchBasedCodeIntelOptions): Promise { + const symbol = await findSymbol({ repository: repo, commit, path, row: position.line, column: position.character }) + if (symbol?.def) { + return [ + { + repo, + file: path, + content: fileContent, + commitID: commit, + range: rangeToExtensionRange(symbol.def), + url: toPrettyBlobURL({ + filePath: path, + revision: commit, + repoName: repo, + commitID: commit, + position: { + line: symbol.def.row + 1, + character: symbol.def.column + 1, + }, + }), + lines: split(fileContent), + precise: false, + }, + ] + } + const filterDefinitions = (results: Location[]): Location[] => { const filteredByName = filter ? results.filter(location => location.file.includes(filter)) : results return spec?.filterDefinitions @@ -284,3 +340,132 @@ async function executeSearchQuery(terms: string[]): Promise { return result.data.search.results.results.filter(isDefined) } + +const findSymbol = async ( + repositoryCommitPathPosition: RepositoryCommitPathPosition +): Promise => { + const payload = await fetchLocalCodeIntelPayload(repositoryCommitPathPosition) + if (!payload) { + return + } + + for (const symbol of payload.symbols) { + if (isInRange(repositoryCommitPathPosition, symbol.def)) { + return symbol + } + + for (const reference of symbol.refs ?? []) { + if (isInRange(repositoryCommitPathPosition, reference)) { + return symbol + } + } + } + + return undefined +} + +const cache = ( + func: (...args: Arguments) => V, + options: LRU.Options +): ((...args: Arguments) => V) => { + const lru = new LRU(options) + return (...args) => { + const key = stringify(args) + if (lru.has(key)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return lru.get(key)! + } + const value = func(...args) + lru.set(key, value) + return value + } +} + +const fetchLocalCodeIntelPayload = cache( + async (repositoryCommitPath: RepositoryCommitPath): Promise => { + const client = await getWebGraphQLClient() + type LocalCodeIntelResponse = GenericBlobResponse<{ localCodeIntel: string }> + const result = await client.query({ + query: getDocumentNode(LOCAL_CODE_INTEL_QUERY), + variables: repositoryCommitPath, + }) + + if (result.error) { + throw createAggregateError([result.error]) + } + + const payloadString = result.data.repository?.commit?.blob?.localCodeIntel + if (!payloadString) { + return undefined + } + + const payload = JSON.parse(payloadString) as LocalCodeIntelPayload + + for (const symbol of payload.symbols) { + if (symbol.refs) { + symbol.refs = sortBy(symbol.refs, reference => reference.row) + } + } + + return payload + }, + { max: 10 } +) + +interface RepositoryCommitPath { + repository: string + commit: string + path: string +} + +type RepositoryCommitPathPosition = RepositoryCommitPath & Position + +interface LocalCodeIntelPayload { + symbols: LocalSymbol[] +} + +interface LocalSymbol { + hover?: string + def: LocalRange + refs?: LocalRange[] +} + +interface LocalRange { + row: number + column: number + length: number +} + +interface Position { + row: number + column: number +} + +const isInRange = (position: Position, range: LocalRange): boolean => { + if (position.row !== range.row) { + return false + } + if (position.column < range.column) { + return false + } + if (position.column > range.column + range.length) { + return false + } + return true +} + +/** The response envelope for all blob queries. */ +interface GenericBlobResponse { + repository: { commit: { blob: R | null } | null } | null +} + +const rangeToExtensionRange = (range: LocalRange): ExtensionRange => ({ + start: { + line: range.row, + character: range.column, + }, + end: { + line: range.row, + character: range.column + range.length, + }, +}) diff --git a/package.json b/package.json index 9f8c8f91045..9fcc9ee3a37 100644 --- a/package.json +++ b/package.json @@ -192,6 +192,7 @@ "@types/js-yaml": "4.0.3", "@types/jsdom": "12.2.4", "@types/lodash": "4.14.167", + "@types/lru-cache": "^7.6.1", "@types/marked": "4.0.3", "@types/mime-types": "2.1.0", "@types/mini-css-extract-plugin": "2.0.1", @@ -418,6 +419,7 @@ "js-yaml": "^4.1.0", "linguist-languages": "^7.14.0", "lodash": "^4.17.20", + "lru-cache": "^7.8.0", "marked": "^4.0.0", "mdi-react": "^8.1.0", "minimatch": "^3.0.4", diff --git a/yarn.lock b/yarn.lock index 5b62380078f..923ca3a7555 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5547,6 +5547,11 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== +"@types/lru-cache@^7.6.1": + version "7.6.1" + resolved "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-7.6.1.tgz#99809260ef1e870b8ef2ab3a625784a33cec5ba9" + integrity sha512-69x+Dhrm2aShFkTqUuPgUXbKYwvq4FH/DVeeQH7MBfTjbKjPX51NGLERxVV1vf33N71dzLvXCko4OLqRvsq53Q== + "@types/markdown-to-jsx@^6.11.3": version "6.11.3" resolved "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz#cdd1619308fecbc8be7e6a26f3751260249b020e" @@ -17503,6 +17508,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.8.0: + version "7.8.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.0.tgz#649aaeb294a56297b5cbc5d70f198dcc5ebe5747" + integrity sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg== + lru-queue@0.1: version "0.1.0" resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"