Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Upcoming (TBD)
==============

Features
--------
* Allow `/tee` and `/once` to complete on any file extension.


Bug Fixes
--------
* Raise Boundary tunnel stabilization pause to 0.2 sec.
Expand Down
2 changes: 1 addition & 1 deletion mycli/packages/completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
'tee',
'/tee',
]:
return [{"type": "file_name"}]
return [{'type': 'file_name', 'all_files': True}]

# todo: why is \edit case-sensitive?
if cmd in [
Expand Down
13 changes: 6 additions & 7 deletions mycli/packages/filepaths.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
DEFAULT_SOCKET_DIRS = ["/var/run", "/var/lib"]


def list_path(root_dir: str) -> list[str]:
def list_path(root_dir: str, *, sql_only: bool = True) -> list[str]:
"""List directory if exists.

:param root_dir: str
Expand All @@ -25,8 +25,7 @@ def list_path(root_dir: str) -> list[str]:
continue
elif os.path.isdir(os.path.join(root_dir, name)):
dirs.append(f'{name}/')
# if .sql is too restrictive it can be made configurable with some effort
elif name.lower().endswith('.sql'):
elif not sql_only or name.lower().endswith('.sql'):
files.append(name)
return files + dirs

Expand Down Expand Up @@ -66,7 +65,7 @@ def parse_path(root_dir: str) -> tuple[str, str, int]:
return base_dir, last_dir, position


def suggest_path(root_dir: str) -> list[str]:
def suggest_path(root_dir: str, *, sql_only: bool = True) -> list[str]:
"""List all files and subdirectories in a directory.

If the directory is not specified, suggest root directory,
Expand All @@ -82,19 +81,19 @@ def suggest_path(root_dir: str) -> list[str]:
"~",
os.curdir,
os.pardir,
*list_path(os.curdir),
*list_path(os.curdir, sql_only=sql_only),
]

if root_dir[0] not in ('/', '~') and root_dir[0:2] != './' and not os.path.dirname(root_dir):
return list_path(os.curdir)
return list_path(os.curdir, sql_only=sql_only)

if "~" in root_dir:
root_dir = os.path.expanduser(root_dir)

if not os.path.exists(root_dir):
root_dir, _ = os.path.split(root_dir)

return list_path(root_dir)
return list_path(root_dir, sql_only=sql_only)


def dir_path_exists(path: str) -> bool:
Expand Down
9 changes: 6 additions & 3 deletions mycli/sqlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1752,7 +1752,10 @@ def get_completions(
elif suggestion["type"] == "file_name":
source_filename = suggestion.get('source_filename')
if source_filename is None:
file_names_m = self.find_files(word_before_cursor)
if suggestion.get('all_files', False):
file_names_m = self.find_files(word_before_cursor, sql_only=False)
else:
file_names_m = self.find_files(word_before_cursor)
else:
source_file_completion_length = len(source_filename)
quote = source_filename[0] if source_filename[:1] in ("'", '"') else None
Expand Down Expand Up @@ -1881,7 +1884,7 @@ def completion_sort_key(item: tuple[str, int, int], text_for_len: str):
for x in uniq_completions_str
)

