fix: use display-buffer for playground previews

This commit is contained in:
Kinneyzhang 2026-08-25 20:23:42 +08:00
parent 238b58956e
commit 09ad2b5ae8
4 changed files with 185 additions and 61 deletions

View File

@ -19,6 +19,29 @@ into the right-hand preview. Saving a source file also refreshes by default.
`etaf-playground-register-example` is available when a companion needs a `etaf-playground-register-example` is available when a companion needs a
non-conventional root or feature name. non-conventional root or feature name.
Preview placement uses the standard Emacs `display-buffer` action stored in
`etaf-playground-display-action`. The default is a right side window using
half the frame:
```elisp
;; Right preview using 40% of the frame.
(setq etaf-playground-display-action
'((display-buffer-in-side-window)
(side . right)
(window-width . 0.4)))
;; Right preview fixed at 100 columns.
(setq etaf-playground-display-action
'((display-buffer-in-side-window)
(side . right)
(window-width . 100)))
;; Preview in its own frame.
(setq etaf-playground-display-action
'((display-buffer-pop-up-frame)
(pop-up-frame-parameters . ((width . 120) (height . 45)))))
```
The low-level `etaf-playground-mount-example` API remains available for batch The low-level `etaf-playground-mount-example` API remains available for batch
tests and consumers that only need a preview buffer. Business Components, tests and consumers that only need a preview buffer. Business Components,
database schemas, palettes, refs, and handlers stay in the example companion. database schemas, palettes, refs, and handlers stay in the example companion.

View File

@ -18,9 +18,32 @@ ETAF Playground 是通用的应用构建工作区:左侧编辑同一个应用
也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用 也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用
`etaf-playground-register-example` 注册覆盖。 `etaf-playground-register-example` 注册覆盖。
预览位置使用标准 Emacs `display-buffer` action
`etaf-playground-display-action` 配置。默认在右侧使用一半 frame
```elisp
;; 右侧预览占 frame 的 40%。
(setq etaf-playground-display-action
'((display-buffer-in-side-window)
(side . right)
(window-width . 0.4)))
;; 右侧预览固定为 100 列。
(setq etaf-playground-display-action
'((display-buffer-in-side-window)
(side . right)
(window-width . 100)))
;; 使用独立 frame。
(setq etaf-playground-display-action
'((display-buffer-pop-up-frame)
(pop-up-frame-parameters . ((width . 120) (height . 45)))))
```
`etaf-playground-mount-example` 仍作为低层 batch/consumer API 保留。业务 `etaf-playground-mount-example` 仍作为低层 batch/consumer API 保留。业务
Component、数据库 schema、palette、refs 和 handlers 都应该留在 example companion Component、数据库 schema、palette、refs 和 handlers 都应该留在 example companion
Playground 只提供 source/preview 会话、读文件、窗口切换与生命周期。 Playground 只提供 source/preview 会话、读文件、标准 `display-buffer` 展示与
生命周期。
Research Shelf 本身还没有复杂到需要 feature 目录,所以完整的可执行 companion Research Shelf 本身还没有复杂到需要 feature 目录,所以完整的可执行 companion
集中在一个 `.el` 文件里,用注释区分 DATA / THEME / STATE / VIEW / ROOT只有 集中在一个 `.el` 文件里,用注释区分 DATA / THEME / STATE / VIEW / ROOT只有

View File

