Skip to content

Commit

Permalink
refactor: cache the files in advance
Browse files Browse the repository at this point in the history
  • Loading branch information
LangLangBart committed Jul 14, 2023
1 parent 40577dd commit 39b2139
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 116 deletions.
237 changes: 124 additions & 113 deletions gh-find-code
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ set -o allexport -o errexit -o nounset -o pipefail

# TODO: add tests

# TODO: Fix display the wrong file in preview when switching between listed items quickly

# ====================== set variables =======================

debug_mode=false
Expand All @@ -27,7 +25,7 @@ BAT_THEME=${BAT_THEME:-Monokai Extended}
SHELL=bash
# a cached version will be used and only then a new one will be pulled
gh_default_cache_time="30m"
gh_default_limit=30
gh_default_limit=20
gh_user_limit=${gh_user_limit:-$gh_default_limit}
gh_accept_json="Accept: application/vnd.github+json"
gh_accept_raw="Accept: application/vnd.github.raw"
Expand All @@ -41,6 +39,10 @@ min_gh_version="2.31.0"
# define colors
COLOR_RESET='\033[0m'
RED_NORMAL='\033[0;31m'
CYAN_NORMAL="\033[0;36m"
CYAN_BOLD="\033[1;36m"
MAGENTA_NORMAL="\033[0;35m"

GREEN_NORMAL='\033[0;32m'
YELLOW_NORMAL='\033[0;33m'
WHITE_BOLD='\033[1;97m'
Expand Down Expand Up @@ -177,7 +179,7 @@ check_version gh "$min_gh_version"
# ===================== helper functions ==========================

