Last active
February 24, 2019 09:53
-
-
Save ipsod/cf432756ad8f2e112796de448b863dc9 to your computer and use it in GitHub Desktop.
neovim icon autocompletion work in progress
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from functools import lru_cache | |
import collections | |
import os | |
import time | |
import neovim | |
from fuzzywuzzy import process | |
DELAY = .100 | |
DELIMITERS = '()' | |
ZS = DELIMITERS.encode()[0] | |
ZE = DELIMITERS.encode()[1] | |
RESULTS_LIMIT = 20 | |
ICONS_FILE = os.path.split(os.path.realpath(__file__))[0] + "/icons.txt" | |
PID = 0 | |
# TODO: it breaks if wraps in middle | |
# temporarily set nowrap to fix? | |
# TODO: it currently acts weird if user moves cursor to the left inside parens | |
# TODO: it currently acts weird breaks if user moves cursor outside of(test) parens | |
class Icon: | |
def __init__(self, name, char, code): | |
self.name = name | |
self.clean_name = '-'.join(self.name.split('-')[1:]) | |
self.char = char | |
self.code = code | |
def __str__(self): | |
return f"{self.name} '{self.char}' {icon.code}" | |
icons = collections.defaultdict(list) | |
icons_by_name = {} | |
with open(ICONS_FILE) as handle: | |
for line in handle.readlines(): | |
line = line.rstrip() # remove \n | |
name, icon, code = line.split(' ') | |
icon = Icon(name, icon, code) | |
icons[icon.clean_name].append(icon) | |
icons_by_name[icon.name] = icon | |
ICONS = icons | |
ICONS_BY_NAME = icons_by_name | |
@lru_cache() | |
def search_icons(string, limit=50): | |
keys = ICONS.keys() | |
search_results = process.extract(string, keys, limit=limit) | |
results = [] | |
for result, strength in search_results: | |
results.extend(ICONS[result]) | |
return results | |
class Phrase: | |
def __init__(self, string, begin, end): | |
self.string = string | |
self.begin = begin | |
self.end = end | |
class MyNvimLine: | |
def __init__(self, nvim, line_nr): | |
self.nvim = nvim | |
self.line_nr = line_nr | |
def __getattr__(self, name): | |
if hasattr(str(self), name): | |
return getattr(str(self), name) | |
def __str__(self): | |
return str(self.nvim.current.buffer[self.line_nr]) | |
def __eq__(self, other): | |
return str(self) == str(other) | |
def __len__(self): | |
return len(str(self)) | |
def __getitem__(self, key): | |
return str(self)[key] | |
def __contains__(self, item): | |
return str(self).__contains__(item) | |
def update(self, value): | |
self.nvim.current.buffer[self.line_nr] = value | |
# TODO: make this treat multi-bit characters as 1 column | |
def col(self): | |
return self.nvim.current.window.cursor[1] | |
# TODO: make this work even if user enters delimiter characters | |
# TODO: _begin was used to keep beginning constant for one completion cycle | |
# but it was broken when refactoring to create this class | |
_begin = None | |
def phrase_inside_delimiters(self, zs=None, ze=None, pos=None): | |
zs = zs if zs else ZS | |
ze = ze if ze else ZE | |
pos = pos if pos is not None else self.col() | |
line = str(self) | |
# raise Exception(self.line_nr, line, line.encode()) | |
bline = line.encode() | |
try: | |
if self._begin is None: | |
begin = self.col() | |
while bline[begin-1] != zs: # can IndexError | |
begin -= 1 | |
self._begin = begin | |
else: | |
begin = self._begin | |
end = self.col() | |
while bline[end] != ze: # can IndexError | |
end += 1 | |
phrase = bline[begin:end+1].decode() | |
except IndexError as e: | |
raise ValueError("Cursor is not inside delimiters") | |
return Phrase(phrase, begin, end) | |
def insert_at_cursor(self, string, offset=0): | |
line = str(self) | |
line = line[:self.col()+offset] + string + line[self.col()+offset:] | |
self.update(line) | |
class MyPlugin: | |
def __init__(self, nvim): | |
self.nvim = nvim | |
_change_0 = None | |
def open_undo_block(self): | |
if self._change_0 is not None: | |
pass | |
# raise Exception('Block already open') | |
self._change_0 = self.nvim.call('changenr') | |
def revert(self): | |
changes_ago = str(self.nvim.call('changenr') - self._change_0) | |
self.nvim.command('earlier' + changes_ago) | |
self._change_0 = None | |
def move_cursor_to_column(self, column): | |
self.cursor([self.cursor()[0], column]) | |
def move_cursor_left(self, count:'-1 for all the way'=1): | |
if count >= 0: | |
return self.move_cursor_horizontal(-count) | |
elif count == -1: | |
return self.move_cursor_to_column(0) | |
else: | |
raise ValueError | |
def move_cursor_right(self, count:'-1 for all the way'=1): | |
if count >= 0: | |
return self.move_cursor_horizontal(count) | |
elif count == -1: | |
return self.move_cursor_to_column(len(self.current_line())) | |
else: | |
raise ValueError | |
def move_cursor_up(self, count:'-1 for all the way'=1): | |
if count >= 0: | |
return self.move_cursor_vertical(-count) | |
elif count == -1: | |
return self.move_cursor_to_row(0) | |
else: | |
raise ValueError | |
def move_cursor_down(self, count:'-1 for all the way'=1): | |
if count >= 0: | |
return self.move_cursor_vertical(count) | |
elif count == -1: | |
return self.move_cursor_to_row(len(self.nvim.current.buffer)) | |
else: | |
raise ValueError | |
def move_cursor_horizontal(self, count): | |
self.cursor([self.cursor()[0], self.cursor()[1]+count]) | |
def move_cursor_vertical(self, count): | |
self.cursor([self.cursor()[0]+count, self.cursor()[1]]) | |
def col(self, value): | |
if value: | |
self.cursor([self.cursor()[0], value]) | |
return self.cursor()[1] | |
def cursor(self, value=None): | |
if value is not None: | |
self.nvim.current.window.cursor = value | |
return self.nvim.current.window.cursor | |
def cursor_line_nr(self): | |
return int(self.cursor()[0]-1) or 0 | |
def cursor_line(self, value=None): | |
line = MyNvimLine(self.nvim, self.cursor_line_nr()) | |
if value: | |
line.update(value) | |
return line | |
@neovim.plugin | |
class IconSearch(MyPlugin): | |
active = False | |
def __init__(self, nvim): | |
self.nvim = nvim | |
assert type(ZS) is int | |
assert type(ZE) is int | |
@neovim.command('IconSearch', nargs='*', range='') | |
def activate(self, args, range): | |
self.open_undo_block() | |
self.original_line = self.cursor_line().insert_at_cursor(DELIMITERS) | |
self.move_cursor_right() | |
self.active = True | |
self.menu() | |
def menu(self): | |
global PID | |
PID += 1 | |
pid = PID | |
time.sleep(DELAY) | |
if (pid != PID): | |
return | |
line = self.cursor_line() | |
try: | |
phrase = line.phrase_inside_delimiters() | |
assert len(phrase.string) | |
except (ValueError, AssertionError): | |
self.deactivate() | |
return | |
# make sure user hasn't typed new input | |
if (pid != PID) or (line != self.cursor_line()): | |
return | |
self.results = self.get_results(phrase.string) | |
results = [str(r) for r in self.results] | |
c = self.nvim.call("complete", phrase.begin+1, results) | |
def get_results(self, search_string): | |
if len(search_string) < 3: | |
return [search_string[:-1]] | |
results = [search_string[:-1]] | |
for icon in search_icons(search_string[1:-1], RESULTS_LIMIT): | |
results.append(icon) | |
return results | |
@neovim.autocmd('InsertLeave') | |
@neovim.autocmd('CompleteDone') | |
def deactivate(self): | |
if not self.active: | |
return | |
self.active = False | |
self._begin = None | |
line = str(self.cursor_line()) | |
try: | |
phrase = self.cursor_line().phrase_inside_delimiters() | |
name = phrase.string.split(' ')[0] | |
except ValueError: | |
name = None | |
self.revert() | |
try: | |
icon = ICONS_BY_NAME[name] | |
self.cursor_line().insert_at_cursor(icon.char, offset=1) | |
except KeyError: | |
pass | |
self.cursor_line(line) | |
try: | |
self.col(phrase.end) | |
except NameError: | |
pass | |
@neovim.autocmd('TextChangedI') | |
@neovim.autocmd('TextChangedP') | |
def on_insertchange(self): | |
if self.active: | |
self.menu() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment