用SDL3重新实现Emacs 的图形窗口系统

邮件列表里有人要重新实现Emcas 的图像窗口系统了https://lists.gnu.org/archive/html/emacs-devel/2025-09/msg00130.html

找个AI 总结一下 Aleksandr Petrosyan 在邮件中提出了一项名为“Emacs Widget Toolkit”的新项目,旨在为 Emacs 引入基于 SDL3 的图形渲染能力,以支持更丰富的图形界面和内容展示,同时保持与现有 Emacs 的兼容性。以下是该项目的核心要点总结:


:puzzle_piece: 项目目标

  • 重新实现 Emacs 的图形窗口系统,仅使用 SDL3 API。
  • 支持从 Elisp 直接控制图形元素,实现真正的图像渲染和交互。
  • 保留 Emacs 的核心理念:如缓冲区、点(point)导航、可扩展性等。
  • 不变成 VSCode 克隆,而是将 Emacs 的文本驱动 UI 演化为支持富媒体的 UI。

:warning: 当前问题

  • 现有图像渲染能力有限:
    • 使用 create-image 创建图像时,频繁操作会导致 GC 无法及时回收,造成内存泄漏。
    • 无法叠加、混合图像,限制了图像交互和动画能力。
  • 富内容集成困难:
    • 项目如 EAF 虽能引入动态内容,但牺牲了与 Emacs 内部的集成,且 Elisp 控制能力有限。

:white_check_mark: 项目计划

  • 在独立仓库中开发,初期不直接合并到主仓库。
  • 使用 C 语言为主,构建一个基于 SDL3 的并行图形实现。
  • 与主分支定期同步,未来可作为编译选项(类似 PGTK、Lucid)。
  • 提供示例项目和文档,如 WYSIWYG 编辑器、矢量图编辑器等。
  • 保持向后兼容,不破坏现有功能。

:pushpin: 总结 该项目试图从根本上扩展 Emacs 的图形能力,解决当前图像处理与富内容展示的瓶颈,同时保持 Emacs 的哲学和架构。它是一次“进化而非替代”的尝试,目标是让 Emacs 能更好地处理现代图形界面需求,同时仍由 Elisp 驱动和控制。

11 个赞

作者主页几个相关的帖子

3 个赞

之前我分享过作者的帖子到 Reddit,有过一些交流,以及一直关注动态。

以及,这哥们最近失业了…希望这个项目能够顺利。

5 个赞

很赞的项目,如果能从底层去搞,是最彻底的。 emacser 都是为爱发⚡️呀。

作为编辑器的 Emacs 可以落后,作为宗教的 Emacs 将永存

2 个赞

哈哈,落后应该打引号,世界的潮流一直在变,谁又会知道这种“落后”将来是否会成为先进的代表。emacs 是内核先进,表象“落后” :wink:

还没开始看,但:

The long term goal is for this work to be available as standard in all Emacs distributions. (我加的粗体)

Emacs on MS-DOS: :face_holding_back_tears:

3 个赞

期待这个能成功。 这是商业公司在做吗? 还有一个团队在做C的开发。

看了作者主页的 About 页面时,我原本也猜想是不是有个商业公司在后面推,不过后面看到上面 @yibie 的回复(这个哥们失业了),就不确定了……

就俩人吧

无比赞同这句话,框架非常牛逼,不管外面怎么变,Emacs 总能稳稳地跟上

We will have a small team working primarily in C, creating a parallel implementation of graphical Emacs completely using only SDL3.

看来这个 team 确实非常小,只有2人。 :grinning_face:

看好。以后 gtk3 不维护了就用这个。

https://appetrosyan.github.io/posts/layoff/

不失业没这项目了

1 个赞

不会又多一堆条件编译宏吧 :joy:

