mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:12:02 +00:00
Svelte: implement new reference panel against new Usages API (#63724)
This implements a first pass at the new references panel built against the new usages API.
This commit is contained in:
parent
f657f99a62
commit
f7511c4a59
@ -1,7 +1,10 @@
|
||||
import type { Preview } from '@storybook/svelte'
|
||||
import { initialize, mswLoader } from 'msw-storybook-addon'
|
||||
|
||||
// Global imports kept in sync with routes/+layout.svelte
|
||||
import '../src/routes/styles.scss'
|
||||
import '@fontsource-variable/roboto-mono'
|
||||
import '@fontsource-variable/inter'
|
||||
|
||||
// Initialize MSW
|
||||
initialize()
|
||||
|
||||
13
client/web-sveltekit/assets/icons/symbols.svg
Normal file
13
client/web-sveltekit/assets/icons/symbols.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id=".Icon / Symbols" clip-path="url(#clip0_4465_142348)">
|
||||
<path id="Vector" d="M1.75 0.75H8.75C8.75 0.75 9.75 0.75 9.75 1.75V8.75C9.75 8.75 9.75 9.75 8.75 9.75H1.75C1.75 9.75 0.75 9.75 0.75 8.75V1.75C0.75 1.75 0.75 0.75 1.75 0.75Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path id="Vector_2" d="M14.25 5.25C14.25 5.84095 14.3664 6.42611 14.5925 6.97208C14.8187 7.51804 15.1502 8.01412 15.568 8.43198C15.9859 8.84984 16.482 9.18131 17.0279 9.40746C17.5739 9.6336 18.1591 9.75 18.75 9.75C19.3409 9.75 19.9261 9.6336 20.4721 9.40746C21.018 9.18131 21.5141 8.84984 21.932 8.43198C22.3498 8.01412 22.6813 7.51804 22.9075 6.97208C23.1336 6.42611 23.25 5.84095 23.25 5.25C23.25 4.65905 23.1336 4.07389 22.9075 3.52792C22.6813 2.98196 22.3498 2.48588 21.932 2.06802C21.5141 1.65016 21.018 1.31869 20.4721 1.09254C19.9261 0.866396 19.3409 0.75 18.75 0.75C18.1591 0.75 17.5739 0.866396 17.0279 1.09254C16.482 1.31869 15.9859 1.65016 15.568 2.06802C15.1502 2.48588 14.8187 2.98196 14.5925 3.52792C14.3664 4.07389 14.25 4.65905 14.25 5.25Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path id="Vector_3" d="M18.7859 13.9771C18.7111 13.8333 18.5982 13.7127 18.4596 13.6286C18.321 13.5445 18.162 13.5 17.9999 13.5C17.8378 13.5 17.6787 13.5445 17.5401 13.6286C17.4016 13.7127 17.2887 13.8333 17.2139 13.9771L12.8769 21.7841C12.7948 21.9334 12.7512 22.1008 12.75 22.2712C12.7488 22.4416 12.79 22.6096 12.8699 22.7601C12.9452 22.906 13.0587 23.0287 13.1984 23.1151C13.3381 23.2014 13.4987 23.2481 13.6629 23.2501H22.3369C22.5011 23.2481 22.6616 23.2014 22.8013 23.1151C22.941 23.0287 23.0546 22.906 23.1299 22.7601C23.2098 22.6096 23.251 22.4416 23.2497 22.2712C23.2485 22.1008 23.2049 21.9334 23.1229 21.7841L18.7859 13.9771Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path id="Vector_4" d="M5.85108 13.0347C5.77808 12.9463 5.68644 12.8751 5.58272 12.8262C5.47899 12.7773 5.36574 12.752 5.25108 12.752C5.13641 12.752 5.02316 12.7773 4.91944 12.8262C4.81571 12.8751 4.72407 12.9463 4.65108 13.0347L0.941077 17.4717C0.817448 17.6203 0.749756 17.8074 0.749756 18.0007C0.749756 18.194 0.817448 18.3811 0.941077 18.5297L4.64908 22.9667C4.72207 23.0551 4.81371 23.1263 4.91744 23.1752C5.02116 23.2241 5.13441 23.2494 5.24908 23.2494C5.36374 23.2494 5.47699 23.2241 5.58072 23.1752C5.68444 23.1263 5.77608 23.0551 5.84908 22.9667L9.55708 18.5297C9.68071 18.3811 9.7484 18.194 9.7484 18.0007C9.7484 17.8074 9.68071 17.6203 9.55708 17.4717L5.85108 13.0347Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4465_142348">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
18
client/web-sveltekit/src/auto-imports.d.ts
vendored
18
client/web-sveltekit/src/auto-imports.d.ts
vendored
@ -10,13 +10,17 @@ declare global {
|
||||
const ILucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default']
|
||||
const ILucideArchive: typeof import('~icons/lucide/archive')['default']
|
||||
const ILucideArrowDownFromLine: typeof import('~icons/lucide/arrow-down-from-line')['default']
|
||||
const ILucideArrowLeftFromLine: typeof import('~icons/lucide/arrow-left-from-line')['default']
|
||||
const ILucideArrowRight: typeof import('~icons/lucide/arrow-right')['default']
|
||||
const ILucideArrowRightFromLine: typeof import('~icons/lucide/arrow-right-from-line')['default']
|
||||
const ILucideBarChartBig: typeof import('~icons/lucide/bar-chart-big')['default']
|
||||
const ILucideBookOpen: typeof import('~icons/lucide/book-open')['default']
|
||||
const ILucideBookX: typeof import('~icons/lucide/book-x')['default']
|
||||
const ILucideBraces: typeof import('~icons/lucide/braces')['default']
|
||||
const ILucideBrackets: typeof import('~icons/lucide/brackets')['default']
|
||||
const ILucideBrainCircuit: typeof import('~icons/lucide/brain-circuit')['default']
|
||||
const ILucideCaseSensitive: typeof import('~icons/lucide/case-sensitive')['default']
|
||||
const ILucideCheck: typeof import('~icons/lucide/check')['default']
|
||||
const ILucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
|
||||
const ILucideChevronFirst: typeof import('~icons/lucide/chevron-first')['default']
|
||||
const ILucideChevronLast: typeof import('~icons/lucide/chevron-last')['default']
|
||||
@ -29,6 +33,7 @@ declare global {
|
||||
const ILucideCodesandbox: typeof import('~icons/lucide/codesandbox')['default']
|
||||
const ILucideCopy: typeof import('~icons/lucide/copy')['default']
|
||||
const ILucideCornerRightDown: typeof import('~icons/lucide/corner-right-down')['default']
|
||||
const ILucideCornerRightUp: typeof import('~icons/lucide/corner-right-up')['default']
|
||||
const ILucideDatabase: typeof import('~icons/lucide/database')['default']
|
||||
const ILucideDiff: typeof import('~icons/lucide/diff')['default']
|
||||
const ILucideEarth: typeof import('~icons/lucide/earth')['default']
|
||||
@ -38,6 +43,7 @@ declare global {
|
||||
const ILucideFile: typeof import('~icons/lucide/file')['default']
|
||||
const ILucideFileCode: typeof import('~icons/lucide/file-code')['default']
|
||||
const ILucideFileJson: typeof import('~icons/lucide/file-json')['default']
|
||||
const ILucideFileSearch2: typeof import('~icons/lucide/file-search2')['default']
|
||||
const ILucideFileStack: typeof import('~icons/lucide/file-stack')['default']
|
||||
const ILucideFileTerminal: typeof import('~icons/lucide/file-terminal')['default']
|
||||
const ILucideFileText: typeof import('~icons/lucide/file-text')['default']
|
||||
@ -48,6 +54,7 @@ declare global {
|
||||
const ILucideFolderGit2: typeof import('~icons/lucide/folder-git2')['default']
|
||||
const ILucideFolderOpen: typeof import('~icons/lucide/folder-open')['default']
|
||||
const ILucideFolderUp: typeof import('~icons/lucide/folder-up')['default']
|
||||
const ILucideFullscreen: typeof import('~icons/lucide/fullscreen')['default']
|
||||
const ILucideGitBranch: typeof import('~icons/lucide/git-branch')['default']
|
||||
const ILucideGitCommitVertical: typeof import('~icons/lucide/git-commit-vertical')['default']
|
||||
const ILucideGitCompare: typeof import('~icons/lucide/git-compare')['default']
|
||||
@ -61,6 +68,7 @@ declare global {
|
||||
const ILucideLock: typeof import('~icons/lucide/lock')['default']
|
||||
const ILucideMenu: typeof import('~icons/lucide/menu')['default']
|
||||
const ILucideOctagonX: typeof import('~icons/lucide/octagon-x')['default']
|
||||
const ILucidePanelBottomClose: typeof import('~icons/lucide/panel-bottom-close')['default']
|
||||
const ILucidePanelLeftClose: typeof import('~icons/lucide/panel-left-close')['default']
|
||||
const ILucidePanelLeftOpen: typeof import('~icons/lucide/panel-left-open')['default']
|
||||
const ILucidePencil: typeof import('~icons/lucide/pencil')['default']
|
||||
@ -69,9 +77,11 @@ declare global {
|
||||
const ILucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
const ILucideSearchX: typeof import('~icons/lucide/search-x')['default']
|
||||
const ILucideSettings: typeof import('~icons/lucide/settings')['default']
|
||||
const ILucideSpline: typeof import('~icons/lucide/spline')['default']
|
||||
const ILucideSquareFunction: typeof import('~icons/lucide/square-function')['default']
|
||||
const ILucideSquareSlash: typeof import('~icons/lucide/square-slash')['default']
|
||||
const ILucideStar: typeof import('~icons/lucide/star')['default']
|
||||
const ILucideSymbols: typeof import('~icons/lucide/symbols')['default']
|
||||
const ILucideTag: typeof import('~icons/lucide/tag')['default']
|
||||
const ILucideText: typeof import('~icons/lucide/text')['default']
|
||||
const ILucideUser: typeof import('~icons/lucide/user')['default']
|
||||
@ -83,12 +93,19 @@ declare global {
|
||||
const IPhFileJpgLight: typeof import('~icons/ph/file-jpg-light')['default']
|
||||
const IPhFilePngLight: typeof import('~icons/ph/file-png-light')['default']
|
||||
const IPhGifFill: typeof import('~icons/ph/gif-fill')['default']
|
||||
const IPhJpgLight: typeof import('~icons/ph/jpg-light')['default']
|
||||
const IPhPngLight: typeof import('~icons/ph/png-light')['default']
|
||||
const IPhPnglight: typeof import('~icons/ph/pnglight')['default']
|
||||
const IPhosphorPngLight: typeof import('~icons/ph/osphor-png-light')['default']
|
||||
const IPhosphorePngLight: typeof import('~icons/ph/osphore-png-light')['default']
|
||||
const ISgBatchChanges: typeof import('~icons/sg/batch-changes')['default']
|
||||
const ISgCody: typeof import('~icons/sg/cody')['default']
|
||||
const ISgMark: typeof import('~icons/sg/mark')['default']
|
||||
const ISgSymbols: typeof import('~icons/sg/symbols')['default']
|
||||
const ISimpleIconsApachegroovy: typeof import('~icons/simple-icons/apachegroovy')['default']
|
||||
const ISimpleIconsBitbucket: typeof import('~icons/simple-icons/bitbucket')['default']
|
||||
const ISimpleIconsC: typeof import('~icons/simple-icons/c')['default']
|
||||
const ISimpleIconsCSS3: typeof import('~icons/simple-icons/c-s-s3')['default']
|
||||
const ISimpleIconsClojure: typeof import('~icons/simple-icons/clojure')['default']
|
||||
const ISimpleIconsCmake: typeof import('~icons/simple-icons/cmake')['default']
|
||||
const ISimpleIconsCoffeescript: typeof import('~icons/simple-icons/coffeescript')['default']
|
||||
@ -115,6 +132,7 @@ declare global {
|
||||
const ISimpleIconsHtml5: typeof import('~icons/simple-icons/html5')['default']
|
||||
const ISimpleIconsJavascript: typeof import('~icons/simple-icons/javascript')['default']
|
||||
const ISimpleIconsJinja: typeof import('~icons/simple-icons/jinja')['default']
|
||||
const ISimpleIconsJpeg: typeof import('~icons/simple-icons/jpeg')['default']
|
||||
const ISimpleIconsJulia: typeof import('~icons/simple-icons/julia')['default']
|
||||
const ISimpleIconsKotlin: typeof import('~icons/simple-icons/kotlin')['default']
|
||||
const ISimpleIconsLlvm: typeof import('~icons/simple-icons/llvm')['default']
|
||||
|
||||
@ -131,6 +131,7 @@
|
||||
|
||||
import { browser } from '$app/environment'
|
||||
import { goto } from '$app/navigation'
|
||||
import { getExplorePanelContext } from '$lib/codenav/ExplorePanel.svelte'
|
||||
import type { LineOrPositionOrRange } from '$lib/common'
|
||||
import { type CodeIntelAPI, Occurrence } from '$lib/shared'
|
||||
import {
|
||||
@ -166,7 +167,7 @@
|
||||
getScrollSnapshot as getScrollSnapshot_internal,
|
||||
} from './codemirror/utils'
|
||||
import { registerHotkey } from './Hotkey'
|
||||
import { goToDefinition, openImplementations, openReferences } from './repo/blob'
|
||||
import { goToDefinition, openImplementations } from './repo/blob'
|
||||
import { createLocalWritable } from './stores'
|
||||
|
||||
export let blobInfo: BlobInfo
|
||||
@ -229,6 +230,7 @@
|
||||
filePath: blobInfo.filePath,
|
||||
languages: blobInfo.languages,
|
||||
}
|
||||
const { openReferences } = getExplorePanelContext()
|
||||
$: codeIntelExtension = codeIntelAPI
|
||||
? createCodeIntelExtension({
|
||||
api: {
|
||||
@ -236,7 +238,7 @@
|
||||
documentInfo: documentInfo,
|
||||
goToDefinition: (view, definition, options) =>
|
||||
goToDefinition(documentInfo, view, definition, options),
|
||||
openReferences,
|
||||
openReferences: (_view, documentInfo, occurrence) => openReferences({ documentInfo, occurrence }),
|
||||
openImplementations,
|
||||
createTooltipView: options => new HovercardView(options.view, options.token, options.hovercardData),
|
||||
},
|
||||
|
||||
@ -8,9 +8,11 @@
|
||||
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
|
||||
export let margin: number
|
||||
export let viewport: HTMLElement | undefined = undefined
|
||||
export let scroller: HTMLElement | undefined = undefined
|
||||
|
||||
export function capture(): Capture {
|
||||
return { scroll: scroller.scrollTop }
|
||||
return { scroll: scroller?.scrollTop || 0 }
|
||||
}
|
||||
|
||||
export function restore(data?: Capture) {
|
||||
@ -31,14 +33,13 @@
|
||||
|
||||
const dispatch = createEventDispatcher<{ more: void }>()
|
||||
|
||||
let viewport: HTMLElement
|
||||
let scroller: HTMLElement
|
||||
|
||||
function handleScroll() {
|
||||
const remaining = scroller.scrollHeight - (scroller.scrollTop + viewport.clientHeight)
|
||||
if (scroller && viewport) {
|
||||
const remaining = scroller.scrollHeight - (scroller.scrollTop + (viewport?.clientHeight ?? 0))
|
||||
|
||||
if (remaining < margin) {
|
||||
dispatch('more')
|
||||
if (remaining < margin) {
|
||||
dispatch('more')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,13 +47,13 @@
|
||||
// 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) {
|
||||
if (scroller && scroller.scrollHeight <= scroller.clientHeight) {
|
||||
dispatch('more')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="viewport" bind:this={viewport}>
|
||||
<div class="viewport" bind:this={viewport} data-viewport>
|
||||
<div class="scroller" bind:this={scroller} on:scroll={handleScroll} data-scroller>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@ -93,11 +93,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
gap: 2rem;
|
||||
|
||||
.actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
margin-right: var(--tabs-horizontal-spacing);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
$: selected = $treeState.selected === nodeID
|
||||
$: tabindex = $treeState.focused === nodeID ? 0 : -1
|
||||
$: children = expandable && expanded ? treeProvider.fetchChildren(entry) : null
|
||||
$: disableScope = $treeState.disableScope
|
||||
|
||||
let level = getContext<TreeNodeContext>('tree-node-nesting')?.level ?? 0
|
||||
setContext('tree-node-nesting', { level: level + 1 })
|
||||
@ -76,30 +77,37 @@
|
||||
{tabindex}
|
||||
data-treeitem
|
||||
data-node-id={nodeID}
|
||||
class:disable-scope={disableScope}
|
||||
style="--tree-node-nested-level: {level}"
|
||||
>
|
||||
<span bind:this={label} class="label" data-treeitem-label class:expandable>
|
||||
<div bind:this={label} class="label" data-treeitem-label class:expandable>
|
||||
<!-- TODO: scoping is an operation specific to the file tree, but this
|
||||
is intended to be a generic tree component. We should not add a scope
|
||||
button here. -->
|
||||
<Button variant="icon" on:click={handleScopeChange} data-scope-button>
|
||||
<Icon icon={ILucideFocus} inline aria-hidden="true" />
|
||||
</Button>
|
||||
<!-- hide the open/close button to preserve alignment with expandable entries -->
|
||||
{#if expandable}
|
||||
<!-- We have to stop even propagation because the tree root listens for click events for
|
||||
selecting items. We don't want the item to be selected when the open/close button is pressed.
|
||||
-->
|
||||
<Button
|
||||
variant="icon"
|
||||
on:click={event => {
|
||||
event.stopPropagation()
|
||||
toggleOpen()
|
||||
}}
|
||||
tabindex={-1}
|
||||
>
|
||||
<Icon icon={expanded ? ILucideChevronDown : ILucideChevronRight} inline />
|
||||
</Button>
|
||||
{/if}
|
||||
<slot {entry} {expanded} toggle={toggleOpen} {label} />
|
||||
</span>
|
||||
<div class="indented">
|
||||
{#if expandable}
|
||||
<!-- We have to stop even propagation because the tree root
|
||||
listens for click events for selecting items. We don't want the
|
||||
item to be selected when the open/close button is pressed. -->
|
||||
<Button
|
||||
variant="icon"
|
||||
on:click={event => {
|
||||
event.stopPropagation()
|
||||
toggleOpen()
|
||||
}}
|
||||
tabindex={-1}
|
||||
aria-label="{expanded ? 'Collapse' : 'Expand'} subtree"
|
||||
>
|
||||
<Icon icon={expanded ? ILucideChevronDown : ILucideChevronRight} inline aria-hidden="true" />
|
||||
</Button>
|
||||
{/if}
|
||||
<slot {entry} {expanded} toggle={toggleOpen} {label} />
|
||||
</div>
|
||||
</div>
|
||||
{#if expanded && children}
|
||||
{#await children}
|
||||
<div class="loading">
|
||||
@ -123,67 +131,70 @@
|
||||
$shiftWidth: 1.25rem;
|
||||
$gap: 0.25rem;
|
||||
|
||||
[role='treeitem'] {
|
||||
border-radius: var(--border-radius);
|
||||
li[role='treeitem'] {
|
||||
--indent-size: calc(var(--tree-node-nested-level) * #{$shiftWidth});
|
||||
|
||||
--scope-size: calc(var(--icon-inline-size) + #{$gap} - 1px);
|
||||
&.disable-scope {
|
||||
--scope-size: 0px;
|
||||
:global([data-scope-button]) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[tabindex='0']:focus {
|
||||
box-shadow: none;
|
||||
|
||||
> .label {
|
||||
box-shadow: var(--focus-box-shadow);
|
||||
box-shadow: var(--focus-shadow-inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
// Indent with two rem since loading represents next nested level
|
||||
margin-left: calc(var(--tree-node-nested-level) * #{$shiftWidth} + 2 * var(--icon-inline-size) + 2 * #{$gap});
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.label {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
padding: 0.2rem $gap;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
padding: 0.2rem $gap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
// Change icon color based on selected item state
|
||||
--icon-color: var(--tree-node-expand-icon-color);
|
||||
color: var(--tree-node-label-color, var(--text-body));
|
||||
|
||||
// Change icon color based on selected item state
|
||||
--icon-color: var(--tree-node-expand-icon-color);
|
||||
color: var(--tree-node-label-color, var(--text-body));
|
||||
|
||||
li[data-treeitem][aria-selected='true'] > & {
|
||||
--icon-color: currentColor;
|
||||
--file-icon-color: currentColor;
|
||||
|
||||
color: var(--tree-node-label-color, var(--body-bg));
|
||||
}
|
||||
|
||||
:global([data-scope-button]) {
|
||||
visibility: hidden;
|
||||
margin-right: calc(var(--tree-node-nested-level) * #{$shiftWidth});
|
||||
}
|
||||
|
||||
&.expandable:hover,
|
||||
&.expandable:focus {
|
||||
:global([data-scope-button]) {
|
||||
visibility: visible;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&.expandable:hover,
|
||||
&.expandable:focus {
|
||||
:global([data-scope-button]) {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.indented {
|
||||
display: inherit;
|
||||
gap: inherit;
|
||||
margin-left: var(--indent-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-left: 1px solid var(--secondary);
|
||||
height: 100%;
|
||||
transform: translateX(
|
||||
calc(var(--tree-node-nested-level) * #{$shiftWidth} + var(--icon-inline-size) * 1.5 + #{$gap} + 2px)
|
||||
);
|
||||
z-index: 1;
|
||||
.loading {
|
||||
// Indent with two rem since loading represents next nested level
|
||||
margin-left: calc(var(--scope-size) + var(--indent-size) + 2 * #{$gap});
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-left: 1px solid var(--secondary);
|
||||
height: 100%;
|
||||
transform: translateX(calc(#{$gap} + var(--scope-size) + var(--icon-inline-size) / 2 - 1px));
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,7 +7,7 @@ export interface TreeProvider<T> {
|
||||
*/
|
||||
getEntries(): T[]
|
||||
/**
|
||||
* Whether or not the provided entry is has (possibly) children or not.
|
||||
* Whether or not the provided entry has (possibly) children or not.
|
||||
*/
|
||||
isExpandable(entry: T): boolean
|
||||
/**
|
||||
@ -29,6 +29,7 @@ export interface SingleSelectTreeState {
|
||||
focused: string
|
||||
selected: string
|
||||
expandedNodes: Set<string>
|
||||
disableScope: boolean
|
||||
}
|
||||
|
||||
export type TreeState = SingleSelectTreeState
|
||||
@ -38,6 +39,7 @@ export function createEmptySingleSelectTreeState(): SingleSelectTreeState {
|
||||
focused: '',
|
||||
selected: '',
|
||||
expandedNodes: new Set(),
|
||||
disableScope: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
50
client/web-sveltekit/src/lib/codenav/ExplorePanel.gql
Normal file
50
client/web-sveltekit/src/lib/codenav/ExplorePanel.gql
Normal file
@ -0,0 +1,50 @@
|
||||
query ExplorePanel_Usages(
|
||||
$repoName: String!
|
||||
$revspec: String!
|
||||
$filePath: String!
|
||||
$rangeStart: PositionInput!
|
||||
$rangeEnd: PositionInput!
|
||||
$symbolComparator: SymbolComparator
|
||||
$first: Int!
|
||||
$afterCursor: String
|
||||
) {
|
||||
usagesForSymbol(
|
||||
symbol: $symbolComparator
|
||||
range: { repository: $repoName, revision: $revspec, path: $filePath, start: $rangeStart, end: $rangeEnd }
|
||||
first: $first
|
||||
after: $afterCursor
|
||||
) {
|
||||
...ExplorePanel_UsageConnection
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExplorePanel_UsageConnection on UsageConnection {
|
||||
nodes {
|
||||
...ExplorePanel_Usage
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExplorePanel_Usage on Usage {
|
||||
provenance
|
||||
usageRange {
|
||||
repository
|
||||
revision
|
||||
path
|
||||
range {
|
||||
start {
|
||||
line
|
||||
character
|
||||
}
|
||||
end {
|
||||
line
|
||||
character
|
||||
}
|
||||
}
|
||||
}
|
||||
surroundingContent
|
||||
usageKind
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
<script lang="ts" context="module">
|
||||
import { Story } from '@storybook/addon-svelte-csf'
|
||||
import { writable, readable } from 'svelte/store'
|
||||
|
||||
import { Range } from '@sourcegraph/shared/src/codeintel/scip'
|
||||
|
||||
import type { InfinityQueryStore } from '$lib/graphql'
|
||||
import { SymbolUsageKind } from '$lib/graphql-types'
|
||||
import { Occurrence } from '$lib/shared'
|
||||
import { createEmptySingleSelectTreeState, type SingleSelectTreeState } from '$lib/TreeView'
|
||||
|
||||
import type { ExplorePanel_UsagesResult, ExplorePanel_UsagesVariables } from './ExplorePanel.gql'
|
||||
import ExplorePanel, { type ExplorePanelInputs } from './ExplorePanel.svelte'
|
||||
|
||||
export const meta = {
|
||||
component: ExplorePanel,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const startingTreeState = writable<SingleSelectTreeState>({
|
||||
...createEmptySingleSelectTreeState(),
|
||||
disableScope: true,
|
||||
})
|
||||
|
||||
const emptyInputs: ExplorePanelInputs = {}
|
||||
const simpleInputs: ExplorePanelInputs = {
|
||||
activeOccurrence: {
|
||||
documentInfo: {
|
||||
repoName: 'test/repo',
|
||||
filePath: 'test/file',
|
||||
commitID: 'deadbeef',
|
||||
revision: 'main',
|
||||
languages: [],
|
||||
},
|
||||
occurrence: new Occurrence(Range.fromNumbers(0, 0, 0, 10)),
|
||||
},
|
||||
usageKindFilter: SymbolUsageKind.REFERENCE,
|
||||
}
|
||||
|
||||
const loadingConnection: InfinityQueryStore<ExplorePanel_UsagesResult, ExplorePanel_UsagesVariables> = readable({
|
||||
fetching: true,
|
||||
}) as InfinityQueryStore<ExplorePanel_UsagesResult, ExplorePanel_UsagesVariables>
|
||||
</script>
|
||||
|
||||
<Story name="Unset">
|
||||
<div class="container">
|
||||
<ExplorePanel inputs={writable(emptyInputs)} connection={undefined} treeState={startingTreeState} />
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
<Story name="Loading">
|
||||
<div class="container">
|
||||
<ExplorePanel inputs={writable(simpleInputs)} connection={loadingConnection} treeState={startingTreeState} />
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
width: 800px;
|
||||
height: 200px;
|
||||
border: 1px solid black;
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
427
client/web-sveltekit/src/lib/codenav/ExplorePanel.svelte
Normal file
427
client/web-sveltekit/src/lib/codenav/ExplorePanel.svelte
Normal file
@ -0,0 +1,427 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface ActiveOccurrence {
|
||||
documentInfo: DocumentInfo
|
||||
occurrence: Occurrence
|
||||
}
|
||||
|
||||
export interface ExplorePanelInputs {
|
||||
activeOccurrence?: ActiveOccurrence
|
||||
usageKindFilter?: SymbolUsageKind
|
||||
treeFilter?: TreeFilter
|
||||
}
|
||||
|
||||
export interface ExplorePanelContext {
|
||||
openReferences(occurrence: ActiveOccurrence): void
|
||||
}
|
||||
|
||||
const exploreContextKey = Symbol('explore context key')
|
||||
export function getExplorePanelContext(): ExplorePanelContext {
|
||||
return getContext(exploreContextKey) ?? { openReferences: () => {} }
|
||||
}
|
||||
export function setExplorePanelContext(ctx: ExplorePanelContext) {
|
||||
setContext(exploreContextKey, ctx)
|
||||
}
|
||||
|
||||
interface RepoTreeEntry {
|
||||
type: 'repo'
|
||||
name: string
|
||||
entries: PathTreeEntry[]
|
||||
}
|
||||
|
||||
interface PathTreeEntry {
|
||||
type: 'path'
|
||||
repo: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type TreeEntry = RepoTreeEntry | PathTreeEntry
|
||||
|
||||
interface PathGroup {
|
||||
path: string
|
||||
usages: ExplorePanel_Usage[]
|
||||
}
|
||||
|
||||
interface RepoGroup {
|
||||
repo: string
|
||||
pathGroups: PathGroup[]
|
||||
}
|
||||
|
||||
function groupUsages(usages: ExplorePanel_Usage[]): RepoGroup[] {
|
||||
const seenRepos: Record<string, { index: number; seenPaths: Record<string, number> }> = {}
|
||||
const repoGroups: RepoGroup[] = []
|
||||
|
||||
for (const usage of usages) {
|
||||
const repo = usage.usageRange!.repository
|
||||
if (seenRepos[repo] === undefined) {
|
||||
seenRepos[repo] = { index: repoGroups.length, seenPaths: {} }
|
||||
repoGroups.push({ repo, pathGroups: [] })
|
||||
}
|
||||
|
||||
const path = usage.usageRange!.path
|
||||
const seenPaths = seenRepos[repo].seenPaths
|
||||
const pathGroups = repoGroups[seenRepos[repo].index].pathGroups
|
||||
|
||||
if (seenPaths[path] === undefined) {
|
||||
seenPaths[path] = pathGroups.length
|
||||
pathGroups.push({ path, usages: [] })
|
||||
}
|
||||
|
||||
pathGroups[seenPaths[path]].usages.push(usage)
|
||||
}
|
||||
|
||||
return repoGroups
|
||||
}
|
||||
|
||||
function treeProviderForEntries(entries: TreeEntry[]): TreeProvider<TreeEntry> {
|
||||
return {
|
||||
getNodeID(entry) {
|
||||
if (entry.type === 'repo') {
|
||||
return `repo-${entry.name}`
|
||||
} else {
|
||||
return `path-${entry.repo}-${entry.name}`
|
||||
}
|
||||
},
|
||||
getEntries(): TreeEntry[] {
|
||||
return entries
|
||||
},
|
||||
isExpandable(entry) {
|
||||
return entry.type === 'repo'
|
||||
},
|
||||
isSelectable() {
|
||||
return true
|
||||
},
|
||||
fetchChildren(entry) {
|
||||
if (entry.type === 'repo') {
|
||||
return Promise.resolve(treeProviderForEntries(entry.entries))
|
||||
} else {
|
||||
throw new Error('path nodes are not expandable')
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function generateOutlineTree(repoGroups: RepoGroup[]): TreeProvider<TreeEntry> {
|
||||
const repoEntries: RepoTreeEntry[] = repoGroups.map(repoGroup => ({
|
||||
type: 'repo',
|
||||
name: repoGroup.repo,
|
||||
entries: repoGroup.pathGroups.map(pathGroup => ({
|
||||
type: 'path',
|
||||
name: pathGroup.path,
|
||||
repo: repoGroup.repo,
|
||||
})),
|
||||
}))
|
||||
return treeProviderForEntries(repoEntries)
|
||||
}
|
||||
|
||||
export function getUsagesStore(client: GraphQLClient, documentInfo: DocumentInfo, occurrence: Occurrence) {
|
||||
const baseVariables = {
|
||||
repoName: documentInfo.repoName,
|
||||
revspec: documentInfo.commitID,
|
||||
filePath: documentInfo.filePath,
|
||||
rangeStart: occurrence.range.start,
|
||||
rangeEnd: occurrence.range.end,
|
||||
symbolComparator: occurrence.symbol
|
||||
? {
|
||||
name: { equals: occurrence.symbol },
|
||||
provenance: {
|
||||
/* equals: TODO */
|
||||
},
|
||||
}
|
||||
: null,
|
||||
first: 100,
|
||||
afterCursor: null,
|
||||
}
|
||||
|
||||
return infinityQuery({
|
||||
client,
|
||||
query: ExplorePanel_Usages,
|
||||
variables: baseVariables,
|
||||
map: result => ({
|
||||
data: result?.data?.usagesForSymbol?.nodes,
|
||||
error: result?.error,
|
||||
nextVariables: result?.data?.usagesForSymbol?.pageInfo?.hasNextPage
|
||||
? {
|
||||
...baseVariables,
|
||||
afterCursor: result?.data?.usagesForSymbol?.pageInfo?.endCursor,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
merge: (previous, next) => (previous ?? []).concat(next ?? []),
|
||||
})
|
||||
}
|
||||
|
||||
function matchesUsageKind(usageKindFilter: SymbolUsageKind | undefined): (usage: ExplorePanel_Usage) => boolean {
|
||||
return usage => usageKindFilter === undefined || usage.usageKind === usageKindFilter
|
||||
}
|
||||
|
||||
interface TreeFilter {
|
||||
repository: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function entryIDForFilter(filter: TreeFilter): string {
|
||||
if (filter.path) {
|
||||
return `path-${filter.repository}-${filter.path}`
|
||||
}
|
||||
return `repo-${filter.repository}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import { type Writable } from 'svelte/store'
|
||||
|
||||
import { infinityQuery, type GraphQLClient, type InfinityQueryStore } from '$lib/graphql'
|
||||
import { SymbolUsageKind } from '$lib/graphql-types'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
import Scroller from '$lib/Scroller.svelte'
|
||||
import CodeHostIcon from '$lib/search/CodeHostIcon.svelte'
|
||||
import LoadingSkeleton from '$lib/search/dynamicFilters/LoadingSkeleton.svelte'
|
||||
import { displayRepoName, Occurrence } from '$lib/shared'
|
||||
import { type SingleSelectTreeState, type TreeProvider } from '$lib/TreeView'
|
||||
import TreeView, { setTreeContext } from '$lib/TreeView.svelte'
|
||||
import type { DocumentInfo } from '$lib/web'
|
||||
import { Alert, PanelGroup } from '$lib/wildcard'
|
||||
import Panel from '$lib/wildcard/resizable-panel/Panel.svelte'
|
||||
import PanelResizeHandle from '$lib/wildcard/resizable-panel/PanelResizeHandle.svelte'
|
||||
|
||||
import type { ExplorePanel_Usage, ExplorePanel_UsagesVariables } from './ExplorePanel.gql'
|
||||
import { ExplorePanel_Usages } from './ExplorePanel.gql'
|
||||
import ExplorePanelFileUsages from './ExplorePanelFileUsages.svelte'
|
||||
|
||||
export let inputs: Writable<ExplorePanelInputs>
|
||||
export let connection: InfinityQueryStore<ExplorePanel_Usage[], ExplorePanel_UsagesVariables> | undefined
|
||||
export let treeState: Writable<SingleSelectTreeState>
|
||||
|
||||
$: setTreeContext(treeState)
|
||||
|
||||
// TODO: it would be really nice if the tree API emitted select events with tree elements, not HTML elements
|
||||
function handleSelect(target: HTMLElement) {
|
||||
const selected = target.querySelector('[data-repo-name]') as HTMLElement
|
||||
const repository = selected.dataset.repoName ?? ''
|
||||
const path = selected.dataset.path
|
||||
inputs.update(old => {
|
||||
const deselect = old.treeFilter && old.treeFilter.repository === repository && old.treeFilter.path === path
|
||||
return {
|
||||
...old,
|
||||
treeFilter: deselect ? undefined : { repository, path },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$: loading = $connection?.fetching
|
||||
$: usages = $connection?.data
|
||||
$: kindFilteredUsages = usages?.filter(matchesUsageKind($inputs.usageKindFilter))
|
||||
$: repoGroups = groupUsages(kindFilteredUsages ?? [])
|
||||
$: outlineTree = generateOutlineTree(repoGroups)
|
||||
$: displayGroups = repoGroups
|
||||
.flatMap(repoGroup => repoGroup.pathGroups.map(pathGroup => ({ repo: repoGroup.repo, ...pathGroup })))
|
||||
.filter(displayGroup => {
|
||||
if ($inputs.treeFilter === undefined) {
|
||||
return true
|
||||
} else if ($inputs.treeFilter.repository !== displayGroup.repo) {
|
||||
return false
|
||||
}
|
||||
return $inputs.treeFilter.path === undefined || $inputs.treeFilter.path === displayGroup.path
|
||||
})
|
||||
|
||||
let referencesScroller: HTMLElement | undefined
|
||||
</script>
|
||||
|
||||
{#if $inputs.activeOccurrence === undefined}
|
||||
<div class="no-selection">
|
||||
<Icon icon={ISgSymbols} />
|
||||
<p>Select a symbol in the code panel to view references.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<PanelGroup id="references">
|
||||
<Panel id="references-sidebar" defaultSize={25} minSize={20} maxSize={60}>
|
||||
<div class="sidebar">
|
||||
<fieldset>
|
||||
<legend hidden>Select usage kind</legend>
|
||||
{#each Object.values(SymbolUsageKind) as usageKind}
|
||||
{@const checked = usageKind === $inputs.usageKindFilter}
|
||||
{@const id = `usage-kind-${usageKind}`}
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={$inputs.usageKindFilter}
|
||||
name="usageKind"
|
||||
value={usageKind}
|
||||
{id}
|
||||
{checked}
|
||||
/>
|
||||
<label for={id}>{usageKind.toLowerCase()}s</label>
|
||||
{/each}
|
||||
</fieldset>
|
||||
<div class="outline">
|
||||
{#if repoGroups.length > 0}
|
||||
<h4>Filter by location</h4>
|
||||
<TreeView treeProvider={outlineTree} on:select={event => handleSelect(event.detail)}>
|
||||
<svelte:fragment let:entry>
|
||||
{#if entry.type === 'repo'}
|
||||
<span class="repo-entry" data-repo-name={entry.name}>
|
||||
<CodeHostIcon repository={entry.name} />
|
||||
{displayRepoName(entry.name)}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="path-entry" data-repo-name={entry.repo} data-path={entry.name}>
|
||||
{entry.name}
|
||||
</span>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Alert slot="error" let:error variant="danger">
|
||||
{error.message}
|
||||
</Alert>
|
||||
</TreeView>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel id="references-content">
|
||||
{#if loading}
|
||||
<div class="loading">
|
||||
<LoadingSkeleton />
|
||||
<LoadingSkeleton />
|
||||
<LoadingSkeleton />
|
||||
</div>
|
||||
{:else}
|
||||
<Scroller bind:viewport={referencesScroller} margin={600} on:more={() => connection?.fetchMore()}>
|
||||
{#if displayGroups.length > 0}
|
||||
<ul>
|
||||
{#each displayGroups as pathGroup}
|
||||
<li>
|
||||
<ExplorePanelFileUsages scrollContainer={referencesScroller} {...pathGroup} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if loading}
|
||||
<LoadingSpinner center />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="no-results">No results.</div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
{/if}
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0.25rem 0;
|
||||
|
||||
input {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
label {
|
||||
text-transform: capitalize;
|
||||
cursor: pointer;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
background-color: var(--primary);
|
||||
color: var(--light-text);
|
||||
}
|
||||
|
||||
input:not(:checked) + label:hover {
|
||||
background-color: var(--secondary-4);
|
||||
}
|
||||
}
|
||||
|
||||
.outline {
|
||||
h4 {
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
padding: 0rem;
|
||||
|
||||
:global([data-treeitem]) > :global([data-treeitem-label]) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--secondary-4);
|
||||
}
|
||||
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
:global([data-treeitem][aria-selected='true']) > :global([data-treeitem-label]) {
|
||||
--tree-node-expand-icon-color: var(--body-bg);
|
||||
--file-icon-color: var(--body-bg);
|
||||
--tree-node-label-color: var(--body-bg);
|
||||
|
||||
background-color: var(--primary);
|
||||
&:hover {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.repo-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375em;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
.path-entry {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
ul {
|
||||
all: unset;
|
||||
li {
|
||||
all: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.no-selection {
|
||||
:global([data-icon]) {
|
||||
flex: none;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
|
||||
import CodeExcerpt from '$lib/CodeExcerpt.svelte'
|
||||
import { observeIntersection } from '$lib/intersection-observer'
|
||||
import { pathHrefFactory } from '$lib/path'
|
||||
import DisplayPath from '$lib/path/DisplayPath.svelte'
|
||||
import { fetchFileRangeMatches } from '$lib/search/api/highlighting'
|
||||
import CodeHostIcon from '$lib/search/CodeHostIcon.svelte'
|
||||
import { displayRepoName } from '$lib/shared'
|
||||
|
||||
import type { ExplorePanel_Usage } from './ExplorePanel.gql'
|
||||
|
||||
export let repo: string
|
||||
export let path: string
|
||||
export let usages: ExplorePanel_Usage[]
|
||||
export let scrollContainer: HTMLElement | undefined
|
||||
|
||||
// TODO: remove all the usageRange! assertions once the backend is updated to
|
||||
// use a non-nullable type in the API. I've already confirmed that it should always
|
||||
// be non-null.
|
||||
//
|
||||
// FIXME: Assumes that all usages for a repo/path combo are at the same revision.
|
||||
$: revision = usages[0].usageRange!.revision
|
||||
|
||||
let highlightedHTMLChunks: string[][] | undefined
|
||||
let visible = false
|
||||
$: if (visible) {
|
||||
fetchFileRangeMatches({
|
||||
result: {
|
||||
repository: repo,
|
||||
commit: revision,
|
||||
path: path,
|
||||
},
|
||||
ranges: usages.map(usage => ({
|
||||
startLine: usage.usageRange!.range.start.line,
|
||||
endLine: usage.usageRange!.range.end.line + 1,
|
||||
})),
|
||||
})
|
||||
.then(result => {
|
||||
highlightedHTMLChunks = result
|
||||
})
|
||||
.catch(err => console.error('Failed to fetch highlighted usages', err))
|
||||
}
|
||||
|
||||
function hrefForUsage(usage: ExplorePanel_Usage): string {
|
||||
const { repository, revision, path, range } = usage.usageRange!
|
||||
return SourcegraphURL.from(`${repository}@${revision}/-/blob/${path}`)
|
||||
.setLineRange({
|
||||
line: range.start.line + 1,
|
||||
character: range.start.character + 1,
|
||||
endLine: range.end.line + 1,
|
||||
endCharacter: range.end.character + 1,
|
||||
})
|
||||
.toString()
|
||||
}
|
||||
|
||||
$: usageExcerpts = usages.map((usage, index) => ({
|
||||
startLine: usage.usageRange!.range.start.line,
|
||||
matches: [
|
||||
{
|
||||
startLine: usage.usageRange!.range.start.line,
|
||||
startCharacter: usage.usageRange!.range.start.character,
|
||||
endLine: usage.usageRange!.range.end.line,
|
||||
endCharacter: usage.usageRange!.range.end.character,
|
||||
},
|
||||
],
|
||||
plaintextLines: [usage.surroundingContent],
|
||||
highlightedHTMLRows: highlightedHTMLChunks?.[index],
|
||||
href: hrefForUsage(usage),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="root"
|
||||
use:observeIntersection={scrollContainer ?? null}
|
||||
on:intersecting={event => (visible = visible || event.detail)}
|
||||
>
|
||||
<div class="header">
|
||||
<CodeHostIcon repository={repo} />
|
||||
<span class="repo-name"><DisplayPath path={displayRepoName(repo)} /></span>
|
||||
<span class="interpunct">⋅</span>
|
||||
<span class="file-name">
|
||||
<DisplayPath
|
||||
{path}
|
||||
pathHref={pathHrefFactory({
|
||||
repoName: repo,
|
||||
revision: revision,
|
||||
fullPath: path,
|
||||
fullPathType: 'blob',
|
||||
})}
|
||||
showCopyButton
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{#each usageExcerpts as excerpt}
|
||||
<a href={excerpt.href}>
|
||||
<CodeExcerpt
|
||||
collapseWhitespace
|
||||
startLine={excerpt.startLine}
|
||||
plaintextLines={excerpt.plaintextLines}
|
||||
matches={excerpt.matches}
|
||||
highlightedHTMLRows={excerpt.highlightedHTMLRows}
|
||||
/>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
:global([data-copy-button]) {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
&:is(:hover, :focus-within) :global([data-copy-button]) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 0.5rem;
|
||||
background-color: var(--secondary-4);
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
align-items: center;
|
||||
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
--icon-color: currentColor;
|
||||
:global([data-icon]) {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.repo-name {
|
||||
:global([data-path-container]) {
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-small);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
:global([data-path-item]) {
|
||||
color: var(--text-title);
|
||||
}
|
||||
}
|
||||
|
||||
.interpunct {
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
:global([data-path-container]) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
text-decoration: none;
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--secondary-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -58,7 +58,7 @@
|
||||
Wrap the anchor element with a span because otherwise it adds
|
||||
spaces around it when copied
|
||||
--><span
|
||||
class:after={last}
|
||||
class:last
|
||||
data-path-item
|
||||
>{#if pathHref}<a href={pathHref(path)}>{part}</a>{:else}{part}{/if}<!--
|
||||
--></span
|
||||
@ -73,8 +73,7 @@
|
||||
elements. Otherwise, an invisible button might wrap to its own line, which looks weird.
|
||||
-->{#if showCopyButton}<!--
|
||||
--><span
|
||||
data-copy-button
|
||||
class="after"><CopyButton value={path} label="Copy path to clipboard" /></span
|
||||
data-copy-button><CopyButton value={path} label="Copy path to clipboard" /></span
|
||||
><!--
|
||||
-->{/if}
|
||||
</span>
|
||||
@ -111,7 +110,7 @@
|
||||
// HACK: The file icon is placed after the file name in the DOM so it
|
||||
// doesn't add any spaces in the file path when copied. This visually
|
||||
// reorders the last path element after the file icon.
|
||||
.after {
|
||||
.last {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
@ -123,6 +122,7 @@
|
||||
}
|
||||
|
||||
[data-copy-button] {
|
||||
order: 1;
|
||||
margin-left: 0.5rem;
|
||||
user-select: none; // Avoids a trailing space on select + copy
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
}
|
||||
|
||||
a {
|
||||
flex: 1;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@ -3,7 +3,7 @@ import { get } from 'svelte/store'
|
||||
|
||||
import { goto as svelteGoto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { toPrettyBlobURL } from '$lib/shared'
|
||||
import { Occurrence, toPrettyBlobURL } from '$lib/shared'
|
||||
import {
|
||||
positionToOffset,
|
||||
type Definition,
|
||||
@ -89,11 +89,7 @@ export async function goToDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
export function openReferences(
|
||||
view: EditorView,
|
||||
documentInfo: DocumentInfo,
|
||||
occurrence: Definition['occurrence']
|
||||
): void {
|
||||
export function openReferences(view: EditorView, documentInfo: DocumentInfo, occurrence: Occurrence): void {
|
||||
const url = toPrettyBlobURL({
|
||||
repoName: documentInfo.repoName,
|
||||
revision: documentInfo.revision,
|
||||
|
||||
@ -5,11 +5,13 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { getId } from './utils/common'
|
||||
import { assert } from './utils/assert'
|
||||
import type { PanelGroupContext, ResizeHandlerAction, ResizeEvent } from './types'
|
||||
|
||||
import { PanelResizeHandleRegistry } from '$lib/wildcard/resizable-panel/PanelResizeHandleRegistry'
|
||||
|
||||
import type { PanelGroupContext, ResizeHandlerAction, ResizeEvent } from './types'
|
||||
import { assert } from './utils/assert'
|
||||
import { getId } from './utils/common'
|
||||
|
||||
export let id: string | null = null
|
||||
|
||||
const resizeHandleId = id ?? getId()
|
||||
@ -78,25 +80,23 @@
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
:root {
|
||||
--resize-handle-bg: var(--border-color);
|
||||
--resize-handle-hover-bg: var(--border-color-2);
|
||||
--resize-handle-drag-bg: var(--oc-blue-3);
|
||||
--resize-handle-size: 1px;
|
||||
--resize-handle-active-size: 3px;
|
||||
}
|
||||
$resize-handle-bg: var(--border-color);
|
||||
$resize-handle-hover-bg: var(--border-color-2);
|
||||
$resize-handle-drag-bg: var(--oc-blue-3);
|
||||
$resize-handle-size: 1px;
|
||||
$resize-handle-active-size: 3px;
|
||||
|
||||
.separator {
|
||||
// Since drag handler is always rendered within flex
|
||||
// PanelGroup component is safe to assume that flex rules
|
||||
// can applied here.
|
||||
flex: 0 0 var(--resize-handle-size);
|
||||
flex: 0 0 $resize-handle-size;
|
||||
display: flex;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--resize-handle-bg);
|
||||
background: $resize-handle-bg;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
@ -108,18 +108,18 @@
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: var(--resize-handle-active-size);
|
||||
min-height: var(--resize-handle-active-size);
|
||||
min-width: $resize-handle-active-size;
|
||||
min-height: $resize-handle-active-size;
|
||||
}
|
||||
|
||||
&[data-resize-handle-state='hover']::before {
|
||||
display: block;
|
||||
background: var(--resize-handle-hover-bg);
|
||||
background: $resize-handle-hover-bg;
|
||||
}
|
||||
|
||||
&[data-resize-handle-state='drag']::before {
|
||||
display: block;
|
||||
background: var(--resize-handle-drag-bg);
|
||||
background: $resize-handle-drag-bg;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,16 +4,17 @@
|
||||
|
||||
import { browser } from '$app/environment'
|
||||
import { beforeNavigate } from '$app/navigation'
|
||||
import { isErrorLike } from '$lib/common'
|
||||
import GlobalHeader from '$lib/navigation/GlobalHeader.svelte'
|
||||
import { TemporarySettingsStorage } from '$lib/shared'
|
||||
import { isLightTheme, setAppContext } from '$lib/stores'
|
||||
import { createTemporarySettingsStorage } from '$lib/temporarySettings'
|
||||
|
||||
// When adding global imports here, they should probably also be added in .storybook/preview.ts
|
||||
import '@fontsource-variable/roboto-mono'
|
||||
import '@fontsource-variable/inter'
|
||||
import './styles.scss'
|
||||
|
||||
import { isErrorLike } from '$lib/common'
|
||||
import { createFeatureFlagStore } from '$lib/featureflags'
|
||||
import FuzzyFinderContainer from '$lib/fuzzyfinder/FuzzyFinderContainer.svelte'
|
||||
import GlobalNotification from '$lib/global-notifications/GlobalNotifications.svelte'
|
||||
|
||||
@ -11,6 +11,8 @@
|
||||
interface Capture {
|
||||
selectedTab: number | null
|
||||
historyPanel: HistoryCapture
|
||||
exploreInputs: ExplorePanelInputs | undefined
|
||||
// TODO: consider also capturing the file in the references panel
|
||||
}
|
||||
|
||||
// Not ideal solution, [TODO] Improve Tabs component API in order
|
||||
@ -37,14 +39,24 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { get, writable } from 'svelte/store'
|
||||
|
||||
import { afterNavigate, goto } from '$app/navigation'
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import ExplorePanel, {
|
||||
setExplorePanelContext,
|
||||
getUsagesStore,
|
||||
entryIDForFilter,
|
||||
type ExplorePanelInputs,
|
||||
type ActiveOccurrence,
|
||||
} from '$lib/codenav/ExplorePanel.svelte'
|
||||
import CodySidebar from '$lib/cody/CodySidebar.svelte'
|
||||
import { isErrorLike, SourcegraphURL } from '$lib/common'
|
||||
import { isErrorLike } from '$lib/common'
|
||||
import { openFuzzyFinder } from '$lib/fuzzyfinder/FuzzyFinderContainer.svelte'
|
||||
import { filesHotkey } from '$lib/fuzzyfinder/keys'
|
||||
import { getGraphQLClient } from '$lib/graphql'
|
||||
import { SymbolUsageKind } from '$lib/graphql-types'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import KeyboardShortcut from '$lib/KeyboardShortcut.svelte'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
@ -56,6 +68,7 @@
|
||||
import TabPanel from '$lib/TabPanel.svelte'
|
||||
import Tabs from '$lib/Tabs.svelte'
|
||||
import Tooltip from '$lib/Tooltip.svelte'
|
||||
import { createEmptySingleSelectTreeState, type SingleSelectTreeState } from '$lib/TreeView'
|
||||
import { Alert, PanelGroup, Panel, PanelResizeHandle, Button } from '$lib/wildcard'
|
||||
import { getButtonClassName } from '$lib/wildcard/Button'
|
||||
|
||||
@ -64,7 +77,6 @@
|
||||
import type { LayoutData, Snapshot } from './$types'
|
||||
import FileTree from './FileTree.svelte'
|
||||
import { createFileTreeStore } from './fileTreeStore'
|
||||
import ReferencePanel from './ReferencePanel.svelte'
|
||||
|
||||
export let data: LayoutData
|
||||
|
||||
@ -73,14 +85,14 @@
|
||||
return {
|
||||
selectedTab,
|
||||
historyPanel: historyPanel?.capture(),
|
||||
exploreInputs: get(exploreInputs),
|
||||
}
|
||||
},
|
||||
async restore(data) {
|
||||
restore(data) {
|
||||
selectedTab = data.selectedTab
|
||||
// Wait until DOM was updated to possibly show the history panel
|
||||
await tick()
|
||||
|
||||
// Restore history panel state if it is open
|
||||
if (data.exploreInputs) {
|
||||
exploreInputs.set(data.exploreInputs)
|
||||
}
|
||||
if (data.historyPanel) {
|
||||
historyPanel?.restore(data.historyPanel)
|
||||
}
|
||||
@ -95,28 +107,30 @@
|
||||
|
||||
$: ({ revision = '', parentPath, repoName, resolvedRevision, isCodyAvailable } = data)
|
||||
$: fileTreeStore.set({ repoName, revision: resolvedRevision.commitID, path: parentPath })
|
||||
// The observable query to fetch references (due to infinite scrolling)
|
||||
$: sgURL = SourcegraphURL.from($page.url)
|
||||
$: selectedLine = sgURL.lineRange
|
||||
$: referenceQuery =
|
||||
sgURL.viewState === 'references' && selectedLine?.line ? data.getReferenceStore(selectedLine) : null
|
||||
|
||||
afterNavigate(async () => {
|
||||
// We need to wait for referenceQuery to be updated before checking its state
|
||||
await tick()
|
||||
|
||||
// todo(fkling): Figure out a proper way to represent bottom panel state
|
||||
if (sgURL.viewState === 'references') {
|
||||
selectedTab = TabPanels.References
|
||||
} else if ($page.url.searchParams.has('rev')) {
|
||||
// The file view/history panel use the 'rev' parameter to specify the commit to load
|
||||
selectedTab = TabPanels.History
|
||||
} else if (selectedTab === TabPanels.References) {
|
||||
// Close references panel when navigating to a URL that doesn't have the 'references' view state
|
||||
selectedTab = null
|
||||
}
|
||||
const exploreTreeState = writable<SingleSelectTreeState>({
|
||||
...createEmptySingleSelectTreeState(),
|
||||
disableScope: true,
|
||||
})
|
||||
|
||||
const exploreInputs = writable<ExplorePanelInputs>({})
|
||||
setExplorePanelContext({
|
||||
openReferences(occurrence: ActiveOccurrence) {
|
||||
exploreInputs.set({ activeOccurrence: occurrence, usageKindFilter: SymbolUsageKind.REFERENCE })
|
||||
// Open the tab when we find references
|
||||
selectedTab = TabPanels.References
|
||||
},
|
||||
})
|
||||
$: usagesConnection = $exploreInputs.activeOccurrence
|
||||
? getUsagesStore(
|
||||
getGraphQLClient(),
|
||||
$exploreInputs.activeOccurrence.documentInfo,
|
||||
$exploreInputs.activeOccurrence.occurrence
|
||||
)
|
||||
: undefined
|
||||
$: exploreTreeState.update(old => ({
|
||||
...old,
|
||||
selected: $exploreInputs.treeFilter ? entryIDForFilter($exploreInputs.treeFilter) : '',
|
||||
}))
|
||||
function selectTab(event: { detail: number | null }) {
|
||||
trackHistoryPanelTabAction(selectedTab, event.detail)
|
||||
|
||||
@ -128,7 +142,7 @@
|
||||
|
||||
function handleBottomPanelExpand() {
|
||||
if (selectedTab == null) {
|
||||
selectedTab = 0
|
||||
selectedTab = TabPanels.History
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,58 +290,52 @@
|
||||
order={2}
|
||||
defaultSize={1}
|
||||
minSize={20}
|
||||
maxSize={50}
|
||||
maxSize={70}
|
||||
collapsible
|
||||
collapsedSize={1}
|
||||
onExpand={handleBottomPanelExpand}
|
||||
onCollapse={handleBottomPanelCollapse}
|
||||
let:isCollapsed
|
||||
>
|
||||
<div class="bottom-panel" class:collapsed={isCollapsed}>
|
||||
<Tabs selected={selectedTab} toggable on:select={selectTab}>
|
||||
<svelte:fragment slot="header-actions">
|
||||
{#if !isCollapsed}
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
aria-label="Hide bottom panel"
|
||||
on:click={handleBottomPanelCollapse}
|
||||
>
|
||||
<Icon icon={ILucideArrowDownFromLine} inline aria-hidden /> Hide
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<TabPanel title="History" shortcut={historyHotkey}>
|
||||
{#key data.filePath}
|
||||
<HistoryPanel
|
||||
bind:this={historyPanel}
|
||||
history={data.commitHistory}
|
||||
enableInlineDiff={$page.data.enableInlineDiff}
|
||||
enableViewAtCommit={$page.data.enableViewAtCommit}
|
||||
/>
|
||||
{/key}
|
||||
</TabPanel>
|
||||
<TabPanel title="References" shortcut={referenceHotkey}>
|
||||
{#if referenceQuery}
|
||||
<ReferencePanel references={referenceQuery} />
|
||||
{:else}
|
||||
<div class="info">
|
||||
<Alert variant="info"
|
||||
>Hover over a symbol and click "Find references" to find references to the
|
||||
symbol.</Alert
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
{#await data.lastCommit then lastCommit}
|
||||
{#if lastCommit && isCollapsed}
|
||||
<div class="last-commit">
|
||||
<LastCommit {lastCommit} />
|
||||
</div>
|
||||
<Tabs selected={selectedTab} toggable on:select={selectTab}>
|
||||
<svelte:fragment slot="header-actions">
|
||||
{#if !isCollapsed}
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
aria-label="Hide bottom panel"
|
||||
on:click={handleBottomPanelCollapse}
|
||||
>
|
||||
<Icon icon={ILucideArrowDownFromLine} inline aria-hidden /> Hide
|
||||
</Button>
|
||||
{:else}
|
||||
{#await data.lastCommit then lastCommit}
|
||||
{#if lastCommit && isCollapsed}
|
||||
<div class="last-commit">
|
||||
<LastCommit {lastCommit} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<TabPanel title="History" shortcut={historyHotkey}>
|
||||
{#key data.filePath}
|
||||
<HistoryPanel
|
||||
bind:this={historyPanel}
|
||||
history={data.commitHistory}
|
||||
enableInlineDiff={$page.data.enableInlineDiff}
|
||||
enableViewAtCommit={$page.data.enableViewAtCommit}
|
||||
/>
|
||||
{/key}
|
||||
</TabPanel>
|
||||
<TabPanel title="Explore" shortcut={referenceHotkey}>
|
||||
<ExplorePanel
|
||||
inputs={exploreInputs}
|
||||
connection={usagesConnection}
|
||||
treeState={exploreTreeState}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</Panel>
|
||||
@ -470,34 +478,6 @@
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-1);
|
||||
color: var(--text-body);
|
||||
|
||||
:global([data-tabs]) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&.collapsed :global([data-tabs]) {
|
||||
// Reset min-width otherwise very long commit messages will overflow
|
||||
// the tabs.
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
.last-commit {
|
||||
min-width: 0;
|
||||
max-width: min-content;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@ -3,17 +3,15 @@ import { dirname } from 'path'
|
||||
import { readable, derived, type Readable } from 'svelte/store'
|
||||
|
||||
import { CodyContextFiltersSchema, getFiltersFromCodyContextFilters } from '$lib/cody/config'
|
||||
import type { LineOrPositionOrRange } from '$lib/common'
|
||||
import { getGraphQLClient, infinityQuery, type GraphQLClient, IncrementalRestoreStrategy } from '$lib/graphql'
|
||||
import { ROOT_PATH, fetchSidebarFileTree } from '$lib/repo/api/tree'
|
||||
import { resolveRevision } from '$lib/repo/utils'
|
||||
import { parseRepoRevision } from '$lib/shared'
|
||||
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { CodyContextFiltersQuery, GitHistoryQuery, LastCommitQuery, RepoPage_PreciseCodeIntel } from './layout.gql'
|
||||
import { CodyContextFiltersQuery, GitHistoryQuery, LastCommitQuery } from './layout.gql'
|
||||
|
||||
const HISTORY_COMMITS_PER_PAGE = 20
|
||||
const REFERENCES_PER_PAGE = 20
|
||||
|
||||
export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
const client = getGraphQLClient()
|
||||
@ -78,35 +76,6 @@ export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
n => ({ first: n.length })
|
||||
),
|
||||
}),
|
||||
|
||||
// We are not extracting the selected position from the URL because that creates a dependency
|
||||
// on the full URL, which causes this loader to be re-executed on every URL change.
|
||||
getReferenceStore: (lineOrPosition: LineOrPositionOrRange & { line: number }) =>
|
||||
infinityQuery({
|
||||
client,
|
||||
query: RepoPage_PreciseCodeIntel,
|
||||
variables: resolvedRevision.then(revspec => ({
|
||||
repoName,
|
||||
revspec,
|
||||
filePath,
|
||||
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,
|
||||
})),
|
||||
map: result => {
|
||||
const references = result.data?.repository?.commit?.blob?.lsif?.references
|
||||
return {
|
||||
nextVariables: references?.pageInfo.hasNextPage
|
||||
? { afterCursor: references.pageInfo.endCursor }
|
||||
: undefined,
|
||||
data: references?.nodes,
|
||||
error: result.error,
|
||||
}
|
||||
},
|
||||
merge: (previous, next) => (previous ?? []).concat(next ?? []),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -98,7 +98,7 @@
|
||||
user-select: none;
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--primary-2);
|
||||
box-shadow: var(--focus-shadow-inner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@
|
||||
}
|
||||
nodesCopy.add(path)
|
||||
|
||||
$treeState = { focused: path, selected: path, expandedNodes: nodesCopy }
|
||||
$treeState = { focused: path, selected: path, expandedNodes: nodesCopy, disableScope: false }
|
||||
}
|
||||
|
||||
// Since context is only set once when the component is created
|
||||
@ -164,18 +164,20 @@
|
||||
div {
|
||||
overflow: auto;
|
||||
|
||||
:global([data-treeitem][aria-selected]) > :global([data-treeitem-label]) {
|
||||
:global([data-treeitem]) > :global([data-treeitem-label]) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-3);
|
||||
background-color: var(--secondary-4);
|
||||
}
|
||||
}
|
||||
|
||||
:global([data-treeitem][aria-selected='true']) > :global([data-treeitem-label]) {
|
||||
--tree-node-expand-icon-color: var(--body-bg);
|
||||
background-color: var(--primary);
|
||||
--file-icon-color: var(--body-bg);
|
||||
--tree-node-label-color: var(--body-bg);
|
||||
|
||||
background-color: var(--primary);
|
||||
&:hover {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,170 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { SourcegraphURL } from '$lib/common'
|
||||
import type { InfinityQueryStore } from '$lib/graphql'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
import Scroller from '$lib/Scroller.svelte'
|
||||
import Tooltip from '$lib/Tooltip.svelte'
|
||||
import { Alert } from '$lib/wildcard'
|
||||
import Panel from '$lib/wildcard/resizable-panel/Panel.svelte'
|
||||
import PanelGroup from '$lib/wildcard/resizable-panel/PanelGroup.svelte'
|
||||
import PanelResizeHandle from '$lib/wildcard/resizable-panel/PanelResizeHandle.svelte'
|
||||
|
||||
import FilePreview from './FilePreview.svelte'
|
||||
import type { ReferencePanel_LocationConnection, ReferencePanel_Location } from './ReferencePanel.gql'
|
||||
import ReferencePanelCodeExcerpt from './ReferencePanelCodeExcerpt.svelte'
|
||||
|
||||
export let references: InfinityQueryStore<ReferencePanel_LocationConnection['nodes']>
|
||||
|
||||
// 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 = $references.data ? unique($references.data) : []
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<PanelGroup id="references">
|
||||
<Panel id="references-list">
|
||||
<Scroller margin={600} on:more={references.fetchMore}>
|
||||
{#if !$references.fetching && !$references.error && locations.length === 0}
|
||||
<div class="info">
|
||||
<Alert variant="info">No references found.</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<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 $references.fetching}
|
||||
<div class="loader"><LoadingSpinner center /></div>
|
||||
{:else if $references.error}
|
||||
<div class="loader">
|
||||
<Alert variant="danger">Unable to load references: {$references.error.message}</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
</Panel>
|
||||
{#if previewURL}
|
||||
<PanelResizeHandle />
|
||||
<Panel defaultSize={50} id="reference-panel-preview">
|
||||
<FilePreview href={previewURL} on:close={() => (selectedLocation = null)} />
|
||||
</Panel>
|
||||
{/if}
|
||||
</PanelGroup>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
height: 100%;
|
||||
|
||||
:global([data-panel-id='reference-panel-preview']) {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
ul:not(:empty) + .loader,
|
||||
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;
|
||||
}
|
||||
|
||||
.loader {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,23 +0,0 @@
|
||||
fragment ReferencePanelCodeExcerpt_Location on Location {
|
||||
resource {
|
||||
content
|
||||
path
|
||||
commit {
|
||||
oid
|
||||
}
|
||||
repository {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
range {
|
||||
start {
|
||||
line
|
||||
character
|
||||
}
|
||||
end {
|
||||
line
|
||||
character
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
<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 CodeExcerpt from '$lib/CodeExcerpt.svelte'
|
||||
import { observeIntersection } from '$lib/intersection-observer'
|
||||
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={null} 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}
|
||||
@ -38,36 +38,3 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ body {
|
||||
--font-size-tiny: 0.8125rem;
|
||||
--code-font-size: 13px;
|
||||
--border-radius: 4px;
|
||||
--focus-shadow-inner: 0 0 0 2px var(--primary-2) inset;
|
||||
|
||||
input::placeholder {
|
||||
color: var(--text-muted);
|
||||
|
||||
@ -6,7 +6,7 @@ import { Occurrence, Position, nonOverlappingOccurrences } from '@sourcegraph/sh
|
||||
// we can't reference that type from here since it is generated by
|
||||
// Svelte-specific build tooling.
|
||||
export interface CodeGraphData {
|
||||
provenance: string
|
||||
provenance: Provenance
|
||||
commit: string
|
||||
toolInfo: {
|
||||
name: string | null
|
||||
@ -16,6 +16,8 @@ export interface CodeGraphData {
|
||||
occurrences: Occurrence[]
|
||||
}
|
||||
|
||||
export type Provenance = 'PRECISE' | 'SYNTACTIC' | 'SEARCH_BASED'
|
||||
|
||||
// IndexedCodeGraphData adds an occurrence index to the code graph
|
||||
// data, which guarantees things like non-overlapping ranges, sorted
|
||||
// ranges, and a line index for faster lookups.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user