Gutter line height doesn't match the actual view line height on initial render

Implementing CodeMirror 6 in react (next.js). Everything works but the initial load seems to render the gutter numbers incorrectly.

As soon as I “click” into the textarea the gutter line heights get adjusted.

Been scouring the documentation and tried to trigger updates, but there doesn’t seem to be anything to available to mess with the initial load height.

Hook for calling codemirror

const useCodeMirror = <T extends Element>(
  props: Props
): [React.MutableRefObject<T | null>, EditorView?] => {
  const refContainer = useRef<T>(null)
  const [editorView, setEditorView] = useState<EditorView>()
  const { onChange } = props

  useEffect(() => {
    if (!refContainer.current) return

    const fixedHeightEditor = EditorView.theme({
      ".cm-gutter, .cm-content": {height: "500px"},
      ".cm-scroller": {overflow: "auto"},
      "&.cm-editor": {border: "1px solid #ddd", borderRadius: "4px"},
    })

    const startState = EditorState.create({
      doc: props.initialDoc,
      extensions: [
        basicSetup,
        keymap.of(defaultKeymap),
        markdown({
          base: markdownLanguage,
          codeLanguages: languages,
          addKeymap: true
        }),
        gutter({renderEmptyElements: true}),
        fixedHeightEditor,
        EditorView.lineWrapping,
        EditorView.updateListener.of(update => {
          if (update.heightChanged) {
            console.log('heightChanged')
          }
          if (update.changes) {
            onChange && onChange(update.state)
          }
        }),
      ]
    })

    const view = new EditorView({
      state: startState,
      parent: refContainer.current,
    })
    setEditorView(view)

    return () => {
      view.destroy()
    }
  }, [refContainer])

  return [refContainer, editorView]
}

Component snippet to call the hook

  const [refContainer, editorView] = useCodeMirror<HTMLDivElement>({
    initialDoc: values['content'],
    onChange: handleChange,
  })

  return (

    <Box mb="15px" w={{ base: '300px', md: '600px', lg: '800px' }}>
      <Heading w="100%" textAlign={'center'} fontWeight="normal">
        {contentField.label}
      </Heading>
      <Box w="full" gap={4}>
        <Box ref={refContainer}>
        </Box>
      </Box>
    </Box>
  )

If the editor’s layout isn’t stable yet on the first animation frame after creating it, this kind of thing can happen. I’m not sure what may be going on here, but if any styles are still loading, stuff is being rendered asynchronously, or an outer component is hiding or otherwise messing with the way the editor is displayed, this could happen. If you can call requestMeasure after you know everything is stable, that might help, but timing that properly first requires figuring out what’s causing the instability in the first place.

I have a decent hunch as to what I can attach the requestMeasure function to.

Any examples of implementing the requestMeasure call to just reflow the entire layout of codemirror?

Just calling view.requestMeasure() should cause it to pick up on height changes.

Unfortunately that didn’t work.

I have a useEffect hook on when the view object changes and once it is triggered, I called requestMeasure() but it didn’t change anything.

What did make it work, was sending a dispatch to re-insert the entire initial entry. It works but feels a little clunky.

I have the same problem.So, how to solve it?

I had the same problem with vue 3. my workaround was setting a timeout and refresheing the vue component

image

I tried a timeout and a refresh and unfortunately still didn’t work.

Here’s the solution I used for react (next.js specifically):

 useEffect(() => {
    if (editorView) {
      editorView.focus()
      editorView.dispatch({
        changes: {
          from: 0,
          to: editorView.state.doc.toString().length,
          insert: content,
        },
      })
    } else {
      // loading editor
    }
  }, [editorView])

Could it be that you’re on @codemirror/view 6.4.1? That had an issue related to this.

I fixed this in my app by adding a 1ms setTimeout which wraps all the code for replacing the editor.

Something like:

setTimeout(() => {
  state = new State...
  if view == null
    view = new View...
  else
    view.setState(state)
}, 1)