Converting between document positions and line and column locations

I wonder if there’s an API to translate between document positions (code point offset) and view line, column positions (which would respect line wrapping)?

My use case is I’m trying to organise vertical movement between several CodeMirror instances while keeping the cursor at the same column offset. Imagine a notebook like UI where each cell is a CodeMirror instance, now if I go to the next cell (down) then I want to end at the first line / same column, if I go to the previous cell (up) then I want to end at the last line / same column.

What I have written so far is some code to use posAtCoords/coordsAtPos to determine the column offset:

export function getEditorViewColumn(view: View.EditorView): number {
  let pos = view.state.selection.main.head;
  let left = view.coordsAtPos(pos)?.left ?? 0;
  let col = view.posAtCoords({ x: left, y: 0 });
  return col ?? 0;
}

And then setting the cursor for the desired column offset at the first/last line:

function setCursorByGoal(
  view: View.EditorView,
  goal: { line?: null | "first" | "last"; col: number }
) {
  let line = goal.line ?? "first";

  if (view.state.doc.length === 0) {
    return;
  }

  let pos = 0;
  if (line === "last") {
    let col = Math.min(goal.col, view.state.doc.length - 1);
    // The last visual line
    let lastLine: View.BlockInfo | null = null;
    view.viewportLines((line) => (lastLine = line));
    // `x` of the first line
    let x = view.coordsAtPos(col, -1)?.left;
    // `y` of the last line
    let y = (lastLine as View.BlockInfo | null)?.bottom;
    pos = view.posAtCoords({ x: x ?? 0, y: y ?? 0 }) ?? 0;
  } else {
    pos = Math.min(goal.col, view.state.doc.length - 1);
  }
  let selection = State.EditorSelection.cursor(pos);
  view.dispatch(
    view.state.update({
      selection,
      scrollIntoView: true,
      annotations: [
        State.Transaction.userEvent.of("keyboardselection"),
        FocusFromKeyboard.of(line),
      ],
    })
  );
}

But I’m wondering if there’s a cleaner way… I don’t think I’ve taken into account all edge cases here…

I think it’s best to use pixel positions for this (which is what CodeMirror does internally for vertical cursor motion), because columns are complicated a lot by tabs, glyphs that don’t correspond to characters, characters that don’t exist in your monospace font, bidirectional text, and so on. So posAtCoords and coordsAtPos are probably the most useful approach.

1 Like