about summary refs log tree commit diff
path: root/saneterm/completion.py
diff options
context:
space:
mode:
authorSören Tempel <soeren+git@soeren-tempel.net>2021-05-31 04:57:44 +0200
committerSören Tempel <soeren+git@soeren-tempel.net>2021-05-31 04:57:44 +0200
commit3ad5afdfb265c29ac32766ee2023a77c9a4ad978 (patch)
tree3e46057041d0f24826a904236a541abf405611cc /saneterm/completion.py
parent170d4f003f2b530a6e892b33caaf71baa258d754 (diff)
Preliminary tab completion support
Diffstat (limited to 'saneterm/completion.py')
-rw-r--r--saneterm/completion.py93
1 files changed, 93 insertions, 0 deletions
diff --git a/saneterm/completion.py b/saneterm/completion.py
new file mode 100644
index 0000000..888c567
--- /dev/null
+++ b/saneterm/completion.py
@@ -0,0 +1,93 @@
+import os
+
+class TabComp():
+    "Implements a state machine for tab completions on a Gtk.TextBuffer"
+
+    def __init__(self, buffer, compfn):
+        self.__buffer = buffer
+        self.__compfn = compfn
+
+        self._tabcomp_mark = None
+        self._tabcomp_index = 0
+
+    def reset(self):
+        """Invalidates any cached completion results. Should be called
+           when the user presses any key other than the key configured
+           for tab completions."""
+
+        self._tabcomp_mark = None
+
+    def next(self, start):
+        "Completes the word starting at the given start iterator."
+
+        buffer = self.__buffer
+
+        # Distinguish two cases:
+        #   1. This is the first time the user pressed tab.
+        #      If so: Determine input between word start and end.
+        #   2. User is switching through generated matches.
+        #      If so: Determine input between word start and _tabcomp_mark.
+        if self._tabcomp_mark is None:
+            # Reset has been called → invalidate completion cache
+            self._tabcomp_matches = None
+            self._tabcomp_index = 0
+
+            end = buffer.get_iter_at_offset(buffer.props.cursor_position)
+            self._tabcomp_mark = buffer.create_mark(None, end, True)
+        else:
+            end = buffer.get_iter_at_mark(self._tabcomp_mark)
+
+        # Extract text, regenerate completion results for
+        # given text if cache has been invalidated above.
+        text = buffer.get_text(start, end, True)
+        if self._tabcomp_matches is None:
+            self._tabcomp_matches = self.__compfn(text)
+            self._tabcomp_matches.append("") # original text
+        c = self._tabcomp_matches[self._tabcomp_index]
+
+        # Insert the matched completion text and delete
+        # text potentially remaining from older completion.
+        buffer.insert(end, c)
+        cursor = buffer.get_iter_at_offset(buffer.props.cursor_position)
+        buffer.delete(end, cursor)
+
+        # Advance current index in matches and wrap-around.
+        self._tabcomp_index = (self._tabcomp_index + 1) % len(self._tabcomp_matches)
+
+class FileName():
+    "Provides file name completions relative to a given directory."
+
+    def __init__(self, cwd):
+        self.__cwd = cwd
+
+    def get_matches(self, input):
+        if input.find("/") != -1:
+            base = os.path.dirname(input)
+            prefix = os.path.basename(input)
+
+            if not os.path.abspath(input):
+                base = os.path.join(self.__cwd, base)
+        else:
+            base = self.__cwd
+            prefix = input
+
+        return self.__get_matches(base, prefix)
+
+    def __get_matches(self, base, prefix):
+        if not os.path.isdir(base):
+            return []
+
+        matches = []
+        with os.scandir(base) as it:
+            for entry in it:
+                name = entry.name
+                if prefix != "" and not name.startswith(prefix):
+                    continue
+                if entry.is_dir():
+                    name += "/"
+
+                # Strip prefix from name
+                name = name[len(prefix):]
+                matches.append(name)
+
+        return matches