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
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
tests and consumers that only need a preview buffer. Business Components,
database schemas, palettes, refs, and handlers stay in the example companion.

View File

@ -18,9 +18,32 @@ ETAF Playground 是通用的应用构建工作区:左侧编辑同一个应用
也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用
`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 保留。业务
Component、数据库 schema、palette、refs 和 handlers 都应该留在 example companion
Playground 只提供 source/preview 会话、读文件、窗口切换与生命周期。
Playground 只提供 source/preview 会话、读文件、标准 `display-buffer` 展示与
生命周期。
Research Shelf 本身还没有复杂到需要 feature 目录,所以完整的可执行 companion
集中在一个 `.el` 文件里,用注释区分 DATA / THEME / STATE / VIEW / ROOT只有

View File

@ -67,9 +67,17 @@
:type 'boolean
:group 'etaf-playground)
(defcustom etaf-playground-window-layout 'side-by-side
"Window layout used by `etaf-playground-open-example'."
:type '(choice (const side-by-side) (const stacked))
(defcustom etaf-playground-display-action
'((display-buffer-in-side-window)
(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)
;; `read' consults this dynamically scoped safety switch. Declaring it also
@ -83,6 +91,8 @@
spec
source-buffers
preview-buffer
preview-window
source-frame
active-extension
owned-source-buffers
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"
(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)
"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."
(interactive)
(let ((session (etaf-playground--session-for-target target)))
;; Make `C-c C-c' useful when the user opened a source file directly
;; instead of entering through `etaf-playground-open'.
(unless session
(when-let* ((source (cond ((bufferp target) target)
((and (stringp target) (get-buffer target))
(get-buffer target))
(t (current-buffer))))
(name (etaf-playground--source-example-name source)))
(if session
(etaf-playground--refresh-session session)
;; Make `C-c C-c' useful when the user opened a source file directly.
;; `etaf-playground-open-example' creates, displays, and refreshes the
;; session once, so this outer entry only returns its mounted preview.
(let* ((source (cond ((bufferp target) target)
((and (stringp target) (get-buffer target))
(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)
(setq session (etaf-playground--session-for-target name))))
(unless 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)))))
(setq session (etaf-playground--session-for-target name))
(and session (etaf-playground-session-preview-buffer session))))))
;;; Source modes and navigation
@ -854,30 +871,22 @@ the preview instead of destroying the source workspace."
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 (etaf-playground-session-previous-window-configuration session)
(setf (etaf-playground-session-previous-window-configuration session)
(current-window-configuration)))
(delete-other-windows)
(let* ((window-min-width 1)
(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
(setf (etaf-playground-session-source-frame session) (selected-frame))
(let* ((source (etaf-playground--source-buffer
session
(etaf-playground-session-active-extension session)))
(preview (etaf-playground-session-preview-buffer session)))
(set-window-buffer left source)
(set-window-buffer right preview)
(select-window left))))
(switch-to-buffer source)
(let ((window (display-buffer preview etaf-playground-display-action)))
(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)
"Return SESSION's active source buffer."
@ -950,6 +959,13 @@ default example there."
(interactive)
(if-let ((session (etaf-playground--session-for-target target)))
(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
(etaf-playground-session-previous-window-configuration session)))
(when-let ((runtime (and (buffer-live-p preview)
@ -965,6 +981,13 @@ default example there."
etaf-playground-source-extension nil))))
(dolist (buffer (etaf-playground-session-owned-source-buffers session))
(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)
(kill-buffer preview))
(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)
(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 ()
"Source tabs work through both header buttons and keyboard shortcuts."
(etaf-playground-test--with-app (buffer database)
@ -217,15 +263,24 @@ database and mounts a test buffer before running BODY."
default-directory))))
(ignore etaf-research-shelf-database-file)
(unwind-protect
(with-current-buffer source
(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))))))
(let ((mount-count 0)
(original-mount
(symbol-function 'etaf-playground-mount-example)))
(cl-letf (((symbol-function 'etaf-playground-mount-example)
(lambda (&rest arguments)
(cl-incf mount-count)
(apply original-mount arguments))))
(with-current-buffer source
(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)
(etaf-playground-close source))))))