问题

怎么去实现一个简单文本编辑器?

回答
好的,实现一个简单的文本编辑器是一个很棒的项目,可以让你深入了解很多基础的计算机科学概念。下面我会尽量详细地讲解如何实现一个简单文本编辑器,涵盖了核心功能和实现思路。

我们将以图形用户界面(GUI)为基础来讲解,因为这是用户最直观的交互方式。

一、 核心功能概述

一个“简单”的文本编辑器通常包含以下核心功能:

1. 文本输入与显示: 允许用户在界面上输入文字,并能实时显示出来。
2. 文件操作:
新建文件: 清空当前内容,准备输入新文本。
打开文件: 加载本地文件内容到编辑器中。
保存文件: 将当前编辑器中的内容保存到本地文件。
另存为: 将当前内容保存到指定的新文件。
3. 编辑功能:
复制 (Copy): 复制选中的文本到剪贴板。
剪切 (Cut): 复制选中的文本到剪贴板,并从原位置删除。
粘贴 (Paste): 将剪贴板中的内容插入到光标所在位置。
撤销 (Undo): 恢复到上一个编辑状态。
重做 (Redo): 重复撤销的操作,恢复到更近的编辑状态。
4. 文本查找与替换:
查找: 在文件中搜索指定的文本。
替换: 找到指定文本后,将其替换为另一段文本。
5. 状态显示: 显示当前行号、列号、文件名等信息。

二、 技术选型(GUI库)

要实现GUI,你需要选择一个GUI库。常见的选择有:

Python:
Tkinter: Python 内置的标准库,简单易学,跨平台。非常适合初学者入门。
PyQt / PySide: 基于 Qt 框架,功能强大,界面美观,但需要安装额外的库,且可能涉及商业授权问题(PySide 更宽松)。
Kivy: 专注于现代UI和多点触控,也适合跨平台开发。
Java:
Swing / AWT: Java 自带的GUI工具包,功能成熟。
JavaFX: 更现代化的Java GUI框架。
C++:
Qt: 功能极其强大且广泛使用的跨平台C++ GUI框架。
wxWidgets: 另一个流行的跨平台C++ GUI库。
JavaScript (Web):
HTML/CSS/JavaScript: 通过浏览器实现,你可以构建一个Web端的文本编辑器,功能可以非常强大,例如使用 `contenteditable` 属性或者更复杂的库如 CodeMirror, Monaco Editor (VS Code 的核心)。

为了方便讲解,我们选择 Python 的 Tkinter 库作为示例。它最容易上手,并且包含了实现上述大部分功能所需的组件。

三、 实现步骤与详细讲解

我们一步步来实现这些功能。

1. 项目结构与基础窗口

首先,我们需要一个主窗口。

