怎样在中英文混排的文档中令光标上下移动时严格对齐?

强迫症又犯了

当上一行是英文,本行是中文时,上下移动光标的行为有点出乎意料:

当光标位于第一个英文字符时:

图片

向下移动到第一汉字上,这没问题:

图片

当光标位于第二个英文字符时:

图片

向下移动就开始不对齐了,这看起来很不爽:

图片

后面基本也是这样:

图片

这个在 insert state 下不是很明显,但是在 evil normal state 下就比较明显了。有没有什么比较好的解决办法?

1 个赞

把英文字符设置成和中文一样宽的全角。(escape)

这个回答霸气。。。我喜欢

:joy:

这应该是对齐强迫症患者的终极 Solution 了。

chinese-character-aligment-in-emacs

从来没遇到这问题,把光标放在第二个汉字,mode-line 显示的位置不是2?

可能还是你配置/字体的原因,emacs -Q 或者 describe-char 分别看看汉字和字母有什么异常。

配成等宽字体就行了 可以参考我的配置

目前的配置 在windows下及macos下是配好的,最近没用linux 所以,linux下的没怎么配, 不过可以参考我的配置进行调整

;; 如果配置好了, 下面20个汉字与40个英文字母应该等长
;; here are 20 hanzi and 40 english chars, see if they are the same width
;;
;; aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
;; 你你你你你你你你你你你你你你你你你你你你|
;; ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,|
;; 。。。。。。。。。。。。。。。。。。。。|
;; 1111111111111111111111111111111111111111|
;; 東東東東東東東東東東東東東東東東東東東東|
;; ここここここここここここここここここここ|
;; ココココココココココココココココココココココココココココココココココココココココ|
;; 까까까까까까까까까까까까까까까까까까까까|

