diff --git a/client/common/src/util/highlightNode.ts b/client/common/src/util/highlightNode.ts index eeb15451ca4..dec11b8d98b 100644 --- a/client/common/src/util/highlightNode.ts +++ b/client/common/src/util/highlightNode.ts @@ -150,7 +150,9 @@ function highlightNodeHelper( } let newNode: Node - if (newNodes.length === 1) { + if (newNodes.length === 0) { + newNode = document.createTextNode('') + } else if (newNodes.length === 1) { // If we only have one new node, no need to wrap it in a containing span newNode = newNodes[0] } else { diff --git a/client/shared/src/codeintel/legacy-extensions/util/api.ts b/client/shared/src/codeintel/legacy-extensions/util/api.ts index 32ff770e51f..7d9ef589918 100644 --- a/client/shared/src/codeintel/legacy-extensions/util/api.ts +++ b/client/shared/src/codeintel/legacy-extensions/util/api.ts @@ -2,6 +2,7 @@ import gql from 'tagged-template-noop' import { isErrorLike } from '@sourcegraph/common' +import { SearchVersion } from '../../../graphql-operations' import type * as sourcegraph from '../api' import { cache } from '../util' @@ -251,6 +252,7 @@ export class API { const data = await queryGraphQL(buildSearchQuery(fileLocal), { query, + version: SearchVersion.V3, }) return data.search.results.results.filter(isDefined) } @@ -322,8 +324,8 @@ function buildSearchQuery(fileLocal: boolean): string { if (fileLocal) { return gql` - query LegacyCodeIntelSearch2($query: String!) { - search(query: $query) { + query LegacyCodeIntelSearch2($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { ...SearchResults ...FileLocal } @@ -334,8 +336,8 @@ function buildSearchQuery(fileLocal: boolean): string { } return gql` - query LegacyCodeIntelSearch3($query: String!) { - search(query: $query) { + query LegacyCodeIntelSearch3($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { ...SearchResults } } diff --git a/client/web-sveltekit/BUILD.bazel b/client/web-sveltekit/BUILD.bazel index 31e568d854c..40a4baba1e5 100644 --- a/client/web-sveltekit/BUILD.bazel +++ b/client/web-sveltekit/BUILD.bazel @@ -194,7 +194,7 @@ copy_to_directory( playwright_test_bin.playwright_test( name = "e2e_test", - timeout = "short", + timeout = "long", args = [ "test", "--config $(location playwright.config.ts)", diff --git a/client/web-sveltekit/prettier.config.cjs b/client/web-sveltekit/prettier.config.cjs index 91358ed51d6..018e218ead9 100644 --- a/client/web-sveltekit/prettier.config.cjs +++ b/client/web-sveltekit/prettier.config.cjs @@ -2,5 +2,5 @@ const baseConfig = require('../../prettier.config.js') module.exports = { ...baseConfig, plugins: [...(baseConfig.plugins || []), 'prettier-plugin-svelte'], - overrides: [...(baseConfig.overrides || []), { files: '*.svelte', options: { parser: 'svelte' } }], + overrides: [...(baseConfig.overrides || []), { files: '*.svelte', options: { parser: 'svelte', htmlWhitespaceSensitivity: 'strict' } }], } diff --git a/client/web-sveltekit/src/lib/Tooltip.svelte b/client/web-sveltekit/src/lib/Tooltip.svelte index 4ab3c436b51..0cfe18ed2c3 100644 --- a/client/web-sveltekit/src/lib/Tooltip.svelte +++ b/client/web-sveltekit/src/lib/Tooltip.svelte @@ -73,12 +73,16 @@ on:mouseleave={hide} on:focusin={show} on:focusout={hide} - data-tooltip-root -> - - -{#if (alwaysVisible || visible) && target && tooltip} -
+ data-tooltip-root>
{#if (alwaysVisible || visible) && target && tooltip}
{tooltip}
diff --git a/client/web-sveltekit/src/lib/dom.ts b/client/web-sveltekit/src/lib/dom.ts index 184eba517d6..ed63f87eb40 100644 --- a/client/web-sveltekit/src/lib/dom.ts +++ b/client/web-sveltekit/src/lib/dom.ts @@ -341,58 +341,58 @@ export const portal: Action boolean; shrink: () => boolean }> = ( target, { grow, shrink } ) => { - async function resize(): Promise { - if (target.scrollWidth > target.clientWidth) { - // Shrink until we fit - while (target.scrollWidth > target.clientWidth) { - if (!shrink()) { - return - } - await tick() - } - } else { - // Grow until we overflow, then shrink once - while (target.scrollWidth <= target.clientWidth && grow()) { - await tick() - } - await tick() - if (target.scrollWidth > target.clientWidth) { - shrink() - await tick() - } - } - } - - // Resizing can (and probably will) trigger mutations, so do not trigger a - // new resize if there is a resize in progress. let resizing = false - function resizeOnce() { + async function resize(): Promise { if (resizing) { + // Growing and shrinking can cause child nodes to be added + // or removed, triggering resize observer during resizing. + // If we're already resizing, we can safely ignore those events. return } resizing = true - resize().then(() => { - resizing = false - }) + // Grow until we overflow + while (target.scrollWidth <= target.clientWidth && grow()) { + await tick() + } + await tick() + // Then shrink until we fit + while (target.scrollWidth > target.clientWidth && shrink()) { + await tick() + } + await tick() + resizing = false } - const resizeObserver = new ResizeObserver(resizeOnce) + const resizeObserver = new ResizeObserver(resize) resizeObserver.observe(target) - const mutationObserver = new MutationObserver(resizeOnce) - mutationObserver.observe(target, { attributes: true, childList: true, characterData: true, subtree: true }) + + function isElement(node: Node): node is Element { + return node.nodeType === Node.ELEMENT_NODE + } + + // If any children change size, that could trigger an overflow, so check the size again + target.childNodes.forEach(child => isElement(child) && resizeObserver.observe(child)) + const mutationObserver = new MutationObserver(mutationList => { + for (const mutation of mutationList) { + mutation.addedNodes.forEach(node => isElement(node) && resizeObserver.observe(node)) + mutation.removedNodes.forEach(node => isElement(node) && resizeObserver.unobserve(node)) + } + }) + mutationObserver.observe(target, { childList: true }) + return { update(params) { grow = params.grow shrink = params.shrink - resizeOnce() + resize() }, destroy() { resizeObserver.disconnect() diff --git a/client/web-sveltekit/src/lib/path/DisplayPath.stories.svelte b/client/web-sveltekit/src/lib/path/DisplayPath.stories.svelte new file mode 100644 index 00000000000..463f2a57812 --- /dev/null +++ b/client/web-sveltekit/src/lib/path/DisplayPath.stories.svelte @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/client/web-sveltekit/src/lib/path/DisplayPath.svelte b/client/web-sveltekit/src/lib/path/DisplayPath.svelte new file mode 100644 index 00000000000..fb1c9fbb7de --- /dev/null +++ b/client/web-sveltekit/src/lib/path/DisplayPath.svelte @@ -0,0 +1,129 @@ + + + + + + {#each parts as { part, path }, index}{@const last = + index === parts.length - 1}{#if index > 0}/{/if}{#if pathHref}{part}{:else}{part}{/if}{#if last}{/if}{/each}{#if showCopyButton}{/if} + + + diff --git a/client/web-sveltekit/src/lib/path/ShrinkablePath.stories.svelte b/client/web-sveltekit/src/lib/path/ShrinkablePath.stories.svelte new file mode 100644 index 00000000000..ef3527dfbfe --- /dev/null +++ b/client/web-sveltekit/src/lib/path/ShrinkablePath.stories.svelte @@ -0,0 +1,49 @@ + + + +
shrinkableDefault.grow(), shrink: () => shrinkableDefault.shrink() }}> + +
+
+ + +
shrinkableLinkified.grow(), shrink: () => shrinkableLinkified.shrink() }}> + + + +
+
+ + diff --git a/client/web-sveltekit/src/lib/path/ShrinkablePath.svelte b/client/web-sveltekit/src/lib/path/ShrinkablePath.svelte new file mode 100644 index 00000000000..2ec7f2a036e --- /dev/null +++ b/client/web-sveltekit/src/lib/path/ShrinkablePath.svelte @@ -0,0 +1,77 @@ + + + + part).join('/')} {showCopyButton} pathHref={scopedPathHref}> + + {#if collapsedParts.length > 0} + + + {#each collapsedParts as { part, path }} + + + {/each} + + / + {/if} + + + diff --git a/client/web-sveltekit/src/lib/path/index.ts b/client/web-sveltekit/src/lib/path/index.ts new file mode 100644 index 00000000000..bf3ce3dce55 --- /dev/null +++ b/client/web-sveltekit/src/lib/path/index.ts @@ -0,0 +1,28 @@ +import { resolveRoute } from '$app/paths' +import { encodeURIPathComponent } from '$lib/common' + +const TREE_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/tree/[...path]' +const BLOB_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]' + +export function pathHrefFactory({ + repoName, + revision, + fullPath, + fullPathType, +}: { + repoName: string + revision: string | undefined + fullPath: string + fullPathType: 'blob' | 'tree' +}): (targetPath: string) => string { + return (targetPath: string) => + resolveRoute( + // If we are targeting the last item in the path, respect the passed-in type. + // Otherwise, we know we are targeting a tree higher up in the path. + fullPath === targetPath && fullPathType === 'blob' ? BLOB_ROUTE_ID : TREE_ROUTE_ID, + { + repo: revision ? `${repoName}@${revision}` : repoName, + path: encodeURIPathComponent(targetPath), + } + ) +} diff --git a/client/web-sveltekit/src/lib/repo/FileHeader.svelte b/client/web-sveltekit/src/lib/repo/FileHeader.svelte index 7d9d5b35e3b..57444227f2a 100644 --- a/client/web-sveltekit/src/lib/repo/FileHeader.svelte +++ b/client/web-sveltekit/src/lib/repo/FileHeader.svelte @@ -1,105 +1,49 @@ -
-

- {#if collapsedBreadcrumbCount > 0} - - - - - {#each breadcrumbs.slice(0, collapsedBreadcrumbCount) as [name, path]} - - - {name} - - {/each} - - / - {/if} - {#each breadcrumbs.slice(collapsedBreadcrumbCount) as [name, path], index} - {@const last = index === breadcrumbs.length - collapsedBreadcrumbCount - 1} - - {#if index > 0} - / - {/if} - {#if last} - - {/if} - {#if path} - {name} - {:else} - {name} - {/if} - - {/each} - +
+

+ + +

@@ -118,10 +62,10 @@
{/if}

-
+ diff --git a/client/web-sveltekit/src/lib/wildcard/menu/MenuText.svelte b/client/web-sveltekit/src/lib/wildcard/menu/MenuText.svelte new file mode 100644 index 00000000000..c768964c269 --- /dev/null +++ b/client/web-sveltekit/src/lib/wildcard/menu/MenuText.svelte @@ -0,0 +1,26 @@ + + +
+ +
+ + + diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte index 9575b6dc2c3..a614b08696c 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte @@ -19,7 +19,7 @@ - + {#if $commit.value?.blob} {/if} diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte index c7563e53baf..5cf134ac7a8 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte @@ -22,6 +22,7 @@ import FileIcon from '$lib/repo/FileIcon.svelte' import { renderMermaid } from '$lib/repo/mermaid' import OpenInEditor from '$lib/repo/open-in-editor/OpenInEditor.svelte' + import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte' import Permalink from '$lib/repo/Permalink.svelte' import { createCodeIntelAPI, replaceRevisionInURL } from '$lib/shared' import { isLightTheme, settings } from '$lib/stores' @@ -36,7 +37,6 @@ import { FileViewGitBlob, FileViewHighlightedFile } from './FileView.gql' import FileViewModeSwitcher from './FileViewModeSwitcher.svelte' import OpenInCodeHostAction from './OpenInCodeHostAction.svelte' - import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte' import { CodeViewMode, toCodeViewMode } from './util' export let data: Extract @@ -166,14 +166,12 @@ {#if embedded} - - - - + + {:else} - + {#if !revisionOverride} {#await data.externalServiceType then externalServiceType} diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts index 6affc477fb1..fe7b1e22958 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts @@ -245,14 +245,14 @@ test.describe('file header', () => { await expect(page.getByRole('link', { name: 'src' })).toBeVisible() }) - test('select and copy file path', async ({ page, context }) => { + test('textContent is exactly the path', async ({ page, context }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']) await page.goto(url) - await page.getByTestId('file-header-path').selectText() - await page.keyboard.press(`Meta+KeyC`) - await page.keyboard.press(`Control+KeyC`) - const clipboardText = await page.evaluate('navigator.clipboard.readText()') - expect(clipboardText, 'path should be copied to clipboard and not contain spaces').toBe('src/readme.md') + // We specifically check the textContent here because this is what is + // used to apply highlights. It must exactly equal the path (no additional + // whitespace) or the highlights will be incorrectly offset. + const pathContainer = page.locator('css=[data-path-container]').first() + await expect(pathContainer).toHaveText(/^src\/readme.md$/) }) test('copy path button', async ({ page, context }) => { 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 eaf33ee58dc..7bd3eade30c 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 @@ -250,7 +250,7 @@ test('history panel', async ({ page, sg }) => { test('file popover', async ({ page, sg }, testInfo) => { // Test needs more time to teardown - test.setTimeout(testInfo.timeout * 3000) + test.setTimeout(testInfo.timeout * 4) await page.goto(`/${repoName}`) @@ -309,7 +309,7 @@ test('file popover', async ({ page, sg }, testInfo) => { await expect(page.getByText('Last Changed')).toBeVisible() // Click the parent dir in the popover and expect to navigate to that page - await page.locator('span').filter({ hasText: /^src$/ }).getByRole('link').click() + await page.locator('div').filter({ hasText: /^src$/ }).getByRole('link').click() await page.waitForURL(/src$/) }) diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte index e99726a31ae..cbaf572d9f3 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte @@ -87,6 +87,21 @@ entry => entry.visibility === 'user' || (entry.visibility === 'admin' && data.user?.siteAdmin) ) $: visibleNavEntryCount = viewableNavEntries.length + function grow(): boolean { + if (visibleNavEntryCount < viewableNavEntries.length) { + visibleNavEntryCount++ + return true + } + return false + } + function shrink(): boolean { + if (visibleNavEntryCount > 0) { + visibleNavEntryCount-- + return true + } + return false + } + $: navEntriesToShow = viewableNavEntries.slice(0, visibleNavEntryCount) $: overflowNavEntries = viewableNavEntries.slice(visibleNavEntryCount) $: allMenuEntries = [...overflowNavEntries, ...menuEntries] @@ -122,7 +137,10 @@ key: 'ctrl+backspace', mac: 'cmd+backspace', }, - ignoreInputFields: false, + // Ctrl/cmd+Backspace is used to delete whole words in inputs + // This would interfere e.g. with the fuzzy finder (but not the search input because + // CodeMirror handles this itself) + ignoreInputFields: true, handler: () => { goto(data.repoURL) }, @@ -135,19 +153,7 @@ -