Skip to content

Text notes: selection gets incomplete when moving multiple lines up/down #5787

@herrkami

Description

@herrkami

Description

This is related to the recent addition of shortcuts to move lines up/down in text notes in #2089 (see #5418). When selecting multiple characters in one line, the selection is maintained correctly after moving the line. However, if the selection stretches over multiple lines, only the start of the selection is preserved correctly but it will end in the same line after the move. This can be annoying when we try to move multiple lines up/down as it only works correctly for the first move. I guess @eliandoran already noticed and mentioned this quirk.

The behavior is related to how ckeditor handles offsets. I once wrote a frontend script for those two shortcuts running into the same issue. At some point I ended up with a functional version that corrects the selection again after each move. It works perfectly for me since January 2024 now:

async function get_selected_blocks(editor) {
  const blocks = editor.model.document.selection.getSelectedBlocks();
  return Array.from(blocks);
}

async function text_move_lines_up() {
  const editor = await api.getActiveContextTextEditor();
  if (editor == null) return;
  const selected_blocks = await get_selected_blocks(editor);
    
  editor.model.change( writer => {
    // Store selection offsets
    const offsets = [
      editor.model.document.selection.getFirstPosition().offset,
      editor.model.document.selection.getLastPosition().offset
    ];
    // Move items (blocks are items are nodes apperently)
    for ( const block of selected_blocks ) {
      // Get the previous position (null if none)
      let target = block.previousSibling;
      if (target != null) {
        target = writer.createPositionBefore(target);
        writer.insert(block, target);
      } else {
        console.log('Cannot move items further up, first position reached');
        break;
      }
    }
    // Restore selection to all items if many have been moved
    const range = writer.createRange(
      writer.createPositionAt(selected_blocks[0], offsets[0]),
      writer.createPositionAt(
        selected_blocks[selected_blocks.length - 1], offsets[1]));
    writer.setSelection(range);
  });
}

// Move lines down
async function text_move_lines_down() {
  const editor = await api.getActiveContextTextEditor();
  if (editor == null) return;
  const selected_blocks = await get_selected_blocks(editor);
    
  editor.model.change( writer => {
    // Remember selection offsets to restore them later
    const offsets = [
      editor.model.document.selection.getFirstPosition().offset,
      editor.model.document.selection.getLastPosition().offset
    ];

    // Move items (blocks are items are nodes apperently)
    for ( const block of selected_blocks.slice().reverse() ) {
      // Get the next position (null if none)
      const target = writer.createPositionAfter(block.nextSibling);
      if (target != null) {
        writer.insert(block, target);
      } else {
        console.log('Cannot move items further down, last position reached');
        break;
      }
    }
    // Restore selection to all items if many have been moved
    const range = writer.createRange(
      writer.createPositionAt(selected_blocks[0], offsets[0]),
      writer.createPositionAt(
        selected_blocks[selected_blocks.length - 1], offsets[1]));
    writer.setSelection(range);
  });
}

api.bindGlobalShortcut('ctrl+alt+up', text_move_lines_up);
api.bindGlobalShortcut('ctrl+alt+down', text_move_lines_down);

(Sorry for the snake cases.)

You can copy/paste this into a frontend code note, execute it (on frontend startup if you like) and test the behavior in a text note with ctrl+alt+up/down.

One quirk remains: if the selection ends or starts at the line end or start, one line is sometimes unselected after moving. This should be very easy to fix but I was too lazy so far. Plus, it can always be avoided by letting the selection start or end one character later or earlier. In the worst case you end up with the default behavior as it is currently implemented in #2089.

I remember a lot of weirdness, inconsistencies, and frustration when I implemented this. If the approach seems a bit twisted and weird, believe me, I started with a more recommended and intuitive approach. Yet that's what I ended up with.

TriliumNext Version

0.94.0

What operating system are you using?

Other Linux

What is your setup?

Local + server sync

Operating System Version

Linux 6.12.31-1-lts

Error logs

Produces no errors

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions