Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4c0e359
tramp: initial working version
MasterCsquare Jul 21, 2026
431fb64
tramp: performance improved
MasterCsquare Jul 21, 2026
514e667
tramp: fix sudo password keystroke leakage into file buffer
MasterCsquare Jul 21, 2026
81c9329
tramp: fix sudo password leaking into file buffer
MasterCsquare Jul 21, 2026
6c19601
tramp: fix tab completion for ssh and sudo paths
MasterCsquare Jul 21, 2026
8946318
fix contract.yml errors
MasterCsquare Jul 21, 2026
ea06bc3
tramp: fix completion filtering by partial input
MasterCsquare Jul 21, 2026
1ae444c
tramp: Add README
MasterCsquare Jul 22, 2026
1100231
tramp: restrict auto-enable to Unix platforms
MasterCsquare Jul 22, 2026
761871f
tramp: shorter for uiop:ensure-list
MasterCsquare Jul 22, 2026
abd86c9
tramp: remove tramp-* prefix
MasterCsquare Jul 22, 2026
36d951f
tramp: re-prompt password when input wrong password
MasterCsquare Jul 22, 2026
1691160
tramp: re-prompt password when input wrong password for ssh
MasterCsquare Jul 22, 2026
d7af587
tramp: add comments for virtual file system hooks
MasterCsquare Jul 22, 2026
90ef8f2
tramp: replace unnessassry :: with :
MasterCsquare Jul 22, 2026
2f568c4
file-utils: extract %call-virtual-handlers to DRY virtual hook dispatch
MasterCsquare Jul 24, 2026
93d8588
tramp: replace probe-file with exist-program-p
MasterCsquare Jul 24, 2026
d5d04d8
tramp: split ssh-ensure-auth into smaller parts
MasterCsquare Jul 24, 2026
df6368a
tramp: simplify file-completion
MasterCsquare Jul 25, 2026
c57d299
tramp: fix file completion replacing entire TRAMP path
MasterCsquare Jul 26, 2026
3c8b87a
tramp: fix password leaking into file content on sudo writes
MasterCsquare Jul 30, 2026
7d24c25
tramp: implement TRAMP-aware terminal support
MasterCsquare Jul 30, 2026
5553b87
tramp: update README
MasterCsquare Jul 30, 2026
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
4 changes: 4 additions & 0 deletions extensions/tramp/lem-tramp.asd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(defsystem "lem-tramp"
:depends-on ("lem/core" "flexi-streams" "str")
:serial t
:components ((:file "tramp")))
600 changes: 600 additions & 0 deletions extensions/tramp/tramp.lisp

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion lem.asd
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@
"lem-tree-sitter"
"lem-git-gutter"
"lem-skk-mode"
"lem-display-time-mode"))
"lem-display-time-mode"
"lem-tramp"))

(defsystem "lem"
:version "2.3.0"
Expand Down
101 changes: 78 additions & 23 deletions src/buffer/file-utils.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@
:file-size
:copy-file-or-directory
:virtual-probe-file
:with-open-virtual-file))
:with-open-virtual-file
;; Virtual filesystem hooks
:*virtual-file-open*
:*virtual-probe-file-functions*
:*virtual-expand-file-name-functions*
:*virtual-directory-files-functions*
:*virtual-file-metadata-functions*
:*virtual-directory-exists-p-functions*
:virtual-directory-exists-p))
(in-package :lem/buffer/file-utils)

(defun guess-host-name (filename)
Expand Down Expand Up @@ -49,8 +57,11 @@

(defun expand-file-name (filename &optional (directory (uiop:getcwd)))
(when (pathnamep filename) (setf filename (namestring filename)))
(let ((pathname (parse-filename filename (pathname-directory directory))))
(namestring (merge-pathnames pathname directory))))
(or (loop :for f :in *virtual-expand-file-name-functions*
:for result := (funcall f filename directory)
:when result :do (return result))
(let ((pathname (parse-filename filename (pathname-directory directory))))
(namestring (merge-pathnames pathname directory)))))

(defun tail-of-pathname (pathname)
(let ((pathname (uiop:ensure-absolute-pathname pathname #p"/")))
Expand All @@ -71,9 +82,12 @@
x2)))))

(defun virtual-probe-file (pathspec &optional (base-dir pathspec))
(cond
((ppcre:scan "^~/.*" (namestring base-dir)) (probe-file% pathspec))
(t (probe-file pathspec))))
(or (loop :for f :in *virtual-probe-file-functions*
:for result := (funcall f pathspec base-dir)
:when result :do (return result))
(cond
((ppcre:scan "^~/.*" (namestring base-dir)) (probe-file% pathspec))
(t (probe-file pathspec)))))

(defun sort-files (pathnames &key (key #'namestring) (test #'string<))
"Sort a list of pathnames."
Expand All @@ -91,11 +105,14 @@
(sort-files files))))

(defun directory-files (pathspec)
(if (uiop:directory-pathname-p pathspec)
(list (pathname pathspec))
(or (mapcar (lambda (x) (virtual-probe-file x pathspec))
(directory pathspec))
(list pathspec))))
(or (loop :for f :in *virtual-directory-files-functions*
Comment thread
MasterCsquare marked this conversation as resolved.
Outdated
:for result := (funcall f pathspec)
:when result :do (return result))
(if (uiop:directory-pathname-p pathspec)
(list (pathname pathspec))
(or (mapcar (lambda (x) (virtual-probe-file x pathspec))
(directory pathspec))
(list pathspec)))))

(defun list-directory (directory &key directory-only (sort-method :pathname))
(delete nil
Expand All @@ -108,21 +125,27 @@
:sort-method sort-method))))))

