Skip to content

Report messages once when running a command for multiple cursors - #2329

Merged
theangelperalta merged 4 commits into
lem-project:mainfrom
theangelperalta:multicursor-message-once
Nov 11, 2026
Merged

theangelperalta merged 4 commits into
lem-project:mainfrom
theangelperalta:multicursor-message-once

Conversation

@theangelperalta

@theangelperalta theangelperalta commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #2263.

Problem

process-each-cursors runs a command's body once per fake cursor and then once for the real cursor. Any message in that body is shown, and logged to *Messages*, once per cursor. #2263 fixed this for mark-set with a call-site guard. Other per-cursor commands still repeat, for example yank-pop / yank-pop-next when the previous command wasn't a yank. Every new command would need the same guard.

Change

  • Add fake-cursor-p (in src/cursors.lisp). process-each-cursors already rebinds the buffer point to each fake cursor, so message and message-without-log check (fake-cursor-p (current-point)) and do nothing during the fake-cursor passes. The real cursor's pass, which always runs last, reports as before.
  • when-real-cursor now uses fake-cursor-p too.
  • Remove the when-real-cursor call in mark-set, which is now redundant. The macro stays exported, so anything already using it keeps working.

This changes message for everyone: a message printed while a command runs for a fake cursor is now dropped, including messages from extensions. Errors are unchanged: editor-error still propagates from any cursor.

Correction to my review on #2263

In that review I said the per-cursor message caused an %always-same-buffer assertion crash. That was an artifact of my headless setup: it called set-window-buffer directly, which left the window's view point in the old buffer. It fails the same way with zero fake cursors. With the buffer switched in properly (switch-to-buffer plus a timer manager), neither mark-set nor yank-pop crashes, before or after #2263. The real symptom was the duplicate messages.

Tests

  • New multiple-cursors-message-once in tests/cursors.lisp. It runs mark-set and yank-pop with two fake cursors and checks that every cursor gets its mark and that each message is logged exactly once. The yank-pop case fails without this change.
  • lem-tests: all 47 pass.

🤖 Generated with Claude Code

process-each-cursors runs a command's body once per fake cursor and then
for the real cursor, so any `message` in the body was shown and logged
once per cursor. lem-project#2263 guarded the call in mark-set, but other
per-cursor commands such as yank-pop still repeat.

Bind *inhibit-message* during the fake-cursor passes so only the real
cursor's pass reports, and drop the now-redundant guard in mark-set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@code-contractor-app

code-contractor-app Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

✅ Code Contractor Validation: PASSED

📌 Result for commit 0b7e57d

=== Contract: contract ===

✓ Code Contractor Validation Result
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📋 Contract Source: Repository

📊 Statistics:
  Files Changed:    6
  Lines Added:      195
  Lines Deleted:    21
  Total Changed:    216
  Delete Ratio:     0.10 (10%)

Status: PASSED ✅

🤖 AI Providers:
  - codex — profile: bedrock-gpt61-terra

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎉 No violations detected. Great job!
📋 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.

          Exception: In major-mode files (e.g., *-mode.lisp), flexible ordering is acceptable when it follows established patterns from similar modes (e.g., clojure-mode, python-mode).

      # === 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*`, constants, and configuration variables (not meant to be modified at runtime).

      - name: dynamic_symbol_call_rule
        prompt: |
          Avoid `uiop:symbol-call`. Rethink architecture instead.
          Exception: Acceptable in extensions when following patterns established in other major-mode extensions (e.g., for optional dependency loading or plugin systems).
          If used, 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.

          Exception: In major-mode files, macro patterns that are standard in other major-modes (e.g., clojure-mode, python-mode) are acceptable.
📚 About Code Contractor

Declarative Code Standards That Learn and Improve

Define domain-specific validation rules in YAML.
Your contracts document team knowledge and evolve into more accurate AI enforcement.

Want this for your repo?
→ Install Code Contractor

@code-contractor-app code-contractor-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor validation failed ❌ — see the sticky comment for full results.

Comment thread tests/cursors.lisp Resolved
The test doesn't write to the kill ring, so it doesn't need its own
binding of lem-core::*killring*. cursor-mark and buffer-cursors are
exported from lem-core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@theangelperalta

Copy link
Copy Markdown
Collaborator Author

/rerun

@code-contractor-app code-contractor-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor validation failed ❌ — see the sticky comment for full results.

Comment thread src/echo.lisp Resolved
process-each-cursors already rebinds the buffer point to each fake
cursor, so message can check the current point instead of a separate
special variable. Add fake-cursor-p and use it in message and
when-real-cursor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@theangelperalta

Copy link
Copy Markdown
Collaborator Author

/rerun

Checking the current point in message had several problems:
call-with-buffer-point changes the buffer point for every thread, so
messages from other threads could be dropped; a message sent from
another buffer was still repeated; and a message that only a fake
cursor sent was lost.

process-each-cursors now binds *cursor-messages* and marks fake-cursor
passes with a dynamic binding. Messages from fake passes are deferred
and shown after the real pass, unless the real cursor already showed
the same one. message-buffer goes through the same path.

Also make C-u mark-set move each fake cursor to its own mark instead of
popping the shared mark ring once per cursor, and export fake-cursor-p.
@theangelperalta
theangelperalta merged commit b72d82b into lem-project:main Nov 11, 2026
10 checks passed
@theangelperalta
theangelperalta deleted the multicursor-message-once branch November 11, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant