Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:48:40 +08:00
commit 87eb9f0383
12 changed files with 4681 additions and 0 deletions

View File

@@ -0,0 +1,436 @@
#!/usr/bin/env bash
#
# interactive-code-finder.sh - Interactive ast-grep + fzf workflow
#
# This script provides an interactive way to search for code patterns
# using ast-grep and select results with fzf for further action.
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
LANGUAGE="${LANGUAGE:-py}"
PREVIEW_LINES=20
# Helper functions
print_header() {
echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} $1${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${CYAN} $1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
# Check dependencies
check_dependencies() {
local missing=()
for cmd in ast-grep fzf bat; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
print_error "Missing required tools: ${missing[*]}"
echo
echo "Install with:"
echo " brew install ast-grep fzf bat"
exit 1
fi
}
# Language-specific pattern examples
get_pattern_examples() {
local lang="$1"
case "$lang" in
py|python)
cat <<EOF
Function definitions: def \$FUNC(\$\$\$):
Class definitions: class \$CLASS:
Function calls: \$FUNC(\$\$\$)
Method calls: \$OBJ.\$METHOD(\$\$\$)
Imports: from \$MOD import \$\$\$
Decorators: @\$DECORATOR
If statements: if \$COND:
For loops: for \$VAR in \$ITER:
Try/except: try: \$\$\$ except \$EXC: \$\$\$
Context managers: with \$EXPR as \$VAR:
EOF
;;
js|javascript|ts|typescript)
cat <<EOF
Function declarations: function \$FUNC(\$\$\$) { \$\$\$ }
Arrow functions: (\$\$\$) => \$BODY
Class definitions: class \$CLASS { \$\$\$ }
Imports: import \$\$ from "\$MODULE"
Export default: export default \$EXPR
React components: function \$COMP() { \$\$\$ }
useState: const [\$STATE, \$SETTER] = useState(\$\$\$)
Async functions: async function \$FUNC(\$\$\$) { \$\$\$ }
EOF
;;
rs|rust)
cat <<EOF
Function definitions: fn \$FUNC(\$\$\$) { \$\$\$ }
Struct definitions: struct \$NAME { \$\$\$ }
Impl blocks: impl \$TYPE { \$\$\$ }
Trait impl: impl \$TRAIT for \$TYPE { \$\$\$ }
Macros: \$MACRO!(\$\$\$)
Match expressions: match \$EXPR { \$\$\$ }
Result returns: fn \$FUNC(\$\$\$) -> Result<\$\$\$> { \$\$\$ }
EOF
;;
go)
cat <<EOF
Function definitions: func \$FUNC(\$\$\$) \$\$\$ { \$\$\$ }
Method definitions: func (\$RECV \$TYPE) \$METHOD(\$\$\$) \$\$\$ { \$\$\$ }
Struct definitions: type \$NAME struct { \$\$\$ }
Interface definitions: type \$NAME interface { \$\$\$ }
Goroutines: go \$FUNC(\$\$\$)
Defer statements: defer \$FUNC(\$\$\$)
Error checks: if err != nil { \$\$\$ }
EOF
;;
zig)
cat <<EOF
Public functions: pub fn \$FUNC(\$\$\$) \$\$\$ { \$\$\$ }
Struct definitions: const \$NAME = struct { \$\$\$ };
Error handling: try \$EXPR
Test blocks: test "\$NAME" { \$\$\$ }
Comptime: comptime \$EXPR
EOF
;;
rb|ruby)
cat <<EOF
Method definitions: def \$METHOD(\$\$\$); \$\$\$; end
Class definitions: class \$CLASS; \$\$\$; end
Blocks with do/end: \$EXPR do |\$PARAM|; \$\$\$; end
Blocks with braces: \$EXPR { |\$PARAM| \$\$\$ }
Rails routes: get "\$PATH", to: "\$HANDLER"
ActiveRecord queries: \$MODEL.where(\$\$\$)
EOF
;;
*)
echo "Generic pattern: \$PATTERN"
;;
esac
}
# Show pattern examples
show_examples() {
print_header "Pattern Examples for $LANGUAGE"
get_pattern_examples "$LANGUAGE"
echo
}
# Interactive pattern input
get_pattern() {
show_examples
print_info "Enter your ast-grep pattern (or 'examples' to see more):"
echo -e "${YELLOW}Examples:${NC}"
echo " - def \$FUNC(\$\$\$):"
echo " - class \$CLASS:"
echo " - \$FUNC(\$\$\$)"
echo
read -rp "Pattern: " pattern
if [[ "$pattern" == "examples" ]]; then
show_examples
get_pattern
return
fi
if [[ -z "$pattern" ]]; then
print_error "Pattern cannot be empty"
exit 1
fi
echo "$pattern"
}
# Search with ast-grep and show results with fzf
search_pattern() {
local pattern="$1"
local lang="$2"
print_info "Searching for: $pattern"
echo
# Create temporary file for results
local results
results=$(mktemp)
# Run ast-grep and save results
if ! ast-grep --pattern "$pattern" -l "$lang" > "$results" 2>&1; then
print_error "ast-grep search failed"
cat "$results"
rm "$results"
exit 1
fi
# Check if we have results
if [[ ! -s "$results" ]]; then
print_warning "No matches found for pattern: $pattern"
rm "$results"
exit 0
fi
# Count results
local count
count=$(wc -l < "$results")
print_success "Found $count match(es)"
echo
# Interactive selection with fzf
local selected
selected=$(cat "$results" | fzf \
--preview "bat --color=always --highlight-line {2} {1}" \
--preview-window=right:60%:wrap \
--delimiter : \
--prompt "Select file > " \
--header "Press Enter to open, Ctrl-Y to copy path, Ctrl-C to exit" \
--bind "ctrl-y:execute-silent(echo {1} | pbcopy)+abort" \
--bind "ctrl-/:toggle-preview" \
--height=100% \
|| true)
rm "$results"
if [[ -z "$selected" ]]; then
print_info "No file selected"
exit 0
fi
# Extract file path and line number
local file line
file=$(echo "$selected" | cut -d: -f1)
line=$(echo "$selected" | cut -d: -f2)
# Show action menu
show_action_menu "$file" "$line" "$pattern" "$lang"
}
# Action menu for selected file
show_action_menu() {
local file="$1"
local line="$2"
local pattern="$3"
local lang="$4"
print_header "Selected: $file:$line"
echo "What would you like to do?"
echo
echo "1. Open in editor (\$EDITOR)"
echo "2. Show context (20 lines)"
echo "3. Show all matches in file"
echo "4. Copy file path to clipboard"
echo "5. Find usages of pattern in project"
echo "6. Exit"
echo
read -rp "Choice: " choice
case "$choice" in
1)
print_info "Opening $file at line $line..."
${EDITOR:-vim} "+$line" "$file"
;;
2)
print_header "Context around $file:$line"
bat --color=always --highlight-line "$line" --line-range "$((line-10)):$((line+10))" "$file"
;;
3)
print_header "All matches in $file"
ast-grep --pattern "$pattern" -l "$lang" "$file"
;;
4)
echo -n "$file" | pbcopy
print_success "Copied to clipboard: $file"
;;
5)
print_header "Finding usages in project..."
# Extract the identifier from the pattern
local identifier
identifier=$(echo "$pattern" | grep -oE '\$[A-Z_]+' | head -1 | sed 's/\$//')
if [[ -n "$identifier" ]]; then
print_info "Searching for references to: $identifier"
rg "\b$identifier\b" -t "$lang"
else
print_warning "Could not extract identifier from pattern"
fi
;;
6)
print_info "Exiting..."
exit 0
;;
*)
print_error "Invalid choice"
show_action_menu "$file" "$line" "$pattern" "$lang"
;;
esac
}
# Multi-pattern search mode
multi_pattern_search() {
print_header "Multi-Pattern Search"
print_info "Enter patterns (one per line, empty line to finish):"
local patterns=()
while true; do
read -rp "Pattern $(( ${#patterns[@]} + 1 )): " pattern
if [[ -z "$pattern" ]]; then
break
fi
patterns+=("$pattern")
done
if [[ ${#patterns[@]} -eq 0 ]]; then
print_error "No patterns provided"
exit 1
fi
print_info "Searching for ${#patterns[@]} pattern(s)..."
local all_results
all_results=$(mktemp)
for pattern in "${patterns[@]}"; do
ast-grep --pattern "$pattern" -l "$LANGUAGE" >> "$all_results" 2>&1 || true
done
# Remove duplicates and sort
sort -u "$all_results" | fzf \
--preview "bat --color=always {1}" \
--preview-window=right:60%:wrap \
--delimiter : \
--multi \
--prompt "Select files > " \
--header "Tab to select multiple, Enter to confirm" \
--bind "ctrl-a:select-all" \
--bind "ctrl-d:deselect-all" \
--height=100% \
|| true
rm "$all_results"
}
# Help message
show_help() {
cat <<EOF
${BLUE}Interactive Code Finder${NC}
Usage: $0 [OPTIONS]
Options:
-p, --pattern PATTERN Search for specific pattern
-l, --language LANG Set language (default: py)
-m, --multi Multi-pattern search mode
-e, --examples Show pattern examples and exit
-h, --help Show this help message
Languages:
py, python Python
js, javascript JavaScript
ts, typescript TypeScript
rs, rust Rust
go Go
zig Zig
rb, ruby Ruby
Examples:
$0 # Interactive mode
$0 -p "def \$FUNC(\$\$\$):" # Search for Python functions
$0 -l js -p "function \$F(\$\$\$)" # Search for JS functions
$0 -m # Multi-pattern search
$0 -e # Show pattern examples
Pattern Syntax:
\$VAR - Single node (identifier, expression)
\$\$\$ - Zero or more nodes (variadic)
\$\$ - Statement sequence
EOF
}
# Main function
main() {
local pattern=""
local multi_mode=false
# Check dependencies first
check_dependencies
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--pattern)
pattern="$2"
shift 2
;;
-l|--language)
LANGUAGE="$2"
shift 2
;;
-m|--multi)
multi_mode=true
shift
;;
-e|--examples)
show_examples
exit 0
;;
-h|--help)
show_help
exit 0
;;
*)
print_error "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Multi-pattern mode
if [[ "$multi_mode" == true ]]; then
multi_pattern_search
exit 0
fi
# Get pattern if not provided
if [[ -z "$pattern" ]]; then
pattern=$(get_pattern)
fi
# Search
search_pattern "$pattern" "$LANGUAGE"
}
main "$@"