question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Getting the "word" under the cursor is really, really complicated.

See original GitHub issue

Problem The problem is that I want to be able to get the word under the cursor (collapsed) and the range of that word within a block element. The problem is that slate’s Editor.blah functions don’t seem sufficient to do it without some crazy logic.

For my use-case a “word” includes the dash and dot (-,.) characters.

I’ll use ‘|’ as cursor location. If you have ‘hello| world’ and call Editor.after with the word unit, you’ll get the point after world. If you have ‘hello world|’ and you call Editor.after with the word unit, you’ll get the first point in the next block. The same applies to Editor.after

So to actually get the word under the cursor, this is the logic I have:

// Get start and end, modify it as we move along.
let [start, end] = Range.edges(selection);

// Move forward along until I hit a different tree depth
while (true) {
  const after = Editor.after(editor, end, {
    unit: 'word',
  });
  const wordAfter =
    after && Editor.string(editor, { anchor: end, focus: after });
  if (after && wordAfter && wordAfter.length && wordAfter[0] !== ' ') {
    end = after;
    if (end.offset === 0) { // Means we've wrapped to beginning of another block
      break;
    }
  } else {
    break;
  }
}

// Move backwards
while (true) {
  const before = Editor.before(editor, start, {
    unit: 'word',
  });
  const wordBefore =
    before && Editor.string(editor, { anchor: before, focus: start });
  if (
    before &&
    wordBefore &&
    wordBefore.length &&
    wordBefore[wordBefore.length - 1] !== ' '
  ) {
    start = before;
    if (start.offset === 0) { // Means we've wrapped to beginning of another block
      break;
    }
  } else {
    break;
  }
}

And then I have my word and range:

const wordRange = { anchor: start, focus: end };
const word = Editor.string(editor, wordRange);

Solution A solution would be to not include “space” as part of word boundaries. Or someway for me to tell the Editor.before/after APIs to use the word unit but include specific characters and use other characters as terminations: e.g.

Editor.after(editor, selection.anchor, { unit: 'word', include: '-._', terminateOn: ' ' });

Or to allow { edge: 'end' } in the options so that it doesn’t pass the end of the block?

Context Here’s a screen shot of a slack thread that has more details:

image

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:8
  • Comments:8

github_iconTop GitHub Comments

9reactions
williamsteincommented, Apr 2, 2021

For integration of Slate into my product, I also had to write a ridiculously complicated function to get the current word, and I expect many other people have done so as well. I’ll sure mine too, in case anybody finds it helpful when they tackle this issue. This isEqual below is from lodash.

// Expand collapsed selection to range containing exactly the
// current word, even if selection potentially spans multiple
// text nodes.  If cursor is not *inside* a word (being on edge
// is not inside) then returns undefined.  Otherwise, returns
// the Range containing the current word.
function currentWord(editor): Range | undefined {
  const {selection} = editor;
  if (selection == null || !Range.isCollapsed(selection)) {
    return; // nothing to do -- no current word.
  }
  const { focus } = selection;
  const [node, path] = Editor.node(editor, focus);
  if (!Text.isText(node)) {
    // focus must be in a text node.
    return;
  }
  const { offset } = focus;
  const siblings: any[] = Node.parent(editor, path).children as any;

  // We move to the left from the cursor until leaving the current
  // word and to the right as well in order to find the
  // start and end of the current word.
  let start = { i: path[path.length - 1], offset };
  let end = { i: path[path.length - 1], offset };
  if (offset == siblings[start.i]?.text?.length) {
    // special case when starting at the right hand edge of text node.
    moveRight(start);
    moveRight(end);
  }
  const start0 = { ...start };
  const end0 = { ...end };

  function len(node): number {
    // being careful that there could be some non-text nodes in there, which
    // we just treat as length 0.
    return node?.text?.length ?? 0;
  }

  function charAt(pos: { i: number; offset: number }): string {
    const c = siblings[pos.i]?.text?.[pos.offset] ?? "";
    return c;
  }

  function moveLeft(pos: { i: number; offset: number }): boolean {
    if (pos.offset == 0) {
      pos.i -= 1;
      pos.offset = Math.max(0, len(siblings[pos.i]) - 1);
      return true;
    } else {
      pos.offset -= 1;
      return true;
    }
    return false;
  }

  function moveRight(pos: { i: number; offset: number }): boolean {
    if (pos.offset + 1 < len(siblings[pos.i])) {
      pos.offset += 1;
      return true;
    } else {
      if (pos.i + 1 < siblings.length) {
        pos.offset = 0;
        pos.i += 1;
        return true;
      } else {
        if (pos.offset < len(siblings[pos.i])) {
          pos.offset += 1; // end of the last block.
          return true;
        }
      }
    }
    return false;
  }

  while (charAt(start).match(/\w/) && moveLeft(start)) {}
  // move right 1.
  moveRight(start);
  while (charAt(end).match(/\w/) && moveRight(end)) {}
  if (isEqual(start, start0) || isEqual(end, end0)) {
    // if at least one endpoint doesn't change, cursor was not inside a word,
    // so we do not select.
    return;
  }

  const path0 = path.slice(0, path.length - 1);
  return {
    anchor: { path: path0.concat([start.i]), offset: start.offset },
    focus: { path: path0.concat([end.i]), offset: end.offset },
  };
}
7reactions
AlexanderArvidssoncommented, May 15, 2022

