Saving cursor and scrollbar position in localStorage

I have a project in NextJS (React) and I want to save the cursor position and scrollbar into localStorage.

My first attempt was using this view.scrollDOM object. I use ReactCodeMirror and it appears that there may be some timing issue between CodeMirror is rendered and when it restores the scroll position. It’s in a different location than it was set. I’ve used DevTools to confirm the scroller.scrollTop has a different value than it was set to.

So my next approach is using the CodeMirror API, and for scrolling there is a ScrollSnapshot.

I’ve tried to just serialize the snapshot but got an error: Failed to restore scroll snapshot: TypeError: effect.is is not a function

TL;DR: But how can I save the scrollSnapshot in localStorage?

I only want to restore scrollbar and cursor position.

Do you have any documentation about scroll snapshots? Maybe some examples? The reference manual is difficult to follow.

I needed to read the source code and reference AI:

Looking at the code, I can see exactly how to replicate the scrollSnapshot() functionality using the exported APIs. The key insight is that ScrollTarget class and the scrollIntoView effect are exported by the library.

Here’s how you can implement the same functionality as scrollSnapshot():

import { EditorView, EditorSelection } from '@codemirror/view';

// Get the current scroll position
const { scrollTop, scrollLeft } = view.scrollDOM;

// Get the scroll anchor reference (you'll need to calculate this)
// The scrollSnapshot() method uses viewState. scrollAnchorAt(scrollTop)
// which finds a reference line near the top of the viewport
const ref = view.viewState.scrollAnchorAt(scrollTop);

// Create a ScrollTarget with isSnapshot=true (the last parameter)
// This is the key - the isSnapshot flag tells the view to treat this
// as a snapshot restore rather than a scroll-into-view request
const scrollEffect = EditorView.scrollIntoView(
  ref. from,  // cursor position at the reference line
  {
    y:  'start',
    x:  'start',
    yMargin: ref.top - scrollTop,  // The yMargin stores the offset from line top
    xMargin: scrollLeft  // The xMargin stores the scrollLeft position
  }
);

// However, there's a problem:  the isSnapshot flag is NOT exposed through
// the public EditorView. scrollIntoView() API

Unfortunately, there’s a limitation: The ScrollTarget constructor’s isSnapshot parameter is not accessible through the public EditorView.scrollIntoView() method. Looking at the implementation:

static scrollIntoView(pos: number | SelectionRange, options: {
  y?:  ScrollStrategy,
  x?:  ScrollStrategy,
  yMargin?: number,
  xMargin?: number,
} = {}): StateEffect<unknown> {
  return scrollIntoView.of(new ScrollTarget(typeof pos == "number" ? EditorSelection.cursor(pos) : pos,
                                            options.y, options. x, options.yMargin, options.xMargin))
}

The isSnapshot parameter is always false when using the public API.

Solution: Import ScrollTarget directly

You need to import ScrollTarget and scrollIntoView directly from the extension module:

import { EditorView, EditorSelection } from '@codemirror/view';
// You may need to import these from the internal exports
// Check if these are exported in the main package index

// If exported, you can do:
import { scrollIntoView, ScrollTarget } from '@codemirror/view';

const { scrollTop, scrollLeft } = view.scrollDOM;
const ref = view.viewState.scrollAnchorAt(scrollTop);

view.dispatch({
  selection: { anchor: position, head: position },
  effects: [
    scrollIntoView. of(
      new ScrollTarget(
        EditorSelection.cursor(ref.from),
        'start',
        'start',
        ref.top - scrollTop,  // yMargin stores the offset
        scrollLeft,           // xMargin stores scrollLeft
        true                  // isSnapshot = true
      )
    )
  ]
});

Check if exported: You’ll need to verify if ScrollTarget and scrollIntoView (the StateEffect, not the static method) are exported in the package’s main exports. If they’re not, you might need to either:

  1. Request the CodeMirror maintainers to export them
  2. Use the scrollSnapshot() method directly on the view: view.scrollSnapshot()
  3. Access them from the internal module path (not recommended as it’s not part of the public API)

The search results show these are limited to 10 results. You can view more in GitHub.