Skip to content

Commit

Permalink
fix #168
Browse files Browse the repository at this point in the history
  • Loading branch information
bckohan committed Feb 7, 2025
1 parent f6a1e13 commit 6876b5d
Show file tree
Hide file tree
Showing 12 changed files with 60 additions and 28 deletions.
4 changes: 4 additions & 0 deletions django_typer/management/commands/shellcompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,9 @@ def complete(
),
),
] = "",
cursor: t.Annotated[
t.Optional[int], Argument(help=t.cast(str, _("The cursor position.")))
] = None,
fallback: t.Annotated[
t.Optional[str],
Option(
Expand Down Expand Up @@ -442,6 +445,7 @@ def complete(
:width: 80
:convert-png: latex
"""
command = command[:cursor] if cursor is not None else command
args = split_arg_string(command.replace("\\", "\\\\"))
if args:
try:
Expand Down
12 changes: 10 additions & 2 deletions django_typer/shells/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import typing as t
from abc import abstractmethod
from functools import cached_property
from importlib.resources import files
from pathlib import Path

Expand Down Expand Up @@ -122,7 +123,7 @@ def get_completions(
self, args: t.List[str], incomplete: str
) -> t.List[CompletionItem]:
"""
need to remove the django command name from the arg completions
Get the completions for the current command string and incomplete string.
"""
if self.command.fallback:
return self.command.fallback(args, incomplete)
Expand Down Expand Up @@ -183,10 +184,17 @@ def source_vars(self) -> t.Dict[str, t.Any]:
"fallback": f" --fallback {self.command.fallback_import}"
if self.command.fallback
else "",
"is_installed": not isinstance(self.command.manage_script, Path),
"is_installed": self.is_installed,
"shell": self.name,
}

@cached_property
def is_installed(self) -> bool:
"""
Whether or not the manage script is a command on the path.
"""
return not isinstance(self.command.manage_script, Path)

def load_template(self) -> t.Union[BaseTemplate, DjangoTemplate]:
"""
Return a compiled Template object for the completion script template.
Expand Down
22 changes: 7 additions & 15 deletions django_typer/shells/zsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,8 @@ def install_dir(self) -> Path:
return install_dir

def format_completion(self, item: CompletionItem) -> str:
def escape(s: str) -> str:
# TODO is any of this necessary?
return (
s.replace('"', '""')
.replace("'", "''")
.replace("$", "\\$")
.replace("`", "\\`")
# .replace(":", r"\\:")
)

return (
f"{item.type}\n{escape(self.process_rich_text(item.value))}"
f"\n{escape(self.process_rich_text(item.help)) if item.help else '_'}"
)
hlp = self.process_rich_text(item.help.replace("\n", " ")) if item.help else "_"
return f"{item.type}\n{self.process_rich_text(item.value)}\n{hlp}"

def install(self) -> Path:
assert self.prog_name
Expand All @@ -89,7 +77,11 @@ def install(self) -> Path:
zshrc_source += f"autoload -Uz compinit{os.linesep}"
zshrc_source += f"compinit{os.linesep}"

style = f"zstyle ':completion:*:*:{self.prog_name}:*' menu select"
style = (
f"zstyle ':completion:*:*:{self.prog_name}:*' menu select"
if self.is_installed
else "zstyle ':completion:*' menu select"
)
if style not in zshrc_source:
zshrc_source += f"{style}{os.linesep}"
zshrc.write_text(zshrc_source)
Expand Down
2 changes: 1 addition & 1 deletion django_typer/templates/shell_complete/bash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
response=()
while IFS= read -r line; do
response+=("$line")
done < <( $1 {{ django_command }} --shell bash ${settings_option:+${settings_option}} ${pythonpath_option:+${pythonpath_option}} {{ color }} complete {{ fallback }} "${COMP_WORDS[*]}" )
done < <( $1 {{ django_command }} --shell bash ${settings_option:+${settings_option}} ${pythonpath_option:+${pythonpath_option}} {{ color }} complete {{ fallback }} "${COMP_WORDS[*]}" "$COMP_POINT" )

COMPREPLY=()
set_mode=true
Expand Down
2 changes: 1 addition & 1 deletion django_typer/templates/shell_complete/fish.fish
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function __fish_{{prog_name}}_complete
set cmd (commandline)
set cursor (commandline -C)

set completeCmd {{ django_command }} --shell fish {{ color }} complete {{ fallback }} "$cmd"
set completeCmd {{ django_command }} --shell fish {{ color }} complete {{ fallback }} "$cmd" "$cursor"

# We'll extract --settings=... and --pythonpath=... if present
set settingsOption ''
Expand Down
2 changes: 1 addition & 1 deletion django_typer/templates/shell_complete/powershell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ $scriptblock = {
$pythonPathOption = "--pythonpath=$($matches[1])"
}

$results = {{ manage_script_name }} {{ django_command }} $settingsOption $pythonPathOption --shell {{ shell }} {{ color }} complete {{ fallback }} "$($commandText)"
$results = {{ manage_script_name }} {{ django_command }} $settingsOption $pythonPathOption --shell {{ shell }} {{ color }} complete {{ fallback }} "$($commandText)" "$($cursorPosition)"

if ($results.Count -eq 0) {
# avoid default path completion
Expand Down
4 changes: 2 additions & 2 deletions django_typer/templates/shell_complete/zsh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
esac
done

response=("${(@f)$("${manage}" {{ django_command }} --shell zsh ${settings_option:+${settings_option}} ${pythonpath_option:+${pythonpath_option}} {{ color }} complete {{ fallback }} "${words[*]}")}")
response=("${(@f)$("${manage}" {{ django_command }} --shell zsh ${settings_option:+${settings_option}} ${pythonpath_option:+${pythonpath_option}} {{ color }} complete {{ fallback }} "${words[*]}" "$CURSOR")}")

for type key descr in ${response}; do
if [[ "$type" == "dir" ]]; then
Expand All @@ -56,7 +56,7 @@
done

if [ -n "$completions_with_descriptions" ]; then
_describe -V unsorted completions_with_descriptions -U
_describe -V unsorted completions_with_descriptions
fi

if [ -n "$completions" ]; then
Expand Down
4 changes: 2 additions & 2 deletions django_typer/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ def handle(self, verbosity: Verbosity = 1):
Option(
help=cast(
str,
_(
(
"The Python path to a settings module, e.g. "
'"myproject.settings.main". If this isn\'t provided, the '
'"myproject.settings.main". If this is not provided the '
"DJANGO_SETTINGS_MODULE environment variable will be used."
),
),
Expand Down
1 change: 1 addition & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Change Log
v3.0.0 (202X-XX-XX)
===================

* Fixed `Completions before the end of the typed command string do not work. <https://github.com/django-commons/django-typer/issues/168>`_
* BREAKING `Default rich traceback should not show locals - its too much information. <https://github.com/django-commons/django-typer/issues/166>`_
* Implemented `Migrate pyproject.toml to poetry 2 and portable project specifiers. <https://github.com/django-commons/django-typer/issues/164>_`
* BREAKING `Split parsers.py and completers.py into submodules. <https://github.com/django-commons/django-typer/issues/163>_`
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ rich = ["rich>=10.11.0,<14.0.0"]
"Code_of_Conduct" = "https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md"

# for testing
# [project.scripts]
# manage = "tests.manage:main"
[project.scripts]
manage = "tests.manage:main"

[tool.poetry.group.dev.dependencies]
ipdb = "^0.13.13"
Expand Down
21 changes: 19 additions & 2 deletions tests/shellcompletion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def remove(self, script=None):

if platform.system() == "Windows":

def get_completions(self, *cmds: str, scrub_output=True) -> str:
def get_completions(self, *cmds: str, scrub_output=True, position=0) -> str:
import winpty

assert self.shell
Expand Down Expand Up @@ -163,6 +163,7 @@ def read_all() -> str:
output = read_all() + read_all()

pty.write(" ".join(cmds))
pty.write(position * ("\x1b[C" if position > 0 else "\x1b[D"))
time.sleep(0.1)
pty.write(self.tabs)

Expand All @@ -173,7 +174,7 @@ def read_all() -> str:

else:

def get_completions(self, *cmds: str, scrub_output=True) -> str:
def get_completions(self, *cmds: str, scrub_output=True, position=0) -> str:
import fcntl
import termios
import pty
Expand Down Expand Up @@ -215,6 +216,12 @@ def read(fd):

cmd = " ".join(cmds)
os.write(master_fd, cmd.encode())

# positioning does not seem to work :(
if position < 0:
os.write(master_fd, f"\033[{abs(position)}D".encode())
elif position > 0:
os.write(master_fd, f"\033[{abs(position)}C".encode())
time.sleep(0.5)

print(f'"{cmd}"')
Expand Down Expand Up @@ -398,6 +405,16 @@ def test_path_completion(self):
self.remove()
self.verify_remove()

# todo - cursor positioning not working
# def test_cursor_position(self):
# self.install()
# self.verify_install()
# cmd = [self.launch_script, "shellcompletion", "--set ", "install"]
# completions = self.get_completions(*cmd, position=-9)
# self.assertIn("--settings", completions)
# self.remove()
# self.verify_remove()


class _ScriptCompleteTestCase(_CompleteTestCase):
manage_script: str = "manage.py"
Expand Down
10 changes: 10 additions & 0 deletions tests/test_parser_completers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,16 @@ def test_return_queryset(self):
for obj in objects:
self.assertEqual(data1[str(obj.id)], "P541D")

def test_cursor_position(self):
completions = get_values(
self.shellcompletion.complete("shellcompletion --set install", 21)
)
self.assertTrue("--settings" in completions)
completions = get_values(
self.shellcompletion.complete("shellcompletion --settings install", 27)
)
self.assertTrue("tests" in completions)


class TestDateTimeParserCompleter(ParserCompleterMixin, TestCase):
tz_info = None
Expand Down

0 comments on commit 6876b5d

Please sign in to comment.