Skip to content

Instantly share code, notes, and snippets.

@ipsod
Last active February 24, 2019 09:53
Show Gist options
  • Save ipsod/cf432756ad8f2e112796de448b863dc9 to your computer and use it in GitHub Desktop.
Save ipsod/cf432756ad8f2e112796de448b863dc9 to your computer and use it in GitHub Desktop.
neovim icon autocompletion work in progress
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