Skip to content
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

✨ Setup Autocomplete #341

Merged
merged 30 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0d3a45a
WIP setup-autocomplete command
mayankpatibandla Mar 29, 2024
a2e3f24
WIP writing to file
mayankpatibandla Apr 1, 2024
b674840
Autocomplete for bash and zsh
mayankpatibandla Apr 1, 2024
77edbed
Autocomplete for fish
mayankpatibandla Apr 1, 2024
dde23ea
Add comments
mayankpatibandla Apr 1, 2024
7256d61
Add error messages if writing completion script fails
mayankpatibandla Apr 1, 2024
80f6b22
Error handling for fish
mayankpatibandla Apr 1, 2024
df0f4e5
Improved error handling
mayankpatibandla Apr 3, 2024
55fb410
Add confirmation prompt
mayankpatibandla Apr 7, 2024
73eebab
Autocomplete for PowerShell and Windows PowerShell
mayankpatibandla Apr 11, 2024
0ae05b3
Config file errors for PowerShell
mayankpatibandla Apr 11, 2024
11fa5c1
Cross platform profile path
mayankpatibandla Apr 11, 2024
7e1b76c
Run in shell
mayankpatibandla Apr 11, 2024
f58a30a
Formatting
mayankpatibandla Apr 11, 2024
e205c7f
Skip confirmation prompts
mayankpatibandla Apr 11, 2024
57e5667
Better profile path for PowerShell
mayankpatibandla Apr 11, 2024
424bd28
Bash on windows
mayankpatibandla Apr 12, 2024
6f03011
WIP cross platform compatibility
mayankpatibandla Apr 12, 2024
cc212f9
Move scripts to separate files
mayankpatibandla Apr 15, 2024
f2b5fa3
Fix relative path
mayankpatibandla Apr 15, 2024
1e646ab
Use pathlib
mayankpatibandla Apr 15, 2024
a8386b8
Expand user
mayankpatibandla Apr 15, 2024
e36eec7
Formatting
mayankpatibandla Apr 15, 2024
c00e253
Enable shell mode
mayankpatibandla Apr 16, 2024
5fd6f96
Shorten help message
mayankpatibandla Apr 22, 2024
719da6d
Save autocomplete script in PROS directory
mayankpatibandla Jun 6, 2024
302be40
Disable completion for bash < 4
mayankpatibandla Jun 6, 2024
0c9cf81
Surround script path in " "
mayankpatibandla Jun 6, 2024
7f4ca69
Disable nosort on bash 3
mayankpatibandla Jun 6, 2024
83eaa2f
Add more comments
mayankpatibandla Jun 6, 2024
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
26 changes: 26 additions & 0 deletions pros/autocomplete/.pros-complete.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
_pros_completion() {
local IFS=$'\n'
local response
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _PROS_COMPLETE=bash_complete $1)
for completion in $response; do
IFS=',' read type value <<<"$completion"
if [[ $type == 'dir' ]]; then
COMPREPLY=()
compopt -o dirnames
elif [[ $type == 'file' ]]; then
COMPREPLY=()
compopt -o default
elif [[ $type == 'plain' ]]; then
COMPREPLY+=($value)
fi
done
return 0
}
_pros_completion_setup() {
if [[ ${BASH_VERSINFO[0]} -ge 4 ]]; then
complete -o nosort -F _pros_completion pros
else
complete -F _pros_completion pros
fi
}
_pros_completion_setup
31 changes: 31 additions & 0 deletions pros/autocomplete/.pros-complete.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
_pros_completion() {
local -a completions
local -a completions_with_descriptions
local -a response
(( ! $+commands[pros] )) && return 1
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _PROS_COMPLETE=zsh_complete pros)}")
for type key descr in ${response}; do
if [[ "$type" == "plain" ]]; then
if [[ "$descr" == "_" ]]; then
completions+=("$key")
else
completions_with_descriptions+=("$key":"$descr")
fi
elif [[ "$type" == "dir" ]]; then
_path_files -/
elif [[ "$type" == "file" ]]; then
_path_files -f
fi
done
if [ -n "$completions_with_descriptions" ]; then
_describe -V unsorted completions_with_descriptions -U
fi
if [ -n "$completions" ]; then
compadd -U -V unsorted -a completions
fi
}
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
_pros_completion "$@"
else
compdef _pros_completion pros
fi
45 changes: 45 additions & 0 deletions pros/autocomplete/pros-complete.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Modified from https://github.com/StephLin/click-pwsh/blob/main/click_pwsh/shell_completion.py#L11
Register-ArgumentCompleter -Native -CommandName pros -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$env:COMP_WORDS = $commandAst
$env:COMP_WORDS = $env:COMP_WORDS.replace('\\', '/')
$incompleteCommand = $commandAst.ToString()
$myCursorPosition = $cursorPosition
if ($myCursorPosition -gt $incompleteCommand.Length) {
$myCursorPosition = $incompleteCommand.Length
}
$env:COMP_CWORD = @($incompleteCommand.substring(0, $myCursorPosition).Split(" ") | Where-Object { $_ -ne "" }).Length
if ( $wordToComplete.Length -gt 0) { $env:COMP_CWORD -= 1 }
$env:_PROS_COMPLETE = "powershell_complete"
pros | ForEach-Object {
$type, $value, $help = $_.Split(",", 3)
if ( ($type -eq "plain") -and ![string]::IsNullOrEmpty($value) ) {
[System.Management.Automation.CompletionResult]::new($value, $value, "ParameterValue", $value)
}
elseif ( ($type -eq "file") -or ($type -eq "dir") ) {
if ([string]::IsNullOrEmpty($wordToComplete)) {
$dir = "./"
}
else {
$dir = $wordToComplete.replace('\\', '/')
}
if ( (Test-Path -Path $dir) -and ((Get-Item $dir) -is [System.IO.DirectoryInfo]) ) {
[System.Management.Automation.CompletionResult]::new($dir, $dir, "ParameterValue", $dir)
}
Get-ChildItem -Path $dir | Resolve-Path -Relative | ForEach-Object {
$path = $_.ToString().replace('\\', '/').replace('Microsoft.PowerShell.Core/FileSystem::', '')
$isDir = $false
if ((Get-Item $path) -is [System.IO.DirectoryInfo]) {
$path = $path + "/"
$isDir = $true
}
if ( ($type -eq "file") -or ( ($type -eq "dir") -and $isDir ) ) {
[System.Management.Automation.CompletionResult]::new($path, $path, "ParameterValue", $path)
}
}
}
}
$env:COMP_WORDS = $null | Out-Null
$env:COMP_CWORD = $null | Out-Null
$env:_PROS_COMPLETE = $null | Out-Null
}
14 changes: 14 additions & 0 deletions pros/autocomplete/pros.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function _pros_completion;
set -l response (env _PROS_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) pros);
for completion in $response;
set -l metadata (string split "," $completion);
if test $metadata[1] = "dir";
__fish_complete_directories $metadata[2];
else if test $metadata[1] = "file";
__fish_complete_path $metadata[2];
else if test $metadata[1] = "plain";
echo $metadata[2];
end;
end;
end;
complete --no-files --command pros --arguments "(_pros_completion)";
201 changes: 159 additions & 42 deletions pros/cli/misc_commands.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,159 @@
import pros.common.ui as ui
from pros.cli.common import *
from pros.ga.analytics import analytics