@ -67,9 +67,17 @@
:type 'boolean :type 'boolean
:group 'etaf-playground) :group 'etaf-playground)
(defcustom etaf-playground-window-layout 'side-by-side (defcustom etaf-playground-display-action
"Window layout used by `etaf-playground-open-example'." '((display-buffer-in-side-window)
:type '(choice (const side-by-side) (const stacked)) (side . right)
(slot . 0)
(window-width . 0.5))
"Standard `display-buffer' action used for the preview buffer.
The default shows the preview in a right side window using half the frame.
Set `window-width' to a float for a ratio or an integer for a fixed column
count. Use `display-buffer-pop-up-frame' to place the preview in its own
frame."
:type 'sexp
:group 'etaf-playground) :group 'etaf-playground)
;; `read' consults this dynamically scoped safety switch. Declaring it also ;; `read' consults this dynamically scoped safety switch. Declaring it also
@ -83,6 +91,8 @@
spec spec
source-buffers source-buffers
preview-buffer preview-buffer
preview-window
source-frame
active-extension active-extension
owned-source-buffers owned-source-buffers
companion-dirty-p companion-dirty-p
@ -503,6 +513,29 @@ forwarded to `etaf-mount', including an optional initial viewport."
(format " ETAF Preview %s | ERROR | g retry q close" (format " ETAF Preview %s | ERROR | g retry q close"
(etaf-playground-session-name session))))))) (etaf-playground-session-name session)))))))
(defun etaf-playground--refresh-session (session)
"Refresh the single mounted preview owned by SESSION."
(let ((preview (etaf-playground-session-preview-buffer session)))
(condition-case err
(progn
(when-let ((runtime (etaf-runtime-for-buffer preview)))
(with-current-buffer preview
(etaf-unmount runtime)))
(with-current-buffer preview
(setq-local buffer-read-only nil))
(etaf-playground-mount-example
preview (etaf-playground-session-name session) session)
(with-current-buffer preview
(setq-local buffer-read-only t
header-line-format
(format " ETAF Preview %s | g refresh q close"
(etaf-playground-session-name session))))
(force-mode-line-update t)
preview)
((error quit)
(etaf-playground--render-error session err)
nil))))
(defun etaf-playground-refresh (&optional target) (defun etaf-playground-refresh (&optional target)
"Refresh the preview associated with TARGET or the current source buffer. "Refresh the preview associated with TARGET or the current source buffer.
@ -512,38 +545,22 @@ is mounted again from the Playground's perspective. Errors stay visible in
the preview instead of destroying the source workspace." the preview instead of destroying the source workspace."
(interactive) (interactive)
(let ((session (etaf-playground--session-for-target target))) (let ((session (etaf-playground--session-for-target target)))
;; Make `C-c C-c' useful when the user opened a source file directly (if session
;; instead of entering through `etaf-playground-open'. (etaf-playground--refresh-session session)
(unless session ;; Make `C-c C-c' useful when the user opened a source file directly.
(when-let* ((source (cond ((bufferp target) target) ;; `etaf-playground-open-example' creates, displays, and refreshes the
((and (stringp target) (get-buffer target)) ;; session once, so this outer entry only returns its mounted preview.
(get-buffer target)) (let* ((source (cond ((bufferp target) target)
(t (current-buffer)))) ((and (stringp target) (get-buffer target))
(name (etaf-playground--source-example-name source))) (get-buffer target))
(t (current-buffer))))
(name (etaf-playground--source-example-name source)))
(unless name
(user-error
"No ETAF Playground workspace is associated with this buffer"))
(etaf-playground-open-example name) (etaf-playground-open-example name)
(setq session (etaf-playground--session-for-target name)))) (setq session (etaf-playground--session-for-target name))
(unless session (and session (etaf-playground-session-preview-buffer session))))))
(user-error "No ETAF Playground workspace is associated with this buffer"))
(let ((preview (etaf-playground-session-preview-buffer session)))
(condition-case err
(progn
(when-let ((runtime (etaf-runtime-for-buffer preview)))
(with-current-buffer preview
(etaf-unmount runtime)))
(with-current-buffer preview
(setq-local buffer-read-only nil))
(etaf-playground-mount-example
preview (etaf-playground-session-name session) session)
(with-current-buffer preview
(setq-local buffer-read-only t
header-line-format
(format " ETAF Preview %s | g refresh q close"
(etaf-playground-session-name session))))
(force-mode-line-update t)
preview)
((error quit)
(etaf-playground--render-error session err)
nil)))))
;;; Source modes and navigation ;;; Source modes and navigation
@ -854,30 +871,22 @@ the preview instead of destroying the source workspace."
session)))) session))))
(defun etaf-playground--show-session (session) (defun etaf-playground--show-session (session)
"Display SESSION in a left-source/right-preview layout." "Display SESSION through standard `display-buffer' policy."
(unless noninteractive (unless noninteractive
(unless (etaf-playground-session-previous-window-configuration session) (unless (etaf-playground-session-previous-window-configuration session)
(setf (etaf-playground-session-previous-window-configuration session) (setf (etaf-playground-session-previous-window-configuration session)
(current-window-configuration))) (current-window-configuration)))
(delete-other-windows) (setf (etaf-playground-session-source-frame session) (selected-frame))
(let* ((window-min-width 1) (let* ((source (etaf-playground--source-buffer
(window-min-height 1)
(left (selected-window))
(right (if (eq etaf-playground-window-layout 'stacked)
(progn (set-window-buffer
left
(etaf-playground--source-buffer
session
(etaf-playground-session-active-extension session)))
(split-window-below))
(split-window-right)))
(source (etaf-playground--source-buffer
session session
(etaf-playground-session-active-extension session))) (etaf-playground-session-active-extension session)))
(preview (etaf-playground-session-preview-buffer session))) (preview (etaf-playground-session-preview-buffer session)))
(set-window-buffer left source) (switch-to-buffer source)
(set-window-buffer right preview) (let ((window (display-buffer preview etaf-playground-display-action)))
(select-window left)))) (unless (window-live-p window)
(error "ETAF Playground display action did not return a live window"))
(setf (etaf-playground-session-preview-window session) window)
window))))
(defun etaf-playground--active-source-buffer (session) (defun etaf-playground--active-source-buffer (session)
"Return SESSION's active source buffer." "Return SESSION's active source buffer."
@ -950,6 +959,13 @@ default example there."
(interactive) (interactive)
(if-let ((session (etaf-playground--session-for-target target))) (if-let ((session (etaf-playground--session-for-target target)))
(let* ((preview (etaf-playground-session-preview-buffer session)) (let* ((preview (etaf-playground-session-preview-buffer session))
(preview-window
(etaf-playground-session-preview-window session))
(source-frame
(etaf-playground-session-source-frame session))
(preview-frame
(and (window-live-p preview-window)
(window-frame preview-window)))
(configuration (configuration
(etaf-playground-session-previous-window-configuration session))) (etaf-playground-session-previous-window-configuration session)))
(when-let ((runtime (and (buffer-live-p preview) (when-let ((runtime (and (buffer-live-p preview)
@ -965,6 +981,13 @@ default example there."
etaf-playground-source-extension nil)))) etaf-playground-source-extension nil))))
(dolist (buffer (etaf-playground-session-owned-source-buffers session)) (dolist (buffer (etaf-playground-session-owned-source-buffers session))
(when (buffer-live-p buffer) (kill-buffer buffer))) (when (buffer-live-p buffer) (kill-buffer buffer)))
(when (window-live-p preview-window)
(if (and (frame-live-p preview-frame)
(frame-live-p source-frame)
(not (eq preview-frame source-frame))
(= (length (window-list preview-frame 'no-minibuf)) 1))
(delete-frame preview-frame)
(quit-window nil preview-window)))
(when (buffer-live-p preview) (when (buffer-live-p preview)
(kill-buffer preview)) (kill-buffer preview))
(when (and configuration (window-configuration-p configuration)) (when (and configuration (window-configuration-p configuration))

View File

@ -173,6 +173,52 @@ database and mounts a test buffer before running BODY."
(should (equal (etaf-playground-read-ecss "research-shelf" session) (should (equal (etaf-playground-read-ecss "research-shelf" session)
(etaf-component-styles 'etaf-research-shelf-shell))))))) (etaf-component-styles 'etaf-research-shelf-shell)))))))
(ert-deftest etaf-playground-show-session-uses-display-buffer-action ()
"Workspace display delegates preview placement to standard `display-buffer'."
(let* ((source (get-buffer-create " *etaf-display-source*"))
(preview (get-buffer-create " *etaf-display-preview*"))
(session
(etaf-playground--session-create
:name "display-test"
:source-buffers (list (cons ".etaf" source))
:preview-buffer preview
:active-extension ".etaf"))
(action '((display-buffer-in-side-window)
(side . right)
(window-width . 0.4)))
calls)
(unwind-protect
(let ((noninteractive nil)
(etaf-playground-display-action action))
(cl-letf (((symbol-function 'current-window-configuration)
(lambda () 'saved-window-configuration))
((symbol-function 'switch-to-buffer)
(lambda (buffer)
(push (list 'source buffer) calls)
buffer))
((symbol-function 'display-buffer)
(lambda (buffer supplied-action)
(push (list 'preview buffer supplied-action) calls)
(selected-window)))
((symbol-function 'delete-other-windows)
(lambda (&rest _)
(ert-fail "Workspace must not delete user windows")))
((symbol-function 'split-window-right)
(lambda (&rest _)
(ert-fail "Workspace must not split windows directly")))
((symbol-function 'split-window-below)
(lambda (&rest _)
(ert-fail "Workspace must not split windows directly"))))
(should (windowp (etaf-playground--show-session session))))
(should
(equal
(list (list 'source source) (list 'preview preview action))
(nreverse calls)))
(should (eq (etaf-playground-session-preview-window session)
(selected-window))))
(when (buffer-live-p source) (kill-buffer source))
(when (buffer-live-p preview) (kill-buffer preview)))))
(ert-deftest etaf-playground-source-tabs-have-buttons-and-shortcuts () (ert-deftest etaf-playground-source-tabs-have-buttons-and-shortcuts ()
"Source tabs work through both header buttons and keyboard shortcuts." "Source tabs work through both header buttons and keyboard shortcuts."
(etaf-playground-test--with-app (buffer database) (etaf-playground-test--with-app (buffer database)
@ -217,15 +263,24 @@ database and mounts a test buffer before running BODY."
default-directory)))) default-directory))))
(ignore etaf-research-shelf-database-file) (ignore etaf-research-shelf-database-file)
(unwind-protect (unwind-protect
(with-current-buffer source (let ((mount-count 0)
(should (eq #'etaf-playground-refresh (original-mount
(key-binding (kbd "C-c C-c")))) (symbol-function 'etaf-playground-mount-example)))
(should (etaf-playground-refresh source)) (cl-letf (((symbol-function 'etaf-playground-mount-example)
(let ((session (buffer-local-value 'etaf-playground-session source))) (lambda (&rest arguments)
(should (etaf-playground-session-p session)) (cl-incf mount-count)
(should (etaf-runtime-p (apply original-mount arguments))))
(etaf-runtime-for-buffer (with-current-buffer source
(etaf-playground-session-preview-buffer session)))))) (should (eq #'etaf-playground-refresh
(key-binding (kbd "C-c C-c"))))
(should (etaf-playground-refresh source))
(let ((session
(buffer-local-value 'etaf-playground-session source)))
(should (etaf-playground-session-p session))
(should (etaf-runtime-p
(etaf-runtime-for-buffer
(etaf-playground-session-preview-buffer session)))))))
(should (= mount-count 1)))
(when (buffer-live-p source) (when (buffer-live-p source)
(etaf-playground-close source)))))) (etaf-playground-close source))))))