Skip to content

Commit

Permalink
Add a scrollSnapshot method
Browse files Browse the repository at this point in the history
FEATURE: The new `EditorView.scrollSnapshot` method returns an effect that
can be used to reset to a previous scroll position.

https://bugs.chromium.org/p/chromium/issues/detail?id=1498152
  • Loading branch information
marijnh committed Nov 3, 2023
1 parent 86f2cd1 commit 50ceab7
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 6 deletions.
7 changes: 7 additions & 0 deletions src/docview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,13 @@ export class DocView extends ContentView {
}

scrollIntoView(target: ScrollTarget) {
if (target.isSnapshot) {
let ref = this.view.viewState.lineBlockAt(target.range.head)
this.view.scrollDOM.scrollTop = ref.top - target.yMargin
this.view.scrollDOM.scrollLeft = target.xMargin
return
}

let {range} = target
let rect = this.coordsAt(range.head, range.empty ? range.assoc : range.head > range.anchor ? -1 : 1), other
if (!rect) return
Expand Down
24 changes: 21 additions & 3 deletions src/editorview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export interface EditorViewConfig extends EditorStateConfig {
/// from the parent.
root?: Document | ShadowRoot,
/// Pass an effect created with
/// [`EditorView.scrollIntoView`](#view.EditorView^scrollIntoView)
/// [`EditorView.scrollIntoView`](#view.EditorView^scrollIntoView) or
/// [`EditorView.scrollSnapshot`](#view.EditorView.scrollSnapshot)
/// here to set an initial scroll position.
scrollTo?: StateEffect<any>,
/// Override the way transactions are
Expand Down Expand Up @@ -198,7 +199,7 @@ export class EditorView {

this.viewState = new ViewState(config.state || EditorState.create(config))
if (config.scrollTo && config.scrollTo.is(scrollIntoView))
this.viewState.scrollTarget = config.scrollTo.value
this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state)
this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec))
for (let plugin of this.plugins) plugin.update(this)
this.observer = new DOMObserver(this)
Expand Down Expand Up @@ -303,7 +304,7 @@ export class EditorView {
main.empty ? main : EditorSelection.cursor(main.head, main.head > main.anchor ? -1 : 1))
}
for (let e of tr.effects)
if (e.is(scrollIntoView)) scrollTarget = e.value
if (e.is(scrollIntoView)) scrollTarget = e.value.clip(this.state)
}
this.viewState.update(update, scrollTarget)
this.bidiCache = CachedOrder.update(this.bidiCache, update.changes)
Expand Down Expand Up @@ -860,6 +861,23 @@ export class EditorView {
options.y, options.x, options.yMargin, options.xMargin))
}

/// Return an effect that resets the editor to its current (at the
/// time this method was called) scroll position. Note that this
/// only affects the editor's own scrollable element, not parents.
/// See also
/// [`EditorViewConfig.scrollTo`](#view.EditorViewConfig.scrollTo).
///
/// The effect should be used with a document identical to the one
/// it was created for. Failing to do so is not an error, but may
/// not scroll to the expected position. You can
/// [map](#state.StateEffect.map) the effect to account for changes.
scrollSnapshot() {
let {scrollTop, scrollLeft} = this.scrollDOM
let ref = this.viewState.scrollAnchorAt(scrollTop)
return scrollIntoView.of(new ScrollTarget(EditorSelection.cursor(ref.from), "start", "start",
ref.top - scrollTop, scrollLeft, true))
}

/// Facet to add a [style
/// module](https://github.com/marijnh/style-mod#documentation) to
/// an editor view. The view will ensure that the module is
Expand Down
17 changes: 15 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {EditorState, Transaction, ChangeSet, ChangeDesc, Facet,
StateEffect, Extension, SelectionRange, RangeSet} from "@codemirror/state"
StateEffect, Extension, SelectionRange, RangeSet, EditorSelection} from "@codemirror/state"
import {StyleModule} from "style-mod"
import {DecorationSet, Decoration} from "./decoration"
import {EditorView, DOMEventHandlers} from "./editorview"
Expand Down Expand Up @@ -45,10 +45,23 @@ export class ScrollTarget {
readonly x: ScrollStrategy = "nearest",
readonly yMargin: number = 5,
readonly xMargin: number = 5,
// This data structure is abused to also store precise scroll
// snapshots, instead of a `scrollIntoView` request. When this
// flag is `true`, `range` points at a position in the reference
// line, `yMargin` holds the difference between the top of that
// line and the top of the editor, and `xMargin` holds the
// editor's `scrollLeft`.
readonly isSnapshot = false
) {}

map(changes: ChangeDesc) {
return changes.empty ? this : new ScrollTarget(this.range.map(changes), this.y, this.x, this.yMargin, this.xMargin)
return changes.empty ? this :
new ScrollTarget(this.range.map(changes), this.y, this.x, this.yMargin, this.xMargin, this.isSnapshot)
}

clip(state: EditorState) {
return this.range.to <= state.doc.length ? this :
new ScrollTarget(EditorSelection.cursor(state.doc.length), this.y, this.x, this.yMargin, this.xMargin, this.isSnapshot)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/viewstate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ export class ViewState {

let viewportChange = !this.viewportIsAppropriate(this.viewport, bias) ||
this.scrollTarget && (this.scrollTarget.range.head < this.viewport.from ||
this.scrollTarget.range.head > this.viewport.to)
this.scrollTarget.range.head > this.viewport.to)
if (viewportChange) this.viewport = this.getViewport(bias, this.scrollTarget)
this.updateForViewport()
if ((result & UpdateFlag.Height) || viewportChange) this.updateViewportLines()
Expand Down

0 comments on commit 50ceab7

Please sign in to comment.