Tramp for Lem#2271
Conversation
Replaced the unconditional password prompt in the sudo auth path with a `sudo -n true` pre-check. On a fresh connection this runs in ~0.02s, providing a natural UI settling delay — the same reason SSH never had this problem (its BatchMode connection test takes ~3s). When `sudo -n true` succeeds (recent sudo timestamp still valid), no password prompt is needed at all, matching terminal sudo behaviour.
|
❌ Code Contractor Validation: FAILED 📋 Contract Configuration: contract (Source: Repository)version: 2
trigger:
paths:
- "extensions/**"
- "frontends/**/*.lisp"
- "src/**"
- "tests/**"
- "contrib/**"
- "**/*.asd"
head_branches:
exclude:
- 'revert-*'
validation:
limits:
max_total_changed_lines: 400
max_delete_ratio: 0.5
max_files_changed: 10
severity: warning
ai:
system_prompt: |
You are a senior Common Lisp engineer reviewing code for Lem editor.
Lem is a text editor with multiple frontends (ncurses, SDL2, webview).
Focus on maintainability, consistency with existing code, and Lem-specific conventions.
rules:
# === File Structure ===
- name: defpackage_rule
prompt: |
First form must be `defpackage` or `uiop:define-package`.
Package name should match filename (e.g., `foo.lisp` → `:lem-ext/foo` or `:lem-foo`).
Extensions must use `lem-` prefix (e.g., `:lem-python-mode`).
- name: file_structure_rule
prompt: |
File organization (top to bottom):
1. defpackage
2. defvar/defparameter declarations
3. Key bindings (define-key, define-keys)
4. Class/struct definitions
5. Functions and commands
Only flag violations when elements appear OUT OF ORDER (e.g., key bindings AFTER functions, or defvar AFTER functions).
Key bindings appearing BEFORE functions is correct and expected.
# === Style ===
- name: loop_keywords_rule
prompt: |
Loop keywords must use colons: `(loop :for x :in list :do ...)`
NOT: `(loop for x in list do ...)`
- name: naming_conventions_rule
prompt: |
Naming conventions:
- Functions/variables: kebab-case (e.g., `find-buffer`)
- Special variables: *earmuffs* (e.g., `*global-keymap*`)
- Constants: +plus-signs+ (e.g., `+default-tab-size+`)
- Predicates: -p suffix for functions (e.g., `buffer-modified-p`)
- Do NOT use -p suffix for user-configurable variables
# === Documentation ===
- name: docstring_rule
prompt: |
Required docstrings for:
- Exported functions, methods, classes
- `define-command` (explain what the command does)
- Generic functions (`:documentation` option)
Important functions should explain "why", not just "what".
severity: warning
# === Lem-Specific ===
- name: internal_symbol_rule
prompt: |
Use exported symbols from `lem` or `lem-core` package.
Avoid `lem::internal-symbol` access.
If internal access is necessary, document why.
- name: error_handling_rule
prompt: |
- `error`: Internal/programming errors
- `editor-error`: User-facing errors (displayed in echo area)
Always use `editor-error` for messages shown to users.
- name: frontend_interface_rule
prompt: |
Frontend-specific code must use `lem-if:*` protocol.
Do not call frontend implementation directly from core.
severity: warning
# === Functional Style ===
- name: functional_style_rule
prompt: |
Prefer explicit function arguments over dynamic variables.
Avoid using `defvar` for state passed between functions.
Exception: Well-documented cases like `*current-buffer*`.
- name: dynamic_symbol_call_rule
prompt: |
Avoid `uiop:symbol-call`. Rethink architecture instead.
If unavoidable, document the reason.
# === Libraries ===
- name: alexandria_usage_rule
prompt: |
Alexandria utilities allowed: `if-let`, `when-let`, `with-gensyms`, etc.
Avoid: `alexandria:curry` (use explicit lambdas)
Avoid: `alexandria-2:*` functions not yet used in codebase
# === Macros ===
- name: macro_style_rule
prompt: |
Keep macros small. For complex logic, use `call-with-*` pattern:
```lisp
(defmacro with-foo (() &body body)
`(call-with-foo (lambda () ,@body)))
```
Prefer `list` over backquote outside macros.💬 Feedback Reply to a violation comment with:
🔁 Re-validation Validation does not re-run automatically when you push fixes. Trigger it with one of:
📚 About Code ContractorDeclarative Code Standards That Learn and Improve Define domain-specific validation rules in YAML. Want this for your repo? |
| ;;; Password Management | ||
| ;;; ------------------------------------------------------------------ | ||
|
|
||
| (defvar *tramp-passwords* (make-hash-table :test 'equal) |
There was a problem hiding this comment.
Code Contractor: file_structure_rule
Contract: contract
AI check failed: "file_structure_rule"
Reason:
Added defvar declarations appear after function definitions in tramp.lisp, which violates the required top-to-bottom file order.
💬 Reply /dismiss <reason> to dismiss this violation.
| (close in) | ||
| (let ((stdout | ||
| (with-output-to-string (s) | ||
| (loop for line = (read-line out nil nil) |
There was a problem hiding this comment.
Code Contractor: loop_keywords_rule
Contract: contract
AI check failed: "loop_keywords_rule"
Reason:
An added loop form uses unqualified loop keywords instead of the required colon-prefixed form.
💬 Reply /dismiss <reason> to dismiss this violation.
| ;;; Password Management | ||
| ;;; ------------------------------------------------------------------ | ||
|
|
||
| (defvar *tramp-passwords* (make-hash-table :test 'equal) |
There was a problem hiding this comment.
Code Contractor: functional_style_rule
Contract: contract
AI check failed: "functional_style_rule"
Reason:
The change introduces new global dynamic state with defvar for passwords and caches, instead of passing state explicitly.
💬 Reply /dismiss <reason> to dismiss this violation.
| (cmd-args (if (listp command) command (list command))) | ||
| (control-opts (ssh-control-options))) | ||
| (if password | ||
| `("sshpass" "-p" ,password |
There was a problem hiding this comment.
Code Contractor: macro_style_rule
Contract: contract
AI check failed: "macro_style_rule"
Reason:
The added code uses backquote to construct lists in ordinary functions, where this rule requires preferring list outside macros.
💬 Reply /dismiss <reason> to dismiss this violation.
Two-pronged fix for keystrokes from the sudo password prompt appearing at the top of the opened buffer: 1. Move the password prompt into the expand-file-name handler, which runs first in the file-open pipeline — before any directory probes and before the target buffer is created. This gives the UI a clean slate with no buffer to leak into. 2. Add a defensive strip in %read-remote-file: if the stdout from the remote cat starts with the cached password (a lem-webview prompt overlay cleanup race), remove it before the content reaches the buffer.
Two problems broke completion:
1. parse-tramp-path regex "(.+)$" required at least one character after
the second colon, so "/ssh:host:" (typed before pressing Tab) failed
to parse. Changed to "(.*)$" with nil/empty defaulting to "/".
2. completion-file calls list-directory, which uses uiop:subdirectories
and uiop:directory-files directly — no virtual filesystem hooks.
TRAMP paths don't exist locally, so list-directory returned nil and
the completion candidate list was always empty.
Fixes:
- Override *prompt-file-completion-function* with a TRAMP-aware wrapper
that calls directory-files (which does have virtual hooks) directly.
- For :sudo, delegate directory listing to the local filesystem (strip
the "/sudo::" prefix, list locally, re-add the prefix) so completion
works without triggering a password prompt.
- Add pathname→string guards in tramp-list-directory-1 and
tramp-sudo-directory-files (completion-file passes pathname objects
from merge-pathnames).
Two bugs in tramp-file-completion:
1. directory-files was called with the full user input (e.g.
/sudo::/etc/p) instead of the directory part (/sudo::/etc/),
causing the delegation to local filesystem to look up a
non-existent directory path and return nothing.
2. No filtering was applied to the completion candidates — Tab always
showed every file in the directory regardless of what the user
had already typed. Added case-insensitive prefix matching against
the partial filename.
| (setf (documentation *package* t) | ||
| "TRAMP-like remote file editing for Lem. | ||
| Supports /ssh:user@host:/path and /sudo::/path syntax for transparent | ||
| remote file access via SSH and sudo. |
There was a problem hiding this comment.
This looks strange to me. Why not a (:documentation "…") clause in the defpackage?
There was a problem hiding this comment.
Because ANSI Common Lisp's defpackage doesn't have a :documentation option in the standard.
The standard defpackage options are only: :export, :import-from, :shadow, :shadowing-import-from, :use, and :nicknames. No :documentation slot exists.
So the portable way to attach a docstring to a package is to set it after the defpackage form:
(setf (documentation package t) "docstring here")
Some implementations (like SBCL) extend defpackage with a non-standard :documentation option, but Lem targets portability, so the setf documentation approach is the consistent convention across the project.
| :output nil | ||
| :error-output nil | ||
| :ignore-error-status t))) | ||
| (probe-file "/usr/bin/sshpass") |
There was a problem hiding this comment.
this is unix only, please use a feature flag.
| "Check if the sshpass utility is available on the system." | ||
| (or (eql 0 (nth-value 2 | ||
| (uiop:run-program '("which" "sshpass") | ||
| :output nil |
There was a problem hiding this comment.
which needs to be installed and that's to be taken care of. There's a Lem primitive for this "wich" kind of checks, forgot its name right now. It's also unix only.
| :label (or label (namestring f))))) | ||
| filtered)))) | ||
| (funcall *tramp-original-completion-function* | ||
| string directory :directory-only directory-only)))) |
There was a problem hiding this comment.
surely this function can be split in smaller parts? (to have less indentation levels and nested conditionals, etc)
| (or (mapcar (lambda (x) (virtual-probe-file x pathspec)) | ||
| (directory pathspec)) | ||
| (list pathspec)))) | ||
| (or (loop :for f :in *virtual-directory-files-functions* |
There was a problem hiding this comment.
This loop could be a function with a meaningful name?
plus it would save the copy-pasting of below.
| (defparameter *virtual-file-open* nil) | ||
|
|
||
| (defparameter *virtual-probe-file-functions* nil | ||
| "A list of functions for virtual probe-file. |
There was a problem hiding this comment.
Can you add context what this is used for? For a new contributor reading this file-utils and not knowing abous lem-tramp it could be puzzling.
|
Thanks for the PR! Can you disclose your LLM usage and methodology? How long have you been using it, how much tested is it? Your PR message is useful, can you add it to a README next to the sources? So the functionality is unix only right? We'll need a feature flag somewhere, to error gracefully for Windows users. Is the name Tramp appropriate? Since this module isn't a copy, and Lem doesn't aim at being an Emacs copy either. Honest question. In general I think all the variable and functions names in It would be great to have more testers before merging! (I can't test right now for the next few days) Thanks! |
|
Thanks for the review, I will fix them later. For LLM, I'm using claude + deepseek. Because deepseek is not the best(compared to chatgpt and Fable, but cheap enough), i have to guide him more, which helps me understanding his code better. LLM is just like a man playing three role at the same time, teacher, friend, slave. Teacher for he known so many things and can explain it in detail. Friend for he can help you do many things. Slave for he can get no tired at all. Almostly my workflow for LLM is simple, i do it step by step to make the codebase larger until the first working version done. After that ,test by myself and fix the bugs. For performance improving , i ask him to analyze if it possible to improve, and check what he said, when everything he said is right, i ask him to implement it. I only using it for about 2 days, testing is not so complete. README will be added later. Right, it only for Unix. I think Tramp is a suitable name, for nothing related to emacs in this name, and can help the new incoming user to understand what it is faster. Maybe the LLM read the emacs codes, so it's elisp-y, fix later. Test is required, currently when input a wrong password, the feedback is not normal, there are still some bugs. |
lem-tramp
Transparent remote file editing for Lem. Works like
Emacs TRAMP — type a remote path
into
C-x C-fand edit the file as if it were local.Supported Methods
ssh/ssh:user@host:/remote/pathsudo/sudo::/local/pathUsage
Open a file with
C-x C-fusing the TRAMP path syntax:The remote file is loaded into a buffer. Edit normally, then save with
C-x C-s.Authentication
SSH (
/ssh:)Key-based auth (recommended) — no prompt, works automatically if you have
SSH keys set up and your key is in the remote host's
authorized_keys.Password auth — requires the
sshpassutility:When key auth is unavailable, a password prompt appears in the minibuffer.
The password is cached for the session; subsequent file operations on the
same host reuse it without re-prompting.
Sudo (
/sudo::)A password prompt appears if your sudo timestamp has expired (typically
5-15 minutes since your last
sudoinvocation in a terminal). If youhave passwordless sudo configured (
NOPASSWDin sudoers), no prompt isshown.
How It Works
lem-tramp hooks into Lem's virtual filesystem layer:
cat /remote/pathvia SSH (or sudo), loads theoutput directly into an in-memory buffer.
cat > /remote/path.ls -1afor completion and directory-mode.stat -c '%s %Y'for file size and modification time.No temporary files are created on either the local or remote side.
Dependencies
Performance
SSH connections use
ControlMastermultiplexing — the first commandestablishes the TCP connection, and all subsequent commands reuse it.
A 5-second filesystem cache eliminates redundant
test -d/test -f/statcalls when Lem probes the same path multiple times during a singlefile-open operation.