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

TAB-complete files and binaries on PATH #72

Merged
merged 2 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 81 additions & 4 deletions repl.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -420,23 +420,100 @@ strings to match candidates against (for example in the form \"package:sym\")."
(select-completions "str:con" (list "str:containsp" "str:concat" "str:constant-case"))
:test #'string-equal)))

(defun complete-filename-p (text start end &key (line-buffer rl:*line-buffer*))
"Return T if we should feed the tab completion candidates filenames, instead of the regular Lisp symbols.
We answer yes when we are tab-completing a secord word on the prompt and a quote comes before it.

TEXT, START and END: see `custom-complete'.

Ex:

!ls \"test TAB => yes return files instead of lisp symbols for completion.
!\"tes TAB => well, no.
(load \"test TAB => yes
(load (test TAB => no
"
(declare (ignore end))
(and (not (shell-passthrough-p text))
(> start 1) ;; 1 is an opening parenthesis.
(char-equal #\" (elt line-buffer (1- start))) ;; after an opening quote.
))

#+test-ciel
(progn
(assert (complete-filename-p "test" 7 10 :line-buffer "(load \"test"))
(assert (complete-filename-p "test" 7 10 :line-buffer "(!foo \"test"))
(assert (not (complete-filename-p "test" 1 5 :line-buffer "\"test")))
)

(defun filter-candidates (text file-candidates)
"Return a list of files (strings) in the current directory that start with TEXT."
;; yeah, this calls for more features. Hold on a minute will you.
(remove-if #'null
(mapcar (lambda (path)
(let ((namestring (file-namestring path)))
(when (str:starts-with-p text namestring)
namestring)))
file-candidates)))

(defun complete-binaries-from-path-p (text start end &key (line-buffer rl:*line-buffer*))
"Return T if we should TAB-complete shell executables, and search them on the PATH.

START must be 0: we are writing the first word on the readline prompt,
TEXT must start with ! the mark of the shell pass-through."
(declare (ignore end line-buffer))
(and (zerop start)
(str:starts-with-p "!" text)))

(defun find-binaries-candidates (text)
"Find binaries starting with TEXT in PATH.

Return: a list of strings."
(loop with s = (string-left-trim "!" text)
for dir in (uiop:getenv-absolute-directories "PATH")
for res = (filter-candidates s (uiop:directory-files dir))
collect res into candidates
finally (return
;; we got "!text", we have to return candidates
;; with the "!" prefix, so that readline agrees they are completions.
(mapcar (lambda (bin)
(str:concat "!" bin))
(alexandria:flatten candidates)))))

(defun custom-complete (text &optional start end)
"Custom completer function for readline, triggered when we press TAB.

START and END are required in the lambda list but are not used. We
only complete package and function names, they would help to
complete arguments."
(declare (ignore start end))
Complete filenames on the current directory when appropriate (after a quote).

TEXT is the current word being type. Not the full command line.

START is the start of this word. If we type the first word of the command
and TAB-complete it, then START equals 0. For a second word, START != 0.

Ex:

!ls te TAB

TEXT is \"te\", START is 4 and END is 6.

That way we give other completion candidates depending on START."
(when (string-equal text "")
(return-from custom-complete nil))
(destructuring-bind (sym-name pkg-name external-p)
(get-package-for-search (string-upcase text))
(when (and pkg-name
(not (find-package pkg-name)))
(return-from custom-complete nil))

(select-completions
(str:downcase text)
(cond
((complete-binaries-from-path-p text start end :line-buffer rl:*line-buffer*)
(find-binaries-candidates text))
((complete-filename-p text start end :line-buffer rl:*line-buffer*)
;; complete file names on the current directory.
;; Yes we could complete both: lisp symbols AND files. See with usage.
(filter-candidates text (uiop:directory-files ".")))
((zerop (length pkg-name))
(list-symbols-and-packages sym-name))
(external-p
Expand Down
7 changes: 7 additions & 0 deletions shell-utils.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
(when arg
(namestring (pathname-name arg)))))

(defun shell-passthrough-p (arg)
"Return t if arg (string) starts with \"!\".

This is used to offer custom TAB completion, not to launch shell commands.
The Clesh readtable is responsible of that."
(str:starts-with-p "!" arg))

(defun shell-command-wrapper-p (command)
"Is this command (string) a shell wrapper? (such as sudo or env)

Expand Down