feat(pj)!: add new subcommands and autocomplete support

BREAKING CHANGE: The `pj` command has been rewritten with new subcommands.
Existing usage might no longer work. Use `pj add`, `pj mv`, `pj rm`, `pj ls`,
and `pj open -e` instead.

Co-authored-by: ibrahimcetin <mail@ibrahimcetin.dev>
This commit is contained in:
İbrahim Çetin 2025-02-15 10:54:39 +03:00
parent 92da3108b5
commit 7a6635b8a2
2 changed files with 440 additions and 41 deletions

View File

@ -1,11 +1,6 @@
# pj
# pj plugin
The `pj` plugin (short for `Project Jump`) allows you to define several
folders where you store your projects, so that you can jump there directly
by just using the name of the project directory.
Original idea and code by Jan De Poorter ([@DefV](https://github.com/DefV))
Source: https://gist.github.com/pjaspers/368394#gistcomment-1016
The `pj` plugin (short for `Project Jump`) allows you to define a list of directories where your projects are located. You can quickly jump to a project directory using `pj project-name` or open it in your preferred editor with `pj open project-name`.
## Usage
@ -15,31 +10,108 @@ Source: https://gist.github.com/pjaspers/368394#gistcomment-1016
plugins=(... pj)
```
2. Set `$PROJECT_PATHS` in your ~/.zshrc:
2. Add project to the registry:
```zsh
PROJECT_PATHS=(~/src ~/work ~/"dir with spaces")
$ pj add
```
You can now use one of the following commands:
> This will add the current directory to the registry, using the directory name as the project name.
##### `pj my-project`:
3. Jump to project directory:
`cd` to the directory named "my-project" found in one of the `$PROJECT_PATHS`
directories. If there are several directories named the same, the first one
to appear in `$PROJECT_PATHS` has preference.
```zsh
$ pj project-name
```
> `pj` has auto-complete support for project names.
4. Open the project in your defined `$EDITOR`:
```zsh
$ pjo project-name
```
> Opens the project in your default $EDITOR. You can override the editor using `-e`.
## Commands
#### `pj <project-name>`
`cd` to the project directory with the given name. Note: you can use auto-complete for project names.
For example:
```zsh
PROJECT_PATHS=(~/code ~/work)
$ ls ~/code # ~/code/blog ~/code/react
$ ls ~/work # ~/work/blog ~/work/project
$ pj blog # <-- will cd to ~/code/blog
$ pj my-project
```
##### `pjo my-project`
#### `pj add [path] [name]`
Open the project directory with your defined `$EDITOR`. This follows the same
directory rules as the `pj` command above.
Add a project to the registry.
Note: `pja` is an alias of `pj add`.
For example:
```zsh
$ pja
$ # Add the current directory with the name "my-project"
$ pja . my-project
$ # Add the specified directory to the registry with the name "my-project"
$ pja /path/to/project my-project
```
##### `pj open <project-name>`
Open the project with your defined `$EDITOR` or specify an editor with the `-e` flag.
Note: `pjo` is an alias of `pj open`.
For example:
```zsh
$ pjo my-project
$ # open the project path named "my-project" with VSCode
$ pjo -e code my-project
$ # open multiple projects
$ pjo my-project another-project
```
##### `pj ls [pattern]`
List all the projects in the registry.
Note: `pjl` is an alias of `pj ls`.
For example:
```zsh
$ pj ls
$ # list all the projects in the registry that match the pattern 'web-*'
$ pjl 'web-*'
```
##### `pj rm <project-name>`
Remove a project from the registry.
For example:
```zsh
$ pj rm my-project
$ # remove multiple projects from the registry
$ pj rm my-project another-project
```
#### `pj mv <old-name> <new-name>`
Rename a project in the registry.
For example:
```zsh
$ pj mv old-name new-name
```
## Aliases
| Alias | Command |
|-------|---------|
| `pja` | `pj add` |
| `pjo` | `pj open` |
| `pjl` | `pj ls` |
## Contributors
Code by [@ibrahimcetin](https://github.com/ibrahimcetin)
Original idea and code by Jan De Poorter ([@DefV](https://github.com/DefV))
Source: https://gist.github.com/pjaspers/368394#gistcomment-1016

367
plugins/pj/pj.plugin.zsh Normal file → Executable file
View File

@ -1,34 +1,361 @@
alias pjo="pj open"
PJ_DB=~/.pj_projects # Project database file
touch "$PJ_DB"
function pj() {
local cmd="cd"
local project="$1"
_pj_help() {
cat <<EOF
Project Jump (pj) - Quick directory navigation for projects
if [[ "open" == "$project" ]]; then
shift
project=$*
cmd=${=EDITOR}
else
project=$*
Usage:
pj PROJECT_NAME Jump to a project directory
pj add [PATH] [NAME] Add a project to registry. Defaults to current directory
pj open [-e] (NAME ...) Open project(s) in editor. Uses \$EDITOR (${EDITOR:-not set}) by default
pj ls [PATTERN] List all projects. Use PATTERN to filter results
pj rm (PROJECT_NAME ...) Remove project(s) from registry
pj mv OLD_NAME NEW_NAME Rename a project in registry
pj help Show this help message
Examples:
pj add # Add current directory
pj add . my-project # Add current directory as 'my-project'
pj rm my-project # Remove my-project from registry
pj open my-project # Open my-project in editor
pj open my-project my-other # Open multiple projects
pj open -e vim my-project # Open my-project in nano. Specify editor with -e
pj ls # List all projects
pj ls my-project # List projects matching 'my-project'
pj ls 'subfolder/*my*' # List projects matching 'subfolder/*my*'. Use quotes for globs
Aliases:
pja - pj add
pjo - pj open
pjl - pj ls
EOF
}
_pj_jump() {
local project_name="$1"
local target_path
# Check for extra arguments
if [[ $# -gt 1 ]]; then
echo "Error: Too many arguments" >&2
echo "Usage: pj [PROJECT_NAME]" >&2
return 1
fi
for basedir ($PROJECT_PATHS); do
if [[ -d "$basedir/$project" ]]; then
$cmd "$basedir/$project"
return
# Validate input
if [[ -z "$project_name" ]]; then
echo "Error: Missing project name" >&2
echo "Usage: pj [PROJECT_NAME]" >&2
return 1
fi
# Find project in database
if [[ -f "$PJ_DB" ]]; then
target_path=$(awk -F: -v project="$project_name" '
$1 == project {print $2; exit}
' "$PJ_DB")
fi
# Handle found project
if [[ -n "$target_path" ]]; then
if [[ -d "$target_path" ]]; then
cd "$target_path" || {
echo "Error: Failed to access $target_path" >&2
return 1
}
return 0
else
echo "Error: Path not exists for project '$project_name'" >&2
echo "Path: $target_path" >&2
return 1
fi
fi
# Project not found
echo "Error: Project not found - $project_name" >&2
return 1
}
_pj_add() {
local path_input=${1:-.}
local name=${2:-}
local resolved_path="${path_input:a}"
local default_name="${resolved_path:t}"
# Check for extra arguments
if [[ $# -gt 2 ]]; then
echo "Error: Too many arguments" >&2
echo "Usage: pj add [PATH] [NAME]" >&2
return 1
fi
# Check if the name is a command name
case "$name" in
add|rm|ls|open|mv|help)
echo "Error: Invalid project name '$name'. It conflicts with a command name." >&2
return 1
;;
esac
# Validate directory exists
if [[ ! -d "$resolved_path" ]]; then
echo "Error: Invalid directory path '$path_input'" >&2
echo "Resolved to: $resolved_path" >&2
return 1
fi
# Set default name if not provided
if [[ -z "$name" ]]; then
name="$default_name"
fi
# Validate name format
if [[ "$name" =~ [^a-zA-Z0-9_-] ]]; then
echo "Error: Invalid name '$name'" >&2
echo "Allowed characters: A-Z, 0-9, -, _" >&2
return 1
fi
# Check for existing project name
if [[ -f "$PJ_DB" ]] && grep -q "^${name}:" "$PJ_DB"; then
local existing_path=$(awk -F: -v n="$name" '$1 == n {print $2}' "$PJ_DB")
echo "Error: Project name '$name' already exists" >&2
echo "Existing path: $existing_path" >&2
return 1
fi
# Add new entry
echo "${name}:${resolved_path}" >> "$PJ_DB"
echo "Added project: ${name} -> ${resolved_path}"
}
_pj_ls() {
local pattern="${1:-*}" # Default to all projects
local -a projects=()
local name path
# Read database file
while IFS=: read -r name path; do
# Case-insensitive glob matching
if [[ "${name:l}" == *${~pattern:l}* || \
"${path:l}" == *${~pattern:l}* ]]; then
projects+=("${name} -> ${path}")
fi
done < "$PJ_DB" 2>/dev/null
# Process projects using Zsh built-ins
if (( ${#projects} > 0 )); then
# Remove duplicates and sort
projects=(${(u)projects}) # Remove duplicates
projects=(${(o)projects}) # Sort alphabetically
# Print results
print -l "${projects[@]}"
fi
}
_pj_open() {
local editor="$EDITOR"
local project_names=()
local target_path
local errors=0
# Parse options
while [[ "$1" =~ ^- ]]; do
case "$1" in
-e|--editor)
shift
editor="$1"
;;
*)
echo "Error: Invalid option $1" >&2
echo "Usage: pj open [-e editor] (PROJECT_NAME ...)" >&2
return 1
;;
esac
shift
done
# Collect project names
project_names=("$@")
# Validate input
if [[ ${#project_names[@]} -eq 0 ]]; then
echo "Error: Missing project name(s)" >&2
echo "Usage: pj open [-e editor] [PROJECT_NAME ...]" >&2
return 1
fi
# Validate editor
if [[ -z "$editor" ]]; then
echo "Error: No editor specified" >&2
echo "Set \$EDITOR or use -e option" >&2
return 1
fi
for project_name in "${project_names[@]}"; do
# Find project in database
target_path=$(awk -F: -v project="$project_name" '$1 == project {print $2; exit}' "$PJ_DB")
# Handle project path
if [[ -n "$target_path" ]]; then
if [[ -d "$target_path" ]]; then
${=editor} "$target_path"
else
echo "Error: Invalid path for project '$project_name'" >&2
echo "Path: $target_path" >&2
errors=$((errors + 1))
fi
else
echo "Error: Project not found - $project_name" >&2
errors=$((errors + 1))
fi
done
echo "No such project '${project}'."
return $errors
}
_pj () {
local -a projects
for basedir ($PROJECT_PATHS); do
projects+=(${basedir}/*(/N))
_pj_rm() {
local project_names=("$@")
local errors=0
# Validate input
if [[ ${#project_names[@]} -eq 0 ]]; then
echo "Error: Missing project name(s)" >&2
echo "Usage: pj rm [PROJECT_NAME ...]" >&2
return 1
fi
for project_name in "${project_names[@]}"; do
# Verify project exists
if ! grep -q "^${project_name}:" "$PJ_DB"; then
echo "Error: Project not found - $project_name" >&2
errors=$((errors + 1))
continue
fi
# Remove entry from database
local project_path=$(awk -F: -v project="$project_name" '$1 == project {print $2; exit}' "$PJ_DB")
sed -i.bak "/^${project_name}:/d" "$PJ_DB" && rm -f "$PJ_DB.bak"
echo "Removed project: $project_name -> $project_path"
done
compadd ${projects:t}
return $errors
}
_pj_mv() {
local project_name="$1"
local new_name="$2"
# Validate input
if [[ -z "$project_name" || -z "$new_name" ]]; then
echo "Error: Missing project name or new name" >&2
echo "Usage: pj mv [PROJECT_NAME] [NEW_NAME]" >&2
return 1
fi
# Verify project exists
if ! grep -q "^${project_name}:" "$PJ_DB"; then
echo "Error: Project not found - $project_name" >&2
return 1
fi
# Verify new name is not taken
if grep -q "^${new_name}:" "$PJ_DB"; then
echo "Error: New project name already exists - $new_name" >&2
return 1
fi
# Move project
sed -i.bak "s/^${project_name}:/${new_name}:/g" "$PJ_DB" && rm -f "$PJ_DB.bak"
echo "Moved project: $project_name -> $new_name"
}
# Main function
function pj() {
if [ $# -eq 0 ]; then
_pj_help
return 1
fi
# Command dispatch
case "$1" in
"add")
shift # Remove 'add' from arguments
_pj_add "$@"
;;
"rm")
shift
_pj_rm "$@"
;;
"ls")
shift
_pj_ls "$@"
;;
"open")
shift
_pj_open "$@"
;;
"mv")
shift
_pj_mv "$@"
;;
"help"|"--help"|"-h"|"")
_pj_help
return 1
;;
*)
_pj_jump "$@"
;;
esac
}
# Main completion entry point
_pj() {
local context state state_descr line
typeset -A opt_args
# Parse command line state
_arguments -C \
'1: :->command_or_project' \
'*: :->args'
case $state in
(command_or_project)
_pj_show_projects
;;
(args)
_pj_handle_subcommand_args
;;
esac
}
# Helper: Handle arguments after subcommand
_pj_handle_subcommand_args() {
case $line[1] in
(add)
_files -/
;;
(open|rm|ls|mv)
_pj_show_projects
;;
esac
}
# Helper: Show just project names
_pj_show_projects() {
local projects=(${(f)"$(cut -d: -f1 "$PJ_DB" 2>/dev/null)"})
_describe 'projects' projects
}
# Register completion
compdef _pj pj
# Editor aliases
alias pja="pj add"
alias pjo="pj open"
alias pjl="pj ls"