CodeMirror6 long text print missing the last few lines

Hello

I’m using CodeMirror6 to build my document editor, but I encountered a problem. If the document is large, when I print it, I always lose the last few lines (mostly 4 lines).
I first suspected that it was a problem with my code, so I built a very basic Demo, and the problem still exists.
Through the afterprint debugger, I found that the cm-scroller height is less than the height of cm-gutters, and I had to modify the overflow property of cm-scroller.

My environment:MacOS Chrome 130.0.6723.117(arm64) (Safari everything is ok)

This is my Demo code(Ignore this long code, just to construct more lines)

import './App.css';
import { useEffect } from 'react';
import { lineNumbers, EditorView } from "@codemirror/view"
import { EditorState } from "@codemirror/state"

function App() {
  useEffect(() => {
    const state = EditorState.create({
      doc: `
        ProseMirror
        Examples
        Documentation
        Discuss
        GitHub
        Twitter
        Embedded code editor
        It can be useful to have the in-document representation of some node, such as a code block, math formula, or image, show up as a custom editor control specifically for such content. Node views are a ProseMirror feature that make this possible.

        In this example, we set up code blocks, as they exist in the basic schema, to be rendered as instances of CodeMirror, a code editor component. The general idea is quite similar to the footnote example, but instead of popping up the node-specific editor when the user selects the node, it is always visible.

        Wiring such a node view and keymap into an editor gives us something like this:

        Insert
        Type...
        ⬚
        The code block is a code editor
        This editor has been wired up to render code blocks as instances of the CodeMirror code editor, which provides syntax highlighting, auto-indentation, and similar.

        function max(a, b) {
        return a > b ? a : b
        }
        The content of the code editor is kept in sync with the content of the code block in the rich text editor, so that it is as if you're directly editing the outer document, using a more convenient interface.

        Because we want changes in the code editor to be reflected in the ProseMirror document, our node view must flush changes to its content to ProseMirror as soon as they happen. To allow ProseMirror commands to act on the right selection, the code editor will also sync its current selection to ProseMirror.

        The first thing we do in our code block node view is create an editor with some basic extensions, a few extra key bindings, and an update listener that will do the synchronization.

        import {
        EditorView as CodeMirror, keymap as cmKeymap, drawSelection
        } from "@codemirror/view"
        import {javascript} from "@codemirror/lang-javascript"
        import {defaultKeymap} from "@codemirror/commands"
        import {syntaxHighlighting, defaultHighlightStyle} from "@codemirror/language"

        import {exitCode} from "prosemirror-commands"
        import {undo, redo} from "prosemirror-history"

        class CodeBlockView {
        constructor(node, view, getPos) {
            // Store for later
            this.node = node
            this.view = view
            this.getPos = getPos

            // Create a CodeMirror instance
            this.cm = new CodeMirror({
            doc: this.node.textContent,
            extensions: [
                cmKeymap.of([
                ...this.codeMirrorKeymap(),
                ...defaultKeymap
                ]),
                drawSelection(),
                syntaxHighlighting(defaultHighlightStyle),
                javascript(),
                CodeMirror.updateListener.of(update => this.forwardUpdate(update))
            ]
            })

            // The editor's outer node is our DOM representation
            this.dom = this.cm.dom

            // This flag is used to avoid an update loop between the outer and
            // inner editor
            this.updating = false
        }
        When the code editor is focused, translate any update that changes the document or selection to a ProseMirror transaction. The getPos that was passed to the node view can be used to find out where our code content starts, relative to the outer document (the + 1 skips the code block opening token).

        forwardUpdate(update) {
            if (this.updating || !this.cm.hasFocus) return
            let offset = this.getPos() + 1, {main} = update.state.selection
            let selFrom = offset + main.from, selTo = offset + main.to
            let pmSel = this.view.state.selection
            if (update.docChanged || pmSel.from != selFrom || pmSel.to != selTo) {
            let tr = this.view.state.tr
            update.changes.iterChanges((fromA, toA, fromB, toB, text) => {
                if (text.length)
                tr.replaceWith(offset + fromA, offset + toA,
                                schema.text(text.toString()))
                else
                tr.delete(offset + fromA, offset + toA)
                offset += (toB - fromB) - (toA - fromA)
            })
            tr.setSelection(TextSelection.create(tr.doc, selFrom, selTo))
            this.view.dispatch(tr)
            }
        }
        When adding steps to a transaction for content changes, the offset is adjusted for the changes in length caused by the change, so that further steps are created in the correct position.

        The setSelection method on a node view will be called when ProseMirror tries to put the selection inside the node. Our implementation makes sure the CodeMirror selection is set to match the position that is passed in.

        setSelection(anchor, head) {
            this.cm.focus()
            this.updating = true
            this.cm.dispatch({selection: {anchor, head}})
            this.updating = false
        }
        A somewhat tricky aspect of nesting editor like this is handling cursor motion across the edges of the inner editor. This node view will have to take care of allowing the user to move the selection out of the code editor. For that purpose, it binds the arrow keys to handlers that check if further motion would ‘escape’ the editor, and if so, return the selection and focus to the outer editor.

        The keymap also binds keys for undo and redo, which the outer editor will handle, and for ctrl-enter, which, in ProseMirror's base keymap, creates a new paragraph after a code block.

        codeMirrorKeymap() {
            let view = this.view
            return [
            {key: "ArrowUp", run: () => this.maybeEscape("line", -1)},
            {key: "ArrowLeft", run: () => this.maybeEscape("char", -1)},
            {key: "ArrowDown", run: () => this.maybeEscape("line", 1)},
            {key: "ArrowRight", run: () => this.maybeEscape("char", 1)},
            {key: "Ctrl-Enter", run: () => {
                if (!exitCode(view.state, view.dispatch)) return false
                view.focus()
                return true
            }},
            {key: "Ctrl-z", mac: "Cmd-z",
            run: () => undo(view.state, view.dispatch)},
            {key: "Shift-Ctrl-z", mac: "Shift-Cmd-z",
            run: () => redo(view.state, view.dispatch)},
            {key: "Ctrl-y", mac: "Cmd-y",
            run: () => redo(view.state, view.dispatch)}
            ]
        }

        maybeEscape(unit, dir) {
            let {state} = this.cm, {main} = state.selection
            if (!main.empty) return false
            if (unit == "line") main = state.doc.lineAt(main.head)
            if (dir < 0 ? main.from > 0 : main.to < state.doc.length) return false
            let targetPos = this.getPos() + (dir < 0 ? 0 : this.node.nodeSize)
            let selection = Selection.near(this.view.state.doc.resolve(targetPos), dir)
            let tr = this.view.state.tr.setSelection(selection).scrollIntoView()
            this.view.dispatch(tr)
            this.view.focus()
        }
        When a node update comes in from ProseMirror, for example because of an undo action, we sort of have to do the inverse of what forwardUpdate did—check for text changes, and if present, propagate them from the outer to the inner editor.

        To avoid needlessly clobbering the state of the inner editor, this method only generates a replacement for the range of the content that was changed, by comparing the start and end of the old and new content.

        update(node) {
            if (node.type != this.node.type) return false
            this.node = node
            if (this.updating) return true
            let newText = node.textContent, curText = this.cm.state.doc.toString()
            if (newText != curText) {
            let start = 0, curEnd = curText.length, newEnd = newText.length
            while (start < curEnd &&
                    curText.charCodeAt(start) == newText.charCodeAt(start)) {
                ++start
            }
            while (curEnd > start && newEnd > start &&
                    curText.charCodeAt(curEnd - 1) == newText.charCodeAt(newEnd - 1)) {
                curEnd--
                newEnd--
            }
            this.updating = true
            this.cm.dispatch({
                changes: {
                from: start, to: curEnd,
                insert: newText.slice(start, newEnd)
                }
            })
            this.updating = false
            }
            return true
        }
        The updating property is used to disable the event listener on the code editor, so that it doesn't try to forward the change (which just came from ProseMirror) back to ProseMirror.


        selectNode() { this.cm.focus() }
        stopEvent() { return true }
        }
        Handling cursor motion from the outer to the inner editor must be done with a keymap on the outer editor, because the browser's native behavior won't handle this. The arrowHandler function uses the endOfTextblock method to determine, in a bidi-text-aware way, whether the cursor is at the end of a given textblock. If it is, and the next block is a code block, the selection is moved into it.

        import {keymap} from "prosemirror-keymap"

        function arrowHandler(dir) {
        return (state, dispatch, view) => {
            if (state.selection.empty && view.endOfTextblock(dir)) {
            let side = dir == "left" || dir == "up" ? -1 : 1
            let $head = state.selection.$head
            let nextPos = Selection.near(
                state.doc.resolve(side > 0 ? $head.after() : $head.before()), side)
            if (nextPos.$head && nextPos.$head.parent.type.name == "code_block") {
                dispatch(state.tr.setSelection(nextPos))
                return true
            }
            }
            return false
        }
        }

        const arrowHandlers = keymap({
        ArrowLeft: arrowHandler("left"),
        ArrowRight: arrowHandler("right"),
        ArrowUp: arrowHandler("up"),
        ArrowDown: arrowHandler("down")
        })
        
        Backers
        Code of Conduct
        Discuss
        Report an Issue
        ProseMirror
        Examples
        Documentation
        Discuss
        GitHub
        Twitter
        Embedded code editor
        It can be useful to have the in-document representation of some node, such as a code block, math formula, or image, show up as a custom editor control specifically for such content. Node views are a ProseMirror feature that make this possible.

        In this example, we set up code blocks, as they exist in the basic schema, to be rendered as instances of CodeMirror, a code editor component. The general idea is quite similar to the footnote example, but instead of popping up the node-specific editor when the user selects the node, it is always visible.

        Wiring such a node view and keymap into an editor gives us something like this:

        Insert
        Type...
        ⬚
        The code block is a code editor
        This editor has been wired up to render code blocks as instances of the CodeMirror code editor, which provides syntax highlighting, auto-indentation, and similar.

        function max(a, b) {
        return a > b ? a : b
        }
        The content of the code editor is kept in sync with the content of the code block in the rich text editor, so that it is as if you're directly editing the outer document, using a more convenient interface.

        Because we want changes in the code editor to be reflected in the ProseMirror document, our node view must flush changes to its content to ProseMirror as soon as they happen. To allow ProseMirror commands to act on the right selection, the code editor will also sync its current selection to ProseMirror.

        The first thing we do in our code block node view is create an editor with some basic extensions, a few extra key bindings, and an update listener that will do the synchronization.

        import {
        EditorView as CodeMirror, keymap as cmKeymap, drawSelection
        } from "@codemirror/view"
        import {javascript} from "@codemirror/lang-javascript"
        import {defaultKeymap} from "@codemirror/commands"
        import {syntaxHighlighting, defaultHighlightStyle} from "@codemirror/language"

        import {exitCode} from "prosemirror-commands"
        import {undo, redo} from "prosemirror-history"

        class CodeBlockView {
        constructor(node, view, getPos) {
            // Store for later
            this.node = node
            this.view = view
            this.getPos = getPos

            // Create a CodeMirror instance
            this.cm = new CodeMirror({
            doc: this.node.textContent,
            extensions: [
                cmKeymap.of([
                ...this.codeMirrorKeymap(),
                ...defaultKeymap
                ]),
                drawSelection(),
                syntaxHighlighting(defaultHighlightStyle),
                javascript(),
                CodeMirror.updateListener.of(update => this.forwardUpdate(update))
            ]
            })

            // The editor's outer node is our DOM representation
            this.dom = this.cm.dom

            // This flag is used to avoid an update loop between the outer and
            // inner editor
            this.updating = false
        }
        When the code editor is focused, translate any update that changes the document or selection to a ProseMirror transaction. The getPos that was passed to the node view can be used to find out where our code content starts, relative to the outer document (the + 1 skips the code block opening token).

        forwardUpdate(update) {
            if (this.updating || !this.cm.hasFocus) return
            let offset = this.getPos() + 1, {main} = update.state.selection
            let selFrom = offset + main.from, selTo = offset + main.to
            let pmSel = this.view.state.selection
            if (update.docChanged || pmSel.from != selFrom || pmSel.to != selTo) {
            let tr = this.view.state.tr
            update.changes.iterChanges((fromA, toA, fromB, toB, text) => {
                if (text.length)
                tr.replaceWith(offset + fromA, offset + toA,
                                schema.text(text.toString()))
                else
                tr.delete(offset + fromA, offset + toA)
                offset += (toB - fromB) - (toA - fromA)
            })
            tr.setSelection(TextSelection.create(tr.doc, selFrom, selTo))
            this.view.dispatch(tr)
            }
        }
        When adding steps to a transaction for content changes, the offset is adjusted for the changes in length caused by the change, so that further steps are created in the correct position.

        The setSelection method on a node view will be called when ProseMirror tries to put the selection inside the node. Our implementation makes sure the CodeMirror selection is set to match the position that is passed in.

        setSelection(anchor, head) {
            this.cm.focus()
            this.updating = true
            this.cm.dispatch({selection: {anchor, head}})
            this.updating = false
        }
        A somewhat tricky aspect of nesting editor like this is handling cursor motion across the edges of the inner editor. This node view will have to take care of allowing the user to move the selection out of the code editor. For that purpose, it binds the arrow keys to handlers that check if further motion would ‘escape’ the editor, and if so, return the selection and focus to the outer editor.

        The keymap also binds keys for undo and redo, which the outer editor will handle, and for ctrl-enter, which, in ProseMirror's base keymap, creates a new paragraph after a code block.

        codeMirrorKeymap() {
            let view = this.view
            return [
            {key: "ArrowUp", run: () => this.maybeEscape("line", -1)},
            {key: "ArrowLeft", run: () => this.maybeEscape("char", -1)},
            {key: "ArrowDown", run: () => this.maybeEscape("line", 1)},
            {key: "ArrowRight", run: () => this.maybeEscape("char", 1)},
            {key: "Ctrl-Enter", run: () => {
                if (!exitCode(view.state, view.dispatch)) return false
                view.focus()
                return true
            }},
            {key: "Ctrl-z", mac: "Cmd-z",
            run: () => undo(view.state, view.dispatch)},
            {key: "Shift-Ctrl-z", mac: "Shift-Cmd-z",
            run: () => redo(view.state, view.dispatch)},
            {key: "Ctrl-y", mac: "Cmd-y",
            run: () => redo(view.state, view.dispatch)}
            ]
        }

        maybeEscape(unit, dir) {
            let {state} = this.cm, {main} = state.selection
            if (!main.empty) return false
            if (unit == "line") main = state.doc.lineAt(main.head)
            if (dir < 0 ? main.from > 0 : main.to < state.doc.length) return false
            let targetPos = this.getPos() + (dir < 0 ? 0 : this.node.nodeSize)
            let selection = Selection.near(this.view.state.doc.resolve(targetPos), dir)
            let tr = this.view.state.tr.setSelection(selection).scrollIntoView()
            this.view.dispatch(tr)
            this.view.focus()
        }
        When a node update comes in from ProseMirror, for example because of an undo action, we sort of have to do the inverse of what forwardUpdate did—check for text changes, and if present, propagate them from the outer to the inner editor.

        To avoid needlessly clobbering the state of the inner editor, this method only generates a replacement for the range of the content that was changed, by comparing the start and end of the old and new content.

        update(node) {
            if (node.type != this.node.type) return false
            this.node = node
            if (this.updating) return true
            let newText = node.textContent, curText = this.cm.state.doc.toString()
            if (newText != curText) {
            let start = 0, curEnd = curText.length, newEnd = newText.length
            while (start < curEnd &&
                    curText.charCodeAt(start) == newText.charCodeAt(start)) {
                ++start
            }
            while (curEnd > start && newEnd > start &&
                    curText.charCodeAt(curEnd - 1) == newText.charCodeAt(newEnd - 1)) {
                curEnd--
                newEnd--
            }
            this.updating = true
            this.cm.dispatch({
                changes: {
                from: start, to: curEnd,
                insert: newText.slice(start, newEnd)
                }
            })
            this.updating = false
            }
            return true
        }
        The updating property is used to disable the event listener on the code editor, so that it doesn't try to forward the change (which just came from ProseMirror) back to ProseMirror.


        selectNode() { this.cm.focus() }
        stopEvent() { return true }
        }
        Handling cursor motion from the outer to the inner editor must be done with a keymap on the outer editor, because the browser's native behavior won't handle this. The arrowHandler function uses the endOfTextblock method to determine, in a bidi-text-aware way, whether the cursor is at the end of a given textblock. If it is, and the next block is a code block, the selection is moved into it.

        import {keymap} from "prosemirror-keymap"

        function arrowHandler(dir) {
        return (state, dispatch, view) => {
            if (state.selection.empty && view.endOfTextblock(dir)) {
            let side = dir == "left" || dir == "up" ? -1 : 1
            let $head = state.selection.$head
            let nextPos = Selection.near(
                state.doc.resolve(side > 0 ? $head.after() : $head.before()), side)
            if (nextPos.$head && nextPos.$head.parent.type.name == "code_block") {
                dispatch(state.tr.setSelection(nextPos))
                return true
            }
            }
            return false
        }
        }

        const arrowHandlers = keymap({
        ArrowLeft: arrowHandler("left"),
        ArrowRight: arrowHandler("right"),
        ArrowUp: arrowHandler("up"),
        ArrowDown: arrowHandler("down")
        })
        
        Backers
        Code of Conduct
        Discuss
        Report an Issue
        `,
    extensions: [
    lineNumbers(),
    ],
    });
    const cm = new EditorView({
      state,
      parent: document.querySelector("#editor123456"),
    })
  }, [])
  return (
    <div className="App" id="editor123456">
    </div>
  );
}

export default App;

This is a phenomenon

I hava no idea about this, can anyone help me?

I can reproduce this, but only on Chrome—Firefox does fine. I’ve confirmed that the editor actually does render those last lines when a print is happening, but the browser doesn’t resize the wrapping cm-scroller the way I expect, causing those lines to be hidden. This only seems to happen in the layout done for print—if I set up the editor to render fully in a normal, non-printing situation the element has the size I expect.

I’m kind of suspecting this might be a Chrome bug, but I don’t have time to try and reduce it to a simple, library-less reproduction case at this time.

Thanks for reply.
simple, library-less seems necessary, I’m trying to do this.

Actually I couldn’t leave this alone, and it wasn’t that hard to simplify. It is definitely a Chrome issue. I’ve reported #379788104. I don’t think this is something that can reasonably be fixed by CodeMirror.

1 Like