-
-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add tab completion for file paths in save/open prompts #323
base: main
Are you sure you want to change the base?
Changes from 3 commits
ecece9b
c6f9ad0
3d5d498
77943fc
bd003e1
0541559
9fae655
d3a5bd8
4dffbc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ | |
|
||
import curses | ||
import enum | ||
import glob | ||
import os | ||
from typing import TYPE_CHECKING | ||
|
||
from babi.horizontal_scrolling import line_x | ||
|
@@ -14,12 +16,20 @@ | |
|
||
|
||
class Prompt: | ||
def __init__(self, screen: Screen, prompt: str, lst: list[str]) -> None: | ||
def __init__( | ||
self, | ||
screen: Screen, | ||
prompt: str, lst: list[str], | ||
file_glob: bool = False, | ||
) -> None: | ||
self._screen = screen | ||
self._prompt = prompt | ||
self._lst = lst | ||
self._y = len(lst) - 1 | ||
self._x = len(self._s) | ||
self._dispatch = Prompt.DISPATCH.copy() | ||
if file_glob: | ||
self._dispatch[b'^I'] = Prompt._file_complete | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unknown keys are already ignored in prompts -- the completion callback should just check if it's enabled or not rather than having to copy this dict every time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think I understand this point - a key seems to be considered "known" as soon as it's in the dispatch dict, so I can't just add it globally. If I didn't copy the dict, I'd be modifying the global Also, for the callback to check on whether globbing is enabled, i'd have to make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm saying you should handle the option inside the callback |
||
|
||
@property | ||
def _s(self) -> str: | ||
|
@@ -95,6 +105,17 @@ def _delete(self) -> None: | |
def _cut_to_end(self) -> None: | ||
self._s = self._s[:self._x] | ||
|
||
def _file_complete(self) -> None: | ||
partial = self._s[:self._x] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this doesn't seem quite right -- it's going to take imagine foo.txt is on disk and you've written There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sometimes, this is what I want, and I use this with tools that allow it. This is especially useful to me to create a new file in an existing folder. I understand this is a question of preference, I'd personally prefer this behaviour to stay as is. [I usually add a space to the right of the cursor before hitting tab for visual separation and then delete it after completion has done its job, but that uses the same mechanic. I doubt we'd want to check for whitespace after the cursor and make it conditional] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in that case it could listen to tab if it is at the end of the string or before a path separator I guess? |
||
completions = glob.glob(partial + '*') | ||
InsanePrawn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if not completions: | ||
return | ||
common = os.path.commonprefix(completions) | ||
if not common or common == partial: | ||
return | ||
self._s = common + self._s[self._x:] # don't eat text behind cursor | ||
self._x = len(common) | ||
|
||
def _resize(self) -> None: | ||
self._screen.resize() | ||
|
||
|
@@ -180,8 +201,8 @@ def run(self) -> PromptResult | str: | |
self._render_prompt() | ||
|
||
key = self._screen.get_char() | ||
if key.keyname in Prompt.DISPATCH: | ||
ret = Prompt.DISPATCH[key.keyname](self) | ||
if key.keyname in self._dispatch: | ||
ret = self._dispatch[key.keyname](self) | ||
if ret is not None: | ||
return ret | ||
elif key.keyname == b'STRING': | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -397,6 +397,7 @@ def prompt( | |
history: str | None = None, | ||
default_prev: bool = False, | ||
default: str | None = None, | ||
file_glob: bool = False, | ||
) -> str | PromptResult: | ||
default = default or '' | ||
self.status.clear() | ||
|
@@ -407,7 +408,7 @@ def prompt( | |
else: | ||
history_data = [default] | ||
|
||
ret = Prompt(self, prompt, history_data).run() | ||
ret = Prompt(self, prompt, history_data, file_glob=file_glob).run() | ||
|
||
if ret is not PromptResult.CANCELLED and history is not None: | ||
if ret: # only put non-empty things in history | ||
|
@@ -720,7 +721,9 @@ def save(self) -> PromptResult | None: | |
# TODO: strip trailing whitespace? | ||
# TODO: save atomically? | ||
if self.file.filename is None: | ||
filename = self.prompt('enter filename') | ||
filename = self.prompt( | ||
'enter filename', default=self.file.filename, file_glob=True, | ||
) | ||
Comment on lines
+729
to
+731
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this changes the behaviour. these shouldn't have a default |
||
if filename is PromptResult.CANCELLED: | ||
return PromptResult.CANCELLED | ||
else: | ||
|
@@ -762,15 +765,19 @@ def save(self) -> PromptResult | None: | |
return None | ||
|
||
def save_filename(self) -> PromptResult | None: | ||
response = self.prompt('enter filename', default=self.file.filename) | ||
response = self.prompt( | ||
'enter filename', default=self.file.filename, file_glob=True, | ||
) | ||
if response is PromptResult.CANCELLED: | ||
return PromptResult.CANCELLED | ||
else: | ||
self.file.filename = response | ||
return self.save() | ||
|
||
def open_file(self) -> EditResult | None: | ||
response = self.prompt('enter filename', history='open') | ||
response = self.prompt( | ||
'enter filename', history='open', file_glob=True, | ||
) | ||
if response is not PromptResult.CANCELLED: | ||
opened = File( | ||
response, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,37 @@ def test_open(run, tmpdir): | |
|
||
h.press('^X') | ||
h.await_text('hello world') | ||
h.press('^X') | ||
h.await_exit() | ||
|
||
|
||
def test_file_glob(run, tmpdir): | ||
base = 'globtest' | ||
prefix = base + 'ffff.txt' | ||
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rather than variables I'd rather see the actual strings in the assertions -- rather than having to hunt around for what is actually being tested the expressions should be clear in tests. https://testing.googleblog.com/2014/07/testing-on-toilet-dont-put-logic-in.html |
||
f = tmpdir.join(prefix + 'f') | ||
f.write('hello world') | ||
g = tmpdir.join(base + 'fggg') | ||
g.write('goodbye world') | ||
nonexistant = str(tmpdir.join('NONEXISTANT')) | ||
|
||
with run(str(g)) as h: | ||
h.await_text('goodbye world') | ||
|
||
h.press('^P') | ||
h.press(nonexistant) | ||
h.press('Tab') | ||
h.await_text(f'«{nonexistant[-7:]}') | ||
h.press('^C') | ||
h.await_text('cancelled') | ||
h.press('^P') | ||
h.press(str(tmpdir.join(base + 'fff'))) | ||
h.press('Tab') | ||
h.await_text(str(f)[-7:]) | ||
h.press('Tab') # second tab shouldn't change anything | ||
h.await_text(str(f)[-7:]) | ||
h.press('Enter') | ||
h.await_text('[2/2]') | ||
h.await_text('hello world') | ||
h.press('^X') | ||
h.press('^X') | ||
h.await_exit() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a boolean trap -- it should be named-only if anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, not sure I understand what exactly you'd like me to do here.
Something like
lst: list[str], *, file_glob: bool[ = False]
?Do you want me to drop the default value as well?
[Also realised I forgot a line break between
prompt
andlst
]There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hi, I hope my comment doesn't come out of place -- anthony made a video on boolean traps