Lem 的作者在 SDL2 上遇到有 key-binding 的 issue, 不知道用 SDL3 会不会有这些问题. (

很期待! 不知道上了SDL3 之后,emacs的大图片和gif展示能力是否会增强。

如果会的话,那我就有机会给我的emacs界面塞满各种花里胡哨的gif,完全不敢想象那样会有多酷炫,美滋滋!!!

2 个赞

那你还不如直接给他捐点

1 个赞

可行当然是可行,不过要从SDL这种渲染库搓出一个GUI库来,想必工作量相当大……(而且当然不能直接用那些给游戏插件做的SDL GUI库,毕竟Emacs要是也一秒重绘60帧笔记本电池要烂掉了……

1 个赞

我来更新一下开发者最新的想法与进展,总之,他在阅读了 Emacs 的 C 端代码之后,发现复杂度进一步变高了,整篇文章处处表达着「酸爽」的感觉——

The Emacs widget saga: viewports and windows 《The Emacs widget saga: viewports and windows》

September 20, 2025 17-minute read 17 分钟阅读

Preface 前言

The updates are sparse. There are a few reasons. I am actively looking for a new job. On a good day, this process is long and arduous. But I have a few complicating factors.

更新很少。有几个原因。我正在积极寻找新工作。在好的一天里,这个过程是漫长而艰辛的。但我有几个复杂因素。

The second reason is because the amount of work that would be necessary to make this project work was severely under-estimated by yours truly. I have spent the better part of a day trying to figure out why the Emacs source code is organised the way that it is. At the risk of ruffling some feathers, I have given up on the idea of re-using the code that is already there. The new system is going to be implemented from scratch.

第二个原因是,我认为完成这个项目所需的工作量严重被低估了。我花了一整天的时间试图弄清楚为什么 Emacs 源代码是那样组织的。冒着惹人反感的风险,我已经放弃了重用现有代码的想法。新系统将从头开始实现。

Introduction 引言

Every book on Emacs starts with a preface that says what a frame, window and buffer mean in Emacs. For our purposes:

每一本关于 Emacs 的书都以一个前言开始,说明 Emacs 中的 frame 、 window 和 buffer 是什么意思。对我们的目的而言:

frame

an entity that is often called a window in most desktop environments. It represents a set of decorations a menu bar, a title bar, a tool bar (if you like me, have not disabled it), and some select few additional properties that a user never sees. The frame represents a connection to the Emacs server. If the Emacs application is run in non-client-server mode, closing or killing the last frame means killing the Emacs process.

一个在大多数桌面环境中通常被称为窗口的实体。它代表一组装饰,包括菜单栏、标题栏、工具栏(如果你喜欢我,没有禁用它),以及一些用户永远不会看到的额外属性。 frame 代表与 Emacs 服务器的连接。如果 Emacs 应用程序以非客户端-服务器模式运行,关闭或 killing 最后一个 frame 意味着终止 Emacs 进程。

window

an entity that is used to display the edited content. It is where the text appears, where in reader-mode one gets the PDF. It is also what gets created with the vertical and horizontal split commands. The window at the moment is a very simple structure. It is given here for reference.

一个用于显示编辑内容的实体。它是文本出现的地方,在 reader-mode 中获取 PDF 的地方。它也是使用垂直和水平分割命令创建的内容。 window 目前是一个非常简单的结构。它在此处提供作为参考。

struct window
    {
union vectorlike_header header;
Lisp_Object frame;
Lisp_Object next;
Lisp_Object prev;
Lisp_Object parent;
Lisp_Object normal_lines;
Lisp_Object normal_cols;
Lisp_Object new_total;
Lisp_Object new_normal;
Lisp_Object new_pixel;
Lisp_Object contents;
Lisp_Object prev_buffers;
Lisp_Object next_buffers;
Lisp_Object old_buffer;
Lisp_Object start;
Lisp_Object pointm;
Lisp_Object old_pointm;
Lisp_Object temslot;
Lisp_Object vertical_scroll_bar;
Lisp_Object vertical_scroll_bar_type;
Lisp_Object horizontal_scroll_bar;
Lisp_Object horizontal_scroll_bar_type;
Lisp_Object display_table;
Lisp_Object dedicated;
Lisp_Object combination_limit;
Lisp_Object window_parameters;
Lisp_Object cursor_type;
Lisp_Object mode_line_help_echo;
struct glyph_matrix *current_matrix;
struct glyph_matrix *desired_matrix;
EMACS_INT use_time;
EMACS_INT sequence_number;
int change_stamp;
int pixel_left;
int pixel_top;
int left_col;
int top_line;
int pixel_width;
int pixel_height;
int old_pixel_width;
int old_pixel_height;
int old_body_pixel_width;
int old_body_pixel_height;
int total_cols;
int total_lines;
ptrdiff_t hscroll;
ptrdiff_t min_hscroll;
ptrdiff_t hscroll_whole;
modiff_count last_modified;
modiff_count last_overlay_modified;
ptrdiff_t last_point;
  #ifdef HAVE_TEXT_CONVERSION
ptrdiff_t ephemeral_last_point;
  #endif
ptrdiff_t last_mark;
ptrdiff_t base_line_number;
ptrdiff_t base_line_pos;
ptrdiff_t column_number_displayed;
int nrows_scale_factor, ncols_scale_factor;
struct cursor_pos cursor;
struct cursor_pos phys_cursor;
struct cursor_pos output_cursor;
int last_cursor_vpos;
  #ifdef HAVE_WINDOW_SYSTEM
enum text_cursor_kinds phys_cursor_type;
int phys_cursor_width;
int phys_cursor_ascent, phys_cursor_height;
  #endif /* HAVE_WINDOW_SYSTEM */
int left_fringe_width;
int right_fringe_width;
int left_margin_cols;
int right_margin_cols;
int scroll_bar_width;
int scroll_bar_height;
int mode_line_height;
int header_line_height;
int tab_line_height;
ptrdiff_t window_end_pos;
int window_end_vpos;
bool_bf mini : 1;
bool_bf horizontal : 1;
bool_bf update_mode_line : 1;
bool_bf last_had_star : 1;
bool_bf start_at_line_beg : 1;
bool_bf force_start : 1;
bool_bf optional_new_start : 1;
bool_bf phys_cursor_on_p : 1;
bool_bf cursor_off_p : 1;
bool_bf last_cursor_off_p : 1;
bool_bf must_be_updated_p : 1;
bool_bf pseudo_window_p : 1;
bool_bf fringes_outside_margins : 1;
bool_bf fringes_persistent : 1;
bool_bf scroll_bars_persistent : 1;
bool_bf window_end_valid : 1;
bool_bf redisplay : 1;
bool_bf suspend_auto_hscroll : 1;
bool_bf preserve_vscroll_p : 1;
int vscroll;
ptrdiff_t window_end_bytepos;
  }

Realistically, as you can probably tell, adjusting this object will have some quite drastic consequences. The state that is being persisted is very much everything and the kitchen sink. The information is not segregated and segmented in any way. The window simply has all of the properties. The window is responsible for many of the interactions with the UI. While the implication of presenting the rather large structure in full is that the interactions are undesirable, it is in fact the opposite. While it would certainly be prudent to refactor this object a little, the fact that the window has all of its behaviour encoded directly allows one to expand the behaviour of the window.

实际上,正如你可能已经猜到的,调整这个对象会带来一些相当剧烈的后果。被持久化的状态几乎包含了所有东西。信息没有任何方式被隔离或分割。窗口简单地拥有所有属性。窗口负责许多与 UI 的交互。虽然完整呈现这个相当大的结构意味着交互可能不受欢迎,但实际上情况正好相反。虽然重构这个对象肯定会更明智,但窗口直接编码了所有行为的事实允许扩展窗口的行为。

More on that later. 稍后再说。

buffer

The buffer is the bread and butter of any editor. In effect, the buffer is what defines the behaviour of the editor as a whole. A buffer is supposed to be a rather simple structure; the actual code is presented below.

缓冲区是任何编辑器的核心。实际上,缓冲区定义了整个编辑器的行为。缓冲区应该是一个相当简单的结构;实际的代码如下。

{
    union vectorlike_header header;
    Lisp_Object name_;
    Lisp_Object last_name_;
    Lisp_Object filename_;
    Lisp_Object directory_;
    Lisp_Object backed_up_;
    Lisp_Object save_length_;
    Lisp_Object auto_save_file_name_;
    Lisp_Object read_only_;
    Lisp_Object mark_;
    Lisp_Object local_var_alist_;
    Lisp_Object major_mode_;
    Lisp_Object local_minor_modes_;
    Lisp_Object mode_name_;
    Lisp_Object mode_line_format_;
    Lisp_Object header_line_format_;
    Lisp_Object tab_line_format_;
    Lisp_Object keymap_;
    Lisp_Object abbrev_table_;
    Lisp_Object syntax_table_;
    Lisp_Object category_table_;
    Lisp_Object tab_width_;
    Lisp_Object fill_column_;
    Lisp_Object left_margin_;
    Lisp_Object auto_fill_function_;
    Lisp_Object downcase_table_;
    Lisp_Object upcase_table_;
    Lisp_Object case_canon_table_;
    Lisp_Object case_eqv_table_;
    Lisp_Object truncate_lines_;
    Lisp_Object word_wrap_;
    Lisp_Object ctl_arrow_;
    Lisp_Object bidi_display_reordering_;
    Lisp_Object bidi_paragraph_direction_;
    Lisp_Object bidi_paragraph_separate_re_;
    Lisp_Object bidi_paragraph_start_re_;
    Lisp_Object selective_display_;
    Lisp_Object selective_display_ellipses_;
    Lisp_Object overwrite_mode_;
    Lisp_Object abbrev_mode_;
    Lisp_Object display_table_;
    Lisp_Object mark_active_;
    Lisp_Object enable_multibyte_characters_;
    Lisp_Object buffer_file_coding_system_;
    Lisp_Object file_format_;
    Lisp_Object auto_save_file_format_;
    Lisp_Object cache_long_scans_;
    Lisp_Object width_table_;
    Lisp_Object pt_marker_;
    Lisp_Object begv_marker_;
    Lisp_Object zv_marker_;
    Lisp_Object point_before_scroll_;
    Lisp_Object file_truename_;
    Lisp_Object invisibility_spec_;
    Lisp_Object last_selected_window_;
    Lisp_Object display_count_;
    Lisp_Object left_margin_cols_;
    Lisp_Object right_margin_cols_;
    Lisp_Object left_fringe_width_;
    Lisp_Object right_fringe_width_;
    Lisp_Object fringes_outside_margins_;
    Lisp_Object scroll_bar_width_;
    Lisp_Object scroll_bar_height_;
    Lisp_Object vertical_scroll_bar_type_;
    Lisp_Object horizontal_scroll_bar_type_;
    Lisp_Object indicate_empty_lines_;
    Lisp_Object indicate_buffer_boundaries_;
    Lisp_Object fringe_indicator_alist_;
    Lisp_Object fringe_cursor_alist_;
    Lisp_Object display_time_;
    Lisp_Object scroll_up_aggressively_;
    Lisp_Object scroll_down_aggressively_;
    Lisp_Object cursor_type_;
    Lisp_Object extra_line_spacing_;
  #ifdef HAVE_TREE_SITTER
    /* A list of tree-sitter parsers for this buffer.  */
    Lisp_Object ts_parser_list_;
  #endif
    Lisp_Object text_conversion_style_;
    Lisp_Object cursor_in_non_selected_windows_;
    struct buffer_text own_text;
    struct buffer_text *text;
    ptrdiff_t pt;
    ptrdiff_t pt_byte;
    ptrdiff_t begv;
    ptrdiff_t begv_byte;
    ptrdiff_t zv;
    ptrdiff_t zv_byte;
    struct buffer *base_buffer;
    int indirections;
    int window_count;
    char local_flags[MAX_PER_BUFFER_VARS];
    struct timespec modtime;
    off_t modtime_size;
    modiff_count auto_save_modified;
    modiff_count display_error_modiff;
    time_t auto_save_failure_time;
    ptrdiff_t last_window_start;
    struct region_cache *newline_cache;
    struct region_cache *width_run_cache;
    struct region_cache *bidi_paragraph_cache;
    bool_bf prevent_redisplay_optimizations_p : 1;
    bool_bf clip_changed : 1;
    bool_bf inhibit_buffer_hooks : 1;
    bool_bf long_line_optimizations_p : 1;
    struct itree_tree *overlays;
  #ifdef HAVE_TREE_SITTER
    struct ts_linecol ts_linecol_begv;
    struct ts_linecol ts_linecol_point;
    struct ts_linecol ts_linecol_zv;
  #endif
    Lisp_Object undo_list_;
  }

The buffer is responsible for figuring out how to edit itself. there are quite a few functions that get called. Unfortunately, a lot of what I assumed was well-encapsulated code, as for example, abbrev-related functionality is also present in buffer.c.

缓冲区负责弄清楚如何编辑自身。有相当多的函数会被调用。不幸的是,我所认为的良好封装的代码中,有很多内容,例如与 abbrev 相关的功能,也存在于 buffer.c 中。

While it is true that buffer.c is responsible for some of the upkeep that is happening with the gap buffer that we have above, reality is that quite a bit of it is happening in cmds.c as well.

虽然 buffer.c 确实负责了我们上面间隙缓冲区发生的一些维护工作,但现实是其中相当一部分也是在 cmds.c 中发生的。

DEFUN ("self-insert-command", Fself_insert_command, Sself_insert_command, 1, 2,
       "(list (prefix-numeric-value current-prefix-arg) last-command-event)",
       doc: /* Insert the character you type.
Whichever character C you type to run this command is inserted.
The numeric prefix argument N says how many times to repeat the insertion.
Before insertion, `expand-abbrev' is executed if the inserted character does
not have word syntax and the previous character in the buffer does.
After insertion, `internal-auto-fill' is called if
`auto-fill-function' is non-nil and if the `auto-fill-chars' table has
a non-nil value for the inserted character.  At the end, it runs
`post-self-insert-hook'.  */)
  (Lisp_Object n, Lisp_Object c)
{
  CHECK_FIXNUM (n);
  if (NILP (c))
    c = last_command_event;
  else
    last_command_event = c;
  if (XFIXNUM (n) < 0)
    error ("Negative repetition argument %"pI"d", XFIXNUM (n));
  if (XFIXNAT (n) < 2)
    call0 (Qundo_auto_amalgamate);
  if (!CHARACTERP (c))
    bitch_at_user ();
  else
    {
      int character = translate_char (Vtranslation_table_for_input,
				      XFIXNUM (c));
      int val = internal_self_insert (character, XFIXNAT (n));
      if (val == 2)
	Fset (Qundo_auto__this_command_amalgamating, Qnil);
      frame_make_pointer_invisible (SELECTED_FRAME ());
    }

  return Qnil;
}

Which of course is not the real function that does the work, but rather is the front-end for a backend function with a great deal of complexity:

这当然不是实际执行工作的函数,而是具有大量复杂性的后端函数的前端

static int
internal_self_insert (int c, EMACS_INT n)
{
  int hairy = 0;
  Lisp_Object tem;
  register enum syntaxcode synt;
  Lisp_Object overwrite;
  /* Length of multi-byte form of C.  */
  int len;
  /* Working buffer and pointer for multi-byte form of C.  */
  unsigned char str[MAX_MULTIBYTE_LENGTH];
  ptrdiff_t chars_to_delete = 0;
  ptrdiff_t spaces_to_insert = 0;

  overwrite = BVAR (current_buffer, overwrite_mode);
  if (!NILP (Vbefore_change_functions) || !NILP (Vafter_change_functions))
    hairy = 1;

  /* At first, get multi-byte form of C in STR.  */
  if (!NILP (BVAR (current_buffer, enable_multibyte_characters)))
    {
      len = CHAR_STRING (c, str);
      if (len == 1)
	/* If C has modifier bits, this makes C an appropriate
	   one-byte char.  */
	c = *str;
    }
  else
    {
      str[0] = SINGLE_BYTE_CHAR_P (c) ? c : CHAR_TO_BYTE8 (c);
      len = 1;
    }
  if (!NILP (overwrite)
      && PT < ZV)
    {
      /* In overwrite-mode, we substitute a character at point (C2,
	 hereafter) by C.  For that, we delete C2 in advance.  But,
	 just substituting C2 by C may move a remaining text in the
	 line to the right or to the left, which is not preferable.
	 So we insert more spaces or delete more characters in the
	 following cases: if C is narrower than C2, after deleting C2,
	 we fill columns with spaces, if C is wider than C2, we delete
	 C2 and several characters following C2.  */

      /* This is the character after point.  */
      int c2 = FETCH_CHAR (PT_BYTE);

      int cwidth;

      /* Overwriting in binary-mode always replaces C2 by C.
	 Overwriting in textual-mode doesn't always do that.
	 It inserts newlines in the usual way,
	 and inserts any character at end of line
	 or before a tab if it doesn't use the whole width of the tab.  */
      if (EQ (overwrite, Qoverwrite_mode_binary))
	chars_to_delete = min (n, PTRDIFF_MAX);
      else if (c != '\n' && c2 != '\n'
	       && (cwidth = XFIXNAT (Fchar_width (make_fixnum (c)))) != 0)
	{
	  ptrdiff_t pos = PT;
	  ptrdiff_t pos_byte = PT_BYTE;
	  ptrdiff_t curcol = current_column ();

	  if (n <= (min (MOST_POSITIVE_FIXNUM, PTRDIFF_MAX) - curcol) / cwidth)
	    {
	      /* Column the cursor should be placed at after this insertion.
		 The value should be calculated only when necessary.  */
	      ptrdiff_t target_clm = curcol + n * cwidth;

	      /* The actual cursor position after the trial of moving
		 to column TARGET_CLM.  It is greater than TARGET_CLM
		 if the TARGET_CLM is middle of multi-column
		 character.  In that case, the new point is set after
		 that character.  */
	      ptrdiff_t actual_clm
		= XFIXNAT (Fmove_to_column (make_fixnum (target_clm), Qnil));

	      chars_to_delete = PT - pos;

	      if (actual_clm > target_clm)
		{
		  /* We will delete too many columns.  Let's fill columns
		     by spaces so that the remaining text won't move.  */
		  ptrdiff_t actual = PT_BYTE;
		  actual -= prev_char_len (actual);
		  if (FETCH_BYTE (actual) == '\t')
		    /* Rather than add spaces, let's just keep the tab. */
		    chars_to_delete--;
		  else
		    spaces_to_insert = actual_clm - target_clm;
		}

	      SET_PT_BOTH (pos, pos_byte);
	    }
	}
      hairy = 2;
    }

  synt = SYNTAX (c);

  if (!NILP (BVAR (current_buffer, abbrev_mode))
      && synt != Sword
      && NILP (BVAR (current_buffer, read_only))
      && PT > BEGV
      && (SYNTAX (!NILP (BVAR (current_buffer, enable_multibyte_characters))
		  ? XFIXNAT (Fprevious_char ())
		  : UNIBYTE_TO_CHAR (XFIXNAT (Fprevious_char ())))
	  == Sword))
    {
      modiff_count modiff = MODIFF;
      Lisp_Object sym;

      sym = call0 (Qexpand_abbrev);

      /* If we expanded an abbrev which has a hook,
	 and the hook has a non-nil `no-self-insert' property,
	 return right away--don't really self-insert.  */
      if (SYMBOLP (sym) && ! NILP (sym)
	  && ! NILP (XSYMBOL (sym)->u.s.function)
	  && SYMBOLP (XSYMBOL (sym)->u.s.function))
	{
	  Lisp_Object prop;
	  prop = Fget (XSYMBOL (sym)->u.s.function, Qno_self_insert);
	  if (! NILP (prop))
	    return 1;
	}

      if (MODIFF != modiff)
	hairy = 2;
    }

  if (chars_to_delete)
    {
      int mc = ((NILP (BVAR (current_buffer, enable_multibyte_characters))
		 && SINGLE_BYTE_CHAR_P (c))
		? UNIBYTE_TO_CHAR (c) : c);
      Lisp_Object string = Fmake_string (make_fixnum (n), make_fixnum (mc),
					 Qnil);

      if (spaces_to_insert)
	{
	  tem = Fmake_string (make_fixnum (spaces_to_insert),
			      make_fixnum (' '), Qnil);
	  string = concat2 (string, tem);
	}

      ptrdiff_t to;
      if (ckd_add (&to, PT, chars_to_delete))
	to = PTRDIFF_MAX;
      replace_range (PT, to, string, true, true, false);
      Fforward_char (make_fixnum (n));
    }
  else if (n > 1)
    {
      USE_SAFE_ALLOCA;
      char *strn, *p;
      SAFE_NALLOCA (strn, len, n);
      for (p = strn; n > 0; n--, p += len)
	memcpy (p, str, len);
      insert_and_inherit (strn, p - strn);
      SAFE_FREE ();
    }
  else if (n > 0)
    insert_and_inherit ((char *) str, len);

  if ((CHAR_TABLE_P (Vauto_fill_chars)
       ? !NILP (CHAR_TABLE_REF (Vauto_fill_chars, c))
       : (c == ' ' || c == '\n'))
      && !NILP (BVAR (current_buffer, auto_fill_function)))
    {
      Lisp_Object auto_fill_result;

      if (c == '\n')
	/* After inserting a newline, move to previous line and fill
	   that.  Must have the newline in place already so filling and
	   justification, if any, know where the end is going to be.  */
	SET_PT_BOTH (PT - 1, PT_BYTE - 1);
      auto_fill_result = call0 (Qinternal_auto_fill);
      /* Test PT < ZV in case the auto-fill-function is strange.  */
      if (c == '\n' && PT < ZV)
	SET_PT_BOTH (PT + 1, PT_BYTE + 1);
      if (!NILP (auto_fill_result))
	hairy = 2;
    }

  /* Run hooks for electric keys.  */
  run_hook (Qpost_self_insert_hook);

  return hairy;
}

I may have been a bit uncharitable to Emacs, by picking what I was convinced is an extremely complex function from first principles. But it should also show one how much complexity is inherent in a buffer. It is not a trivial structure.

我可能在选择一个我确信从基础原理开始就极其复杂的函数时,对 Emacs 有些苛刻。但这也能展示一个缓冲区中固有的复杂性。它不是一个简单的结构。

In fact, almost everything that I had originally assumed to be well-factored and easy-to-work-with code fragments were in fact, huge detours containing such pearls of wizdom as the bitch_at_user function.

事实上,我原本认为结构良好且易于使用的几乎所有代码片段,实际上都是巨大的绕道,其中包含着像 bitch_at_user 函数这样的智慧结晶。

When I originally set out on the journey of figuring out how the Emacs internals work, the main goal was to find the reason why some architectural decisions were made, and carefully, retaining backwards compatibility fix some of the issues. The Augean Stables require herculean solutions. The alpheus and peneus in this case shall be the SDL-based display engine and a new interaction model.

当我最初开始探索 Emacs 内部工作原理时,主要目标是找出某些架构决策的原因,并仔细地保持向后兼容性来修复一些问题。这个 Augean Stables 需要超人的解决方案。在这个情况下,Alpheus 和 Peneus 将是基于 SDL 的显示引擎和新的交互模型。

Proposed new architecture 建议的新架构

Unfortunately, for my project to be successful, you should be able to boot up Emacs with no modifications, load a package that potentially touches these objects and still be fine. As such, I cannot get rid of these structures and refactor the entire paradigm. But I can make some adjustments to the paradigm, which would make it backwards incompatible to some extent, but grant you much more programmability, dear reader.

不幸的是,为了我的项目成功,你应该能够无修改地启动 Emacs,加载一个可能触及这些对象的包,并且仍然一切正常。因此,我无法完全摒弃这些结构并重构整个范式。但我可以对范式进行一些调整,这将使其在一定程度上向后不兼容,但会赋予你更多的可编程性,亲爱的读者。

As it stands now, the event processing and display systems for buffers are hard-coded and identical across windows/frames. That is one thing I wish to change.

目前,缓冲区的事件处理和显示系统是硬编码的,并且在窗口/框架之间是相同的。这是我希望改变的一点。

I would like to 我希望

Expand the meaning and role of a major mode

扩展主要模式的含义和作用

Decouple the concept of buffer from the current textual representations,

将缓冲区的概念与当前的文本表示形式分离,

Decouple the concept of a window from the textual representations

将窗口的概念与文本表示形式分离

Provide a natural and obvious method by which projects such as reader-mode can create graphical viewports into content.

提供一种自然且明显的方法,使项目如 reader-mode 能够创建内容图形视口。

For this to work, we must adopt a specific worldview informed by the past, but also supplemented with some observations.

要使这有效,我们必须采纳一个基于过去、但也补充了一些观察的世界观。

The Emacs can be viewed as an interactive program that in general, decoupled from the popular perception that it is only a (plain)-text editor, is a programming environment. The distinction is crucial; Emacs is a really an REPL, where the read and evaluate parts are relatively standard, but the print and loop aspects are rather more involved.

Emacs 可以被视为一个交互式程序,它通常与人们普遍认为它只是一个(纯)文本编辑器的观点相分离,而是一个编程环境。这种区别至关重要;Emacs 实际上是一个 REPL,其中 read 和 evaluate 部分相对标准,但打印和循环方面则更为复杂。

Typically the objects that we want to modify are files. These files are typically, but not universally plain text with some encoding. They are accompanied by a buffer, which is the in-memory representation of the on-disk file. The buffer is for all intents and purposes, data.

我们通常想要修改的对象是文件。这些文件通常是(但并非普遍)带有某种编码的纯文本。它们伴随着一个缓冲区,这是磁盘文件在内存中的表示。从本质上讲,缓冲区就是数据。

That data goes through an output function. This function determines how to display the contents of the buffer on-screen. Typically, this is done through a window. However, because the menu items per-major-mode and therefore per-buffer can differ, the active buffer is reflected on the frame as well.

这些数据通过一个输出函数进行处理。该函数决定了如何在屏幕上显示缓冲区的内容。通常,这是通过一个窗口完成的。然而,因为每个主要模式(因此每个缓冲区)的菜单项可能不同,活动缓冲区也会在框架中反映出来。

This output function is meant to be bijective. By that we mean that if the two buffers are different, their on-screen representations should be as well. Conversely, if the two on-screen representations signal a difference, the buffers should be different as well.

这个输出函数应该是双射的。这意味着如果两个缓冲区不同,它们的屏幕表示也应该不同。反之,如果两个屏幕表示显示出差异,那么缓冲区也应该不同。

As of today, there is a one-size-fits-all solution. The output function is one that uses a mixture of the textual glyphs and overlays.

截至目前,存在一种通用的解决方案。输出函数是使用文本字形和覆盖层的混合体。

The latter are the key to why Emacs has so many impressive features, and is also the major source of pain. Contrary to popular belief, overlays are dead simple structures. This time unironically so.

后者是 Emacs 拥有众多令人印象深刻的功能的关键,也是主要的痛点来源。与普遍认知相反,覆盖层是非常简单的结构。这次确实是如此。

struct Lisp_Overlay
/* An overlay's real data content is:
   - plist
   - buffer
   - itree node
   - start buffer position (field of the itree node)
   - end buffer position (field of the itree node)
   - insertion types of both ends (fields of the itree node).  */
  {
    union vectorlike_header header;
    Lisp_Object plist;
    struct buffer *buffer;        /* eassert (live buffer || NULL). */
    struct itree_node *interval;
  }

The overlays are the pixmaps that can display your rendered latex. They are what is used for displaying the PDFs. Contrary to another popular belief (bordering on propaganda) overlays are not powerful. They do not have the same level of programmability as shaders do. IT is very easy to break them, if the buffer contains characters of different sizes (and mine frequently do). They cannot be blended. They are the rough equivalents of sixels, except sixels can be displayed in the terminal, while most overlays cannot.

覆盖层是可以显示你的渲染 LaTeX 的位图。它们用于显示 PDF。与另一个普遍认知(接近宣传)相反,覆盖层并不强大。它们不具备与着色器相同的可编程性。如果缓冲区包含不同大小的字符(我的经常如此),它们很容易被破坏。它们不能混合。它们是六 els 的粗略等效物,除了六 els 可以在终端显示,而大多数覆盖层不能。

What I propose is a break from this architecture. The hard-coded version of the output function should be replaced with a user controlled one.

我提出的是打破这种架构。硬编码的输出函数应该被用户控制的函数所取代。

Next there is the question of event processing. Emacs is not just a viewer of text, after all, and its great power stems from the layered and extensible system of key bindings. The layer that mediates the interaction of the input peripheral devices and Emacs we shall call an input function.

接下来是事件处理的问题。毕竟,Emacs 不仅仅是一个文本查看器,它的强大力量源于分层且可扩展的键绑定系统。我们将中介输入外设与 Emacs 交互的层称为输入函数。

Today there is only one kind of input function. Every key stroke, regardless of duration and timing is processed individually. Using key-maps, this translates into a function call, modifying either the internal state of the Elisp interpreter, or the buffer, or both. One such example is self-insert-command. The event processing modifies the state of the elisp interpreter. The event processing pipeline routes all printable character key codes to the function self-insert-command which then based on the state of the elisp interpreter modifies the buffer.

目前只有一种输入函数。无论按键持续时间和时间如何,每个按键都会被单独处理。通过键映射,这转化为函数调用,修改 Elisp 解释器的内部状态或缓冲区,或两者都修改。例如 self-insert-command 。事件处理修改了 elisp 解释器的状态。事件处理流程将所有可打印字符的键码路由到函数 self-insert-command ,该函数根据 elisp 解释器的状态来修改缓冲区。

The way in which the event handling is currently done is quite suitable for plain-text editing with a standard QWERTY-keyboard. Working with e.g. Plover on Linux with Wayland, one will soon find problems, caused by the fact that Emacs is unaware of the fact that input into it is emulated. In effect, while a program such as Emacs should in principle be able to route controls such that it would be possible to run a first-person-shooter inside of an Emacs window, the practical possibilities for this are slim to none.

当前事件处理的方式对于使用标准 QWERTY 键盘的纯文本编辑来说相当合适。在 Linux 上使用 Wayland 与 Plover 一起工作时,很快就会发现问题,这是因为 Emacs 不知道输入到它里面是被模拟的。实际上,虽然像 Emacs 这样的程序原则上应该能够路由控制,以便可以在 Emacs 窗口中运行第一人称射击游戏,但实际上的可能性微乎其微。

We should be able to introduce custom input functions. But at the moment we cannot. This is not what I would consider a major issue as of today, but one has to be cognisant of the consequences of their actions. if we are successful in creating a suitable set of graphical functions that can do WYSIWYG editing, the next reasonable ask from the users is that they be able to interact with the new objects, and with the current methods it is not practical to do so.

我们应该能够引入自定义输入函数。但目前我们做不到。就目前而言,我认为这不是一个主要问题,但我们必须意识到自己行为的后果。如果我们成功创建了一套合适的图形函数,能够实现所见即所得的编辑,那么用户下一个合理的要求就是能够与新对象进行交互,而目前的方法并不实用。

So we must introduce a new way to process input. That new way must be exposed to programmatic adjustment following the same conventions that the regular elisp does, and must feel familiar to the programmer. We must use the key map mechanism is what I am saying. But it may not be done as straight forward as just that. We may provide only some of the ways to bind events to functions, which may not use the old conventions requesting a new way of defining the mapping. The sky is the limit, and we have seen time and time again that the programmer is more inventive than our best prognois. This leads us to the following isssue: the way in which this system operates shall be extended in ways that we can not anticipate. So may be that the way should not be limited as well.

因此我们必须引入一种新的处理输入的方式。这种新方式必须遵循常规 elisp 的调整约定,并让程序员感到熟悉。我的意思是,我们必须使用键映射机制。但这可能并不像看起来那么简单。我们可能只提供部分将事件绑定到函数的方式,这些方式可能不使用旧有的约定,而是需要一种新的映射定义方式。天外有天,我们一次又一次地看到,程序员的创造力远超我们最好的预测。这引出了以下问题:这个系统的运行方式应当以我们无法预见的方式扩展。因此,这种方式本身或许也不应受到限制。

What this may look like is a promise that the system will take into account the possible configurations of the key maps, but the general behavior is implementation defined, and not confined to the key maps. It can be further new structure that may or may not be known now, and future implementations can choose to define that structure and add it to elisp. Not bad IMO.

这可能看起来像是一个承诺,即系统将考虑键映射的可能配置,但总体行为是具体实现的,并不局限于键映射。这可能是一个进一步的新结构,现在可能已知或未知,未来的实现可以选择定义该结构并将其添加到 elisp 中。在我看来,这并不坏。

That leaves us with the fun exercise of figuring out how to do the output. There are two ways: we can extend the major modes to include the new information. This means that the programmers will have to extend the definition of what a major mode is to include the part of the input and output functions. This may seem like it has some profound implications, but it does not. The work can be done by the macro. We simply define the default implementation of the two, and is it done. The second option is to extend the vocabulary that we use to talk about emacs, that is to say, we add the concept of a viewport, and of an event processor. We now attach the two to the decisions.

这留给我们一个有趣的练习,那就是思考如何实现输出。有两种方法:我们可以扩展主要模式以包含新的信息。这意味着程序员必须扩展主要模式的定义,以包含输入和输出函数的部分。这看起来似乎有一些深远的影响,但实际上并没有。这项工作可以通过宏来完成。我们只需定义这两个的默认实现,事情就完成了。第二种选择是扩展我们用来谈论 Emacs 的词汇,也就是说,我们添加视口和事件处理器的概念。我们现在将这两者与决策联系起来。

As of today, I do not know which is the better way.

到目前为止,我不知道哪种方法更好。

So in conclusion, there is progress, but it is not as much as I would like. There are unresolved issues that I can not promise will be solved in a satisfying fashion. I can neither promise that there will be progress soon. But I will try.

所以,总而言之,确实有进展,但并不像我期望的那样多。还有一些未解决的问题,我无法保证会以令人满意的方式解决。我也无法保证很快就会有进展。但我会尝试。

7 个赞