Codemirror 6 / Single Line and/or avoid carriage return.

Hi Marijn, please let me thank you for this great work!

Please, let me know if this source piece it’s OK or it’s a bad practice to avoid carriage return to simulate a single line input:

const Editor = new EditorView({
  state: EditorState.create({
    doc,
    extensions: [
      history(),
      oneDark,
      oneDarkHighlightStyle,
      basicSetup,
      keymap.of([defaultTabBinding]),
      css(),
      EditorView.updateListener.of(update => {
        const docString = Editor.state.doc.toString();
        // Avoid carriage return
        if (update.docChanged && (docString.match(/\n/g) || []).length) {
          // Undo changes
          undo(Editor);
        } else if (
          update.docChanged &&
          me.value !== docString &&
          window.CSS.supports("width", docString)
        ) {
          // Update Vue state
          me.value = docString;
        }
      })
    ]
  }),
  parent: this.$refs.editor
});

Many thanks! :slight_smile:

A better way to do this would be a transaction filter that forbids multiline changes. Something like…

EditorState.transactionFilter.of(tr => tr.newDoc.lines > 1 ? [] : tr)
5 Likes

Thanks very much! it works!

I’ve noticed that pasting multi-lined codeblocks is filtered out using that. This works to flatten the text while letting changes go through.

EditorState.transactionFilter.of(tr => (tr.newDoc.lines > 1 ? [tr, { changes: { from: 0, to: tr.newDoc.length, insert: tr.newDoc.sliceString(0, undefined, " ") }, sequential: true }] : [tr] ))
1 Like

this works pretty well. Is there a way to preseve the history of the single line editor? Talking about ctrl+z and ctrl+shift+z action.

The default history extension should still work (?).

Thanks for the hint. I accidentally disabled the history extension

Your snippet is very effective in a minimal setup, but it might interfere with range-based states like decorations, since it replaces the whole document each time a newline were going to be inserted to document. I take another approach to carefully remove each newline character, and only do it when the transaction is a user paste event.

EditorState.transactionFilter.of(tr => {
  if (tr.changes.empty) return tr
  if (tr.newDoc.lines > 1 && !tr.isUserEvent('input.paste')) {
    return []
  }
  
  const removeNLs = []
  tr.changes.iterChanges((_fromA, _toA, fromB, _toB, ins) => {
    const lineIter = ins.iterLines().next()
    if (ins.lines <= 1) return
    // skip the first line
    let len = fromB + lineIter.value.length
    lineIter.next()
    // for the next lines, remove the leading NL
    for (; !lineIter.done; lineIter.next()) {
      removeNLs.push({from: len, to: len + 1})
      len += lineIter.value.length + 1
    }
  })

  return [ tr, { changes: removeNLs, sequential: true } ]
})

See it in action.

Edit: Updated the demo link to be in sync with the sample code.

1 Like