Lock to the first visible line of code when switching git blame on/off (#46684)

This commit is contained in:
Philipp Spiess 2023-01-20 10:52:35 +01:00 committed by GitHub
parent 4f2f4b2073
commit b724b071ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 43 additions and 32 deletions

View File

@ -5,7 +5,7 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { openSearchPanel } from '@codemirror/search'
import { Compartment, EditorState, Extension, Line } from '@codemirror/state'
import { Compartment, EditorState, Extension } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { isEqual } from 'lodash'
@ -26,7 +26,8 @@ import { blobPropsFacet } from './codemirror'
import { createBlameDecorationsExtension } from './codemirror/blame-decorations'
import { syntaxHighlight } from './codemirror/highlight'
import { pin, updatePin } from './codemirror/hovercard'
import { selectableLineNumbers, SelectedLineRange, selectLines, shouldScrollIntoView } from './codemirror/linenumbers'
import { selectableLineNumbers, SelectedLineRange, selectLines } from './codemirror/linenumbers'
import { lockFirstVisibleLine } from './codemirror/lock-line'
import { navigateToLineOnAnyClickExtension } from './codemirror/navigate-to-any-line-on-click'
import { search } from './codemirror/search'
import { sourcegraphExtensions } from './codemirror/sourcegraph-extensions'
@ -277,7 +278,8 @@ export const Blob: React.FunctionComponent<BlobProps> = props => {
// Update blame decorations
useLayoutEffect(() => {
if (editor) {
editor.dispatch({ effects: blameDecorationsCompartment.reconfigure(blameDecorations) })
const effects = [blameDecorationsCompartment.reconfigure(blameDecorations), ...lockFirstVisibleLine(editor)]
editor.dispatch({ effects })
}
// editor is not provided because this should only be triggered after the
// editor was created (i.e. not on first render)
@ -297,14 +299,7 @@ export const Blob: React.FunctionComponent<BlobProps> = props => {
// Update line wrapping
useEffect(() => {
if (editor) {
const effects = [wrapCodeCompartment.reconfigure(wrapCodeSettings)]
const firstLine = firstVisibleLine(editor)
if (firstLine) {
// Avoid jumpy scrollbar when enabling line wrapping by forcing the
// scroll bar to preserve the top line number that's visible.
// Details https://github.com/sourcegraph/sourcegraph/issues/41413
effects.push(EditorView.scrollIntoView(firstLine.from, { y: 'start' }))
}
const effects = [wrapCodeCompartment.reconfigure(wrapCodeSettings), ...lockFirstVisibleLine(editor)]
editor.dispatch({ effects })
}
// editor is not provided because this should only be triggered after the
@ -377,24 +372,3 @@ function useDistinctBlob(blobInfo: BlobInfo): BlobInfo {
return blobRef.current
}, [blobInfo])
}
// Returns the first line that is visible in the editor. We can't directly use
// the viewport for this functionality because the viewport includes lines that
// are rendered but not visible.
function firstVisibleLine(view: EditorView): Line | undefined {
for (const { from, to } of view.visibleRanges) {
for (let pos = from; pos < to; ) {
const line = view.state.doc.lineAt(pos)
// This may be an inefficient way to detect the first visible line
// but it appears to work correctly and this is unlikely to be a
// performance bottleneck since we should only use need to compute
// this for infrequently used code-paths like when enabling/disabling
// line wrapping, or when lazy syntax highlighting gets loaded.
if (!shouldScrollIntoView(view, { line: line.number + 1 })) {
return line
}
pos = line.to + 1
}
}
return undefined
}

View File

@ -0,0 +1,37 @@
import { Line, StateEffect } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { shouldScrollIntoView } from './linenumbers'
// Avoid jumpy scrollbar when the line dimensions change by locking on to the
// first visible line.
//
// Details https://github.com/sourcegraph/sourcegraph/issues/41413
export function lockFirstVisibleLine(view: EditorView): StateEffect<unknown>[] {
const firstLine = firstVisibleLine(view)
if (firstLine) {
return [EditorView.scrollIntoView(firstLine.from, { y: 'start' })]
}
return []
}
// Returns the first line that is visible in the editor. We can't directly use
// the viewport for this functionality because the viewport includes lines that
// are rendered but not visible.
function firstVisibleLine(view: EditorView): Line | undefined {
for (const { from, to } of view.visibleRanges) {
for (let pos = from; pos < to; ) {
const line = view.state.doc.lineAt(pos)
// This may be an inefficient way to detect the first visible line
// but it appears to work correctly and this is unlikely to be a
// performance bottleneck since we should only use need to compute
// this for infrequently used code-paths like when enabling/disabling
// line wrapping, or when lazy syntax highlighting gets loaded.
if (!shouldScrollIntoView(view, { line: line.number + 1 })) {
return line
}
pos = line.to + 1
}
}
return undefined
}