```python
import tkinter as tk
from tkinter import scrolledtext, Menu, messagebox, filedialog

class SimpleTextEditor:
def __init__(self, root):
self.root = root
self.root.title("简单文本编辑器")
self.root.geometry("800x600") 设置初始窗口大小

self.current_file_path = None 用于记录当前打开的文件路径

文本编辑区域
使用 ScrolledText 控件,它自带滚动条
self.text_area = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, font=("Arial", 12))
self.text_area.pack(expand=True, fill="both") 填充整个窗口

菜单栏
self.create_menu()

绑定快捷键
self.bind_shortcuts()

def create_menu(self):
menubar = Menu(self.root)
self.root.config(menu=menubar)

文件菜单
file_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="文件", menu=file_menu)
file_menu.add_command(label="新建", command=self.new_file, accelerator="Ctrl+N")
file_menu.add_command(label="打开...", command=self.open_file, accelerator="Ctrl+O")
file_menu.add_command(label="保存", command=self.save_file, accelerator="Ctrl+S")
file_menu.add_command(label="另存为...", command=self.save_file_as, accelerator="Ctrl+Shift+S")
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)

编辑菜单
edit_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="编辑", menu=edit_menu)
edit_menu.add_command(label="撤销", command=self.undo, accelerator="Ctrl+Z")
edit_menu.add_command(label="重做", command=self.redo, accelerator="Ctrl+Y")
edit_menu.add_separator()
edit_menu.add_command(label="剪切", command=self.cut, accelerator="Ctrl+X")
edit_menu.add_command(label="复制", command=self.copy, accelerator="Ctrl+C")
edit_menu.add_command(label="粘贴", command=self.paste, accelerator="Ctrl+V")
edit_menu.add_separator()
edit_menu.add_command(label="查找...", command=self.find_text, accelerator="Ctrl+F")
edit_menu.add_command(label="替换...", command=self.replace_text, accelerator="Ctrl+H")

帮助菜单 (可选)
help_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="帮助", menu=help_menu)
help_menu.add_command(label="关于", command=self.show_about)

def bind_shortcuts(self):
文件操作快捷键
self.root.bind_all("", lambda event: self.new_file())
self.root.bind_all("", lambda event: self.open_file())
self.root.bind_all("", lambda event: self.save_file())
self.root.bind_all("", lambda event: self.save_file_as())

编辑操作快捷键
self.root.bind_all("", lambda event: self.undo())
self.root.bind_all("", lambda event: self.redo())
self.root.bind_all("", lambda event: self.cut())
self.root.bind_all("", lambda event: self.copy())
self.root.bind_all("", lambda event: self.paste())
self.root.bind_all("", lambda event: self.find_text())
self.root.bind_all("", lambda event: self.replace_text())

文件操作方法
def new_file(self):
if self.is_dirty(): 检查是否有未保存的更改
if messagebox.askyesno("保存文件", "当前文件有未保存的更改,是否保存?"):
if not self.save_file(): 如果保存失败,则不新建
return
self.text_area.delete("1.0", tk.END) 清空文本区域
self.current_file_path = None
self.root.title("简单文本编辑器 未命名")

def open_file(self):
if self.is_dirty():
if messagebox.askyesno("保存文件", "当前文件有未保存的更改,是否保存?"):
if not self.save_file():
return

filepath = filedialog.askopenfilename(
defaultextension=".txt",
filetypes=[("文本文件", ".txt"), ("所有文件", ".")]
)
if not filepath:
return 用户取消

try:
with open(filepath, "r", encoding="utf8") as f:
content = f.read()
self.text_area.delete("1.0", tk.END)
self.text_area.insert("1.0", content)
self.current_file_path = filepath
self.root.title(f"简单文本编辑器 {filepath}")
except Exception as e:
messagebox.showerror("打开文件错误", f"无法打开文件: {e}")

def save_file(self):
if self.current_file_path:
return self.save_to_file(self.current_file_path)
else:
return self.save_file_as()

def save_file_as(self):
filepath = filedialog.asksaveasfilename(
initialfile="未命名.txt",
defaultextension=".txt",
filetypes=[("文本文件", ".txt"), ("所有文件", ".")]
)
if not filepath:
return False 用户取消

return self.save_to_file(filepath)

def save_to_file(self, filepath):
try:
content = self.text_area.get("1.0", tk.END)
with open(filepath, "w", encoding="utf8") as f:
f.write(content)
self.current_file_path = filepath
self.root.title(f"简单文本编辑器 {filepath}")
return True
except Exception as e:
messagebox.showerror("保存文件错误", f"无法保存文件: {e}")
return False

def is_dirty(self):
简单判断:如果当前文件路径存在且内容与保存时不同,或者文件是未命名的
更完善的实现需要跟踪内容变化状态
这里我们简化处理:如果当前内容和文件内容不一致,或者没有文件路径
(实际实现中需要更精确的dirty flag)
current_content = self.text_area.get("1.0", tk.END)
if self.current_file_path:
try:
with open(self.current_file_path, "r", encoding="utf8") as f:
saved_content = f.read()
return current_content != saved_content
except: 如果文件读取错误,也算作有变动
return True
else:
如果是未命名文件,且内容不为空,则算作有变动
return current_content.strip() != ""

编辑操作方法
def undo(self):
try:
self.text_area.edit_undo()
except tk.TclError:
pass 如果没有可撤销的操作,忽略错误

def redo(self):
try:
self.text_area.edit_redo()
except tk.TclError:
pass 如果没有可重做的操作,忽略错误

def cut(self):
self.text_area.event_generate("<>")

def copy(self):
self.text_area.event_generate("<>")

def paste(self):
self.text_area.event_generate("<>")

查找与替换方法
def find_text(self):
self.create_find_replace_window("查找")

def replace_text(self):
self.create_find_replace_window("替换")

def create_find_replace_window(self, mode):
find_replace_window = tk.Toplevel(self.root)
find_replace_window.title(mode)
find_replace_window.geometry("300x150")

tk.Label(find_replace_window, text="查找内容:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.find_entry = tk.Entry(find_replace_window, width=30)
self.find_entry.grid(row=0, column=1, padx=5, pady=5)
self.find_entry.focus_set() 默认聚焦查找输入框

if mode == "替换":
tk.Label(find_replace_window, text="替换内容:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.replace_entry = tk.Entry(find_replace_window, width=30)
self.replace_entry.grid(row=1, column=1, padx=5, pady=5)
self.replace_button = tk.Button(find_replace_window, text="替换", command=lambda: self.perform_replace(find_replace_window, False))
self.replace_button.grid(row=2, column=1, padx=5, pady=5, sticky="e")
self.replace_all_button = tk.Button(find_replace_window, text="全部替换", command=lambda: self.perform_replace(find_replace_window, True))
self.replace_all_button.grid(row=2, column=1, padx=5, pady=5, sticky="w")

self.find_next_button = tk.Button(find_replace_window, text="查找下一个", command=lambda: self.perform_find_next(find_replace_window))
self.find_next_button.grid(row=2, column=1, padx=5, pady=5, sticky="e") 调整位置,确保不被覆盖

绑定回车键
find_replace_window.bind("", lambda event: self.perform_find_next(find_replace_window))
if mode == "替换":
find_replace_window.bind("", lambda event: self.perform_replace(find_replace_window, False))
find_replace_window.bind("", lambda event: self.perform_replace(find_replace_window, True))

查找按钮的父级窗口也会响应快捷键,所以需要额外处理
self.current_find_replace_window = find_replace_window 记录当前窗口,以便后续操作

def perform_find_next(self, window):
search_term = self.find_entry.get()
if not search_term:
return

获取当前光标位置
current_index = self.text_area.search(search_term, "insert", nocase=True, stopindex=tk.END)

if current_index:
清除之前的查找高亮(如果存在)
self.text_area.tag_remove("found", "1.0", tk.END)
标记找到的文本
self.text_area.tag_add("found", current_index, f"{current_index}+{len(search_term)}")
self.text_area.tag_config("found", background="yellow") 设置高亮颜色
self.text_area.mark_set("insert", f"{current_index}+{len(search_term)}") 将光标移到找到文本的末尾
self.text_area.see(current_index) 滚动到可见区域
else:
messagebox.showinfo("查找", f"找不到 '{search_term}'")

def perform_replace(self, window, all_matches):
search_term = self.find_entry.get()
replace_term = self.replace_entry.get() if window.title() == "替换" else ""

if not search_term:
return

if all_matches:
替换所有
count = 0
start_index = "1.0"
while True:
current_index = self.text_area.search(search_term, start_index, nocase=True, stopindex=tk.END)
if not current_index:
break
end_index = f"{current_index}+{len(search_term)}"
self.text_area.delete(current_index, end_index)
self.text_area.insert(current_index, replace_term)
count += 1
start_index = f"{current_index}+{len(replace_term)}" 继续搜索从替换后的位置开始
if count > 0:
messagebox.showinfo("替换", f"已替换 {count} 处匹配项")
else:
messagebox.showinfo("替换", f"未找到 '{search_term}'")
else:
替换一个
current_index = self.text_area.search(search_term, "insert", nocase=True, stopindex=tk.END)
if current_index:
end_index = f"{current_index}+{len(search_term)}"
self.text_area.delete(current_index, end_index)
self.text_area.insert(current_index, replace_term)
self.text_area.mark_set("insert", f"{current_index}+{len(replace_term)}")
self.text_area.see(current_index)
替换后可以关闭查找替换窗口,或者继续查找
window.destroy()
else:
messagebox.showinfo("替换", f"找不到 '{search_term}'")

帮助方法
def show_about(self):
messagebox.showinfo("关于", "一个简单的文本编辑器 使用 Tkinter 构建")

主程序入口
if __name__ == "__main__":
root = tk.Tk()
editor = SimpleTextEditor(root)
root.mainloop()
```

