Repository: openlawlibrary/pygls
Pull request #242 (2022-06-17 13:09:46)

Generalise word_at_position for Documents
## Description 

Hi!  I found myself re-writing slight variations of `Document.word_at_position` for "words" which aren't alphanumeric, but allowing regular expressions to be passed to the method in question means I can re-use it.  So this PR just adds a couple of parameters to `Document.word_at_position`.

Hopefully the exact behaviour should be clear from the docstrings.

The new parameters have default values set to the previous hard-coded patterns so I think this should be fully backward compatible with the same behaviour.

Thanks! :slightly_smiling_face: 

## Code review checklist (for code reviewer to complete)

- [x] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR)
- [x] Title summarizes what is changing
- [x] Commit messages are meaningful (see [this][commit messages] for details)
- [x] Tests have been included and/or updated, as appropriate
- [x] Docstrings have been included and/or updated, as appropriate
- [ ] Standalone docs have been updated accordingly
- [ ] [CONTRIBUTORS.md][contributors] was updated, as appropriate
- [x] Changelog has been updated, as needed (see [CHANGELOG.md][changelog])

[changelog]: https://github.com/openlawlibrary/pygls/blob/master/CHANGELOG.md
[commit messages]: https://chris.beams.io/posts/git-commit/
[contributors]: https://github.com/openlawlibrary/pygls/blob/master/CONTRIBUTORS.md



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8381868..220aec3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning][semver].
 
 ### Added
 
+- Allow custom word matching for `Document.word_at_point`
+
 ### Changed
 
 - Upgraded Python support to 3.10, dropping support for 3.6
diff --git a/pygls/workspace.py b/pygls/workspace.py
index ffbeabf..eb4b631 100644
--- a/pygls/workspace.py
+++ b/pygls/workspace.py
@@ -20,7 +20,7 @@ import io
 import logging
 import os
 import re
-from typing import List
+from typing import List, Pattern
 
 from pygls.lsp.types import (NumType, Position, Range, TextDocumentContentChangeEvent,
                              TextDocumentItem, TextDocumentSyncKind,
@@ -276,9 +276,30 @@ class Document(object):
                 return f.read()
         return self._source
 
-    def word_at_position(self, position: Position) -> str:
-        """
-        Get the word under the cursor returning the start and end positions.
+    def word_at_position(
+            self,
+            position: Position,
+            re_start_word: Pattern = RE_START_WORD,
+            re_end_word: Pattern = RE_END_WORD
+    ) -> str:
+        """Return the word at position.
+
+    Arguments:
+        position (Position):
+            The line and character offset.
+        re_start_word (Pattern):
+            The regular expression for extracting the word backward from
+            position.  Specifically, the first match from a re.findall
+            call on the line up to the character value of position.  The
+            default pattern is '[A-Za-z_0-9]*$'.
+        re_end_word (Pattern):
+            The regular expression for extracting the word forward from
+            position.  Specifically, the last match from a re.findall
+            call on the line from the character value of position.  The
+            default pattern is '^[A-Za-z_0-9]*'.
+
+    Returns:
+        The word (obtained by concatenating the two matches) at position.
         """
         lines = self.lines
         if position.line >= len(lines):
@@ -292,8 +313,8 @@ class Document(object):
 
         # Take end of start and start of end to find word
         # These are guaranteed to match, even if they match the empty string
-        m_start = RE_START_WORD.findall(start)
-        m_end = RE_END_WORD.findall(end)
+        m_start = re_start_word.findall(start)
+        m_end = re_end_word.findall(end)
 
         return m_start[0] + m_end[-1]
 


Test patch:
diff --git a/tests/test_document.py b/tests/test_document.py
index f9d8594..753d484 100644
--- a/tests/test_document.py
+++ b/tests/test_document.py
@@ -16,6 +16,8 @@
 # See the License for the specific language governing permissions and      #
 # limitations under the License.                                           #
 ############################################################################
+import re
+
 from pygls.lsp.types import Position, Range, TextDocumentContentChangeEvent, TextDocumentSyncKind
 from pygls.workspace import (Document, position_from_utf16, position_to_utf16, range_from_utf16,
                              range_to_utf16)
@@ -206,7 +208,7 @@ def test_offset_at_position(doc):
 
 def test_word_at_position(doc):
     """
-    Return the position under the cursor (or last in line if past the end)
+    Return word under the cursor (or last in line if past the end)
     """
     assert doc.word_at_position(Position(line=0, character=8)) == 'document'
     assert doc.word_at_position(Position(line=0, character=1000)) == 'document'
@@ -214,3 +216,15 @@ def test_word_at_position(doc):
     assert doc.word_at_position(Position(line=2, character=0)) == 'testing'
     assert doc.word_at_position(Position(line=3, character=10)) == 'unicode'
     assert doc.word_at_position(Position(line=4, character=0)) == ''
+    assert doc.word_at_position(Position(line=4, character=0)) == ''
+    re_start_word = re.compile(r'[A-Za-z_0-9.]*$')
+    re_end_word = re.compile(r'^[A-Za-z_0-9.]*')
+    assert doc.word_at_position(
+        Position(
+            line=3,
+            character=10,
+        ),
+        re_start_word=re_start_word,
+        re_end_word=re_end_word
+    ) == 'unicode.'
+