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), {