def find_files(self, word: str) -> Generator[tuple[str, int], None, None]:
def find_files(self, word: str, *, sql_only: bool = True) -> Generator[tuple[str, int], None, None]:
"""Yield matching directory or file names.

:param word:
Expand All @@ -1891,7 +1894,7 @@ def find_files(self, word: str) -> Generator[tuple[str, int], None, None]:
# todo position is ignored, but may need to be used
# todo fuzzy matches for filenames
base_path, last_path, position = parse_path(word)
paths = suggest_path(word)
paths = suggest_path(word, sql_only=sql_only)
for name in paths:
suggestion = complete_path(name, last_path)
if suggestion:
Expand Down
9 changes: 6 additions & 3 deletions test/pytests/test_completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,9 +1028,12 @@ def test_suggest_type_handles_parser_results_shorter_than_cursor(monkeypatch):
('source -- ', [SOURCE_FILE_SUGGESTION]),
('source -- query.sql', [{'type': 'file_name', 'quote_spaces': True, 'source_filename': 'query.sql'}]),
('source first.sql second.sql', []),
('\\o ', [{'type': 'file_name'}]),
('\\once ', [{'type': 'file_name'}]),
('tee ', [{'type': 'file_name'}]),
('\\o ', [{'type': 'file_name', 'all_files': True}]),
('/o ', [{'type': 'file_name', 'all_files': True}]),
('\\once ', [{'type': 'file_name', 'all_files': True}]),
('/once ', [{'type': 'file_name', 'all_files': True}]),
('tee ', [{'type': 'file_name', 'all_files': True}]),
('/tee ', [{'type': 'file_name', 'all_files': True}]),
('\\e ', [{'type': 'file_name'}]),
('\\edit ', [{'type': 'file_name'}]),
('\\llm ', [{'type': 'llm'}]),
Expand Down
9 changes: 9 additions & 0 deletions test/pytests/test_filepaths.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,16 @@ def test_list_path_lists_sql_files_and_directories(tmp_path: Path) -> None:
(tmp_path / '.hidden.sql').write_text('select 1\n', encoding='utf-8')
(tmp_path / 'visible.SQL').write_text('select 1\n', encoding='utf-8')
(tmp_path / 'notes.txt').write_text('ignored\n', encoding='utf-8')
(tmp_path / 'output').write_text('ignored\n', encoding='utf-8')
(tmp_path / 'folder').mkdir()

assert filepaths.list_path(str(tmp_path)) == ['visible.SQL', 'folder/']
assert filepaths.list_path(str(tmp_path), sql_only=False) == [
'notes.txt',
'output',
'visible.SQL',
'folder/',
]
assert filepaths.list_path(str(tmp_path / 'missing')) == []


Expand All @@ -67,6 +74,7 @@ def test_complete_path_and_parse_path() -> None:
def test_suggest_path_branches(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / 'query.sql').write_text('select 1\n', encoding='utf-8')
(tmp_path / 'report.csv').write_text('result\n', encoding='utf-8')
(tmp_path / 'subdir').mkdir()

assert filepaths.suggest_path('') == [
Expand All @@ -79,6 +87,7 @@ def test_suggest_path_branches(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
]

assert filepaths.suggest_path('relative') == ['query.sql', 'subdir/']
assert filepaths.suggest_path('relative', sql_only=False) == ['query.sql', 'report.csv', 'subdir/']

home = tmp_path / 'home'
home.mkdir()
Expand Down
2 changes: 1 addition & 1 deletion test/pytests/test_smart_completion_public_schema_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ def test_numbers_no_completion(completer, complete_event):
assert result == [] # ie not INT1


def dummy_list_path(dir_name):
def dummy_list_path(dir_name, *, sql_only=True):
dirs = {
"/": [
"dir1",
Expand Down
23 changes: 22 additions & 1 deletion test/pytests/test_sqlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import re
from types import SimpleNamespace
from unittest.mock import Mock

from prompt_toolkit.document import Document
import pytest
Expand Down Expand Up @@ -466,6 +467,22 @@ def test_file_completions_preserve_rigid_ordering(monkeypatch) -> None:
assert result == ['zeta', 'alpha']


def test_output_file_completions_include_all_file_types(monkeypatch) -> None:
completer = make_completer()
monkeypatch.setattr(
mycli.sqlcompleter,
'suggest_type',
lambda text, before: [{'type': 'file_name', 'all_files': True}],
)
find_files = Mock(return_value=iter([('report.csv', 0)]))
monkeypatch.setattr(completer, 'find_files', find_files)

result = [completion.text for completion in completer.get_completions(Document(text='/tee rep'), None)]

assert result == ['report.csv']
find_files.assert_called_once_with('rep', sql_only=False)


def test_extend_metadata_helpers_and_logging(caplog) -> None:
completer = make_completer()
completer.set_dbname('missing')
Expand Down Expand Up @@ -753,7 +770,11 @@ def test_find_files_populate_scoped_cols_and_enum_helpers(monkeypatch) -> None:
completer.extend_enum_values([('orders', 'status', ['pending', 'shipped'])])

monkeypatch.setattr(mycli.sqlcompleter, 'parse_path', lambda word: ('/tmp', 'fi', 0))
monkeypatch.setattr(mycli.sqlcompleter, 'suggest_path', lambda word: ['file.sql', 'folder/'])
monkeypatch.setattr(
mycli.sqlcompleter,
'suggest_path',
lambda word, *, sql_only: ['file.sql', 'folder/'],
)
monkeypatch.setattr(mycli.sqlcompleter, 'complete_path', lambda name, last_path: name if name == 'file.sql' else None)

assert list(completer.find_files('./fi')) == [('file.sql', Fuzziness.PERFECT)]
Expand Down
Loading