sanitize_input() {
if [[ -n $2 ]]; then
if [[ -n ${2-} ]]; then
# replace spaces with '+' and special characters with percent-encoded values
python -c "import urllib.parse; print(urllib.parse.quote_plus('''$1'''))"
else
Expand All @@ -191,39 +193,114 @@ play_notification_sound() {
afplay /System/Library/Sounds/Basso.aiff 2>/dev/null || echo -e "\a"
}

view_contents() {
declare -a line_numbers bat_args editor_args less_args
local file_name owner_repo file_path patterns
local file_extension tempfile_with_ext matched_line less_move_to_line
local sanitized_patterns sanitized_owner_repo sanitized_file_path
IFS=$'\t' read -r file_name _ owner_repo file_path patterns < <(sed -E $'s/[[:space:]]{2,}/\t/g' <<<"$@")

# check if the file does have a file extension and assign it
[[ $file_name =~ \.[[:alnum:]]+$ ]] && file_extension="${file_name##*.}"
# covers cases where the filename starts with a dot, for example '.zshrc'
[[ $file_name == ".${file_extension}" ]] && file_extension="$file_name"

# strings containing operators like '*' or '+' need to be escaped, but keep the "|"
# character unescaped
sanitized_patterns=$(printf '%q\n' "$patterns" | sed 's/\\\|/|/g')

# these sanitizations are necessary because it is possible that file paths can contain
# special characters, e.g. hashtags (#)
sanitized_owner_repo=$(sanitize_input "$owner_repo")
sanitized_file_path=$(sanitize_input "$file_path")

gh api "repos/${sanitized_owner_repo}/contents/${sanitized_file_path}" \
open_query_in_browser() {
local sanitized_query
sanitized_query=$(sanitize_input "$1" true)
if [ -n "$sanitized_query" ]; then
python -m webbrowser "https://github.com/search?q=${sanitized_query}&type=code"
else
play_notification_sound
fi
}

gh_query() {
local data input="$*"
local items total_count
local index owner_repo_name file_name file_path patterns
local file_extension sanitized_patterns sanitized_owner_repo_name sanitized_file_path
local matched_line
declare -a line_numbers
if data=$(gh api search/code \
--method GET \
--cache "$gh_default_cache_time" \
--header "$gh_accept_raw" \
--header "$gh_accept_json" \
--header "$gh_accept_text_match" \
--header "$gh_rest_api_version" \
>"$store_file_contents" \
2> >(tee "$store_gh_content_debug" >&2) ||
die "API failed for repos/${owner_repo}/contents/${file_path}"
--field "per_page=$gh_user_limit" \
--raw-field q="${input}" \
--jq \
$'"\(.items|length) \(.total_count)",
(.items | to_entries[] | {
owner_repo_name: .value.repository.full_name,
file_name: .value.name,
file_path: .value.path,
index: (.key + 1),
# create a unique list of patterns separated by a vertical line to use in
# extended grep
patterns: ([.value.text_matches[] | .. | .text? | select(type=="string")] as $patterns_array |
if $patterns_array == [] then "null" else $patterns_array | unique | join("|") end)
} | [.index, .owner_repo_name, .file_name, .file_path, .patterns] | @tsv)' 2>"$store_gh_search_debug"); then
{
# first line
IFS=' ' read -r items total_count

# listed items
while IFS=$'\t' read -r index owner_repo_name file_name file_path patterns; do
# check if the file does have a file extension and assign it
[[ $file_name =~ \.[[:alnum:]]+$ ]] && file_extension="${file_name##*.}"
# covers cases where the filename starts with a dot, for example '.zshrc'
[[ $file_name == ".${file_extension}" ]] && file_extension="$file_name"

# strings containing operators like '*' or '+' need to be escaped, but keep the "|"
# character unescaped
sanitized_patterns=$(printf '%q\n' "$patterns" | sed 's/\\\|/|/g')

# these sanitizations are necessary because it is possible that file paths can contain
# special characters, e.g. hashtags (#)
sanitized_owner_repo_name=$(sanitize_input "$owner_repo_name")
sanitized_file_path=$(sanitize_input "$file_path")
cp "$store_file_contents" "${store_file_contents}_${index}"
cp "$store_file_contents" "${store_file_contents}_${index}_line_numbers"

curl --silent \
--request POST "localhost:$(cat "$store_fzf_port")" \
--data "transform-header:printf '%b%s of %s cached...%b' '$DARK_GRAY' '$index' '$gh_user_limit' '$COLOR_RESET'"

gh api "repos/${sanitized_owner_repo_name}/contents/${sanitized_file_path}" \
--cache "$gh_default_cache_time" \
--header "$gh_accept_raw" \
--header "$gh_rest_api_version" \
>"${store_file_contents}_${index}" \
2> >(tee "$store_gh_content_debug" >&2) ||
die "API failed for repos/${owner_repo_name}/contents/${file_path}"
line_numbers=()
[[ $patterns != "null" ]] && while IFS='' read -r matched_line; do
line_numbers+=("$matched_line")
done < <(grep --extended-regexp --line-number "$sanitized_patterns" "${store_file_contents}_${index}" | cut -d: -f1)

echo "${line_numbers[*]}" >"${store_file_contents}_${index}_line_numbers"
printf "%s\t%s\t%s\t%b%s%b/%b%s%b\t%b%s%b\n" \
"${line_numbers:-1}" "$file_extension" "$index" "$CYAN_BOLD" "${owner_repo_name%/*}" "$COLOR_RESET" "$CYAN_NORMAL" "${owner_repo_name#*/}" "$COLOR_RESET" "$MAGENTA_NORMAL" "$file_path" "$COLOR_RESET"
done | column -ts $'\t'
} < <(echo "$data")
curl --silent \
--request POST "localhost:$(cat "$store_fzf_port")" \
--data "transform-header:printf '%b%s of ∑ %s%b | ? help · esc quit%b\n' '$YELLOW_NORMAL' '$items' '$total_count' '$DARK_GRAY' '$COLOR_RESET'"
else
if [ -z "$input" ]; then
curl --silent \
--request POST "localhost:$(cat "$store_fzf_port")" \
--data "transform-header:printf '%bPlease enter a search query.%b' '$DARK_GRAY' '$COLOR_RESET'"
else
curl --silent \
--request POST "localhost:$(cat "$store_fzf_port")" \
--data "transform-header:printf '%bFailed to get the search results, check the query syntax.%b' '$RED_NORMAL' '$COLOR_RESET'"
return 0
fi
fi

}

view_contents() {
declare -a line_numbers bat_args editor_args less_args
local file_extension index _ file_path
local file_name
IFS=$'\t' read -r _ file_extension index _ file_path < <(sed -E $'s/[[:space:]]{2,}/\t/g' <<<"$@")

# '--wrap never' seems to be needed without it the fzf preview occasionally would move
# to the wrong line; '--theme' not needed because of 'BAT_THEME' set at the top
bat_args=(
"--terminal-width ${FZF_PREVIEW_COLUMNS:-$COLUMNS}"
$'--terminal-width ${FZF_PREVIEW_COLUMNS:-$COLUMNS}'
"--wrap never"
"--style numbers,header-filename,grid"
"--color always"
Expand All @@ -232,22 +309,23 @@ view_contents() {
[[ -n $file_extension ]] && if bat --language "$file_extension" <<<"test" >/dev/null 2>&1; then
bat_args+=("--language $file_extension")
fi
IFS=' ' read -ra line_numbers <"${store_file_contents}_${index}_line_numbers"

# NOTE: in 'bat', '--line-range' overwrites prior flags, thus makes it inadequate for
# our use case but '--highlight-line' can be used multiple times, would be handy if
# the preview would only show relevant code with a bit of context around each match
# https://github.com/sharkdp/bat/pull/162#pullrequestreview-125072252
[[ $patterns != "null" ]] && while IFS='' read -r matched_line; do
bat_args+=("--highlight-line $matched_line")
line_numbers+=("$matched_line")
done < <(grep --extended-regexp --line-number "$sanitized_patterns" "$store_file_contents" | cut -d: -f1)
for number in "${line_numbers[@]}"; do
bat_args+=("--highlight-line $number")
done
file_name=$(basename "$file_path")

# replace single quotes with escaped back ticks
bat_args+=("--file-name '${file_name//"'"/\`} │ 🅻 ${line_numbers[*]:-<none>}'")

if $open_in_editor && [[ -n $EDITOR ]]; then
tempfile_with_ext="${store_file_contents}_${file_name}"
cp "$store_file_contents" "$tempfile_with_ext"
tempfile_with_ext="${store_file_contents}_${index}_${file_name}"
cp "${store_file_contents}_${index}" "$tempfile_with_ext"
case $(basename "$EDITOR") in
code | codium)
editor_args=(--reuse-window --goto "${tempfile_with_ext}:${line_numbers:-1}")
Expand Down Expand Up @@ -307,94 +385,27 @@ view_contents() {
# https://github.com/sharkdp/bat#using-a-different-pager
bat_args+=("--pager 'less ${less_args[*]}'")
fi

eval bat "${bat_args[*]}" "$store_file_contents"
return 0
fi

# sending the change to 'fzf' via curl; 'fzf' FTW
curl --silent \
--request POST "localhost:$(cat "$store_fzf_port")" \
--data "change-preview-window(+${line_numbers:-1}+3/3)+change-preview:bat ${bat_args[*]} '$store_file_contents'"
eval bat "${bat_args[*]}" "${store_file_contents}_${index}"
}

gh_query() {
local data input="$*"
if [ -z "$input" ]; then
printf "%bfield_1_hidden Please enter a search query.%b" "$DARK_GRAY" "$COLOR_RESET"
return 0
fi
if data=$(gh api search/code \
--method GET \
--cache "$gh_default_cache_time" \
--header "$gh_accept_json" \
--header "$gh_accept_text_match" \
--header "$gh_rest_api_version" \
--field "per_page=$gh_user_limit" \
--raw-field q="${input}" \
--jq \
$'def colors:
{
"gray_normal":"\u001b[90m",
"cyan_normal": "\u001b[36m",
"cyan_bold": "\u001b[1;36m",
"magenta_normal": "\u001b[35m",
"yellow_normal": "\u001b[33m",
"reset": "\u001b[0m"
};
def colored(text; color):
colors[color] + text + colors.reset;
"field_1_hidden " +
(colored("\(.items|length) of ∑ \(.total_count)"; "yellow_normal")) +
(colored(" | ? help · esc quit"; "gray_normal")),
(.items | to_entries[] | {
name: colored(.value.repository.name; "cyan_bold"),
owner: colored(.value.repository.owner.login; "cyan_normal"),
file_name: .value.name,
file_path: colored(.value.path; "magenta_normal"),
index: (.key + 1),
# create a unique list of patterns separated by a vertical line to use in
# extended grep
patterns: ([.value.text_matches[] | .. | .text? | select(type=="string")] as $patterns_array |
if $patterns_array == [] then "null" else $patterns_array | unique | join("|") end)
# ".file_name" is placed at 1st place to allow the "∑" results line to be
# left-aligned (stupid workaround)
} | [ .file_name, .index, "\(.owner)/\(.name)", .file_path, .patterns] | @tsv)' 2>"$store_gh_search_debug"); then
column -ts $'\t' <<<"$data"
else
printf "%bfield_1_hidden Failed to get the search results, check the query syntax.%b" "$RED_NORMAL" "$COLOR_RESET"
return 0
fi
}

open_query_in_browser() {
local sanitized_query
sanitized_query=$(sanitize_input "$1" true)
if [ -n "$sanitized_query" ]; then
python -m webbrowser "https://github.com/search?q=${sanitized_query}&type=code"
else
play_notification_sound
fi
}

# ===================== lets begin ========================

gh_query "$*" | fzf \
--ansi \
--bind $'start:execute-silent:echo $FZF_PORT > $store_fzf_port' \
--bind "change:first+reload-sync:sleep 0.5; gh_query {q}" \
--bind 'ctrl-b:execute-silent:gh browse --repo {3} {4}' \
--bind "change:first+reload-sync:sleep 0.75; gh_query {q}" \
--bind 'ctrl-b:execute-silent:gh browse --repo {4} {5}' \
--bind 'ctrl-e:execute:[[ -n {q} && -n {} ]] && open_in_editor=true view_contents {}' \
--bind 'ctrl-o:execute-silent:open_query_in_browser {q}' \
--bind 'ctrl-r:reload-sync:gh_query {q}' \
--bind 'ctrl-r:reload:gh_query {q}' \
--bind 'ctrl-u:clear-query' \
--bind 'enter:execute:[[ -n {q} && -n {} ]] && open_in_pager=true view_contents {}' \
--bind "esc:become:" \
--bind "focus:change-preview:sleep 0.5; [[ -n {q} && -n {} ]] && view_contents {}" \
--bind "load:change-preview:[[ -n {q} && -n {} ]] && view_contents {}" \
--bind '?:change-preview(print_help_text)+change-preview-window:+1' \
--bind ';:jump' \
--bind 'tab:toggle-preview+change-preview:[[ -n {q} && -n {} ]] && view_contents {}' \
--bind 'load:change-preview([[ -n {q} && -n {} ]] && view_contents {})+change-preview-window:+{1}+3/3' \
--bind 'tab:toggle-preview+change-preview([[ -n {q} && -n {} ]] && view_contents {})+change-preview-window:+{1}+3/3' \
--border block \
--color 'bg+:233,bg:235,gutter:235,border:238,scrollbar:235' \
--color 'preview-bg:234,preview-border:236,preview-scrollbar:237' \
Expand All @@ -405,17 +416,17 @@ gh_query "$*" | fzf \
--disabled \
--ellipsis '' \
--height=100% \
--header-lines 1 \
--header-lines 0 \
--info hidden \
--jump-labels 'abcdefghijklmnopqrstuvwxyz' \
--layout reverse \
--listen \
--no-multi \
--pointer '' \
--preview "[[ -n {q} && -n {} ]] && view_contents {}" \
--preview-window 'border-block:~3:nohidden:right:nowrap:65%:<70(bottom:75%)' \
--preview-window 'border-block:~3:+{1}+3/3:nohidden:right:nowrap:65%:<70(bottom:75%)' \
--prompt "$fzf_prompt" \
--query "$*" \
--scrollbar '│▐' \
--separator '' \
--with-nth=2..4
--with-nth=3..
5 changes: 2 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ supporting regex.</sub>
| Flags | Description |
| ----- | -------------------------------------------------------- |
| `-d` | debug mode, temporary files are not deleted on exit |
| `-l` | limit the number of listed results (default 30, max 100) |
| `-l` | limit the number of listed results (default 20, max 100) |
| `-h` | help |

| Key Bindings fzf | Description |
Expand Down Expand Up @@ -133,9 +133,8 @@ export FZF_DEFAULT_OPTS="
will automatically scroll to the matching line found.

### Miscellaneous
- Fast movements between list items can cause the wrong file to be displayed in the
preview.
- Be careful which files you open in your editor to avoid triggering something unintended.

---

## 💪 Contributing
Expand Down

0 comments on commit 39b2139

Please sign in to comment.