mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
svelte: Add minimal reference panel (#62168)
This PR adds a *very early* version of the reference panel. Things that work: - Fetch precise code intel information via GraphQL API - Opening the reference panel when clicking "find references" - "No references found" message - Open/close file preview - Error handling Things that don't work yet (to be iterated on): - Support for search-based code intel - Grouping of references, or repo indicator in general - Restore opened/selected preview on back/forward navigation I want to emphasize that this is by no means a "new" reference panel. In the interest of time it's an approximation of the reference panel in the React app. Some things have been simplified, such as omitting grouping of references. Worth noting is using the actual file page as file preview component. This is possible because pages are normal components. I'm still a bit unsure whether this is a good approach or not. - Pro: - Works nicely together with `preloadData` (except for data type casting). There is no need to duplicate the data loading logic or pluck out the right values from the preloaded data. That also means that data will be properly cached. - Ensures some consistency between the file page itself and wherever we show a file preview. - Con: - Might make working on the page itself more difficult since the "embedded" use case has to be kept in mind. - Additional props are required to adjust UI for the preview case. I'd like to give this a try and see how it works, possible also make the search results page use this for the preview panel.
This commit is contained in:
parent
0d7ab3e62e
commit
ea689de4d7
57
client/web-sveltekit/src/lib/CodeExcerpt.stories.svelte
Normal file
57
client/web-sveltekit/src/lib/CodeExcerpt.stories.svelte
Normal file
@ -0,0 +1,57 @@
|
||||
<script lang="ts" context="module">
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { Story } from '@storybook/addon-svelte-csf'
|
||||
|
||||
import CodeExcerpt from './CodeExcerpt.svelte'
|
||||
|
||||
export const meta = {
|
||||
component: CodeExcerpt,
|
||||
}
|
||||
|
||||
const code = `
|
||||
const obj = {
|
||||
key: 'value',
|
||||
key2: 'value2',
|
||||
}
|
||||
`
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
faker.seed(16)
|
||||
const plaintextLines = code.trim().split('\n')
|
||||
const highlightedHTMLRows = plaintextLines.map((line, index) => `<tr><td class="line" data-line="${index + 1}" /><td class="code"><span style="color: ${faker.color.rgb()}">${line}</span></td></tr>`)
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
<h3>Default</h3>
|
||||
<div class="wrapper">
|
||||
<CodeExcerpt startLine={1} {plaintextLines} />
|
||||
</div>
|
||||
|
||||
<h3>Different start line</h3>
|
||||
<div class="wrapper">
|
||||
<CodeExcerpt startLine={10} {plaintextLines} />
|
||||
</div>
|
||||
|
||||
<h3>Hidden line numbers</h3>
|
||||
<div class="wrapper">
|
||||
<CodeExcerpt startLine={1} {plaintextLines} hideLineNumbers/>
|
||||
</div>
|
||||
|
||||
<h3>Collapsed whitespace</h3>
|
||||
<div class="wrapper">
|
||||
<CodeExcerpt startLine={1} {plaintextLines} collapseWhitespace/>
|
||||
</div>
|
||||
|
||||
<h3>With highlighted code</h3>
|
||||
<div class="wrapper">
|
||||
<CodeExcerpt startLine={1} {plaintextLines} {highlightedHTMLRows}/>
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
background-color: var(--code-bg);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -6,10 +6,20 @@
|
||||
import { highlightNodeMultiline } from '$lib/common'
|
||||
import type { MatchGroupMatch } from '$lib/shared'
|
||||
|
||||
/**
|
||||
* Number of the first line in the code excerpt. This is 1-indexed.
|
||||
* Doesn't have any effect when `highlightedHTMLRows` or `hideLineNumbers` are set.
|
||||
*/
|
||||
export let startLine: number
|
||||
export let plaintextLines: string[]
|
||||
export let highlightedHTMLRows: string[] | undefined = undefined
|
||||
export let plaintextLines: readonly string[]
|
||||
export let highlightedHTMLRows: readonly string[] | undefined = undefined
|
||||
export let matches: MatchGroupMatch[] = []
|
||||
/**
|
||||
* Causes whitespace to *not* be preserved. Can be useful to ignore the leading whitespace in a code block,
|
||||
* but will also remove any intentional whitespace formatting.
|
||||
*/
|
||||
export let collapseWhitespace = false
|
||||
export let hideLineNumbers = false
|
||||
|
||||
function highlightMatches(node: HTMLElement, matches: MatchGroupMatch[]) {
|
||||
const visibleRows = node.querySelectorAll<HTMLTableRowElement>('tr')
|
||||
@ -39,14 +49,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<code>
|
||||
<code class:collapseWhitespace class:hideLineNumbers>
|
||||
{#key matches}
|
||||
{#if highlightedHTMLRows === undefined}
|
||||
{#if highlightedHTMLRows === undefined || highlightedHTMLRows.length === 0}
|
||||
<table use:highlightMatches={matches}>
|
||||
<tbody>
|
||||
{#each plaintextLines as line, index}
|
||||
<tr>
|
||||
<td class="line" data-line={startLine + index + 1} />
|
||||
<td class="line" data-line={startLine + index} />
|
||||
<td class="code">{line}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@ -77,11 +87,21 @@
|
||||
:global(td.line::before) {
|
||||
content: attr(data-line);
|
||||
color: var(--text-muted);
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
:global(td.code) {
|
||||
white-space: pre;
|
||||
padding-left: 1rem;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
&.collapseWhitespace {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
&.hideLineNumbers {
|
||||
:global(td.line::before) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -149,7 +149,7 @@
|
||||
export let highlights: string
|
||||
export let wrapLines: boolean = false
|
||||
export let selectedLines: LineOrPositionOrRange | null = null
|
||||
export let codeIntelAPI: CodeIntelAPI
|
||||
export let codeIntelAPI: CodeIntelAPI | null
|
||||
export let staticHighlightRanges: Range[] = []
|
||||
/**
|
||||
* The initial scroll position when the editor is first mounted.
|
||||
@ -187,31 +187,34 @@
|
||||
filePath: blobInfo.filePath,
|
||||
languages: blobInfo.languages,
|
||||
}
|
||||
$: codeIntelExtension = createCodeIntelExtension({
|
||||
api: {
|
||||
api: codeIntelAPI,
|
||||
documentInfo: documentInfo,
|
||||
goToDefinition: (view, definition, options) => goToDefinition(documentInfo, view, definition, options),
|
||||
openReferences,
|
||||
openImplementations,
|
||||
createTooltipView: options => new HovercardView(options.view, options.token, options.hovercardData),
|
||||
},
|
||||
// TODO(fkling): Support tooltip pinning
|
||||
pin: {},
|
||||
navigate: to => {
|
||||
if (typeof to === 'number') {
|
||||
if (to > 0) {
|
||||
history.forward()
|
||||
} else {
|
||||
history.back()
|
||||
}
|
||||
} else {
|
||||
goto(to.toString())
|
||||
}
|
||||
},
|
||||
})
|
||||
$: lineWrapping = wrapLines ? EditorView.lineWrapping : []
|
||||
$: syntaxHighlighting = highlights ? syntaxHighlight.of({ content: blobInfo.content, lsif: highlights }) : []
|
||||
$: codeIntelExtension = codeIntelAPI
|
||||
? createCodeIntelExtension({
|
||||
api: {
|
||||
api: codeIntelAPI,
|
||||
documentInfo: documentInfo,
|
||||
goToDefinition: (view, definition, options) =>
|
||||
goToDefinition(documentInfo, view, definition, options),
|
||||
openReferences,
|
||||
openImplementations,
|
||||
createTooltipView: options => new HovercardView(options.view, options.token, options.hovercardData),
|
||||
},
|
||||
// TODO(fkling): Support tooltip pinning
|
||||
pin: {},
|
||||
navigate: to => {
|
||||
if (typeof to === 'number') {
|
||||
if (to > 0) {
|
||||
history.forward()
|
||||
} else {
|
||||
history.back()
|
||||
}
|
||||
} else {
|
||||
goto(to.toString())
|
||||
}
|
||||
},
|
||||
})
|
||||
: null
|
||||
$: lineWrapping = wrapLines ? EditorView.lineWrapping : null
|
||||
$: syntaxHighlighting = highlights ? syntaxHighlight.of({ content: blobInfo.content, lsif: highlights }) : null
|
||||
$: staticHighlightExtension = staticHighlights(staticHighlightRanges)
|
||||
|
||||
$: blameColumnExtension = showBlame
|
||||
@ -225,7 +228,7 @@
|
||||
}
|
||||
},
|
||||
})
|
||||
: []
|
||||
: null
|
||||
$: blameDataExtension = blameDataFacet(blameData)
|
||||
|
||||
// Reinitialize the editor when its content changes. Update only the extensions when they change.
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
|
||||
export let margin: number
|
||||
|
||||
@ -41,6 +41,15 @@
|
||||
dispatch('more')
|
||||
}
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
// This premptively triggers a 'more' event when the scrollable content is smaller than than
|
||||
// scroller. Without this, the 'more' event would not be triggered because there is nothing
|
||||
// to scroll.
|
||||
if (scroller.scrollHeight <= scroller.clientHeight) {
|
||||
dispatch('more')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="viewport" bind:this={viewport}>
|
||||
|
||||
@ -35,5 +35,6 @@
|
||||
div {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tabs-header {
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<script lang="ts" context="module">
|
||||
import DismissibleAlert, { clearDismissedAlertsState_TEST_ONLY } from './DismissibleAlert.svelte'
|
||||
import { Story } from '@storybook/addon-svelte-csf'
|
||||
|
||||
export const meta = {
|
||||
component: DismissibleAlert,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let reset = 0
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
{#key reset}
|
||||
<DismissibleAlert partialStorageKey="a1" variant="info">This is an info alert</DismissibleAlert>
|
||||
<DismissibleAlert partialStorageKey="a2" variant="danger">This is a danger alert</DismissibleAlert>
|
||||
<DismissibleAlert partialStorageKey={null} variant="warning">This is a warning alert</DismissibleAlert>
|
||||
{/key}
|
||||
|
||||
<hr />
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
clearDismissedAlertsState_TEST_ONLY('a1', 'a2')
|
||||
reset += 1
|
||||
}}>Reset dismissed alerts</button
|
||||
>
|
||||
</Story>
|
||||
|
||||
<style lang="scss">
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
@ -10,6 +10,12 @@
|
||||
function storageKeyForPartial(partialStorageKey: string): string {
|
||||
return `DismissibleAlert/${partialStorageKey}/dismissed`
|
||||
}
|
||||
|
||||
export function clearDismissedAlertsState_TEST_ONLY(...partialStorageKeys: string[]): void {
|
||||
for (const partialStorageKey of partialStorageKeys) {
|
||||
localStorage.removeItem(storageKeyForPartial(partialStorageKey))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@ -35,17 +41,19 @@
|
||||
|
||||
{#if !dismissed}
|
||||
<Alert {variant} size="slim">
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if partialStorageKey}
|
||||
<div class="button-wrapper">
|
||||
<Button variant="icon" aria-label="Dismiss alert" on:click={handleDismissClick}>
|
||||
<Icon aria-hidden={true} svgPath={mdiClose} />
|
||||
</Button>
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if partialStorageKey}
|
||||
<div class="button-wrapper">
|
||||
<Button variant="icon" aria-label="Dismiss alert" on:click={handleDismissClick}>
|
||||
<Icon aria-hidden inline svgPath={mdiClose} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
@ -56,6 +64,12 @@
|
||||
line-height: (20/14);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
align-self: flex-start;
|
||||
color: var(--icon-color);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { mdiDotsHorizontal } from '@mdi/js'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { resolveRoute } from '$app/paths'
|
||||
import { overflow } from '$lib/dom'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import { DropdownMenu } from '$lib/wildcard'
|
||||
@ -9,17 +9,31 @@
|
||||
|
||||
import SidebarToggleButton from './SidebarToggleButton.svelte'
|
||||
import { sidebarOpen } from './stores'
|
||||
import { navFromPath } from './utils'
|
||||
|
||||
$: breadcrumbs = navFromPath($page.params.path, $page.params.repo)
|
||||
const TREE_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/tree/[...path]'
|
||||
const BLOB_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]'
|
||||
|
||||
export let repoName: string
|
||||
export let path: string
|
||||
export let hideSidebarToggle = false
|
||||
export let type: 'blob' | 'tree'
|
||||
|
||||
$: breadcrumbs = path.split('/')
|
||||
.map((part, index, all): [string, string] => [
|
||||
part,
|
||||
resolveRoute(
|
||||
type === 'tree' ? TREE_ROUTE_ID : BLOB_ROUTE_ID,
|
||||
{ repo: repoName, path: all.slice(0, index + 1).join('/') }),
|
||||
])
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<div class="toggle-wrapper" class:hidden={$sidebarOpen}>
|
||||
<div class="toggle-wrapper" class:hidden={hideSidebarToggle || $sidebarOpen}>
|
||||
<SidebarToggleButton />
|
||||
</div>
|
||||
<h2>
|
||||
{#each breadcrumbs as [name, path], index}
|
||||
{@const last = index === breadcrumbs.length - 1}
|
||||
<!--
|
||||
The elements are arranged like this because we want to
|
||||
ensure that the leading / before a segement always stay with
|
||||
@ -37,14 +51,16 @@
|
||||
at all.
|
||||
-->
|
||||
{' '}
|
||||
<span class:last={index === breadcrumbs.length - 1}>
|
||||
<span class:last>
|
||||
{#if index > 0}
|
||||
<span class="slash">/</span>
|
||||
{/if}
|
||||
{#if last}
|
||||
<slot name="icon" />
|
||||
{/if}
|
||||
{#if path}
|
||||
<a href={path}>{name}</a>
|
||||
{:else}
|
||||
<slot name="icon" />
|
||||
{name}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
@ -5,6 +5,7 @@ import { get, type Readable, readable } from 'svelte/store'
|
||||
|
||||
import { goto as svelteGoto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { toPrettyBlobURL } from '$lib/shared'
|
||||
import {
|
||||
positionToOffset,
|
||||
type Definition,
|
||||
@ -94,13 +95,18 @@ export async function goToDefinition(
|
||||
|
||||
export function openReferences(
|
||||
view: EditorView,
|
||||
_documentInfo: DocumentInfo,
|
||||
documentInfo: DocumentInfo,
|
||||
occurrence: Definition['occurrence']
|
||||
): void {
|
||||
const offset = positionToOffset(view.state.doc, occurrence.range.start)
|
||||
if (offset) {
|
||||
showTemporaryTooltip(view, 'Not supported yet: Find references', offset, 2000)
|
||||
}
|
||||
const url = toPrettyBlobURL({
|
||||
repoName: documentInfo.repoName,
|
||||
revision: documentInfo.revision,
|
||||
commitID: documentInfo.commitID,
|
||||
filePath: documentInfo.filePath,
|
||||
range: occurrence.range.withIncrementedValues(),
|
||||
viewState: 'references',
|
||||
})
|
||||
svelteGoto(url)
|
||||
}
|
||||
|
||||
export function openImplementations(
|
||||
|
||||
@ -1,33 +1,5 @@
|
||||
import { resolveRoute } from '$app/paths'
|
||||
|
||||
import type { ResolvedRevision } from '../../routes/[...repo=reporev]/+layout'
|
||||
|
||||
const TREE_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/tree/[...path]'
|
||||
|
||||
/**
|
||||
* Returns a [segment, url] mapping for every segement in `path`.
|
||||
* The URL for the last segment is empty.
|
||||
*
|
||||
* Example:
|
||||
* 'foo/bar/baz' converts to
|
||||
* [
|
||||
* ['foo', '/<repo>/-/tree/foo'],
|
||||
* ['bar', '/<repo>/-/tree/foo/bar'],
|
||||
* ['baz', '/<repo>/-/tree/foo/bar/baz'],
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
export function navFromPath(path: string, repo: string): [string, string][] {
|
||||
const parts = path.split('/')
|
||||
return parts
|
||||
.slice(0, -1)
|
||||
.map((part, index, all): [string, string] => [
|
||||
part,
|
||||
resolveRoute(TREE_ROUTE_ID, { repo, path: all.slice(0, index + 1).join('/') }),
|
||||
])
|
||||
.concat([[parts.at(-1) ?? '', '']])
|
||||
}
|
||||
|
||||
export function getRevisionLabel(
|
||||
urlRevision: string | undefined,
|
||||
resolvedRevision: ResolvedRevision | null
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
// We want to limit the number of imported modules as much as possible
|
||||
|
||||
export type { AbsoluteRepoFile } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
export { parseRepoRevision, buildSearchURLQuery, makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
export {
|
||||
parseRepoRevision,
|
||||
buildSearchURLQuery,
|
||||
makeRepoGitURI,
|
||||
toPrettyBlobURL,
|
||||
toRepoURL,
|
||||
type AbsoluteRepoFile,
|
||||
} from '@sourcegraph/shared/src/util/url'
|
||||
export {
|
||||
isCloneInProgressErrorLike,
|
||||
isRepoSeeOtherErrorLike,
|
||||
|
||||
@ -21,9 +21,6 @@
|
||||
--alert-content-padding: 0.5rem;
|
||||
--alert-background-color: var(--color-bg-1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--body-color);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte'
|
||||
|
||||
import { goto } from '$app/navigation'
|
||||
import { afterNavigate, goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { isErrorLike } from '$lib/common'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
@ -19,13 +19,14 @@
|
||||
import type { LayoutData, Snapshot } from './$types'
|
||||
import FileTree from './FileTree.svelte'
|
||||
import { createFileTreeStore } from './fileTreeStore'
|
||||
import { type GitHistory_HistoryConnection } from './layout.gql'
|
||||
|
||||
import type { GitHistory_HistoryConnection, RepoPage_ReferencesLocationConnection } from './layout.gql'
|
||||
import RepositoryRevPicker from './RepositoryRevPicker.svelte'
|
||||
import ReferencePanel from './ReferencePanel.svelte'
|
||||
|
||||
interface Capture {
|
||||
selectedTab: number | null
|
||||
historyPanel: HistoryCapture
|
||||
scrollTop: number
|
||||
}
|
||||
|
||||
export let data: LayoutData
|
||||
@ -35,17 +36,12 @@
|
||||
return {
|
||||
selectedTab,
|
||||
historyPanel: historyPanel?.capture(),
|
||||
// This works because this specific page is fully scrollable
|
||||
scrollTop: window.scrollY,
|
||||
}
|
||||
},
|
||||
async restore(data) {
|
||||
selectedTab = data.selectedTab
|
||||
// Wait until DOM was updated
|
||||
// Wait until DOM was updated to possibly show the history panel
|
||||
await tick()
|
||||
// `restore` is called before `afterNavigate`, which resets the scroll position
|
||||
// Restore the scroll position after the componentent was updated
|
||||
window.scrollTo(0, data.scrollTop)
|
||||
|
||||
// Restore history panel state if it is open
|
||||
if (data.historyPanel) {
|
||||
@ -67,6 +63,7 @@
|
||||
let selectedTab: number | null = null
|
||||
let historyPanel: HistoryPanel
|
||||
let commitHistory: GitHistory_HistoryConnection | null
|
||||
let references: RepoPage_ReferencesLocationConnection | null
|
||||
let lastCommit: LastCommitFragment | null
|
||||
|
||||
$: ({ revision = '', parentPath, repoName, resolvedRevision } = data)
|
||||
@ -92,6 +89,20 @@
|
||||
|
||||
const sidebarSize = getSeparatorPosition('repo-sidebar', 0.2)
|
||||
$: sidebarWidth = `max(320px, ${$sidebarSize * 100}%)`
|
||||
|
||||
// The observable query to fetch references (due to infinite scrolling)
|
||||
$: referenceQuery = data.references
|
||||
$: references = $referenceQuery?.data?.repository?.commit?.blob?.lsif?.references ?? null
|
||||
$: referencesLoading = ((referenceQuery && !references) || $referenceQuery?.fetching) ?? false
|
||||
|
||||
afterNavigate(() => {
|
||||
if (!!data.references) {
|
||||
references = null
|
||||
selectedTab = 1
|
||||
} else if (selectedTab === 1) {
|
||||
selectedTab = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@ -140,9 +151,18 @@
|
||||
/>
|
||||
{/key}
|
||||
</TabPanel>
|
||||
<TabPanel title="References">
|
||||
<ReferencePanel
|
||||
connection={references}
|
||||
loading={referencesLoading}
|
||||
on:more={referenceQuery?.fetchMore}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
{#if lastCommit && selectedTab === null}
|
||||
<LastCommit {lastCommit} />
|
||||
{#if lastCommit}
|
||||
<div class="last-commit">
|
||||
<LastCommit {lastCommit} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@ -201,29 +221,29 @@
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: var(--code-bg);
|
||||
--align-tabs: flex-start;
|
||||
box-shadow: var(--bottom-panel-shadow);
|
||||
max-height: 50vh;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
padding-right: 0.5rem;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.tabs) {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
box-shadow: var(--bottom-panel-shadow);
|
||||
background-color: var(--code-bg);
|
||||
|
||||
:global(.tabs-header) {
|
||||
:global([data-tab-header]) {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
&.open {
|
||||
height: 30vh;
|
||||
// Disable flex layout so that tabs simply fill the available space
|
||||
display: block;
|
||||
|
||||
.last-commit {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,6 +2,7 @@ import { dirname } from 'path'
|
||||
|
||||
import { from } from 'rxjs'
|
||||
|
||||
import { SourcegraphURL } from '$lib/common'
|
||||
import { getGraphQLClient, infinityQuery, mapOrThrow } from '$lib/graphql'
|
||||
import { GitRefType } from '$lib/graphql-types'
|
||||
import { fetchSidebarFileTree } from '$lib/repo/api/tree'
|
||||
@ -9,15 +10,25 @@ import { resolveRevision } from '$lib/repo/utils'
|
||||
import { parseRepoRevision } from '$lib/shared'
|
||||
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { GitHistoryQuery, LastCommitQuery, RepositoryGitCommits, RepositoryGitRefs } from './layout.gql'
|
||||
import {
|
||||
GitHistoryQuery,
|
||||
LastCommitQuery,
|
||||
RepositoryGitCommits,
|
||||
RepositoryGitRefs,
|
||||
RepoPage_PreciseCodeIntel,
|
||||
} from './layout.gql'
|
||||
|
||||
const HISTORY_COMMITS_PER_PAGE = 20
|
||||
const REFERENCES_PER_PAGE = 20
|
||||
|
||||
export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
export const load: LayoutLoad = async ({ parent, params, url }) => {
|
||||
const client = getGraphQLClient()
|
||||
const { repoName, revision = '' } = parseRepoRevision(params.repo)
|
||||
const parentPath = params.path ? dirname(params.path) : ''
|
||||
const resolvedRevision = resolveRevision(parent, revision)
|
||||
const sgURL = SourcegraphURL.from(url)
|
||||
const lineOrPosition = sgURL.lineRange
|
||||
const view = sgURL.viewState
|
||||
|
||||
// Prefetch the sidebar file tree for the parent path.
|
||||
// (we don't want to wait for the file tree to execute the query)
|
||||
@ -85,6 +96,66 @@ export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
},
|
||||
}),
|
||||
|
||||
references:
|
||||
view === 'references' && lineOrPosition?.line && lineOrPosition?.character
|
||||
? infinityQuery({
|
||||
client,
|
||||
query: RepoPage_PreciseCodeIntel,
|
||||
variables: from(
|
||||
resolvedRevision.then(revspec => ({
|
||||
repoName,
|
||||
revspec,
|
||||
filePath: params.path ?? '',
|
||||
first: REFERENCES_PER_PAGE,
|
||||
// Line and character are 1-indexed, but the API expects 0-indexed
|
||||
line: lineOrPosition.line - 1,
|
||||
character: lineOrPosition.character! - 1,
|
||||
afterCursor: null as string | null,
|
||||
}))
|
||||
),
|
||||
nextVariables: previousResult => {
|
||||
if (previousResult?.data?.repository?.commit?.blob?.lsif?.references.pageInfo.hasNextPage) {
|
||||
return {
|
||||
afterCursor:
|
||||
previousResult.data.repository.commit.blob.lsif.references.pageInfo.endCursor,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
combine: (previousResult, nextResult) => {
|
||||
if (!nextResult.data?.repository?.commit?.blob?.lsif) {
|
||||
return nextResult
|
||||
}
|
||||
|
||||
const previousNodes =
|
||||
previousResult.data?.repository?.commit?.blob?.lsif?.references?.nodes ?? []
|
||||
const nextNodes = nextResult.data?.repository?.commit?.blob?.lsif?.references?.nodes ?? []
|
||||
|
||||
return {
|
||||
...nextResult,
|
||||
data: {
|
||||
repository: {
|
||||
...nextResult.data.repository,
|
||||
commit: {
|
||||
...nextResult.data.repository.commit,
|
||||
blob: {
|
||||
...nextResult.data.repository.commit.blob,
|
||||
lsif: {
|
||||
...nextResult.data.repository.commit.blob.lsif,
|
||||
references: {
|
||||
...nextResult.data.repository.commit.blob.lsif.references,
|
||||
nodes: [...previousNodes, ...nextNodes],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
: null,
|
||||
|
||||
// Repository pickers queries (branch, tags and commits)
|
||||
getRepoBranches: (searchTerm: string) =>
|
||||
getGraphQLClient()
|
||||
|
||||
@ -32,6 +32,11 @@
|
||||
|
||||
export let data: PageData
|
||||
|
||||
// The following props control the look and file of the file page when used
|
||||
// in a preview context.
|
||||
export let embedded = false
|
||||
export let disableCodeIntel = embedded
|
||||
|
||||
export const snapshot: Snapshot<ScrollSnapshot | null> = {
|
||||
capture() {
|
||||
return cmblob?.getScrollSnapshot() ?? null
|
||||
@ -64,7 +69,7 @@
|
||||
$: if (!$combinedBlobData.blobPending) {
|
||||
blob = $combinedBlobData.blob
|
||||
highlights = $combinedBlobData.highlights
|
||||
selectedPosition = SourcegraphURL.from($page.url).lineRange
|
||||
selectedPosition = data.lineOrPosition
|
||||
}
|
||||
$: fileNotFound = $combinedBlobData.blobPending ? null : !$combinedBlobData.blob
|
||||
$: fileLoadingError = $combinedBlobData.blobPending ? null : !$combinedBlobData.blob && $combinedBlobData.blobError
|
||||
@ -74,12 +79,14 @@
|
||||
$: showBlame = viewMode === ViewMode.Blame
|
||||
$: showFormatted = isFormatted && viewMode === ViewMode.Default && !showBlame
|
||||
|
||||
$: codeIntelAPI = createCodeIntelAPI({
|
||||
settings: setting => (isErrorLike(settings?.final) ? undefined : settings?.final?.[setting]),
|
||||
requestGraphQL(options) {
|
||||
return from(graphQLClient.query(options.request, options.variables).then(toGraphQLResult))
|
||||
},
|
||||
})
|
||||
$: codeIntelAPI = disableCodeIntel
|
||||
? null
|
||||
: createCodeIntelAPI({
|
||||
settings: setting => (isErrorLike(settings?.final) ? undefined : settings?.final?.[setting]),
|
||||
requestGraphQL(options) {
|
||||
return from(graphQLClient.query(options.request, options.variables).then(toGraphQLResult))
|
||||
},
|
||||
})
|
||||
|
||||
afterNavigate(event => {
|
||||
// Only restore scroll position when the user used the browser history to navigate back
|
||||
@ -118,14 +125,21 @@
|
||||
|
||||
<!-- Note: Splitting this at this level is not great but Svelte doesn't allow to conditionally render slots (yet) -->
|
||||
{#if data.compare}
|
||||
<FileHeader>
|
||||
<FileHeader type="blob" {repoName} path={filePath}>
|
||||
<FileIcon slot="icon" file={blob} inline />
|
||||
<svelte:fragment slot="actions">
|
||||
<span>{data.compare.revisionToCompare}</span>
|
||||
</svelte:fragment>
|
||||
</FileHeader>
|
||||
{:else if embedded}
|
||||
<FileHeader type="blob" {repoName} path={filePath} hideSidebarToggle>
|
||||
<FileIcon slot="icon" file={blob} inline />
|
||||
<svelte:fragment slot="actions">
|
||||
<slot name="actions" />
|
||||
</svelte:fragment>
|
||||
</FileHeader>
|
||||
{:else}
|
||||
<FileHeader>
|
||||
<FileHeader type="blob" {repoName} path={filePath}>
|
||||
<FileIcon slot="icon" file={blob} inline />
|
||||
<svelte:fragment slot="actions">
|
||||
{#await data.externalServiceType then externalServiceType}
|
||||
@ -153,7 +167,7 @@
|
||||
</FileHeader>
|
||||
{/if}
|
||||
|
||||
{#if blob && !blob.binary && !data.compare}
|
||||
{#if blob && !blob.binary && !data.compare && !embedded}
|
||||
<div class="file-info">
|
||||
<FileViewModeSwitcher
|
||||
aria-label="View mode"
|
||||
|
||||
@ -2,6 +2,7 @@ import { BehaviorSubject, concatMap, from, map } from 'rxjs'
|
||||
|
||||
import { fetchBlameHunksMemoized, type BlameHunkData } from '@sourcegraph/web/src/repo/blame/shared'
|
||||
|
||||
import { SourcegraphURL } from '$lib/common'
|
||||
import { getGraphQLClient, mapOrThrow } from '$lib/graphql'
|
||||
import { resolveRevision } from '$lib/repo/utils'
|
||||
import { parseRepoRevision } from '$lib/shared'
|
||||
@ -15,6 +16,7 @@ export const load: PageLoad = ({ parent, params, url }) => {
|
||||
const { repoName, revision = '' } = parseRepoRevision(params.repo)
|
||||
const resolvedRevision = resolveRevision(parent, revision)
|
||||
const isBlame = url.searchParams.get('view') === 'blame'
|
||||
const lineOrPosition = SourcegraphURL.from(url).lineRange
|
||||
|
||||
// Create a BehaviorSubject so preloading does not create a subscriberless observable
|
||||
const blameData = new BehaviorSubject<BlameHunkData>({ current: undefined, externalURLs: undefined })
|
||||
@ -41,6 +43,7 @@ export const load: PageLoad = ({ parent, params, url }) => {
|
||||
|
||||
return {
|
||||
graphQLClient: client,
|
||||
lineOrPosition,
|
||||
filePath: params.path,
|
||||
blob: resolvedRevision
|
||||
.then(resolvedRevision =>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<title>{data.filePath} - {data.displayRepoName} - Sourcegraph</title>
|
||||
</svelte:head>
|
||||
|
||||
<FileHeader>
|
||||
<FileHeader type="tree" repoName={data.repoName} path={data.filePath}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Permalink commitID={data.resolvedRevision.commitID} />
|
||||
</svelte:fragment>
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { preloadData } from '$app/navigation'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
import { Alert, Button } from '$lib/wildcard'
|
||||
import FilePage from './-/blob/[...path]/+page.svelte'
|
||||
import type { PageData } from './-/blob/[...path]/$types'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import { mdiClose } from '@mdi/js'
|
||||
import { createPromiseStore } from '$lib/utils'
|
||||
|
||||
/**
|
||||
* The URL of the file to preview.
|
||||
*/
|
||||
export let href: string
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: void }>()
|
||||
const filePageData = createPromiseStore<PageData>()
|
||||
$: filePageData.set(
|
||||
preloadData(href).then(result => {
|
||||
if (result.type === 'loaded' && result.status === 200) {
|
||||
return result.data as PageData
|
||||
}
|
||||
throw new Error(`Unable to load file preview.`)
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class:center={$filePageData.pending || $filePageData.error}>
|
||||
{#if $filePageData.pending}
|
||||
<LoadingSpinner />
|
||||
{:else if $filePageData.error}
|
||||
<Alert variant="danger">
|
||||
{$filePageData.error.message}
|
||||
<br />
|
||||
<a {href}>Open file directly</a>
|
||||
</Alert>
|
||||
{:else if $filePageData.value}
|
||||
<FilePage data={$filePageData.value} embedded>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button variant="icon" aria-label="Close preview" on:click={() => dispatch('close')}>
|
||||
<Icon svgPath={mdiClose} aria-hidden inline />
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</FilePage>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
&.center {
|
||||
padding: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,27 @@
|
||||
fragment ReferencePanel_LocationConnection on LocationConnection {
|
||||
nodes {
|
||||
...ReferencePanel_Location
|
||||
}
|
||||
}
|
||||
|
||||
fragment ReferencePanel_Location on Location {
|
||||
...ReferencePanelCodeExcerpt_Location
|
||||
canonicalURL
|
||||
resource {
|
||||
name
|
||||
repository {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
range {
|
||||
start {
|
||||
line
|
||||
character
|
||||
}
|
||||
end {
|
||||
line
|
||||
character
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
<script lang="ts">
|
||||
import type { ReferencePanel_LocationConnection, ReferencePanel_Location } from './ReferencePanel.gql'
|
||||
import Scroller from '$lib/Scroller.svelte'
|
||||
import ReferencePanelCodeExcerpt from './ReferencePanelCodeExcerpt.svelte'
|
||||
import Tooltip from '$lib/Tooltip.svelte'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
import PanelResizeHandle from '$lib/wildcard/resizable-panel/PanelResizeHandle.svelte'
|
||||
import PanelGroup from '$lib/wildcard/resizable-panel/PanelGroup.svelte'
|
||||
import Panel from '$lib/wildcard/resizable-panel/Panel.svelte'
|
||||
import { SourcegraphURL } from '$lib/common'
|
||||
import FilePreview from './FilePreview.svelte'
|
||||
import { Alert } from '$lib/wildcard'
|
||||
|
||||
export let connection: ReferencePanel_LocationConnection | null
|
||||
export let loading: boolean
|
||||
|
||||
// It appears that the backend returns duplicate locations. We need to filter them out.
|
||||
function unique(locations: ReferencePanel_Location[]): ReferencePanel_Location[] {
|
||||
const seen = new Set<string>()
|
||||
return locations.filter(location => {
|
||||
const key = location.canonicalURL
|
||||
if (seen.has(key)) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function getPreviewURL(location: ReferencePanel_Location) {
|
||||
const url = SourcegraphURL.from(location.canonicalURL)
|
||||
if (location.range) {
|
||||
url.setLineRange({
|
||||
line: location.range.start.line + 1,
|
||||
character: location.range.start.character + 1,
|
||||
})
|
||||
}
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
let selectedLocation: ReferencePanel_Location | null = null
|
||||
|
||||
$: previewURL = selectedLocation ? getPreviewURL(selectedLocation) : null
|
||||
$: locations = connection ? unique(connection.nodes) : []
|
||||
$: showUsageInfo = !connection && !loading
|
||||
$: showNoReferencesInfo = !loading && locations.length === 0
|
||||
</script>
|
||||
|
||||
<div class="root" class:show-info={showUsageInfo || showNoReferencesInfo}>
|
||||
{#if showUsageInfo}
|
||||
<Alert variant="info">Hover over a symbol and click "Find references" to find references to the symbol.</Alert>
|
||||
{:else if showNoReferencesInfo}
|
||||
<Alert variant="info">No references found.</Alert>
|
||||
{:else}
|
||||
<PanelGroup id="references">
|
||||
<Panel id="references-list">
|
||||
<Scroller margin={600} on:more>
|
||||
<ul>
|
||||
{#each locations as location (location.canonicalURL)}
|
||||
{@const selected = selectedLocation?.canonicalURL === location.canonicalURL}
|
||||
<!-- todo(fkling): Implement a11y concepts. What to do exactly depends on whether
|
||||
we'll keep the preview panel or not. -->
|
||||
<li
|
||||
class="location"
|
||||
class:selected
|
||||
on:click={() => selectedLocation = selected ? null : location}
|
||||
>
|
||||
<span class="code-file">
|
||||
<span class="code">
|
||||
<ReferencePanelCodeExcerpt {location} />
|
||||
</span>
|
||||
<span class="file">
|
||||
<Tooltip tooltip={location.resource.path}>
|
||||
<span>{location.resource.name}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
{#if location.range}
|
||||
<span class="range"
|
||||
>:{location.range.start.line + 1}:{location.range.start.character + 1}</span
|
||||
>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if loading}
|
||||
<LoadingSpinner center />
|
||||
{/if}
|
||||
</Scroller>
|
||||
</Panel>
|
||||
{#if previewURL}
|
||||
<PanelResizeHandle />
|
||||
<Panel defaultSize={50}>
|
||||
<FilePreview href={previewURL} on:close={() => (selectedLocation = null)} />
|
||||
</Panel>
|
||||
{/if}
|
||||
</PanelGroup>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
height: 100%;
|
||||
|
||||
&.show-info {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
}
|
||||
|
||||
li {
|
||||
display: grid;
|
||||
grid-column: span 2;
|
||||
grid-template-columns: subgrid;
|
||||
color: inherit;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
}
|
||||
|
||||
li + li {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.code-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file {
|
||||
text-align: right;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.range {
|
||||
color: var(--oc-violet-6);
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,23 @@
|
||||
fragment ReferencePanelCodeExcerpt_Location on Location {
|
||||
resource {
|
||||
content
|
||||
path
|
||||
commit {
|
||||
oid
|
||||
}
|
||||
repository {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
range {
|
||||
start {
|
||||
line
|
||||
character
|
||||
}
|
||||
end {
|
||||
line
|
||||
character
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
<script lang="ts" context="module">
|
||||
// Multiple locations will point to the same file, we only need to compute the
|
||||
// lines once.
|
||||
const lineCache = new Map<string, readonly string[]>()
|
||||
|
||||
function getLines(resource: ReferencePanelCodeExcerpt_Location['resource']): readonly string[] {
|
||||
const key = `${resource.repository.id}:${resource.commit.oid}:${resource.path}`
|
||||
if (lineCache.has(key)) {
|
||||
return lineCache.get(key)!
|
||||
}
|
||||
const lines = resource.content.split(/\r?\n/)
|
||||
lineCache.set(key, lines)
|
||||
return lines
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { derived, readable } from 'svelte/store'
|
||||
import { observeIntersection } from '$lib/intersection-observer'
|
||||
import CodeExcerpt from '$lib/CodeExcerpt.svelte'
|
||||
import { fetchFileRangeMatches } from '$lib/search/api/highlighting'
|
||||
import { toReadable } from '$lib/utils'
|
||||
|
||||
import type { ReferencePanelCodeExcerpt_Location } from './ReferencePanelCodeExcerpt.gql'
|
||||
|
||||
export let location: ReferencePanelCodeExcerpt_Location
|
||||
|
||||
$: plaintextLines = location.range ? getLines(location.resource).slice(location.range.start.line, location.range.end.line + 1) : []
|
||||
$: matches = location.range ? [{
|
||||
startLine: location.range.start.line,
|
||||
endLine: location.range.end.line,
|
||||
startCharacter: location.range.start.character,
|
||||
endCharacter: location.range.end.character,
|
||||
}] : []
|
||||
|
||||
let visible = false
|
||||
// We rely on fetchFileRangeMatches to cache the result for us so that repeated
|
||||
// calls will not result in repeated network requests.
|
||||
$: highlightedHTMLRows = visible && location.range ? derived(toReadable(fetchFileRangeMatches({
|
||||
result: {
|
||||
repository: location.resource.repository.name,
|
||||
commit: location.resource.commit.oid,
|
||||
path: location.resource.path,
|
||||
},
|
||||
ranges: [{ startLine: location.range.start.line, endLine: location.range.end.line +1 }],
|
||||
})), result => result.value?.[0] || []) : readable([])
|
||||
</script>
|
||||
|
||||
{#if location.range && plaintextLines.length > 0}
|
||||
<div use:observeIntersection on:intersecting={event => (visible = visible || event.detail)}>
|
||||
<CodeExcerpt
|
||||
collapseWhitespace
|
||||
hideLineNumbers
|
||||
startLine={location.range.start.line}
|
||||
{plaintextLines}
|
||||
{matches}
|
||||
highlightedHTMLRows={$highlightedHTMLRows}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<pre>(no content information)</pre>
|
||||
{/if}
|
||||
@ -51,3 +51,36 @@ fragment GitHistory_HistoryConnection on GitCommitConnection {
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
|
||||
query RepoPage_PreciseCodeIntel(
|
||||
$repoName: String!
|
||||
$revspec: String!
|
||||
$filePath: String!
|
||||
$line: Int!
|
||||
$character: Int!
|
||||
$first: Int
|
||||
$afterCursor: String
|
||||
) {
|
||||
repository(name: $repoName) {
|
||||
id
|
||||
commit(rev: $revspec) {
|
||||
id
|
||||
blob(path: $filePath) {
|
||||
canonicalURL
|
||||
lsif {
|
||||
references(line: $line, character: $character, first: $first, after: $afterCursor) {
|
||||
...RepoPage_ReferencesLocationConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment RepoPage_ReferencesLocationConnection on LocationConnection {
|
||||
...ReferencePanel_LocationConnection
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
import { observeIntersection } from '$lib/intersection-observer'
|
||||
import RepoStars from '$lib/repo/RepoStars.svelte'
|
||||
import { fetchFileRangeMatches } from '$lib/search/api/highlighting'
|
||||
import CodeExcerpt from '$lib/search/CodeExcerpt.svelte'
|
||||
import CodeExcerpt from '$lib/CodeExcerpt.svelte'
|
||||
import { rankContentMatch } from '$lib/search/results'
|
||||
import { getFileMatchUrl, type ContentMatch, rankByLine, rankPassthrough } from '$lib/shared'
|
||||
import { settings } from '$lib/stores'
|
||||
@ -96,7 +96,7 @@
|
||||
-->
|
||||
{#await highlightedHTMLRows}
|
||||
<CodeExcerpt
|
||||
startLine={group.startLine}
|
||||
startLine={group.startLine + 1}
|
||||
matches={group.matches}
|
||||
plaintextLines={group.plaintextLines}
|
||||
--background-color="transparent"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<script lang="ts">
|
||||
import { observeIntersection } from '$lib/intersection-observer'
|
||||
import { fetchFileRangeMatches } from '$lib/search/api/highlighting'
|
||||
import CodeExcerpt from '$lib/search/CodeExcerpt.svelte'
|
||||
import CodeExcerpt from '$lib/CodeExcerpt.svelte'
|
||||
import SymbolKind from '$lib/search/SymbolKind.svelte'
|
||||
import type { SymbolMatch } from '$lib/shared'
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
{#await highlightedHTMLRows then result}
|
||||
<CodeExcerpt
|
||||
startLine={symbol.line - 1}
|
||||
startLine={symbol.line}
|
||||
plaintextLines={['']}
|
||||
highlightedHTMLRows={result?.[index]}
|
||||
--background-color="transparent"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user