diff --git a/client/web/src/repo/blob/CodeMirrorBlob.tsx b/client/web/src/repo/blob/CodeMirrorBlob.tsx index 562366f087d..c7d68f6907f 100644 --- a/client/web/src/repo/blob/CodeMirrorBlob.tsx +++ b/client/web/src/repo/blob/CodeMirrorBlob.tsx @@ -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 = 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 = 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 -} diff --git a/client/web/src/repo/blob/codemirror/lock-line.ts b/client/web/src/repo/blob/codemirror/lock-line.ts new file mode 100644 index 00000000000..662959dd6bf --- /dev/null +++ b/client/web/src/repo/blob/codemirror/lock-line.ts @@ -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[] { + 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 +}