@pros_root
def misc_commands_cli():
pass


@misc_commands_cli.command()
@click.option('--force-check', default=False, is_flag=True,
help='Force check for updates, disregarding auto-check frequency')
@click.option('--no-install', default=False, is_flag=True,
help='Only check if a new version is available, do not attempt to install')
@default_options
def upgrade(force_check, no_install):
"""
Check for updates to the PROS CLI
"""
with ui.Notification():
ui.echo('The "pros upgrade" command is currently non-functioning. Did you mean to run "pros c upgrade"?', color='yellow')

return # Dead code below

analytics.send("upgrade")
from pros.upgrade import UpgradeManager
manager = UpgradeManager()
manifest = manager.get_manifest(force_check)
ui.logger(__name__).debug(repr(manifest))
if manager.has_stale_manifest:
ui.logger(__name__).error('Failed to get latest upgrade information. '
'Try running with --debug for more information')
return -1
if not manager.needs_upgrade:
ui.finalize('upgradeInfo', 'PROS CLI is up to date')
else:
ui.finalize('upgradeInfo', manifest)
if not no_install:
if not manager.can_perform_upgrade:
ui.logger(__name__).error(f'This manifest cannot perform the upgrade.')
return -3
ui.finalize('upgradeComplete', manager.perform_upgrade())
import os
from pathlib import Path
import subprocess

from click.shell_completion import CompletionItem, add_completion_class, ZshComplete

import pros.common.ui as ui
from pros.cli.common import *
from pros.ga.analytics import analytics

@pros_root
def misc_commands_cli():
pass


@misc_commands_cli.command()
@click.option('--force-check', default=False, is_flag=True,
help='Force check for updates, disregarding auto-check frequency')
@click.option('--no-install', default=False, is_flag=True,
help='Only check if a new version is available, do not attempt to install')
@default_options
def upgrade(force_check, no_install):
"""
Check for updates to the PROS CLI
"""
with ui.Notification():
ui.echo('The "pros upgrade" command is currently non-functioning. Did you mean to run "pros c upgrade"?', color='yellow')

return # Dead code below

