mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 16:51:55 +00:00
feat(svelte): Add reblame support to blame column (#63727)
Closes srch-612 This commit adds a link next to the blame commit message that allows reblaming to a prior commit if available. It extends the existing blame extension. This won't have any affect on the React app because it doesn't pass the configuration option needed to add the extra gutter. Some notes: - I originally used the tooltip component instead of `title` but somehow it starts to break when scrolling the document (tooltips don't show up anymore). I don't know if CodeMirror does anything to the DOM elements that causes this to fail. - The reblame URL also selected the corresponding line so that the correct line is scrolled into view. ## Test plan Manual testing
This commit is contained in:
parent
fab128120d
commit
0fc4d2811a
1
client/web-sveltekit/src/auto-imports.d.ts
vendored
1
client/web-sveltekit/src/auto-imports.d.ts
vendored
@ -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']
|
||||
|
||||
@ -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)
|
||||
|
||||
37
client/web-sveltekit/src/lib/blame/ReblameMarker.svelte
Normal file
37
client/web-sveltekit/src/lib/blame/ReblameMarker.svelte
Normal file
@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { SourcegraphURL } from '$lib/common'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import { getURLToFileCommit, type BlameHunk } from '$lib/web'
|
||||
|
||||
export let hunk: BlameHunk
|
||||
|
||||
$: previous = hunk.commit.previous
|
||||
$: href = previous
|
||||
? SourcegraphURL.from(getURLToFileCommit($page.url.href, previous.filename, previous.rev))
|
||||
.setLineRange({
|
||||
line: hunk.startLine,
|
||||
})
|
||||
.toString()
|
||||
: null
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {href} title="Reblame prior to {hunk.rev.slice(0, 7)}">
|
||||
<Icon icon={ILucideFileStack} aria-hidden inline />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.125rem;
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
--icon-color: currentColor;
|
||||
color: var(--text-body);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
client/web-sveltekit/src/lib/blame/reblame.ts
Normal file
44
client/web-sveltekit/src/lib/blame/reblame.ts
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -33,7 +33,7 @@ interface IndexedBlameHunkData extends Pick<BlameHunkData, 'externalURLs'> {
|
||||
}
|
||||
|
||||
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), {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user