emacs-bookmark.zsh 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. # Emacs-based bookmark system
  2. autoload colors && colors
  3. zmodload -F zsh/stat b:zstat
  4. zmodload zsh/datetime
  5. zmodload zsh/mapfile
  6. local __bm_bookmark_cache=()
  7. let __bm_last_read_time=-1
  8. function __bm_offset_to_row_col {
  9. let off="${2}"
  10. let row=1
  11. let col=0
  12. for line in "${(@f)mapfile[${1}]}"; do
  13. let len="${#line}"
  14. if (( off > len )); then
  15. off='off - (len + 1)'
  16. if (( off <= 0 )); then
  17. (( len == 0 )) && col=0 || col='len - 1'
  18. break
  19. fi
  20. row+=1
  21. else
  22. col='off'
  23. off=0
  24. break;
  25. fi
  26. done <"${1}"
  27. if (( off > 0 )); then
  28. row+=-1
  29. fi
  30. printf '%d\0%d' "${row}" "${col}"
  31. }
  32. function __bm_find_user_emacs_dir {
  33. [[ -f "${HOME}/.emacs.d/var/bookmark-default.el" ]] &&
  34. { printf '%s' "${HOME}/.emacs.d/var/bookmark-default.el"; return 0 }
  35. [[ -f "${HOME}/.emacs.d/bookmark-default.el" ]] &&
  36. { printf '%s' "${HOME}/.emacs.d/bookmark-default.el"; return 0 }
  37. [[ -f "${HOME}/.config/emacs/var/bookmark-default.el" ]] &&
  38. { printf '%s' "${HOME}/.config/emacs/var/bookmark-default.el"; return 0 }
  39. [[ -f "${HOME}/.config/emacs/bookmark-default.el" ]] &&
  40. { printf '%s' "${HOME}/.config/emacs/bookmark-default.el"; return 0 }
  41. [[ -f "${XDG_CONFIG_HOME}/emacs/var/bookmark-default.el" ]] &&
  42. { printf '%s' "${XDG_CONFIG_HOME}/emacs/var/bookmark-default.el"; return 0 }
  43. [[ -f "${XDG_CONFIG_HOME}/emacs/bookmark-default.el" ]] &&
  44. { printf '%s' "${XDG_CONFIG_HOME}/emacs/bookmark-default.el"; return 0 }
  45. printf 'Could not discover Emacs bookmark file! Please set $BM_MODE to
  46. "daemon" or define $BM_BOOKMARK_PATH!\n'
  47. return 1
  48. }
  49. BM_BOOKMARK_PATH="${BM_BOOKMARK_PATH:-"$(__bm_find_user_emacs_dir)"}"
  50. function __bm_hash_dirs {
  51. # First empty the hash table
  52. hash -dr
  53. # Then add the hash dirs
  54. for ((i = 1; i < ${#__bm_bookmark_cache}; i+=4)); do
  55. local name="${__bm_bookmark_cache[${i}]://=/}"
  56. hash -d "${name}=${__bm_bookmark_cache[${i} + 2]}"
  57. done
  58. }
  59. function __bm_update_bookmark_list {
  60. __bm_last_read_time="${EPOCHSECONDS}"
  61. local args
  62. local script
  63. case "${BM_MODE}" in
  64. 'daemon')
  65. script=$(<<'EOF'
  66. (progn
  67. (require 'server)
  68. (dolist (entry (server-eval-at
  69. "server"
  70. '(when (boundp 'bookmark-alist)
  71. bookmark-alist)))
  72. (let ((path (alist-get 'filename (cdr entry) ""))
  73. (pos (alist-get 'position (cdr entry) 1)))
  74. (princ (format "%s\0%s\0%s\0%s\0" (car entry) path
  75. (expand-file-name path) pos)))))
  76. EOF
  77. )
  78. ;;
  79. ''|'emacs')
  80. args="--insert=${BM_BOOKMARK_PATH}"
  81. script=$(<<'EOF'
  82. (progn
  83. (dolist (entry (read (current-buffer)))
  84. (let ((path (alist-get 'filename (cdr entry) ""))
  85. (pos (alist-get 'position (cdr entry) 1)))
  86. (princ (format "%s\0%s\0%s\0%s\0" (car entry) path
  87. (expand-file-name path) pos)))))
  88. EOF
  89. )
  90. ;;
  91. *)
  92. printf 'Unknown value for $BM_MODE: "%s"\n' "${BM_MODE}"
  93. return 1
  94. ;;
  95. esac
  96. __bm_bookmark_cache=(${(0)"$(command emacs --batch ${args} --eval "${script}")"})
  97. __bm_hash_dirs
  98. }
  99. function __bm_bookmark_location {
  100. local parts=(${(s:/:)"${2}"})
  101. local bm_name="${parts[1]}"
  102. local rest_arr=(${parts:1})
  103. local rest="${(j:/:)rest_arr}"
  104. for ((i = 1; i < ${#__bm_bookmark_cache}; i+=4)); do
  105. if [[ "${bm_name}" = "${__bm_bookmark_cache[${i}]}" ]]; then
  106. __bm_res=("${__bm_bookmark_cache[${i} + 2]}"
  107. "${__bm_bookmark_cache[${i} + 3]}"
  108. "${rest}")
  109. return 0
  110. fi
  111. done
  112. __bm_res=()
  113. return 1
  114. }
  115. function __bm_list_bookmarks {
  116. for ((i = 1; i < ${#__bm_bookmark_cache}; i+=4)); do
  117. local name="${__bm_bookmark_cache[${i}]}"
  118. local pretty_path="${__bm_bookmark_cache[${i} + 1]}"
  119. local abs_path="${__bm_bookmark_cache[${i} + 2]}"
  120. local print_color=""
  121. if [[ -d "${abs_path}" ]]; then
  122. print_color="${bold_color}${fg[blue]}"
  123. fi
  124. printf "${print_color}%s${reset_color} => %s\n" \
  125. "${name}" "${pretty_path}"
  126. done
  127. }
  128. function _bookmarks {
  129. for ((i = 1; i < ${#__bm_bookmark_cache}; i+=4)); do
  130. compadd -q "${__bm_bookmark_cache[${i}]}"
  131. done
  132. }
  133. alias lsbm="__bm_update_bookmark_list && __bm_list_bookmarks"
  134. function bm {
  135. __bm_update_bookmark_list || \
  136. { printf 'Updating bookmark list failed!\n'; return 1 }
  137. (( ${#} == 0 )) && { __bm_list_bookmarks; return }
  138. local __bm_res
  139. __bm_bookmark_location __bm_res "${1}"
  140. local bm_loc="${__bm_res[1]}"
  141. local target="${__bm_res[1]}/${__bm_res[3]}"
  142. if (( ${#__bm_res} != 0 )) && [[ -e "${bm_loc}" ]]; then
  143. if [[ -d "${target}" ]]; then
  144. cd "${target}"
  145. [[ "${BM_CWD_LS}" == 'true' ]] && ls || true
  146. elif [[ -e "${bm_loc}" ]]; then
  147. let offset="${__bm_res[2]}"
  148. local rowcol=(${(0)"$(__bm_offset_to_row_col "${bm_loc}" "${offset}")"})
  149. ${=EDITOR} "+${rowcol[1]}:${rowcol[2]}" "${bm_loc}"
  150. else
  151. printf 'Bookmark exists, but trailing path doesn'"'"'t: "%s"\n' \
  152. "${(q)__bm_res[3]}"
  153. return 1
  154. fi
  155. else
  156. printf 'No such bookmark: "%s"\n' "${(q)1}"
  157. return 1
  158. fi
  159. }
  160. function _bm {
  161. (( "${CURRENT}" == 2 )) || return
  162. local arg="${(Q)words[${CURRENT}]}"
  163. if ! [[ "${arg}" == */* ]]; then
  164. for ((i = 1; i < ${#__bm_bookmark_cache}; i+=4)); do
  165. compadd -q -S '/' -- "${__bm_bookmark_cache[${i}]}"
  166. done
  167. else
  168. local __bm_res
  169. __bm_bookmark_location __bm_res "${arg}"
  170. if [[ -d "${__bm_res[1]}" ]]; then
  171. local parts=(${(s:/:)__bm_res[3]})
  172. local bm="${${(s:/:)${arg}}[1]}"
  173. local subdir="${(j:/:)parts[1,-2]}"
  174. local search="${parts[${#parts}]}"
  175. if ((${#parts} > 0)) && [[ "${arg}" == */ ]]; then
  176. subdir="${subdir}/${search}"
  177. subdir="${subdir#/}"
  178. search=""
  179. fi
  180. local pre_path="${__bm_res[1]}/${subdir}/"
  181. local raw_arg="${words[${CURRENT}]}"
  182. local prefix="${${(s:/:)${raw_arg}}[1]}/${subdir}"
  183. if ! [[ -z "${subdir}" ]]; then
  184. prefix+='/'
  185. fi
  186. compset -P "${(b)prefix}"
  187. for file in "${pre_path}"*; do
  188. compadd -W "${pre_path}" -f "${file:t}"
  189. done
  190. fi
  191. fi
  192. }
  193. compdef _bm bm
  194. function bmadd {
  195. if [[ "${1}" = '-h' ]]; then
  196. printf 'usage: bmadd [PATH] [NAME]\n'
  197. return 0
  198. fi
  199. [[ "${1}" = '--' ]] && shift 1
  200. local name
  201. local loc
  202. case "${#}" in
  203. 0)
  204. loc="${PWD}"
  205. name="${PWD:t}"
  206. ;;
  207. 1)
  208. loc="${1}"
  209. name="${1:t}"
  210. ;;
  211. *)
  212. loc="${1}"
  213. name="${2}"
  214. ;;
  215. esac
  216. __bm_update_bookmark_list
  217. local __bm_res
  218. if __bm_bookmark_location __bm_res "${name}" >/dev/null; then
  219. printf 'Bookmark "%s" already exists. Overwrite it? [y/N] ' "${(q)name}"
  220. read -q
  221. let ans=${?}
  222. printf '\n'
  223. (( ${ans} != 0 )) && return 1
  224. fi
  225. local res="$(emacsclient --eval \
  226. "(let* ((loc \"${loc:gs#\\#\\\\#:gs#\"#\\\"#}\")
  227. (name \"${name:gs#\\#\\\\#:gs#\"#\\\"#}\")
  228. (res (with-temp-buffer
  229. (set-visited-file-name loc t nil)
  230. (bookmark-set name)
  231. (set-buffer-modified-p nil)))
  232. (inhibit-message t))
  233. (bookmark-save)
  234. res)")"
  235. [[ "${res}" = 'nil' ]] && printf 'Added bookmark "%s"\n' "${(q)name}" \
  236. || { printf '%s\n' "${res}"; return 1 }
  237. __bm_update_bookmark_list
  238. }
  239. function _bmadd {
  240. _arguments ':file:_files' ':name'
  241. }
  242. compdef _bmadd bmadd
  243. function bmrm {
  244. if [[ "${1}" = '-h' ]]; then
  245. printf 'usage: bmrm [NAME]\n'
  246. return 0
  247. fi
  248. [[ "${1}" = '--' ]] && shift 1
  249. if (( ${#} < 1 )); then
  250. printf 'usage: bmrm [NAME]\n'
  251. return 1
  252. fi
  253. __bm_update_bookmark_list
  254. local __bm_res
  255. __bm_bookmark_location __bm_res "${1}" >/dev/null || \
  256. { printf 'No such bookmark: "%s"\n' "${1}"; return 1 }
  257. printf 'Really delete "%s"? [y/N] ' "${(q)1}"
  258. if read -q; then
  259. printf '\n'
  260. local res="$(emacsclient --eval \
  261. "(let* ((res (bookmark-delete \"${1:gs#\\#\\\\#:gs#\"#\\\"#}\"))
  262. (inhibit-message t))
  263. (bookmark-save)
  264. res)")"
  265. [[ "${res}" = 'nil' ]] && printf 'Deleted bookmark "%s"\n' "${(q)1}" \
  266. || { printf '%s\n' "${res}"; return 1 }
  267. __bm_update_bookmark_list
  268. else
  269. printf '\n'
  270. return 1
  271. fi
  272. }
  273. function _bmrm {
  274. _arguments ':bookmark:_bookmarks'
  275. }
  276. compdef _bmrm bmrm
  277. function __bm_precmd_hook {
  278. # Auto reload
  279. if [[ "${BM_AUTO_RELOAD}" == true ]] &&
  280. (( ${__bm_last_read_time} < $(zstat +mtime "${BM_BOOKMARK_PATH}") )); then
  281. __bm_update_bookmark_list
  282. fi
  283. }
  284. (( ${precmd_functions[(I)__bm_precmd_hook]} )) ||
  285. precmd_functions+=(__bm_precmd_hook)
  286. __bm_update_bookmark_list