Skip to content

gf fmt 意外删除空行 #909

Description

@JimZhouZZY

以下代码使用 Goldfish Scheme 18.11.20 gf fmt格式化之后,会意外地删除空行。

diff --git a/TeXmacs/progs/texmacs/texmacs/tm-collab.scm b/TeXmacs/progs/texmacs/texmacs/tm-collab.scm
index 6f01cbf56..df784a9aa 100644
--- a/TeXmacs/progs/texmacs/texmacs/tm-collab.scm
+++ b/TeXmacs/progs/texmacs/texmacs/tm-collab.scm
@@ -205,12 +205,10 @@
     counts
   ) ;let
 ) ;define
-                                    
 (tm-define (collab-new-document)
   (:interactive #t)
   (interactive (lambda (name) (collab-new-document-named name)) "Document name")
 ) ;tm-define
-
 (tm-define (collab-new-document-named name)
   (let ((uname (cork->utf8 name)))
     (cond ((and (string? uname)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; MODULE      : tm-collab.scm
;; DESCRIPTION : 云文档协作(Mogan × Loro CRDT)的 scheme 编排:
;;               新建/加入协作文档——创建空 buffer → 切到其 view → 驱动
;;               C++ 协作会话层连接服务端(CREATE/JOIN),随后由会话层在
;;               收到服务端 snapshot/updates 时把内容同步进 buffer。
;; COPYRIGHT   : (C) 2026  Jim Zhou
;;
;; This software falls under the GNU general public license version 3 or later.
;; It comes with NO WARRANTY WHATSOEVER. For details, see the file LICENSE
;; in the root directory or <http://www.gnu.org/licenses/gpl-3.0.html>.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(texmacs-module (texmacs texmacs tm-collab)
  (:use (texmacs texmacs tm-server) (texmacs texmacs tm-files))
) ;texmacs-module

;; 协作服务端地址优先级:用户经 Collaborative 菜单设置的偏好(collab:server-url)
;; > C++ loro-collab-server-url(native 读 OS env MOGAN_LORO_SERVER;WASM 读
;; window.MOGAN_LORO_SERVER / ?loro_server= 查询参数;都未设回落 ws://127.0.0.1:8765)。
;; 偏好是终端用户上线的主路径,env 保留给开发/CI。

(define collab-server-url-key "collab:server-url")

(tm-define (collab-server-url)
  (with configured
    (get-preference collab-server-url-key)
    (if (!= configured "") configured (loro-collab-server-url))
  ) ;with
) ;tm-define

;; 是否已显式配置服务端(Collaborative 菜单据此切「仅设置项 / 完整菜单」两形态)。
(tm-define (collab-server-configured?)
  (!= (get-preference collab-server-url-key) "")
) ;tm-define

;; === 服务端地址:地址+端口两框 ↔ 完整 URL(纯函数,供单测) ===
;; 地址框默认填纯 host,端口单独一框,拼成 ws://host:port;但地址框亦接受完整
;; ws(s):// URL(隐藏的高级回退,保留 wss/TLS、IPv6、路径等能力)。

;; 判前缀(mogan scheme 无 string-prefix?,手写)。

(define (collab-string-prefix? p s)
  (and (>= (string-length s) (string-length p))
    (string=? (substring s 0 (string-length p)) p)
  ) ;and
) ;define

;; 已存 URL → (address . port) 回填两框。仅处理常见 ws(s)://host:port;
;; 含路径 / 多冒号(IPv6 等)/ 非 ws(s) scheme → 整串塞进 address(端口空),
;; 即「地址框支持完整 URL」的回退。逐字符扫描,避开本模块未导入的 string-index/
;; string-contains(只用 string->list / char=?,与 collab-valid-doc-name? 同套)。

(define (collab-url->fields url)
  (let ((strip (lambda (p)
                 (and (collab-string-prefix? p url)
                   (substring url (string-length p) (string-length url))
                 ) ;and
               ) ;lambda
        ) ;strip
       ) ;
    (let ((rest (or (strip "ws://") (strip "wss://"))))
      (if (not rest)
        (cons url "")
        (let loop
          ((cs (string->list rest)) (i 0) (colon #f))
          (cond ((null? cs)
                 (if (not colon)
                   (cons rest "")
                   (cons (substring rest 0 colon)
                     (substring rest (+ colon 1) (string-length rest))
                   ) ;cons
                 ) ;if
                ) ;
                ((char=? (car cs) #\/) (cons url ""))
                ((and (char=? (car cs) #\:) colon) (cons url ""))
                ((char=? (car cs) #\:) (loop (cdr cs) (+ i 1) i))
                (else (loop (cdr cs) (+ i 1) colon))
          ) ;cond
        ) ;let
      ) ;if
    ) ;let
  ) ;let
) ;define

;; 两框 → URL。地址为完整 URL(ws/wss 开头)→ 原样;地址空 → 清除;
;; 否则按 host[:port] 拼 ws://。

(define (collab-fields->url addr port)
  (cond ((== addr "") "")
        ((or (collab-string-prefix? "ws://" addr) (collab-string-prefix? "wss://" addr))
         addr
        ) ;
        ((== port "") (string-append "ws://" addr))
        (else (string-append "ws://" addr ":" port))
  ) ;cond
) ;define

;; 弹框配置/修改协作服务端:地址 + 端口两框(预填当前生效值);地址框亦接受完整
;; ws(s):// URL。空地址 = 清除偏好(回到 env/默认)。设置后下次展开菜单自动切完整形态。
;; 会话进行中禁止改地址:连接 URL 在连接时固定,改了也不迁移当前会话,故要求先 Leave。
(tm-define (collab-configure-server)
  (:interactive #t)
  (if (loro-collab-active?)
    (set-message "Leave the current session before changing the server address"
      "Collaborative"
    ) ;set-message
    (with cur
      (collab-url->fields (collab-server-url))
      (interactive (lambda (addr port)
                     (with url
                       (collab-fields->url addr port)
                       (if (== url "")
                         (reset-preference collab-server-url-key)
                         (set-preference collab-server-url-key url)
                       ) ;if
                     ) ;with
                   ) ;lambda
        (list "Server address" "string" (car cur))
        (list "Server port" "string" (cdr cur))
      ) ;interactive
    ) ;with
  ) ;if
) ;tm-define

;; === collab 缓冲(云端文档)的标识 ===
;; 单会话:同时只有一个 collab 缓冲。new/join 时把新建 buffer 的 url 记入
;; collab-buffer-url,collab-buffer? 据此判定。下游 buffer-modified?/save-buffer
;; 覆盖用它门控特殊语义(不标修改/不可保存/关闭不提示)。

(define collab-buffer-url #f)

(tm-define (collab-buffer? u) (and collab-buffer-url (== u collab-buffer-url)))

;; 把当前 buffer 标记为 collab 缓冲(在 with-default-view 建 new-buffer 后调用,
;; 此时 (current-buffer) 即新建的协作 buffer)。
(tm-define (collab-mark-current-buffer)
  (set! collab-buffer-url (current-buffer))
) ;tm-define

;; === 文档显示名校验与列表工具(纯函数,供测试覆盖) ===
;; 规则与服务端 tools/loro-server/validate.js 保持一致:trim 后长度 1–64
;; (按字符计,用 utf8-string-length——string-length 计字节数),
;; 禁止 \ / : * ? " < > | 及控制字符。Scheme 侧仅做预校验(即时反馈),
;; 服务端仍是权威校验方。

(define collab-doc-name-forbidden-chars '(#\\ #\/ #\: #\* #\? #\" #\< #\> #\|))

(define (collab-valid-doc-name? name)
  (and (string? name)
    (>= (utf8-string-length name) 1)
    (<= (utf8-string-length name) 64)
    (not (list-find (string->list name)
           (lambda (c)
             (or (in? c collab-doc-name-forbidden-chars)
               (< (char->integer c) 32)
               (== (char->integer c) 127)
             ) ;or
           ) ;lambda
         ) ;list-find
    ) ;not
  ) ;and
) ;define

(define (collab-docs-pairs flat)
  (if (or (null? flat) (null? (cdr flat)))
    '()
    (cons (cons (car flat) (cadr flat)) (collab-docs-pairs (cddr flat)))
  ) ;if
) ;define

(define (collab-doc-label uuid name dup?)
  (if (and (string? name) (> (string-length name) 0))
    (if dup?
      `(concat (verbatim ,name)
         ," "
         (with ,"color"
           ,"dark grey"
           (verbatim ,(string-append "("
                        (substring uuid 0 (min 4 (string-length uuid)))
                        ")"))))
      `(verbatim ,name)
    ) ;if
    uuid
  ) ;if
) ;define

(define (collab-doc-name-duplicates pairs)
  (let ((counts '()))
    (for (p pairs)
      (with name
        (cdr p)
        (when (and (string? name) (> (string-length name) 0))
          (let ((cell (assoc name counts)))
            (if cell
              (set-cdr! cell (+ (cdr cell) 1))
              (set! counts (cons (cons name 1) counts))
            ) ;if
          ) ;let
        ) ;when
      ) ;with
    ) ;for
    counts
  ) ;let
) ;define

(tm-define (collab-new-document)
  (:interactive #t)
  (interactive (lambda (name) (collab-new-document-named name)) "Document name")
) ;tm-define

(tm-define (collab-new-document-named name)
  (cond ((and (string? name)
           (> (string-length name) 0)
           (not (collab-valid-doc-name? name))
         ) ;and
         (set-message "Invalid name: 1-64 chars, no \\ / : * ? \" < > | or control chars"
           "Collaborative"
         ) ;set-message
        ) ;
        (else (with-default-view (if (window-per-buffer?) (open-window) (new-buffer))
                (collab-mark-current-buffer)
                ;; 标题立即设为用户输入的显示名(无名文档保持 No Name,待服务端
                ;; 回 DOC 后 become_ready 用 UUID 兜底),避免 tab 暂显 No Name[n]。
                (when (> (string-length name) 0)
                  (buffer-set-title (current-buffer) name)
                ) ;when
                (loro-collab-create (collab-server-url) name)
                (set-message (string-append "Creating collaborative document (Server "
                               (collab-server-url)
                               ")"
                             ) ;string-append
                  "Collaborative"
                ) ;set-message
              ) ;with-default-view
        ) ;else
  ) ;cond
) ;tm-define

;; 由文件 url 推导共享文档默认显示名:取文件名(去目录与后缀)。

(define (collab-file->doc-name u)
  (let* ((tail (url->system (url-tail u))) (suffix (url-suffix u)))
    (if (and (string? suffix) (> (string-length suffix) 0))
      (substring tail 0 (- (string-length tail) (+ 1 (string-length suffix))))
      tail
    ) ;if
  ) ;let*
) ;define

;; 选定文件后弹显示名输入框(预填文件名),确认走 collab-new-document-from-file-named。

(define (collab-share-file-prompt-name u)
  (interactive (lambda (name) (collab-new-document-from-file-named u name))
    (list "Document name" "string" (collab-file->doc-name u))
  ) ;interactive
) ;define

;; 打开(上传)本地 .tmu/.tm 文件为共享文档:文件对话框选文件 → 输入显示名
;; (预填文件名)→ 加载文件到 buffer → 标记 collab → CREATE。会话就绪时 C++ 端
;; eager-seed(见 loro_collab.cpp become_ready)把文件内容作为初始全量推到服务端,
;; 无需等待首次编辑。
(tm-define (collab-new-document-from-file)
  (:interactive #t)
  (choose-file collab-share-file-prompt-name "Load file to share" "action_open")
) ;tm-define

(tm-define (collab-new-document-from-file-named u name)
  (cond ((and (string? name)
           (> (string-length name) 0)
           (not (collab-valid-doc-name? name))
         ) ;and
         (set-message "Invalid name: 1-64 chars, no \\ / : * ? \" < > | or control chars"
           "Collaborative"
         ) ;set-message
        ) ;
        (else
          ;; 加载文件到 buffer(window-per-buffer 开新窗口,否则新标签页),
          ;; current-buffer 随即切到该文件 buffer。
          (if (window-per-buffer?) (load-buffer-in-new-window u) (load-buffer u))
          (collab-mark-current-buffer)
          (loro-collab-create (collab-server-url) name)
          (set-message (string-append "Uploading file as collaborative document (Server "
                         (collab-server-url)
                         ")"
                       ) ;string-append
            "Collaborative"
          ) ;set-message
        ) ;else
  ) ;cond
) ;tm-define

;; 加入指定 UUID 的协作文档(非交互:UUID 由 Join 子菜单选中项传入,
;; opt-name 为菜单已知的显示名预填,最终以服务端 DOC 帧内 name 为准)。
;; 建空 buffer 并切到它 → 会话层 JOIN。服务端回 DOC 后补发 snapshot/updates,
;; 首帧到达时把内容构建进 buffer。
(tm-define (collab-join-document doc-id . opt-name)
  (let ((name (if (and (nnull? opt-name) (string? (car opt-name))) (car opt-name) "")))
    (when (and (string? doc-id) (> (string-length doc-id) 0))
      (with-default-view (if (window-per-buffer?) (open-window) (new-buffer))
        (collab-mark-current-buffer)
        ;; 标题立即设为显示名(无名回退 UUID);DOC 帧到达后 C++ become_ready
        ;; 会以服务端 name 重设标题(最终一致)
        (buffer-set-title (current-buffer) (if (> (string-length name) 0) name doc-id))
        (loro-collab-join (collab-server-url) doc-id name)
        (set-message (string-append "Joining collaborative document "
                       (if (> (string-length name) 0) name doc-id)
                     ) ;string-append
          "Collaborative"
        ) ;set-message
      ) ;with-default-view
    ) ;when
  ) ;let
) ;tm-define

;; 触发后台拉取服务端可用文档 UUID(异步、幂等:loading 中为 no-op)。
(tm-define (collab-refresh-docs) (loro-collab-fetch-docs (collab-server-url)))

;; Join 子菜单:展开时触发后台拉取(不阻塞 GUI),按状态显示
;;   loading → "(loading...)",error → "(unreachable)",
;;   ready+空 → "(no documents)",ready+非空 → 各文档项(点击即加入;
;;   菜单文字为显示名,无名回退 UUID,见 collab-doc-label)。
;; Refresh 强制重新拉取。状态经 loro-collab-docs-status 轮询,ImGui 每帧重建
;; 菜单时自动刷新到最新结果,无需缓存。
(tm-menu (collab-docs-menu)
  (with status
    (begin
      ;; 首次展开(idle)触发后台拉取;fetch 立即把状态置 loading,故只触发一次,
      ;; 之后每帧轮询到 loading/ready/error 都不再自动重拉(重拉仅靠 Refresh)
      (when (== (loro-collab-docs-status) "idle")
        (collab-refresh-docs)
      ) ;when
      (loro-collab-docs-status)
    ) ;begin
    (cond ((== status "loading") ("(loading...)" (collab-refresh-docs)))
          ((== status "error") ("(server unreachable)" (collab-refresh-docs)))
          ((and (== status "ready") (null? (loro-collab-docs)))
           ("(no documents)" (collab-refresh-docs))
          ) ;
          (else (with pairs
                  (collab-docs-pairs (loro-collab-docs))
                  (with dups
                    (collab-doc-name-duplicates pairs)
                    (for (p pairs)
                      (with uuid
                        (car p)
                        (with name
                          (cdr p)
                          (with dup?
                            (let ((cell (assoc name dups)))
                              (and cell (> (cdr cell) 1))
                            ) ;let
                            ((eval (collab-doc-label uuid name dup?)) (collab-join-document uuid name))
                          ) ;with
                        ) ;with
                      ) ;with
                    ) ;for
                  ) ;with
                ) ;with
          ) ;else
    ) ;cond
    ---
    ("Refresh" (collab-refresh-docs))
  ) ;with
) ;tm-menu

;; 退出当前协作会话(不断开 buffer,仅关闭上行/下行通道)。
(tm-define (collab-leave)
  (loro-collab-disconnect)
  (set-message "Collaboration ended" "Collaborative")
) ;tm-define

;; === collab 缓冲特殊语义的覆盖 ===
;; collab 文档(云端):
;;   - 永不"已修改" → 无星号、关闭不弹保存提示、不进自动备份:
;;     已下沉到 C++ editor::need_save(据 tm_buffer_rep::cloud 标志,由会话层
;;     collab_session_manager::get_or_create 在每次挂接时置位——复用残留会话时
;;     构造函数不跑,故必须在 get_or_create 里显式标记)。need_save 是标题星号
;;     / 关闭提示 / tab 星号 / 自动保存的公共收敛点,故不再在 Scheme 侧覆盖
;;     buffer-modified?。
;;   - Save 无效(文档在云端)→ 提示用 Save as 导出本地副本;Save as 正常
;; 用「模块加载时捕获原始绑定」覆盖 save-buffer,避免对纯 glue 函数用 former
;; (tm-define 对未注册到 tm-defined-table 的函数走 else 分支,former 退化为
;; noop,会全局破坏)。

(define %original-save-buffer save-buffer)

(tm-define (save-buffer . l)
  (if (collab-buffer? (current-buffer))
    (set-message "Cloud document: use Save as to export a local copy" "Save")
    (apply %original-save-buffer l)
  ) ;if
) ;tm-define

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions