diff --git a/client/web-sveltekit/src/auto-imports.d.ts b/client/web-sveltekit/src/auto-imports.d.ts index f7fff1490db..5058d483b69 100644 --- a/client/web-sveltekit/src/auto-imports.d.ts +++ b/client/web-sveltekit/src/auto-imports.d.ts @@ -44,6 +44,7 @@ declare global { 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'] const ILucideFocus: typeof import('~icons/lucide/focus')['default'] diff --git a/client/web-sveltekit/src/lib/CodeMirrorBlob.svelte b/client/web-sveltekit/src/lib/CodeMirrorBlob.svelte index 80e34cc7660..60c1c88e919 100644 --- a/client/web-sveltekit/src/lib/CodeMirrorBlob.svelte +++ b/client/web-sveltekit/src/lib/CodeMirrorBlob.svelte @@ -62,7 +62,6 @@ }, '.cm-gutterElement': { lineHeight: '1.54', - minWidth: '40px !important', '&:hover': { color: 'var(--text-body)', @@ -156,6 +155,7 @@ } from '$lib/web' import BlameDecoration from './blame/BlameDecoration.svelte' + import { ReblameMarker } from './blame/reblame' import { SearchPanel, keyboardShortcut } from './codemirror/inline-search' import { type Range, staticHighlights } from './codemirror/static-highlights' import { @@ -280,6 +280,7 @@ }, } }, + createReblameMarker: (...args) => new ReblameMarker(...args), }) : null $: blameDataExtension = blameDataFacet(blameData) diff --git a/client/web-sveltekit/src/lib/blame/ReblameMarker.svelte b/client/web-sveltekit/src/lib/blame/ReblameMarker.svelte new file mode 100644 index 00000000000..6e6a1412f49 --- /dev/null +++ b/client/web-sveltekit/src/lib/blame/ReblameMarker.svelte @@ -0,0 +1,37 @@ + + +{#if href} + + + +{/if} + + diff --git a/client/web-sveltekit/src/lib/blame/reblame.ts b/client/web-sveltekit/src/lib/blame/reblame.ts new file mode 100644 index 00000000000..8c4b29ee259 --- /dev/null +++ b/client/web-sveltekit/src/lib/blame/reblame.ts @@ -0,0 +1,44 @@ +import { EditorView, GutterMarker } from '@codemirror/view' + +import type { BlameHunk } from '$lib/web' + +import ReblameMarkerComponent from './ReblameMarker.svelte' + +export class ReblameMarker extends GutterMarker { + private marker: ReblameMarkerComponent | null = null + + // hunk can be undefined if when the data is not available yet + constructor(private line: number, private hunk: BlameHunk) { + super() + } + + public eq(other: ReblameMarker): boolean { + // Only consider two markers with the same line equal if + // hunk data is available. Otherwise the marker won't be + // update/recreated as new data becomes available. + return this.line === other.line + } + + public toDOM(_view: EditorView): Node { + console.log('ReblameMarker toDOM') + const dom = document.createElement('div') + dom.style.height = '100%' + if (this.line !== 1) { + dom.classList.add('sg-blame-border-top') + } + + if (this.hunk.commit.previous) { + this.marker = new ReblameMarkerComponent({ + target: dom, + props: { + hunk: this.hunk, + }, + }) + } + return dom + } + + public destroy(): void { + this.marker?.$destroy() + } +} diff --git a/client/web-sveltekit/src/lib/web.ts b/client/web-sveltekit/src/lib/web.ts index fdb9409d7b6..10d5b93ca59 100644 --- a/client/web-sveltekit/src/lib/web.ts +++ b/client/web-sveltekit/src/lib/web.ts @@ -49,7 +49,7 @@ export { defaultSearchModeFromSettings, defaultPatternTypeFromSettings } from '@ export type { FeatureFlagName } from '@sourcegraph/web/src/featureFlags/featureFlags' -export { parseBrowserRepoURL } from '@sourcegraph/web/src/util/url' +export { parseBrowserRepoURL, getURLToFileCommit } from '@sourcegraph/web/src/util/url' export type { EditorSettings, EditorReplacements } from '@sourcegraph/web/src/open-in-editor/editor-settings' export { type Editor, getEditor, supportedEditors } from '@sourcegraph/web/src/open-in-editor/editors' export { diff --git a/client/web/src/repo/blob/codemirror/blame-decorations.ts b/client/web/src/repo/blob/codemirror/blame-decorations.ts index a966b830d65..70634656a3e 100644 --- a/client/web/src/repo/blob/codemirror/blame-decorations.ts +++ b/client/web/src/repo/blob/codemirror/blame-decorations.ts @@ -33,7 +33,7 @@ interface IndexedBlameHunkData extends Pick { } const highlightedLineDecoration = Decoration.line({ class: 'highlighted-line' }) -const startOfHunkDecoration = Decoration.line({ class: 'border-top' }) +const startOfHunkDecoration = Decoration.line({ class: 'sg-blame-border-top' }) const highlightedLineGutterMarker = new (class extends GutterMarker { public elementClass = 'highlighted-line' @@ -268,7 +268,7 @@ class RecencyMarker extends GutterMarker { dom.className = 'sg-recency-marker' if (this.hunk) { if (this.hunk.startLine === this.line) { - dom.classList.add('border-top') + dom.classList.add('sg-blame-border-top') } dom.style.backgroundColor = getBlameRecencyColor(new Date(this.hunk.author.date), !!this.darkTheme) } @@ -276,52 +276,83 @@ class RecencyMarker extends GutterMarker { } } -const blameGutter: Extension = [ - // By default, gutters are fixed, meaning they don't scroll along with the content horizontally (position: sticky). - // We override this behavior when blame decorations are shown to make inline decorations column-like view work. - gutters({ fixed: false }), +interface BlameGutterConfig { + /** + * If set will add another gutter for reblaming. + */ + createReblameMarker?: (line: number, hunk: BlameHunk) => GutterMarker +} - // Gutter for recency indicator - gutter({ - class: 'sg-recency-gutter', - lineMarker(view, line) { - const lineNumber = view.state.doc.lineAt(line.from).number - const hunks = view.state.facet(blameDataFacet).lines - return new RecencyMarker(lineNumber, hunks[lineNumber], view.state.facet(EditorView.darkTheme)) - }, - lineMarkerChange(update) { - return ( - update.state.facet(blameDataFacet) !== update.startState.facet(blameDataFacet) || - update.state.facet(EditorView.darkTheme) !== update.startState.facet(EditorView.darkTheme) - ) - }, - }), +function blameGutter(config: BlameGutterConfig): Extension { + return [ + // By default, gutters are fixed, meaning they don't scroll along with the content horizontally (position: sticky). + // We override this behavior when blame decorations are shown to make inline decorations column-like view work. + gutters({ fixed: false }), - // Render gutter with no content only to create a column with specified background. - // This column is used by .cm-content shifted to the left by var(--blame-decoration-width) - // to achieve column-like view of inline blame decorations. - gutter({ - class: 'blame-gutter', - }), + // Gutter for recency indicator + gutter({ + class: 'sg-recency-gutter', + lineMarker(view, line) { + const lineNumber = view.state.doc.lineAt(line.from).number + const hunks = view.state.facet(blameDataFacet).lines + return new RecencyMarker(lineNumber, hunks[lineNumber], view.state.facet(EditorView.darkTheme)) + }, + lineMarkerChange(update) { + return ( + update.state.facet(blameDataFacet) !== update.startState.facet(blameDataFacet) || + update.state.facet(EditorView.darkTheme) !== update.startState.facet(EditorView.darkTheme) + ) + }, + }), - EditorView.theme({ - '.blame-gutter': { - background: 'var(--body-bg)', - width: 'var(--blame-decoration-width)', - }, - '.sg-recency-gutter': { - width: 'var(--blame-recency-width)', - minWidth: 'var(--blame-recency-width)', - }, - '.sg-recency-marker': { - position: 'relative', - height: '100%', - width: 'var(--blame-recency-width)', - }, - }), -] + config.createReblameMarker + ? gutter({ + class: 'sg-blame-reblame-gutter', + lineMarker(view, line) { + const lineNumber = view.state.doc.lineAt(line.from).number + const hunk = view.state.facet(blameDataFacet).lines[lineNumber] + return hunk && lineNumber === hunk.startLine + ? config.createReblameMarker!(lineNumber, hunk) + : null + }, + lineMarkerChange(update) { + return update.state.facet(blameDataFacet) !== update.startState.facet(blameDataFacet) + }, + }) + : [], -interface BlameConfig { + // Render gutter with no content only to create a column with specified background. + // This column is used by .cm-content shifted to the left by var(--blame-decoration-width) + // to achieve column-like view of inline blame decorations. + gutter({ + class: 'blame-gutter', + }), + + EditorView.theme({ + '.blame-gutter': { + background: 'var(--body-bg)', + width: 'var(--blame-decoration-width)', + }, + '.sg-blame-reblame-gutter': { + background: 'var(--body-bg)', + }, + '.sg-recency-gutter': { + width: 'var(--blame-recency-width)', + minWidth: 'var(--blame-recency-width)', + }, + '.sg-recency-marker': { + position: 'relative', + height: '100%', + width: 'var(--blame-recency-width)', + }, + '.sg-blame-border-top': { + borderTop: 'var(--border-width) solid var(--border-color)', + }, + }), + ] +} + +interface BlameConfig extends BlameGutterConfig { createBlameDecoration: ( container: HTMLElement, spec: { @@ -332,6 +363,7 @@ interface BlameConfig { externalURLs: BlameHunkData['externalURLs'] } ) => { destroy?: () => void } + createReblameMarker?: BlameGutterConfig['createReblameMarker'] } /** @@ -339,7 +371,7 @@ interface BlameConfig { */ export function showBlame(config: BlameConfig): Extension { return [ - blameGutter, + blameGutter(config), hoveredHunk, blameDecorationTheme, ViewPlugin.define(view => new BlameDecorationViewPlugin(view, config), {