四、 代码详解与核心概念

1. `tk.Tk()`: 创建主窗口对象。
2. `tk.Toplevel(parent)`: 创建一个独立于主窗口的顶级窗口,通常用于对话框(如查找替换)。
3. `scrolledtext.ScrolledText`: Tkinter 的一个非常有用的组件,它是一个文本区域,并且内置了垂直滚动条。
`wrap=tk.WORD`: 设置自动换行,当文本超出边界时,会在单词边界处换行。
`font=("Arial", 12)`: 设置文本的字体和大小。
4. `pack(expand=True, fill="both")`: 将文本区域控件放置在窗口中,`expand=True` 允许控件随窗口大小改变而扩展,`fill="both"` 使控件在水平和垂直方向上都填充可用空间。
5. `Menu` & `Menu.add_cascade` & `Menu.add_command`: 用于创建菜单栏、下拉菜单以及菜单项。
`tearoff=0`: 移除菜单上的虚线,使菜单可以“撕下”成为独立窗口(通常不想要这个行为)。
`command=self.method_name`: 将菜单项绑定到一个函数。
`accelerator`: 显示快捷键提示,例如 "Ctrl+N"。
6. `root.bind_all("", handler)`: 绑定全局按键事件。这里我们用它来绑定快捷键。
``: 表示按下 Ctrl 键和 N 键。
`lambda event: self.method()`: Tkinter 的事件绑定函数会传递一个 `event` 对象,我们使用 `lambda` 来创建一个函数,该函数调用我们的方法并且忽略 `event` 参数。
7. `self.text_area.delete(index1, index2)`: 删除指定范围内的文本。
`"1.0"`: 表示第一行(line 1),第零个字符(character 0)。
`tk.END`: 表示文本的末尾。
8. `self.text_area.insert(index, text)`: 在指定位置插入文本。
9. `self.text_area.get(index1, index2)`: 获取指定范围内的文本。
10. `filedialog.askopenfilename(...)` & `filedialog.asksaveasfilename(...)`: 打开标准的文件选择对话框。
`defaultextension`: 默认的文件扩展名。
`filetypes`: 定义可选择的文件类型,用于过滤文件列表。
11. `with open(filepath, "r", encoding="utf8") as f:`: Python 中安全地打开和关闭文件的推荐方式。指定 `encoding="utf8"` 以支持广泛的字符集。
12. `messagebox.showinfo(...)` & `messagebox.showerror(...)` & `messagebox.askyesno(...)`: 显示信息、错误或确认消息框。
13. `self.text_area.edit_undo()` & `self.text_area.edit_redo()`: Tkinter 的文本控件内置了撤销/重做堆栈,可以直接调用这些方法。
14. `self.text_area.event_generate("<>"): 模拟控件的内部事件。剪切、复制、粘贴都通过触发这些标准事件来实现。
15. `self.text_area.search(pattern, start, stopindex, nocase=False)`: 在文本控件中搜索字符串。
`"insert"`: 从当前光标位置开始搜索。
`nocase=True`: 忽略大小写。
16. `self.text_area.tag_add("tag_name", start_index, end_index)`: 给文本添加标签(tag)。
17. `self.text_area.tag_config("tag_name", options)`: 配置标签的显示样式,例如背景颜色。
18. `self.text_area.mark_set("mark_name", index)`: 在特定位置设置一个标记。
19. `self.text_area.see(index)`: 滚动文本区域,使指定索引处的文本可见。
20. `is_dirty()`: 这是一个关键但实现起来可能有点微妙的方法。
简单的实现: 对比当前内容和最后保存时文件的内容。如果文件从未保存过(`self.current_file_path` 为 None),则只要文本区域中有内容就认为“脏”(dirty)。
更健壮的实现: 在每次用户输入(如 `insert`、`delete` 事件)时,设置一个内部标志 `self.is_dirty = True`。在文件保存成功后,设置 `self.is_dirty = False`。新建文件时也重置此标志。这样可以避免重复读取文件。

五、 进阶功能与改进

1. 行号显示:
可以在文本区域的左侧创建一个 `tk.Text` 或 `tk.Label` 控件,内容为行号。
当文本区域滚动时,同步更新行号的显示。
当文本内容变化时,重新计算并显示行号。
2. 状态栏:
在窗口底部添加一个 `tk.Label` 或 `tk.Frame` 作为状态栏。
显示当前行号、列号、文件编码、是否已保存等信息。
监听文本区域的光标位置变化 (``, ``) 来更新行/列号。
3. 语法高亮:
这是最复杂的功能之一。需要识别不同语言的关键字、字符串、注释等,并为它们应用不同的标签(tag)和样式。
可以使用正则表达式或者更专业的库来解析文本。
4. 自动换行开关: 在“视图”或“编辑”菜单中添加一个选项来切换 `wrap=tk.WORD` 或 `wrap=tk.NONE`。
5. 字体和颜色设置:
提供一个对话框,让用户选择字体、字号、文本颜色、背景颜色等。
将用户的选择保存起来,并在后续启动时加载。
6. 编码支持:
在打开和保存文件时,提供选择文件编码的选项(如 UTF8, GBK, ASCII)。
7. 多标签页: 实现类似现代浏览器的多标签页功能,允许用户同时编辑多个文件。这需要更复杂的窗口管理。
8. 性能优化: 对于非常大的文件,直接一次性加载到 `ScrolledText` 中可能会导致性能问题。可以考虑只加载可见部分,或者使用更优化的文本渲染技术。
9. 剪贴板集成: Tkinter 的复制粘贴已经利用了操作系统的剪贴板,这是基本功能。

六、 总结

实现一个简单的文本编辑器,核心在于:

GUI框架的使用: 熟悉控件(文本框、菜单、对话框)、布局管理器和事件绑定。
文件I/O: 理解如何读取和写入文件,以及处理文件路径。
文本操作: 如何获取、插入、删除文本,以及使用标签(tag)进行样式化。
状态管理: 如何跟踪文件的修改状态(dirty flag)、当前文件名等。
用户体验: 通过菜单、快捷键、提示信息等提升用户交互的便利性。

这是一个从易到难可以逐步扩展的项目。从最基础的文本输入和保存开始,然后逐步添加编辑、查找、撤销等功能,你会学到很多实用的编程知识。祝你实现成功!

网友意见

user avatar

这是一个大坑... 跳坑多年仍然没出来.
The craft of text editing 讲得比较全了, 我再补充一些.

编辑器大致分为源代码编辑器和富文本两种.

源代码编辑器为了省事可以设置相同的行高方便计算, 不过现在多数支持变化行高了可以从富文本编辑器开始做.

首先, 挑一个 GUI 框架

跨平台的 GUI 通常很吸引人, 例如 Fox, FLTK, Tk, GTK+, Qt, WxWidgets 等, 大部分都有一个编辑源代码的控件, 而这个控件基本是 Scintilla 之上的包装 (再研究 Scintilla 你会发现其实各种 GUI 框架的编程模型包装都不需要, 按照 Scintilla 的设计去用就可以了). 学习下来你会发现各个 GUI 框架都自带一个特别的观感: Fox 的光标是个不可改变的巨型铁轨截面, wxWidgets 尽量模仿原生组件 (E-texteditor 就是用 wxWidgets 做的), GTK 就尽量自己画... 其编程模式实质差异并不大, 因为都是 C 和 C++ ... 最惨的是在 Windows 看着还可以, 一放到 Mac 就觉得丑爆了. 做了其他语言的绑定还是感觉在写 C 和 C++. 当然也有做得不一样的:

  • Tcl/Tk 最简洁
  • REBOL view 最 fancy
  • Paul Graham 最推崇 Arc


另一大类 GUI 框架是 XUL. 写个 XML 界面, 然后在 XML 界面上画东西. XHTML+JavaScript 就是一种 XUL 方案. Sun, 很小很柔软, 摸斯拉 等等大公司都推过自家的 XUL 方案. 然而 XML 根本就不适合人类编写, 作为 model 格式也过于巨大不好维护. 最初魔兽世界的插件也是推荐 XML 写界面然后绑定 lua 的动作, 但由于太不灵活也没有一个拖控件的界面, 所以玩家开发了 Ace 系列的 UI lib, 完全不用 XML 纯用 lua 写了. 拖拽式画界面只能骗骗小朋友, PaintCode 也比 XML 解释器性能更好, 所以现在 XUL 基本绝迹, 连直接用 HTML 写界面都不时髦了.

如果不跨平台, 用图形操作系统的 GUI 框架会更能解决很多实际问题, 性能也有保证. Win32API, MFC, ATL, WinForms, WPF, Carbon, Cocoa, CGContext, CoreText ... 就是操作系统商人心狠手辣变幻无常, 一心搞个大新闻还处处夹带私货, 一路学来也是挺累人的. 另一方面嘛 X11 这种更难学, 我就卡在了 motif ...

虽然跨平台的 GUI 框架在慢慢衰亡, 但 OpenGL 这类更接近底层硬件的图形库给人类提供了新的希望. 利用 OpenGL 的成功案例就有 Sublime Text. 我觉得 Cairo, SDL 这种半 GUI 框架的高性能图形库是比较适合的, 就是用的人少了点.

鉴于图形化界面的巨坑... 何不写个纯命令行的编辑器呢? 这时候我们有各种行编辑库可以用: readline, libedit, termcap, Antirez 的轻量 linenoise ... 再用脚本语言的话, 由于内建正则语法和一些字符串处理函数, 很容易在一两万行内写个功能齐全的编辑器解决战斗, 例如 Daikonos.

就算用 C, 如果只实现最简单的功能, 1024 行以内也是可以的: Writing an editor in less than 1000 lines of code, just for fun

纯字符界面缺点也很多, 平滑滚动没有, 动画高亮没有, 文字显示揪细点想调个 kerning 啊 ligature 啊也没办法. 那就自己做一个图形框架? Eclipse 就自掘巨坑组合 C++ 和 Awt 搞出个 SWT. 其实 Awt 和 Swing (NetBeans, IntJ 都是基于 Swing) 处理 Unicode 都有大量的坑, 我都不喜欢... 曾经有个我关注的编辑器 Redcar, 最初用 GTK 编写, 后来转成了 Swing, 然后逐渐就做不动了... jEdit 作者弃编辑器坑, 后来挖了个基于栈的语言新坑 Factor. 后来? 后来也不搞了...

现在 GUI 基本被 ES 的大流统治. 用 Web 做编辑器可以做出一些非常棒的用户体验, 现在浏览器引擎也优化得比几年前好太多. Atom, Monaco Code Editor 都是在 Web 上做的成功案例. 为了容易上手估计 ES 是首选. 缺点是某些细的 UX 不好实现, 正经的优化会花掉更多时间 (例如 Monaco 为了分析性能点连 IR Hydra 都用上了).

介绍两个 Helloworld, GUI 框架 + Scintilla 实现常见一个编辑框
基于 FxRuby 的:

       require 'fox16'  include Fox  app = FXApp.new window = FXMainWindow.new app,   "My Editor",   nil, nil, DECOR_ALL, 100, 100, 710, 550  sci = FXScintilla.new window,   nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y  app.create window.show app.run     


基于我自己写的 GUI 框架的就更简单了 (谁不年少轻狂造过几个 GUI 框架轮子?)

       require 'cici' app = Cici.app 'scintilla' c = app.paint [600, 600], Cici::ZoomLayout c.scintilla [500, 500] app.message_loop     


其实还有各种 GUI 框架的编辑器 hello world 都差不多, 但用框架就是跟着别人走, 很难做出更好的用户体验.

如果从更底层点的地方开始, 例如 Win32API 和 Carbon, 站稳脚跟学习图形界面编程, 前面的道路会... 更狭窄 (公司刚裁了很多桌面程序员并对 Web 产品加大投入...). 不过你理解事件模型的实现和常见优化手段以后, 就算编辑器不成功, 也可以自己写个游戏引擎玩玩嘛.

然后, 挑一个 text storage 数据结构

例如 Cocoa 就自己提供了一个 NSTextStorage, 自己造大约有几个主流选项:

  • Gap buffer: 例子有 Emacs. 很简单的数据结构, 光标前一个 buffer, 光标后一个 buffer. 能极大的减少 buffer 重新分配次数. 扩展一下变成 multi-gap buffer, 多光标编辑也很流畅.
  • Chain of lines: 例子有 TextMate. 每行一个 buffer, 一行不拆散. 对压缩的文件高亮时会比较卡. 但是可以和功能强大的正则引擎 Oniguruma 完美集成.
  • Cell buffer: 例子有 Scintilla. Cell 大小固定, 如果一行超出 Cell 的固定大小, 就分拆成多个 Cell. 用过 Scite 或者 Code::Blocks 或者 Notepad++ 等会发现, 打开大文件, 高亮都还流畅, 因为 Scintilla 的 Cell buffer 和重绘计算的效率很高. 但由于拆行, 只能集成 input driven 的功能较弱的正则引擎或者 lexer, 而这会对实现很多功能带来麻烦.
  • Zipper: Immutable 的数据结构, 如果用 Haskell 做后端会非常适合. 同时还能顺便实现树形历史.


text storage 到界面显示之前, 需要一个排版引擎. 最简单的排版引擎就是把显示区域等分成很多格子, 把等宽字符直接填到格子里 -- 但是这并不好看. 一个文本框的显示得考虑:

  • 如果当前字体包含这个字符不? 是不是从别的字体里找替代?
  • 这些字符组合起来占多宽和多高? 会不会被上面和下面的行挡住? 注意字符的组合并不等于它们的宽度之和, 你要理解 kerning, ligature, baseline 等等 type setting 概念先.
  • 当你排好看以后, 排版的效率往往就不能保证了... 优化也是很难的.


在浏览器里有一套默认的设置, 还有 font-kerning, letter-spacing 等 CSS 属性的帮助, 处理这些问题要简易很多.

text storage (文本模型) + text container (排版引擎) + text view (显示引擎) 是不是就够了呢? 你还得考虑输入法和文字方向... 这一块光看文档是不够的, 自绘的文本输入框, 连很多大厂都没把输入法兼容好... Windows 的 input method editor 和 Cocoa 的 NSTextInput 都能让你抓狂很久.

然后, 考虑一下语言模型...

很多浏览器的文本编辑框里可以用中文词为单位移动光标 (ctrl + 方向键 / opt + 方向键), 这是怎么实现的呢? 有个库叫 ICU, 里面提供了很多边界分析(分字分词分句)用的函数. 浏览器就是用它实现的. ICU 甚至提供了多语言的排版引擎.

如果你要做智能提示和自动完成... 所谓智能提示往往并没有那么智能, 不如参考 vim, 用更依赖用户主动性的设计, 把自动完成的快捷键分为 line complete, dictionary complete, omni complete 等等, 给程序一个更 specific 的指令, 它就能完成得更精准快捷.

但现在的主流是被动性编程, 编辑器/IDE 给你一堆选项, 让你挨个选... 实现这类型的智能提示, 你得写很多代码把语法/类型/先验知识编进去. 而提升智能的方式是, 分析当前的 skip-gram 中最高概率出现的词, 把它排到更优先的位置去 --- 所以先学点计算语言学, 把 word2vec 玩熟吧.

------

除了上述几个大的 design decision 以外, 还有很多很多设计和编码的 Topic 可以讲...

  • 模块和插件机制 -- 正面例子有 Atom 的插件机制, 反面例子有 Osgi...
  • 协程/线程调度后台任务
  • 动态编译和 shader
  • 集成脚本语言 -- 可以方便组合功能和实现插件. 例如 Emacs 有 elisp, Vim 有 vimscript/tcl/perl/python/ruby/mzscheme, Scite 有 lua. 执行脚本时要考虑隔离, 又不影响界面的重绘. 语言特性对编写插件影响巨大, 专门设计一个语言是有很多好处的.
  • ...


当初我只是想用趁手的编辑器写篇博客而已, 好几年过去了, 现在都还在写脚本语言... 所以请慎重跳坑...

———

2020 年补充:现在不写脚本语言了,在写编译器。还要山寨 Revery 。再次忠告请慎重跳坑。

另外,现在对文本编辑器后端,有不少成熟的 buffer 管理项目

  • 如果想做 VSCode 这类的传统编辑器,推荐使用 libvim
  • 如果想做多人协同编辑,推荐选一款 CRDT
  • 如果想做基于 web 的编辑器,可以选择像 Prosemirror 等 web based text editor 为基础

类似的话题

  • 回答
    好的,实现一个简单的文本编辑器是一个很棒的项目,可以让你深入了解很多基础的计算机科学概念。下面我会尽量详细地讲解如何实现一个简单文本编辑器,涵盖了核心功能和实现思路。我们将以图形用户界面(GUI)为基础来讲解,因为这是用户最直观的交互方式。一、 核心功能概述一个“简单”的文本编辑器通常包含以下核心功.............
  • 回答
    想从头开始搭建一个属于自己的数据库系统?听起来有点像个大工程,但别担心,这其实是一个循序渐进的过程。我来跟你好好聊聊,怎么从最基础的概念出发,一点一点地构建一个能用的、简单的数据库系统。这篇文章不会像那些冷冰冰的AI教程,咱们就当是老朋友之间聊技术,把事情说透了。首先,我们得明确“数据库系统”到底是.............
  • 回答
    想要在一台电脑上处理好与几台电脑之间文件上传下载的活儿,最直接也最省事的办法,就是把这台电脑变成一个“小服务器”。这么一来,其他电脑就能像访问一个公共文件柜一样,轻松地把文件传上来或者从中取走。具体怎么做呢?最常用也最容易上手的,就是利用Windows自带的“文件共享”功能。首先,在你打算做“服务器.............
  • 回答
    想要一份营养、简单又实惠的早餐?没问题!这事儿一点都不难,关键在于选对食材和掌握一些小窍门。咱们今天就来聊聊,一个人怎么轻松搞定这早餐大事。核心理念:主食+蛋白质+蔬菜/水果,均衡是王道!别被“营养”两个字吓到,其实你早餐摄取的营养,不一定非要很复杂。遵循“主食+蛋白质+蔬菜/水果”这个金三角,就能.............
  • 回答
    没关系想去区法院实习?这事儿,对咱们大一的学生来说,听起来确实有点玄乎,但也不是没辙!别着急,我给你拆解拆解,一步步来捋捋怎么操作。首先,得明白法院实习这事儿,咱们得摆正心态。法院的实习机会,尤其是在司法系统里,确实会涉及一些人脉资源,但也不是完全排斥没有关系的学生。关键在于你能不能通过自己的努力,.............
  • 回答
    我理解你现在的感受,站在毕业的关口,面对即将到来的实习和找工作,性格内向又有些不敢独立完成事情,这种担忧和迷茫是很正常的。别担心,这并不意味着你不行,而是我们需要一些方法和策略来帮助你更好地应对。首先,咱们得明白一点,性格内向并不是缺点,它只是你认知和与世界互动的一种方式。很多人内向,但依然能把工作.............
  • 回答
    哥们,这事儿真他妈倒霉透顶!刚看到你的求助,我这心都提到了嗓子眼。大四实习一个月不容易啊,结果回来摊上这事儿,真是让人血压飙升。别慌,咱们一步步来捋捋,看看怎么把这破事儿给解决了。首先,得把事情的原委弄清楚。你室友那电饭锅为什么会放在你桌子上?是他自己没地方放,还是知道你不在,趁机塞过来的?这事儿虽.............
  • 回答
    哥们儿,我懂你!你是不是每次打开第二个 CAD 文件,它都像个“粘人精”一样挤进同一个窗口里,搞得你任务栏那叫一个“清净”,一次只能看到一个 CAD 图标?这感觉太折磨人了,尤其是当你需要在不同图纸间频繁切换的时候。别急,这个问题咱来好好说道说道,保证让你把 CAD 的多文件显示弄得明明白白,任务栏.............
  • 回答
    您好!您这个问题非常有意思,也触及到了实际应用中的一些挑战。您提到电源是超低频交流,整流后需要0.几F(比如0.1F、0.2F、0.5F)的滤波电容。在超低频交流下,要实现这样大容量的滤波电容,或者说达到同等的滤波效果,确实需要一些特殊的考量和技术手段。首先,我们来理解一下为什么在超低频下需要这么大.............
  • 回答
    这可是个天大的难题,而且对象还是个邪神,一不留神就可能掉进万丈深渊。不过,如果真的有这么一个机会,我肯定会绞尽脑汁,把每个字都抠到极致,力求万无一失。首先,不能想着一步登天,比如“我想要永远年轻,永远富有,永远健康”。这种贪得无厌的愿望,邪神听了只会在心里偷笑,然后给你一个扭曲的、让你欲哭无泪的实现.............
  • 回答
    .......
  • 回答
    穿越到乾隆时期的西藏,成为一名农奴,想要摆脱农奴身份实现逆袭,这条路注定异常艰难,甚至可以说是九死一生。清朝乾隆时期的西藏,社会结构森严,农奴制度根深蒂固,人身依附关系极强。然而,正如历史上的许多例子所示,即使在最绝望的环境中,智慧、毅力和一些机遇也能创造奇迹。以下是一个详细的逆袭计划,从生存到最终.............
  • 回答
    刚研一,导师让你去实验室跟着师兄师姐做实验,结果每次过去,他们都让你回去“玩”,不用过来。这事儿听着挺让人心里不是个滋味,尤其是刚开始研究生生活,对一切都充满了好奇和学习的渴望,结果却被晾在一边,心里肯定会犯嘀咕。咱们一步步来分析,看看这到底是怎么回事,以及你该怎么办。首先,咱们得冷静地分析一下,师.............
  • 回答
    想证明一个实对称矩阵是零矩阵,最直接也最根本的方法,就是证明它所有的元素都是零。听起来简单,但具体怎么做,又可以从不同的角度切入,每种角度都有其道理和可操作性。下面我来详细说说,尽量把每一步都说清楚,让你觉得是在跟一个懂行的人在交流。首先,我们得明确什么是实对称矩阵,什么是零矩阵。 实对称矩阵(.............
  • 回答
    一字涨停一字跌停,听起来像是市场里的“传说”,但其实,它是特定条件下市场行为的一种极端体现,背后有着清晰的逻辑和机制支撑。想要实现这种极端走势,需要 极度的供需失衡 且这种失衡在交易时段内 持续且不可打破。咱们一点点来拆解。 为什么叫“一字”?首先,这个“一字”就很有画面感。在股票K线图上,当股价在.............
  • 回答
    电影《1917》那令人震撼的“一镜到底”视觉效果,绝对是影片成功的一大关键,也让观众仿佛身临其境,与主角一同经历那场惊心动魄的生死之旅。这玩意儿可不是简单地按下一个“录制”按钮就完事儿了,背后是团队无数的心血和精妙的设计。首先,得明确一点,所谓的“一镜到底”并非真的没有剪辑点。在电影里,一个镜头最长.............
  • 回答
    《戴森球计划》之所以如此火爆,并不仅仅是因为它“堆料”式的建造和宏大的目标,更在于其背后许多精心设计的技术细节,这些细节共同营造了令人沉浸的游戏体验。下面我们就来详细剖析一下它在技术上是如何实现的,并且尽量深入细节: 1. 动态生成的庞大宇宙与多线程处理核心问题: 如何在一个可观测宇宙尺度上,动态生.............
  • 回答
    没问题,这事儿包在我身上!你想把 CSV 文件里三列数字加起来,但有个小条件:要是哪一列的数字正好是 30000,那它就得“罢工”,不参与求和,只让剩下的那一两列乖乖相加。这事儿操作起来其实挺灵活的,我们一步一步来把它搞定。首先,你需要一个工具来读取和处理 CSV 文件。Python 语言配合 Pa.............
  • 回答
    关于曾仕强教授的预言,以及它们是否真的“实现”,这确实是一个引人入胜且复杂的话题。人们对他预言的关注,很大程度上源于他清晰的逻辑、对中国传统文化的深刻解读,以及他能够将这些抽象的道理与生活中的种种现象联系起来,并最终指向一些似乎“应验”的事件。曾仕强教授的预言风格:首先,理解曾仕强教授的预言风格很重.............
  • 回答
    .......

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有