【Python】prompt-toolkitのtext_before_cursorとget_word_before_cursorの違い(おまけでWORD引数についての説明も)


[http-prompt](https://github.com/eliangcs/http-prompt)でそれぞれ使っていたため、自力で調べてみたけど違いがわからず、[prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit)のIssuesで質問してみました

[Question: What is the difference between text_before_cursor and get_word_before_cursor? · Issue #823 · prompt-toolkit/python-prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit/issues/823)

## 動作環境

```
> python --version
Python 3.7.1

> pip3 list | grep prompt-toolkit
prompt-toolkit         2.0.8
```

## それぞれの違い

`text_before_cursor`：
  その行のカーソル位置より前のテキストを返すプロパティ

`get_word_before_cursor`：
  (`WORD=True` (default))カーソル前の単語のまとまりを返す(Vimの`i_CTRL-W`で消去する文字列のまとまりを返す)
  (`WORD=False`)カーソル前からスペースまでの文字列を返す

## [text_before_cursor](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/81c30157422989957c8a56b333b17ba35ab1a223/prompt_toolkit/document.py#L155-L157)

その行のカーソル位置より前のテキスト

`hogehoge hogege|Hello world!`の場合、
`hogehoge hogege`の部分が取得できる

## [get_word_before_cursor](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/81c30157422989957c8a56b333b17ba35ab1a223/prompt_toolkit/document.py#L410-L425)

カーソル前の単語を取得する

`WORD`の指定によって、取得する単語が変わる

- `WORD=True` (default) カーソル前の単語のまとまりを返す(Vimの`i_CTRL-W`で消去する文字列のまとまりを返す)
- `WORD=False` カーソル前からスペースまでの文字列を返す

### `WORD=False`の場合(default)

取得する文字列の正規表現は[`_FIND_WORD_RE`](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/fc53fe9befbfdb86f6de0a2f4de14affb5d449b3/prompt_toolkit/document.py#L26)となる

```python
_FIND_WORD_RE = re.compile(r'([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)')
```

以下のどちらかの文字列

- 単語を構成する文字の文字列(`[a-zA-Z0-9_]`は`\w`と同じ意味)
- 記号の文字列(`[^a-zA-Z0-9_\s]`は`[^\w\s]`と同じ意味)

```python
>>> import re
>>> re.findall(r'([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)', 'hoge hoge--hogege   guhehe')
['hoge', 'hoge', '--', 'hogege', 'guhehe']
```

Vimの`i_CTRL-W`の単語のまとまりと同じ

### `WORD=True`の場合

取得する文字列の正規表現は[`_FIND_BIG_WORD_RE`](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/fc53fe9befbfdb86f6de0a2f4de14affb5d449b3/prompt_toolkit/document.py#L32)となる

```python
_FIND_BIG_WORD_RE = re.compile(r'([^\s]+)')
```

スペース以外の文字の文字列(スペース区切りとなる)

```python
>>> import re
>>> re.findall(r'[^\s]+', 'hoge hoge--hogege   guhehe')
['hoge', 'hoge--hogege', 'guhehe']
```

スペースで区切られた文字列のリストが取得できた

---

## `get_word_before_cursor`の処理が面白かったからメモ

- 取得文字列の開始位置をカーソルからの相対位置で取得(`find_start_of_previous_word`を呼び出す)
    - カーソル前のテキストを反転
    - `WORD`によって、取得文字列の正規表現を決める
    - マッチをテスト
    - マッチした文字列の開始位置(反転してるから`end()`で取得)
    - `-`演算子で負の数で返す(カーソルからの相対位置を返す)
- カーソルの相対位置をもとに、文字列を取得する

```python
>>> import re
# WORD=Falseとして動作確認してみる
>>> regex = re.compile(r'([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)')

# この文字列の末尾にカーソルがあると想定する
>>> text_before_cursor = 'Hello Python'

# 反転
>>> text_before_cursor_reverse = text_before_cursor[::-1]
>>> text_before_cursor_reverse
'nohtyP olleH'

# マッチのテスト
>>> iter = regex.finditer(text_before_cursor_reverse)

# 取得文字列の開始位置をカーソルからの相対位置で取得
>>> start = - next(iter).end(0)
>>> start
-6

# 文字列の取得
>>> text_before_cursor[len(text_before_cursor) + start:]
'Python'
```

単語とみなす文字列のまとまりを取得するための反転が面白い

[ソースコード](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/fc53fe9befbfdb86f6de0a2f4de14affb5d449b3/prompt_toolkit/document.py#L410-L455)

```python
    def get_word_before_cursor(self, WORD=False, pattern=None):
        """
        Give the word before the cursor.
        If we have whitespace before the cursor this returns an empty string.

        :param pattern: (None or compiled regex). When given, use this regex
            pattern.
        """
        text_before_cursor = self.text_before_cursor
        start = self.find_start_of_previous_word(WORD=WORD, pattern=pattern)

        if start is None:
            # Space before the cursor or no text before cursor.
            return ''

        return text_before_cursor[len(text_before_cursor) + start:]

    def find_start_of_previous_word(self, count=1, WORD=False, pattern=None):
        """
        Return an index relative to the cursor position pointing to the start
        of the previous word. Return `None` if nothing was found.

        :param pattern: (None or compiled regex). When given, use this regex
            pattern.
        """
        assert not (WORD and pattern)

        # Reverse the text before the cursor, in order to do an efficient
        # backwards search.
        text_before_cursor = self.text_before_cursor[::-1]

        if pattern:
            regex = pattern
        elif WORD:
            regex = _FIND_BIG_WORD_RE
        else:
            regex = _FIND_WORD_RE

        iterator = regex.finditer(text_before_cursor)

        try:
            for i, match in enumerate(iterator):
                if i + 1 == count:
                    return - match.end(0)
        except StopIteration:
            pass
```

## 参考文献

- [Question: What is the difference between text_before_cursor and get_word_before_cursor? · Issue #823 · prompt-toolkit/python-prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit/issues/823)