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

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

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

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