[+] update vim, python3 plugin

1. handle escape character;
  2. handle items filtering
    based on entered letters;
  3. redraw UI when the filter
    changes;
  4. TODO
    handle selection changes via arrows, hjkl;
  5. TODO
    handle selection after Enter has been pressed;
This commit is contained in:
Siarhei Siniak 2025-10-16 17:13:50 +03:00
parent f59e0bbe6c
commit f657d63522
4 changed files with 139 additions and 93 deletions

@ -25,12 +25,14 @@ logger = logging.getLogger(__name__)
MODULE_NAME = 'online_fxreader_pr34_vim'
def future_dump_exception(future: Any) -> None:
try:
future.result()
except:
logger.exception('')
class FastSelect:
_instance: ClassVar[Optional['FastSelect']] = None
@ -45,10 +47,14 @@ class FastSelect:
self._buffer_last_used: dict[int, int] = dict()
self._filter_pattern: Optional[str] = None
self._items: Optional[list[tuple[str, int]]] = None
self._filtered_ids: Optional[set[int]] = None
self._queue: collections.deque[Callable[[], None]] = collections.deque()
self._lock = threading.Lock()
self.popup_id: Optional[int] = None
self.thread.start()
self._option_id: asyncio.Future[Optional[int]] = None
self._options: list[str] = None
@ -59,24 +65,25 @@ class FastSelect:
'close',
).capitalize()
vim.command(r'''
vim.command(r"""
func! UIThread(timer_id)
python3 online_fxreader_pr34_vim.beta.FastSelect.singleton().ui_thread()
endfunc
''')
Vim.run_command(r'''
""")
Vim.run_command(r"""
call timer_start(100, 'UIThread', {'repeat': -1})
''')
Vim.run_command(r'''
""")
Vim.run_command(
r"""
augroup {auto_group}
autocmd!
autocmd VimLeavePre * python3 online_fxreader_pr34_vim.beta.FastSelect.singleton().close()
autocmd BufEnter * python3 online_fxreader_pr34_vim.beta.FastSelect.singleton().on_buf_enter()
augroup END
'''.format(
""".format(
auto_group=auto_group,
))
)
)
def __del__(self) -> None:
self.close()
@ -96,28 +103,21 @@ augroup END
return cls._instance
def pick_option_put_id(self, option_id: int) -> None:
self.loop.call_soon_threadsafe(
lambda: self._option_id.set_result(option_id)
)
self.loop.call_soon_threadsafe(lambda: self._option_id.set_result(option_id))
async def _switch_buffer(self) -> None:
buffers_future: asyncio.Future[list[tuple[str, int]]] = asyncio.Future()
def get_buffers() -> list[tuple[str, int]]:
res = [
(o.name, o.number)
for o in vim.buffers
]
res = [(o.name, o.number) for o in vim.buffers]
res_sorted = sorted(
res,
# key=lambda x: -self._buffer_frequency.get(x[1], 0)
key=lambda x: -self._buffer_last_used.get(x[1], 0)
key=lambda x: -self._buffer_last_used.get(x[1], 0),
)
self.loop.call_soon_threadsafe(
lambda: buffers_future.set_result(res_sorted)
)
self.loop.call_soon_threadsafe(lambda: buffers_future.set_result(res_sorted))
with self._lock:
self._queue.append(get_buffers)
@ -126,8 +126,13 @@ augroup END
logger.info(dict(buffers=buffers[:3]))
self._items = buffers
with self._lock:
self._set_filter_pattern('')
selected_id = await self._pick_option_from_popup(
[o[0] for o in buffers]
# [o[0] for o in buffers]
)
logger.info(dict(selected_id=selected_id))
@ -146,25 +151,22 @@ augroup END
def switch_buffer(self) -> None:
logger.info(dict(msg='before switch_buffer started'))
result = asyncio.run_coroutine_threadsafe(
self._switch_buffer(),
self.loop
)
result = asyncio.run_coroutine_threadsafe(self._switch_buffer(), self.loop)
result.add_done_callback(future_dump_exception)
logger.info(dict(msg='after switch_buffer started'))
async def _pick_option_from_popup(
self,
options: list[str],
# options: list[str],
) -> Optional[int]:
logger.info(dict(msg='started'))
self._filter_pattern = ''
self._popup_id = None
self._options = options
# self._options = options
self._option_id = asyncio.Future[int]()
@ -192,7 +194,7 @@ augroup END
#'''.format(datetime.datetime.now().isoformat()))
while len(self._queue) > 0:
cmd = self._queue.pop();
cmd = self._queue.pop()
logger.warning(dict(msg='start command', cmd=inspect.getsource(cmd)))
try:
cmd()
@ -208,7 +210,7 @@ augroup END
buf_number=vim.current.buffer.number,
buf_name=pathlib.Path(vim.current.buffer.name),
),
self.loop
self.loop,
)
result.add_done_callback(future_dump_exception)
@ -216,6 +218,18 @@ augroup END
def on_filter_key(self, key: str) -> None:
logger.info(dict(msg='got key', key=key))
if key == bytes([27]):
logger.info(dict(msg='closing popup'))
vim.Function('popup_close')(self._popup_id)
return 1
if key == b'\x80kb':
logger.info(dict(msg='backspace'))
with self._lock:
self._set_filter_pattern(self._filter_pattern[:-1])
else:
try:
key_str = key.decode('utf-8')
except:
@ -223,12 +237,31 @@ augroup END
if not key_str.isprintable():
return 0
else:
with self._lock:
self._filter_pattern += key_str
self._set_filter_pattern(self._filter_pattern + key_str)
self._update_popup()
return 1
def _set_filter_pattern(self, filter_pattern: str) -> None:
self._filter_pattern = filter_pattern
pattern = re.compile(self._filter_pattern)
self._filtered_ids = [i for i, o in enumerate(self._items) if not pattern.search(o[0]) is None]
self._options = [self._items[o][0] for o in self._filtered_ids]
def _update_popup(self) -> None:
vim.Function('popup_settext')(
self._popup_id,
self._options,
)
vim.Function('popup_setoptions')(self._popup_id, {'title': 'Select a file, [%s]' % self._filter_pattern})
async def _on_buf_enter(
self,
buf_number: int,
@ -246,12 +279,14 @@ augroup END
self._buffer_last_used[buf_number] = datetime.datetime.now().timestamp()
logger.info(dict(
logger.info(
dict(
msg='updated',
buf_path=str(buf_name),
frequency=self._buffer_frequency[buf_number],
buf_number=buf_number,
))
)
)
async def _pick_option_start_popup(
self,
@ -271,7 +306,8 @@ augroup END
if int(vim.eval('exists("{}")'.format(callback_name))) == 1:
logger.warning(dict(msg='callback already defined, %s' % callback_name))
vim.command(r"""
vim.command(
r"""
function! {callback_name}(id, result)
if a:result > 0
call py3eval('online_fxreader_pr34_vim.beta.FastSelect.singleton().pick_option_put_id(' . (a:result - 1). ')')
@ -281,23 +317,26 @@ augroup END
endfunction
""".format(
callback_name=callback_name,
))
)
)
vim.command(r"""
vim.command(
r"""
function! {filter_name}(win_id, key)
return py3eval('online_fxreader_pr34_vim.beta.FastSelect.singleton().on_filter_key(key)', #{key: a:key})
endfunction
""".replace(
'{filter_name}', filter_name,
))
'{filter_name}',
filter_name,
)
)
logger.info(dict(msg='before popup'))
popup_menu = vim.Function('popup_menu')
with self._lock:
self._queue.append(
lambda : popup_menu(
def create_popup():
self._popup_id = popup_menu(
self._options,
{
'title': 'Select a file',
@ -309,8 +348,12 @@ augroup END
'resize': 1,
'drag': 1,
'maxheight': '16',
}
},
)
with self._lock:
self._queue.append(
create_popup,
# lambda : vim.command(
# "call popup_menu({options}, {'title': '{title}', 'callback': '{callback}'})".replace(
# '{options}', '[%s]' % ','.join([
@ -330,5 +373,6 @@ augroup END
# logger.info(dict(msg='after popup'))
def init():
FastSelect.singleton()

@ -27,6 +27,7 @@ from .utils import Vim
MODULE_NAME = 'online_fxreader_pr34_vim'
def f1():
t1 = vim.current.window
t2 = t1.width
@ -163,12 +164,12 @@ class EditorConfigModeline:
dict[str, str],
] = dict()
Vim.run_command(r'''
Vim.run_command(r"""
augroup EditorConfigModeline
autocmd!
autocmd BufEnter * python3 import online_fxreader_pr34_vim.main; online_fxreader_pr34_vim.main.EditorConfigModeline.singleton().on_buffer()
augroup END
''')
""")
@classmethod
def singleton(cls) -> Self:
@ -255,7 +256,9 @@ augroup END
# raise NotImplementedError
# EditorConfigModeline.singleton()
def init():
EditorConfigModeline.singleton()

@ -1,5 +1,6 @@
import vim
class Vim:
@classmethod
def run_command(cls, cmd) -> list[str]:
@ -9,8 +10,6 @@ class Vim:
for line in cmd.splitlines():
if line.strip() == '':
continue
output.append(
vim.command(line)
)
output.append(vim.command(line))
return output

@ -81,7 +81,7 @@ include = [
'./*.py',
'online/**/*.py',
'online/**/*.pyi',
'../dotfiles/.module.vimrc.py',
'../dotfiles/.vim/**/*.py',
]
exclude = [
'.venv',