(defun create-frame-font-mac()          ;emacs 若直接启动 启动时调用此函数似乎无效
  (set-face-attribute
   'default nil :font "Menlo 12")
  ;; Chinese Font
  (dolist (charset '( han symbol cjk-misc bopomofo)) ;script 可以通过C-uC-x=查看当前光标下的字的信息
    (set-fontset-font (frame-parameter nil 'font)
                      charset
                      (font-spec :family "PingFang SC" :size 14)))

  (set-fontset-font (frame-parameter nil 'font)
                    'kana                 ;script ココココココココココココココココココココココココココココココココココココココココ
                    (font-spec :family "Hiragino Sans" :size 14))
  (set-fontset-font (frame-parameter nil 'font)
                    'hangul               ;script 까까까까까까까까까까까까까까까까까까까까
                    (font-spec :family "Apple SD Gothic Neo" :size 16))

  )
(when (and (equal system-type 'darwin) (window-system))
  (add-hook 'after-init-hook 'create-frame-font-mac))

(defun create-frame-font-w32()          ;emacs 若直接启动 启动时调用此函数似乎无效
  (set-face-attribute
   'default nil :font "Courier New 10")
  ;; Chinese Font
  (dolist (charset '( han symbol cjk-misc bopomofo)) ;script 可以通过C-uC-x=查看当前光标下的字的信息
    (set-fontset-font (frame-parameter nil 'font)
                      charset
                      (font-spec :family "新宋体" :size 16)))

  (set-fontset-font (frame-parameter nil 'font)
                    'kana                 ;script ココココココココココココココココココココココココココココココココココココココココ
                    (font-spec :family "MS Mincho" :size 16))
  (set-fontset-font (frame-parameter nil 'font)
                    'hangul               ;script 까까까까까까까까까까까까까까까까까까까까
                    (font-spec :family "GulimChe" :size 16)))

(when (and (equal system-type 'windows-nt) (window-system))
  (add-hook 'after-init-hook 'create-frame-font-w32))
1 个赞

是 2,然后从左往右:

中文是 0 2 4 6 … 英文是 0 1 2 3 4 …

英文在 1 3 5 7 … 等位置时向下移动会向右偏移,相应地移动到中文的 2 4 6 8 … 位

要避免这种现象,应该是相应地移动到中文的 0 2 4 6 位才对。但是这个函数貌似是 c 函数,要修改 emacs 源代码。因为这个函数相对底层。很多地方都要用到,用 elisp advice 太笨重了。

我的字体是等宽的

你用的 mac-port 吧?我这估计是 linux 原生 emacs 的锅

macport 的作者难道是亚洲人?很多这种细节上的优化啊!

是的,原作者是日本人,现在也有世界各地的维护者。

这就能解释了,好多让我头疼的问题在 macport 那根本不存在

要是有人把 macport 再反向 port 到 linux 就好了 :joy:

截图用的是原生 Emacs,我觉得跟这应该没关系,登录到远程 Linux 跑了一下 Emacs 也没问题。

如果要强行对齐,只要 advice 一下 next-line 之类的函数就可以做到吧,在光标下移之后,判断当前 point 是汉字就向左移动一格。会不会是你配置某个地方刚好做了相反的操作?

还是 Emacs -Q 看看吧。

还真是你说的这样!那这个怎么 debug?

先看看你的 evil-next(-visual)-line 有没有被 advice,

没有再把 evil 禁掉,

还找不到就二分法整个配置。

1 个赞

应该是 evil 的锅无误了,我 emacs -Q 然后只加载 evil 也是这个行为

多谢 @twlz0ne 的耐心指点,看来很多问题还是不要着急下结论比较好,学习了!

内部函数是这个:

;; The purpose of this function is the provide line motions which
;; preserve the column. This is how `previous-line' and `next-line'
;; work, but unfortunately the behaviour is hard-coded: if and only if
;; the last command was `previous-line' or `next-line', the column is
;; preserved. Furthermore, in contrast to Vim, when we cannot go
;; further, those motions move point to the beginning resp. the end of
;; the line (we never want point to leave its column). The code here
;; comes from simple.el, and I hope it will work in future.
(defun evil-line-move (count &optional noerror)
  "A wrapper for line motions which conserves the column.
Signals an error at buffer boundaries unless NOERROR is non-nil."
  (cond
   (noerror
    (condition-case nil
        (evil-line-move count)
      (error nil)))
   (t
    (evil-signal-without-movement
      (setq this-command (if (>= count 0)
                             #'next-line
                           #'previous-line))
      (let ((opoint (point)))
        (condition-case err
            (with-no-warnings
              (funcall this-command (abs count)))
          ((beginning-of-buffer end-of-buffer)
           (let ((col (or goal-column
                          (if (consp temporary-goal-column)
                              (car temporary-goal-column)
                            temporary-goal-column))))
             (if line-move-visual
                 (vertical-motion (cons col 0))
               (line-move-finish col opoint (< count 0)))
             ;; Maybe we should just `ding'?
             (signal (car err) (cdr err))))))))))

正在分析问题出在哪,能给点指点就更好了, @twlz0ne

这个问题是 evil-next-line 引起的,而 evil-next-visual-line 则没问题。

由于我一直用的是 evil-next-visual-line,以至于都不记得有这个问题了,所以前面回答有些地方不太对。

其实你先不着急挖那么深,只要对比上面两个函数的差别,关键就在 line-move-visual 这个变量。

1 个赞

感谢你再次为我指明了方向! @twlz0ne

它俩的核心都是 evil-line-move,然后区别只是在 line-move-visual 变量是否为 t

关键就是这一句:

             (if line-move-visual
                 (vertical-motion (cons col 0))
               (line-move-finish col opoint (< count 0)))

现在正在看 line-move-finish 函数。。。这函数连文档都没有,不过 comment 比较详细

(defun line-move-finish (column opoint forward)
  (let ((repeat t))
    (while repeat
      ;; Set REPEAT to t to repeat the whole thing.
      (setq repeat nil)

      (let (new
	    (old (point))
	    (line-beg (line-beginning-position))
	    (line-end
	     ;; Compute the end of the line
	     ;; ignoring effectively invisible newlines.
	     (save-excursion
	       ;; Like end-of-line but ignores fields.
	       (skip-chars-forward "^\n")
	       (while (and (not (eobp)) (invisible-p (point)))
		 (goto-char (next-char-property-change (point)))
		 (skip-chars-forward "^\n"))
	       (point))))

	;; Move to the desired column.
	(line-move-to-column (truncate column))

	;; Corner case: suppose we start out in a field boundary in
	;; the middle of a continued line.  When we get to
	;; line-move-finish, point is at the start of a new *screen*
	;; line but the same text line; then line-move-to-column would
	;; move us backwards.  Test using C-n with point on the "x" in
	;;   (insert "a" (propertize "x" 'field t) (make-string 89 ?y))
	(and forward
	     (< (point) old)
	     (goto-char old))

	(setq new (point))

	;; Process intangibility within a line.
	;; With inhibit-point-motion-hooks bound to nil, a call to
	;; goto-char moves point past intangible text.

	;; However, inhibit-point-motion-hooks controls both the
	;; intangibility and the point-entered/point-left hooks.  The
	;; following hack avoids calling the point-* hooks
	;; unnecessarily.  Note that we move *forward* past intangible
	;; text when the initial and final points are the same.
	(goto-char new)
	(let ((inhibit-point-motion-hooks nil))
	  (goto-char new)

	  ;; If intangibility moves us to a different (later) place
	  ;; in the same line, use that as the destination.
	  (if (<= (point) line-end)
	      (setq new (point))
	    ;; If that position is "too late",
	    ;; try the previous allowable position.
	    ;; See if it is ok.
	    (backward-char)
	    (if (if forward
		    ;; If going forward, don't accept the previous
		    ;; allowable position if it is before the target line.
		    (< line-beg (point))
		  ;; If going backward, don't accept the previous
		  ;; allowable position if it is still after the target line.
		  (<= (point) line-end))
		(setq new (point))
	      ;; As a last resort, use the end of the line.
	      (setq new line-end))))

	;; Now move to the updated destination, processing fields
	;; as well as intangibility.
	(goto-char opoint)
	(let ((inhibit-point-motion-hooks nil))
	  (goto-char
	   ;; Ignore field boundaries if the initial and final
	   ;; positions have the same `field' property, even if the
	   ;; fields are non-contiguous.  This seems to be "nicer"
	   ;; behavior in many situations.
	   (if (eq (get-char-property new 'field)
	   	   (get-char-property opoint 'field))
	       new
	     (constrain-to-field new opoint t t
				 'inhibit-line-move-field-capture))))

	;; If all this moved us to a different line,
	;; retry everything within that new line.
	(when (or (< (point) line-beg) (> (point) line-end))
	  ;; Repeat the intangibility and field processing.
	  (setq repeat t))))))