Skip to content
Draft
4 changes: 4 additions & 0 deletions extensions/smart-parens-mode/README.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
* Smart Parens Mode

** Future Features
- When selecting text and adding a opening character, close the pair character at after the text selected
4 changes: 4 additions & 0 deletions extensions/smart-parens-mode/lem-smart-parens-mode.asd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(defsystem "lem-smart-parens-mode"
:serial t
:depends-on ("lem/core")
:components ((:file "smart-parens-mode")))
99 changes: 99 additions & 0 deletions extensions/smart-parens-mode/smart-parens-mode.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
(defpackage :smart-parens-mode
(:use :cl :lem)
(:export :smart-parens-mode))

Comment thread
daninus14 marked this conversation as resolved.
Outdated
(in-package :smart-parens-mode)
Comment thread
daninus14 marked this conversation as resolved.
Outdated

(define-minor-mode smart-parens-mode
(:name "smart-parens-mode"
:keymap *smart-parens-keymap*))

;;; From paredit
(defun bolp (point)
(zerop (point-charpos point)))
Comment thread
daninus14 marked this conversation as resolved.
Outdated

(defun eolp (point)
Comment thread
daninus14 marked this conversation as resolved.
Outdated
(let ((len (length (line-string point))))
(or (zerop len)
(>= (point-charpos point)
(1- len)))))

(defun integer-char-p (char)
(< (char-code #\0) (char-code char) (char-code #\9)))

(defun sharp-literal-p (char point)
(with-point ((p point))
(character-offset p -1)
(and (character-at p)
(char-equal (character-at p) char)
(eql (character-at p -1) #\#))))

(defun sharp-n-literal-p (char point)
(with-point ((p point))
(character-offset p -1)
(when (char-equal char (character-at p))
(character-offset p -1)
(skip-chars-backward p #'integer-char-p)
(and (integer-char-p (character-at p))
(eql (character-at p -1) #\#)))))

(defparameter *non-space-following-chars*
'(#\Space #\( #\' #\` #\, #\[ #\{))

(defparameter *non-space-preceding-chars*
'(#\Space #\) #\] #\}))

(defun non-space-following-context-p (&optional (p (current-point)))
(or (bolp p)
(find (character-at p -1)
*non-space-following-chars*)
(eql (character-at p -1) #\#)
(and (eql (character-at p -1) #\@)
(eql (character-at p -2) #\,))
(sharp-literal-p #\' p)
(sharp-literal-p #\. p)
(sharp-literal-p #\S p)
(sharp-literal-p #\C p)
(sharp-literal-p #\+ p)
(sharp-literal-p #\- p)
(sharp-n-literal-p #\A p)
(sharp-n-literal-p #\= p)))

(defun editor-insert-pair (open close)
(let ((p (current-point)))
(cond ((in-string-or-comment-p p)
(insert-character p open))
((syntax-escape-point-p p 0)
(insert-character p open))
(t
(unless (non-space-following-context-p p)
(insert-character p #\Space))
(insert-character p open)
(insert-character p close)
(unless (or (eolp p) (find (character-at p) *non-space-preceding-chars*))
(insert-character p #\Space)
(character-offset p -1))
(character-offset p -1)))))

;;; smart parens specific code

(define-command smart-parens-insert-paren () ()
(editor-insert-pair #\( #\)))

(define-command smart-parens-insert-bracket () ()
(editor-insert-pair #\[ #\]))

(define-command smart-parens-insert-brace () ()
(editor-insert-pair #\{ #\}))

(define-command smart-parens-insert-double-quote () ()
(editor-insert-pair #\" #\"))

(define-command smart-parens-insert-single-quote () ()
(editor-insert-pair #\' #\'))

(define-key *smart-parens-keymap* "\"" 'smart-parens-insert-double-quote)
(define-key *smart-parens-keymap* "'" 'smart-parens-insert-single-quote)
(define-key *smart-parens-keymap* "(" 'smart-parens-insert-paren)
(define-key *smart-parens-keymap* "[" 'smart-parens-insert-bracket)
(define-key *smart-parens-keymap* "{" 'smart-parens-insert-brace)
Comment on lines +62 to +93

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One problem with this implementation is that it implicitly relies on C-like syntax.
It would be better to refer to the syntax-table for each mode.

for example

CL-USER> (lem:syntax-open-paren-char-p #\()
(#\( . #\))
CL-USER> (lem:syntax-open-paren-char-p #\[)
(#\[ . #\])
CL-USER> (lem:syntax-open-paren-char-p #\{)
(#\{ . #\})

@daninus14 daninus14 Aug 13, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hm, do you mean that instead of using define-key I should use some function which gets the currently inserted character like this:

(defun close-paren (given-char)
  (when (lem:syntax-open-paren-char-p given-char)
    (apply #'editor-insert-pair (lem:syntax-open-paren-char-p given-char))))

That seems like it would only work for the parens and not the " and ' quote characters.

I don't know lem well enough. What function or macro would I pass this close-paren function to?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:paren-pairs '((#\( . #\))

syntax-table is defined here in C, for example.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

for example, like this

(defmethod execute ((mode smart-parens-mode) (command self-insert) argument)
  (let ((c (get-self-insert-char)))
    (alexandria:when-let (pair (syntax-open-paren-char-p c))
      (editor-insert-pair (car pair) (cdr pair))
      (return-from execute))
    (when (syntax-string-quote-char-p c)
      (editor-insert-pair c c)
      (return-from execute))
    (call-next-method)))

1 change: 1 addition & 0 deletions lem.asd
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@
"lem-lua-mode"
#-os-windows "lem-terminal"
"lem-legit"
"lem-smart-parens-mode"
"lem-dashboard"
"lem-copilot"))

Expand Down