I ended up writing my own stepper which goes character by character and includes options as to which characters to include.

If anyone is interested, here it is. You may have to adjust typings. Credits to @williamstein for parts of it, but it works a little bit different according to my needs (character steps, instead of word steps). It also allows you to pass in a location instead. To adjust this to match the Transforms API, maybe use an “at” property instead. I would be happy to create a PR with this after modifying it to match the rest of the Transforms API.

export function word(
  editor: CustomEditor,
  location: Range,
  options: {
    terminator?: string[]
    include?: boolean
    directions?: 'both' | 'left' | 'right'
  } = {},
): Range | undefined {
  const { terminator = [' '], include = false, directions = 'both' } = options

  const { selection } = editor
  if (!selection) return

  // Get start and end, modify it as we move along.
  let [start, end] = Range.edges(location)

  let point: Point = start

  function move(direction: 'right' | 'left'): boolean {
    const next =
      direction === 'right'
        ? Editor.after(editor, point, {
            unit: 'character',
          })
        : Editor.before(editor, point, { unit: 'character' })

    const wordNext =
      next &&
      Editor.string(
        editor,
        direction === 'right' ? { anchor: point, focus: next } : { anchor: next, focus: point },
      )

    const last = wordNext && wordNext[direction === 'right' ? 0 : wordNext.length - 1]
    if (next && last && !terminator.includes(last)) {
      point = next

      if (point.offset === 0) {
        // Means we've wrapped to beginning of another block
        return false
      }
    } else {
      return false
    }

    return true
  }

  // Move point and update start & end ranges

  // Move forwards
  if (directions !== 'left') {
    point = end
    while (move('right'));
    end = point
  }

  // Move backwards
  if (directions !== 'right') {
    point = start
    while (move('left'));
    start = point
  }

  if (include) {
    return {
      anchor: Editor.before(editor, start, { unit: 'offset' }) ?? start,
      focus: Editor.after(editor, end, { unit: 'offset' }) ?? end,
    }
  }

  return { anchor: start, focus: end }
}

Include decides whether to include the terminator. Direction allows you to specify which directions to step in.

I have two use cases for this: Emojis and Mentions. You can see how to use it here:

Mentions:

        const range =
          beforeRange &&
          word(editor, beforeRange, {
            terminator: [' ', '@'],
            directions: 'left',
            include: true,
          })

Emojis:

        const beforeWordRange =
          beforeRange &&
          word(editor, beforeRange, { terminator: [' ', ':'], include: true, directions: 'left' })
Read more comments on GitHub >

github_iconTop Results From Across the Web

Cursor Microsoft Word 2016
In Word 2016, the cursor changes to a small vertical two dot cursor ... But the two dots are so light it is...
Read more >
Placing cursor middle of word - iOS 13
Then, long-press the middle of the word, let go, and watch your cursor appear in the middle. So extra complicated!! Is there any...
Read more >
How to get the word under the cursor in Windows?
One possible solution is to trigger the other application to paint a portion of it's window by invalidating the area directly under the...
Read more >
9 ways to fix your Mac's mouse when it keeps disappearing
Sometimes it could not be that your cursor is disappearing so much as you're losing track of it because it's moving too fast....
Read more >
How to Make the Mouse Pointer and Text Cursor Bigger in ...
Text Cursor settings can be found in the Ease of Access settings bar right beneath the Mouse Pointer setting. Click on it to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found