diff --git a/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.gql b/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.gql
new file mode 100644
index 00000000000..439dc63a572
--- /dev/null
+++ b/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.gql
@@ -0,0 +1,63 @@
+query FileOrDirPopoverQuery($repoName: String!, $revision: String!, $filePath: String!) {
+ repository(name: $repoName) {
+ commit(rev: $revision) {
+ path(path: $filePath) {
+ ...on GitBlob {
+ ...FilePopoverFragment
+ }
+ ...on GitTree {
+ ...DirPopoverFragment
+ }
+ }
+ }
+ }
+}
+
+fragment FilePopoverFragment on GitBlob {
+ __typename
+ path
+ languages
+ byteSize
+ totalLines
+ history(first: 1) {
+ nodes {
+ commit {
+ ...FilePopoverLastCommitFragment
+ }
+ }
+ }
+ ...FileIcon_GitBlob
+}
+
+fragment DirPopoverFragment on GitTree {
+ __typename
+ path
+ # TODO(camdencheek): fix this to use a count, which currently does not exist in our API.
+ # This currently does not scale well with very large directories.
+ files {
+ name
+ }
+ directories {
+ name
+ }
+ history(first: 1) {
+ nodes {
+ commit {
+ ...FilePopoverLastCommitFragment
+ }
+ }
+ }
+}
+
+fragment FilePopoverLastCommitFragment on GitCommit {
+ abbreviatedOID
+ oid
+ subject
+ canonicalURL
+ author {
+ date
+ person {
+ ...Avatar_Person
+ }
+ }
+}
diff --git a/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.stories.svelte b/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.stories.svelte
new file mode 100644
index 00000000000..01e2bb60d93
--- /dev/null
+++ b/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.stories.svelte
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
diff --git a/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.svelte b/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.svelte
new file mode 100644
index 00000000000..be51681a694
--- /dev/null
+++ b/client/web-sveltekit/src/lib/repo/filePopover/FilePopover.svelte
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+ {displayRepoName(repoName).replaceAll('/', ' / ')}
+ ·
+ {dirName ? `${dirName.replaceAll('/', ' / ')}` : '/'}
+
+
+
+
+ {#if entry.__typename === 'GitBlob'}
+
+
+
{baseName}
+
+ {entry.languages[0] ? `${entry.languages[0]} ·` : ''}
+ {entry.totalLines}
+ {pluralize('Line', entry.totalLines)} ·
+ {formatBytes(entry.byteSize)}
+
+
+ {:else if entry.__typename === 'GitTree'}
+
+
+
{baseName}
+
+ Subdirectories {entry.directories.length}
+ · Files {entry.files.length}
+
+
+ {/if}
+
+
+
Last Changed @
+
+
+
+
+
diff --git a/client/web-sveltekit/src/lib/repo/filePopover/NodeLine.svelte b/client/web-sveltekit/src/lib/repo/filePopover/NodeLine.svelte
new file mode 100644
index 00000000000..ff99725b4a0
--- /dev/null
+++ b/client/web-sveltekit/src/lib/repo/filePopover/NodeLine.svelte
@@ -0,0 +1,29 @@
+
+
+
diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/+layout.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/+layout.svelte
index 29cdc450dec..cc47ec80772 100644
--- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/+layout.svelte
+++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/+layout.svelte
@@ -96,9 +96,9 @@
}
$: if (!!lastCommitQuery) {
- // Reset commit history when the query observable changes. Without
- // this we are showing the commit history of the previously selected
- // file/folder until the new commit history is loaded.
+ // Reset last commit when the query observable changes. Without
+ // this we are showing the last commit of the previously selected
+ // file/folder until the last commit is loaded.
lastCommit = null
}
diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/FileTree.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/FileTree.svelte
index b1f6b755a61..3d681ec0096 100644
--- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/FileTree.svelte
+++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/FileTree.svelte
@@ -5,12 +5,14 @@
import { goto } from '$app/navigation'
import Icon from '$lib/Icon.svelte'
+ import Popover from '$lib/Popover.svelte'
import { type FileTreeProvider, NODE_LIMIT, type TreeEntry } from '$lib/repo/api/tree'
import FileIcon from '$lib/repo/FileIcon.svelte'
+ import FilePopover, { fetchPopoverData } from '$lib/repo/filePopover/FilePopover.svelte'
import { getSidebarFileTreeStateForRepo } from '$lib/repo/stores'
import { replaceRevisionInURL } from '$lib/shared'
import TreeView, { setTreeContext } from '$lib/TreeView.svelte'
- import { createForwardStore } from '$lib/utils'
+ import { createForwardStore, delay } from '$lib/utils'
import { Alert } from '$lib/wildcard'
export let repoName: string
@@ -122,19 +124,27 @@
We handle navigation via the TreeView's select event, to preserve the focus state.
Using a link here allows us to benefit from data preloading.
-->
- {}}
- data-go-up={isRoot ? true : undefined}
- >
- {#if entry.isDirectory}
-
- {:else}
-
- {/if}
- {isRoot ? '..' : entry.name}
-
+
+ {}}
+ tabindex={-1}
+ data-go-up={isRoot ? true : undefined}
+ use:registerTrigger
+ >
+ {#if entry.isDirectory}
+
+ {:else}
+
+ {/if}
+ {isRoot ? '..' : entry.name}
+
+
+ {#await delay(fetchPopoverData({ repoName, revision, filePath: entry.path }), 300) then entry}
+
+ {/await}
+
+
{/if}
diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts
index be88a39bc71..66d1be5a193 100644
--- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts
+++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts
@@ -239,3 +239,15 @@ test('history panel', async ({ page, sg }) => {
await page.getByRole('tab', { name: 'History' }).click()
await expect(page.getByText('Test commit')).toBeHidden()
})
+
+test('file popover', async ({ page }) => {
+ await page.goto(`/${repoName}`)
+
+ await page.locator('#sidebar-panel').getByRole('button').click()
+
+ await page.getByRole('link', { name: 'index.js' }).hover()
+ await expect(page.getByText('Last Changed')).toBeVisible()
+
+ await page.getByRole('link', { name: 'Sourcegraph', exact: true }).hover()
+ await expect(page.getByText('Last Changed')).toBeHidden()
+})