analytics.send("upgrade")
from pros.upgrade import UpgradeManager
manager = UpgradeManager()
manifest = manager.get_manifest(force_check)
ui.logger(__name__).debug(repr(manifest))
if manager.has_stale_manifest:
ui.logger(__name__).error('Failed to get latest upgrade information. '
'Try running with --debug for more information')
return -1
if not manager.needs_upgrade:
ui.finalize('upgradeInfo', 'PROS CLI is up to date')
else:
ui.finalize('upgradeInfo', manifest)
if not no_install:
if not manager.can_perform_upgrade:
ui.logger(__name__).error(f'This manifest cannot perform the upgrade.')
return -3
ui.finalize('upgradeComplete', manager.perform_upgrade())


_SCRIPT_FILES = {
'bash': '.pros-complete.bash',
'zsh': '.pros-complete.zsh',
'fish': 'pros.fish',
'pwsh': 'pros-complete.ps1',
'powershell': 'pros-complete.ps1',
}


def _get_shell_script(shell: str) -> str:
script_file = Path(__file__).parent.parent / 'autocomplete' / _SCRIPT_FILES[shell]
with script_file.open('r') as f:
return f.read()


@add_completion_class
class PowerShellComplete(ZshComplete):
"""Shell completion for PowerShell and Windows PowerShell."""

name = "powershell"
source_template = _get_shell_script("powershell")

def format_completion(self, item: CompletionItem) -> str:
return super().format_completion(item).replace("\n", ",")


@misc_commands_cli.command()
@click.argument('shell', type=click.Choice(['bash', 'zsh', 'fish', 'pwsh', 'powershell']), required=True)
@click.argument('config_path', type=click.Path(resolve_path=True), default=None, required=False)
@click.option('--force', '-f', is_flag=True, default=False, help='Skip confirmation prompts')
@default_options
def setup_autocomplete(shell, config_path, force):
"""
Set up autocomplete for PROS CLI

SHELL: The shell to set up autocomplete for

CONFIG_PATH: The configuration path to add the autocomplete script to. If not specified, the default configuration
file for the shell will be used.

Example: pros setup-autocomplete bash ~/.bashrc
"""

# https://click.palletsprojects.com/en/8.1.x/shell-completion/

default_config_paths = {
'bash': '~/.bashrc',
'zsh': '~/.zshrc',
'fish': '~/.config/fish/completions/',
'pwsh': None,
'powershell': None,
}

if shell in ('pwsh', 'powershell') and config_path is None:
try:
profile_command = f'{shell} -NoLogo -NoProfile -Command "Write-Output $PROFILE"' if os.name == 'nt' else f"{shell} -NoLogo -NoProfile -Command 'Write-Output $PROFILE'"
default_config_paths[shell] = subprocess.run(profile_command, shell=True, capture_output=True, check=True, text=True).stdout.strip()
except subprocess.CalledProcessError as exc:
raise click.UsageError("Failed to determine the PowerShell profile path. Please specify a valid config file.") from exc

if config_path is None:
config_path = default_config_paths[shell]
ui.echo(f"Using default config path {config_path}. To specify a different config path, run 'pros setup-autocomplete {shell} [CONFIG_PATH]'.\n")
config_path = Path(config_path).expanduser().resolve()

if shell in ('bash', 'zsh', 'pwsh', 'powershell'):
if config_path.is_dir():
raise click.UsageError(f"Config file {config_path} is a directory. Please specify a valid config file.")
if not config_path.exists():
raise click.UsageError(f"Config file {config_path} does not exist. Please specify a valid config file.")

# Write the autocomplete script to a shell script file
script_file = Path(click.get_app_dir("PROS")) / "autocomplete" / _SCRIPT_FILES[shell]
script_file.parent.mkdir(exist_ok=True)
with script_file.open('w') as f:
f.write(_get_shell_script(shell))

if shell in ('bash', 'zsh'):
source_autocomplete = f'. "{script_file.as_posix()}"\n'
elif shell in ('pwsh', 'powershell'):
source_autocomplete = f'"{script_file}" | Invoke-Expression\n'
if force or ui.confirm(f"Add the autocomplete script to {config_path}?", default=True):
# Source the autocomplete script in the config file
with config_path.open('r+') as f:
# Only append if the source command is not already in the file
if source_autocomplete not in f.readlines():
f.write("\n# PROS CLI autocomplete\n")
f.write(source_autocomplete)
else:
ui.echo(f"Autocomplete script written to {script_file}.")
ui.echo(f"Add the following line to {config_path} then restart your shell to enable autocomplete:\n")
ui.echo(source_autocomplete)
return
elif shell == 'fish':
if config_path.is_file():
script_dir = config_path.parent
script_file = config_path
else:
script_dir = config_path
script_file = config_path / _SCRIPT_FILES[shell]

if not script_dir.exists():
raise click.UsageError(f"Completions directory {script_dir} does not exist. Please specify a valid completions file or directory.")

# Write the autocomplete script to a shell script file
with script_file.open('w') as f:
f.write(_get_shell_script(shell))

ui.echo(f"Succesfully set up autocomplete for {shell} in {config_path}. Restart your shell to apply changes.")
Loading