(defun file-size (pathname)
#+sbcl
(sb-posix:stat-size (sb-posix:stat pathname))
#+lispworks
(system:file-size pathname)
#+(and (not lispworks) win32)
(return-from file-size nil)
#-win32
(ignore-errors (with-open-file (in pathname) (file-length in))))
(or (loop :for f :in *virtual-file-metadata-functions*
:for result := (funcall f pathname :size)
:when result :do (return result))
#+sbcl
(sb-posix:stat-size (sb-posix:stat pathname))
#+lispworks
(system:file-size pathname)
#+(and (not lispworks) win32)
(return-from file-size nil)
#-win32
(ignore-errors (with-open-file (in pathname) (file-length in)))))

(defun file-mtime (pathname)
"Return the file's last data modification time."
#+sbcl
(sb-posix:stat-mtime (sb-posix:stat pathname))
#-sbcl
(error "file-utils: file-mtime is not implemented for your implementation."))
(or (loop :for f :in *virtual-file-metadata-functions*
:for result := (funcall f pathname :mtime)
:when result :do (return result))
#+sbcl
(sb-posix:stat-mtime (sb-posix:stat pathname))
#-sbcl
(error "file-utils: file-mtime is not implemented for your implementation.")))

(defun copy-file-or-directory (from to)
(let ((base-dir from))
Expand All @@ -142,6 +165,38 @@

(defparameter *virtual-file-open* nil)

(defparameter *virtual-probe-file-functions* nil
"A list of functions for virtual probe-file.
Comment thread
MasterCsquare marked this conversation as resolved.
Each function receives (pathspec &optional base-dir) and should return
a pathname if the file exists, or nil to pass to the next handler.")

(defparameter *virtual-expand-file-name-functions* nil
"A list of functions for virtual expand-file-name.
Each function receives (filename &optional directory) and should return
an expanded filename string, or nil to pass to the next handler.")

(defparameter *virtual-directory-files-functions* nil
"A list of functions for virtual directory-files.
Each function receives (pathspec) and should return a list of pathnames,
or nil to pass to the next handler.")

(defparameter *virtual-file-metadata-functions* nil
"A list of functions for virtual file metadata (size, mtime).
Each function receives (pathname op) where op is :size, :mtime, or :write-date,
and should return the value, or nil to pass to the next handler.")

(defparameter *virtual-directory-exists-p-functions* nil
"A list of functions for virtual directory-exists-p.
Each function receives (directory) and should return the directory if it exists,
or nil to pass to the next handler.")

(defun virtual-directory-exists-p (directory)
"Check if a directory exists, using virtual filesystem hooks if applicable."
(or (loop :for f :in *virtual-directory-exists-p-functions*
:for result := (funcall f directory)
:when result :do (return result))
(uiop:directory-exists-p directory)))

(defun open-virtual-file (filename &key external-format direction element-type)
(apply #'values
(or (loop :for f :in *virtual-file-open*
Expand Down
15 changes: 9 additions & 6 deletions src/buffer/file.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@
(when (pathnamep filename)
(setf filename (namestring filename)))
(setf filename (expand-file-name filename))
(unless (uiop:directory-exists-p (directory-namestring filename))
(unless (virtual-directory-exists-p (directory-namestring filename))
(error 'directory-does-not-exist :directory (directory-namestring filename)))
(alexandria:when-let (it (probe-file filename)) (setf filename (namestring it)))
(alexandria:when-let (it (virtual-probe-file filename)) (setf filename (namestring it)))
(cond ((uiop:directory-pathname-p filename)
(if *find-directory-function*
(funcall *find-directory-function* filename)
Expand All @@ -91,7 +91,7 @@
:enable-undo-p nil
:temporary temporary)))
(setf (buffer-filename buffer) filename)
(when (probe-file filename)
(when (virtual-probe-file filename)
(let ((*inhibit-modification-hooks* t))
(let ((encoding
(handler-bind ((encoding-read-error
Expand Down Expand Up @@ -186,15 +186,18 @@
(%%write-region-to-file encoding out))))))

(defun file-write-date* (buffer)
(if (probe-file (buffer-filename buffer))
(file-write-date (buffer-filename buffer))))
(if (virtual-probe-file (buffer-filename buffer))
(or (loop :for f :in *virtual-file-metadata-functions*
:for result := (funcall f (buffer-filename buffer) :write-date)
:when result :do (return result))
(file-write-date (buffer-filename buffer)))))

(defun update-changed-disk-date (buffer)
(setf (buffer-last-write-date buffer)
(file-write-date* buffer)))

(defun changed-disk-p (buffer)
(and (buffer-filename buffer)
(probe-file (buffer-filename buffer))
(virtual-probe-file (buffer-filename buffer))
(not (eql (buffer-last-write-date buffer)
(file-write-date* buffer)))))
2 changes: 1 addition & 1 deletion src/buffer/internal/buffer.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ Options that can be specified by arguments are ignored if `temporary` is NIL and
(namestring (uiop:getcwd))))

(defun (setf buffer-directory) (directory &optional (buffer (current-buffer)))
(let ((result (uiop:directory-exists-p directory)))
(let ((result (virtual-directory-exists-p directory)))
(unless result
(error 'directory-does-not-exist :directory directory))
(setf (buffer-%directory buffer)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/file.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@

(defun directory-for-file-or-lose (filename)
(let ((directory (directory-namestring filename)))
(unless (or (uiop:directory-exists-p directory)
(unless (or (virtual-directory-exists-p directory)
(maybe-create-directory directory))
(error 'editor-abort))
directory))
Expand Down
Loading