Textual TextArea 多行文本编辑器完整指南:语法高亮、主题定制与代码编辑实战
【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual
TextArea 是 Textual 框架内置的多行文本编辑组件,支持文本选择、软换行、基于 tree-sitter 的可选语法高亮以及丰富的按键绑定。本文以 Textual 官方文档为主体,结合仓库源码与示例,系统讲解 TextArea 的加载与读取、光标与选区操作、主题系统、缩进与撤销/重做、只读模式、行号控制、按键拦截扩展以及自定义语言支持,帮助你在终端应用中快速构建从普通多行输入框到完整代码编辑器的各类场景。
概览:TextArea 是什么
TextArea(位于 src/textual/widgets/_text_area.py)是一个可聚焦、非容器的编辑组件,用于编辑可能跨越多行的文本。
- 可聚焦(Focusable):是
- 容器(Container):否
- 版本说明:
TextArea在 0.38.0 版本中加入,软换行(soft wrapping)在 0.48.0 版本中加入。
默认情况下,TextArea是一个启用了软换行的标准多行输入框。它天然支持文本选择、软换行、可选的语法高亮以及一系列键位绑定,足以应对大部分文本编辑需求。
代码编辑 vs 纯文本编辑
如果你感兴趣的是编辑代码,可以直接使用TextArea.code_editor便捷构造函数。从源码可以看到,它默认返回一个新的TextArea,并带有如下差异化的配置:
- 软换行关闭(
soft_wrap=False); - 行号开启(
show_line_numbers=True); - Tab 键行为设置为插入
\t(tab_behavior="indent"); - 主题默认为
"monokai"; - 默认
max_checkpoints=50、highlight_cursor_line=True。
一个典型的用法示例如下(对应示例文件 docs/examples/widgets/text_area_example.py):
from textual.app import App, ComposeResult from textual.widgets import TextArea TEXT = """\ def hello(name): print("hello" + name) def goodbye(name): print("goodbye" + name) """ class TextAreaExample(App): def compose(self) -> ComposeResult: yield TextArea.code_editor(TEXT, language="python") app = TextAreaExample() if __name__ == "__main__": app.run()语法高亮依赖
要启用语法高亮,需要安装syntax额外依赖:
=== "pip"
```bash pip install "textual[syntax]" ```=== "poetry"
```bash poetry add "textual[syntax]" ```这会安装tree-sitter与tree-sitter-languages两个包。这两个包以二进制 wheel 形式分发,因此如果你的应用运行环境中没有对应的 wheel,可能会受到限制。安装完成后,设置language响应式属性即可开启高亮:
# Set the language to Markdown text_area.language = "markdown"加载与读取文本内容
加载初始文本
在compose中直接传入文本字符串即可完成初始加载(见上文示例)。需要程序化更新内容时,直接给text属性赋字符串值:
text_area.text = "new content"从 TextArea 读取内容
有几种方式可以取回TextArea中的内容:
TextArea.text属性:返回文本区域内的全部内容(字符串)。TextArea.selected_text属性:返回当前选中区域对应的文本。TextArea.get_text_range方法:返回两个位置之间的文本。
无论哪种方式,当取回的内容跨越多行时,都会使用文档的行分隔符(见下文“行分隔符”一节)。
编辑内容
TextArea的内容可以通过replace方法更新,该方法等价于“先选中一段文本再粘贴”的程序化操作。
此外还提供了一些便捷方法:
insert:在指定位置插入文本;delete:删除指定范围的文本;clear:清空全部内容。
小技巧:TextArea.document.end属性返回文档末尾的位置,在程序化编辑时非常方便。
光标与选区操作
移动光标
光标位置通过cursor_location属性获取,它是一个(row_index, column_index)元组,两个索引均从 0 开始,表示光标在内容中的位置。给cursor_location赋新值会立即更新光标位置:
>>> text_area = TextArea() >>> text_area.cursor_location (0, 0) >>> text_area.cursor_location = (0, 4) >>> text_area.cursor_location (0, 4)cursor_location是程序化移动光标的简单方式,但它不能帮我们选中文本。
选中文本
要选中文本,可以使用selection响应式属性。下面的示例选中文档的前两行(对应示例 docs/examples/widgets/text_area_selection.py):
from textual.app import App, ComposeResult from textual.widgets import TextArea from textual.widgets.text_area import Selection TEXT = """\ def hello(name): print("hello" + name) def goodbye(name): print("goodbye" + name) """ class TextAreaSelection(App): def compose(self) -> ComposeResult: text_area = TextArea.code_editor(TEXT, language="python") text_area.selection = Selection(start=(0, 0), end=(2, 0)) # (1)! yield text_area app = TextAreaSelection() if __name__ == "__main__": app.run()- 选中前两行文本。
注意,选区可以发生在两个方向,因此Selection((2, 0), (0, 0))同样是合法的。
小技巧:selection.end属性始终等于TextArea.cursor_location。换句话说,cursor_location只是访问text_area.selection.end的一个便捷入口。
更多光标工具
位置信息
TextArea上存在大量以cursor_at_开头、返回布尔值的属性,用来描述光标当前所在位置。例如cursor_at_start_of_line告诉我们光标是否位于行首。
我们还可以检查“如果移动光标,光标会到达的位置”。例如get_cursor_right_location返回光标向右移动一步后会到达的位置。这类方法还有很多,命名模式为get_cursor_*_location。
光标移动方法
move_cursor方法允许将光标移动到新位置,同时可以选择文本,也可以边移动边滚动以保持光标居中:
# Move the cursor from its current location to row index 4, # column index 8, while selecting all the text between. text_area.move_cursor((4, 8), select=True)move_cursor_relative提供非常相似的接口,但它是相对于当前光标位置移动。
常用选区
以下方法可以让常见选区操作更便捷:
select_line:按行号选中一行,默认绑定 ++f6++ 键。select_all:选中全部文本,默认绑定 ++f7++ 键。
主题系统
TextArea自带若干内置主题,并且很容易添加自定义主题。主题控制整体外观与风格,包括语法高亮、光标、选区、行号槽(gutter)等。
默认主题
TextArea的默认主题名为css,其所有取值全部来自 CSS。这意味着组件的默认外观与标准 Textual 应用浑然一体,在深色和浅色模式下都表现正常。
使用css主题时,可以通过组件类来为TextArea的各元素设置样式。例如 CSS 代码TextArea .text-area--cursor { background: green; }会让光标变成绿色。
而像代码编辑器这类更复杂的应用,可能更希望使用预定义主题(如monokai),这需要用到TextAreaTheme对象,我们会在下面详细介绍。它允许在代码层面完全自定义TextArea,包括语法高亮。
使用内置主题
TextArea的初始主题由theme参数决定:
# Create a TextArea with the 'dracula' theme. yield TextArea.code_editor("print(123)", language="python", theme="dracula")可以使用available_themes属性查看可用的主题:
>>> text_area = TextArea() >>> print(text_area.available_themes) {'css', 'dracula', 'github_light', 'monokai', 'vscode_dark'}创建TextArea之后,可以通过设置theme属性在可用主题间切换:
text_area.theme = "vscode_dark"设置该属性后,TextArea会立即刷新并显示更新后的主题。
自定义主题
注意:自定义主题仅对想要定制语法高亮的用户有意义。如果只是编辑纯文本、想给
TextArea的某些元素重新着色,应该使用组件类(即上面提到的.text-area--*系列)。
使用自定义(非内置)主题分两步:
- 创建
TextAreaTheme实例; - 使用
TextArea.register_theme注册它。
第一步:创建主题
下面创建一个名为"my_cool_theme"的简单主题:光标为蓝底白字、光标行为黄色背景,并将字符串高亮为红色、注释高亮为品红色:
from rich.style import Style from textual.widgets.text_area import TextAreaTheme my_theme = TextAreaTheme( # This name will be used to refer to the theme... name="my_cool_theme", # Basic styles such as background, cursor, selection, gutter, etc... cursor_style=Style(color="white", bgcolor="blue"), cursor_line_style=Style(bgcolor="yellow"), # `syntax_styles` is for syntax highlighting. # It maps tokens parsed from the document to Rich styles. syntax_styles={ "string": Style(color="red"), "comment": Style(color="magenta"), } )cursor_style、cursor_line_style这类属性对组件应用的是与语言无关的通用样式。如果你不为其中某个属性提供值,它将从 CSS 组件样式中取值。
syntax_styles属性用于语法高亮,它依赖当前使用的language。更多细节见下文“语法高亮”一节。
如果你想在现有主题的基础上扩展,可以通过TextAreaTheme.get_builtin_theme类方法拿到某个内置主题的引用:
from textual.widgets.text_area import TextAreaTheme monokai = TextAreaTheme.get_builtin_theme("monokai")第二步:注册主题
现在把主题注册到TextArea实例上:
text_area.register_theme(my_theme)注册之后,它就会出现在available_themes中:
>>> print(text_area.available_themes) {'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'}然后就可以切换到该主题:
text_area.theme = "my_cool_theme"这会立即更新TextArea的外观。完整的可运行示例见 docs/examples/widgets/text_area_custom_theme.py,其中还展示了通过text_area.cursor_blink = False关闭光标闪烁的细节。
Tab 与 Escape 行为
默认情况下,按下 ++tab++ 键会把焦点移到应用中的下一个组件——这与 Textual 中其他组件的行为一致。
要让 ++tab++ 插入\t字符,可以把tab_behavior属性设置为字符串"indent"。在该模式下,可以通过按下 ++escape++ 键来切换焦点。
缩进
按下 Tab 时插入的字符由indent_type属性控制,可选值为tabs或spaces。
如果indent_type == "spaces",按下 ++tab++ 会插入最多indent_width个空格,以便与下一个制表位对齐。indent_width的默认值是 4(见下方“响应式属性”表格)。
撤销与重做
TextArea提供undo和redo方法。默认情况下,undo绑定 ++ctrl+z++,redo绑定 ++ctrl+y++。
TextArea使用一种启发式策略在特定类型的编辑之后放置检查点(checkpoint)。当你调用undo时,从现在到最近一个检查点之间的所有编辑都会被回退。你也可以手动添加检查点,通过调用TextArea.history.checkpoint()实例方法实现(对应EditHistory类)。
撤销/重做历史采用基于栈的结构,栈中的每一项代表一个检查点。在内存受限的环境中,你可能希望限制检查点的最大数量,可以通过向TextArea构造函数传入max_checkpoints参数来实现(code_editor构造函数的默认值为 50)。
只读模式
TextArea.read_only是一个布尔响应式属性,设为True时禁止用户修改TextArea中的内容。
- 在
read_only=True期间,你仍然可以通过程序修改内容。 - 该模式激活时,
TextArea会获得-read-onlyCSS 类,你可以用它为只读模式提供自定义样式。
行分隔符
内容加载进TextArea时,会从头到尾扫描内容,并记录遇到的第一个行分隔符。
之后通过text属性读取内容时,会统一使用这个分隔符。TextArea不支持导出包含混合行尾(mixed line endings)的文本。
同理,粘贴进TextArea的换行符也会被转换。
可以通过TextArea.document.newline查看当前文档的行分隔符:
>>> text_area = TextArea() >>> text_area.document.newline '\n'行号
左侧包含行号的槽(gutter)可以通过设置show_line_numbers属性为True或False来开关:
text_area.show_line_numbers = True设置该属性会立即重绘TextArea以反映新值。
你还可以通过设置line_number_start响应式属性来改变起始行号(槽中最顶部的行号)。该属性在源码中声明为reactive(1, init=False)(见 src/textual/widgets/_text_area.py#L498),默认值为 1。
扩展 TextArea
有时候你可能希望继承TextArea来添加额外功能。
拦截按键
可以通过重写_on_key来拦截特定按键并注入自定义功能。
示例:自动补全括号
下面扩展TextArea,加入自动闭合括号并把光标移动到合适位置的功能(对应示例 docs/examples/widgets/text_area_extended.py):
from textual import events from textual.app import App, ComposeResult from textual.widgets import TextArea class ExtendedTextArea(TextArea): """A subclass of TextArea with parenthesis-closing functionality.""" def _on_key(self, event: events.Key) -> None: if event.character == "(": self.insert("()") self.move_cursor_relative(columns=-1) event.prevent_default() class TextAreaKeyPressHook(App): def compose(self) -> ComposeResult: yield ExtendedTextArea.code_editor(language="python") app = TextAreaKeyPressHook() if __name__ == "__main__": app.run()这段代码在按下"("时拦截按键处理:插入"()",然后把光标移到开闭括号之间。现在往TextArea里输入def hello(时,括号会被自动闭合,光标正好落在括号中间。
进阶概念
语法高亮原理
TextArea内的语法高亮由名为tree-sitter的库驱动。
每当TextArea中的文档被更新时,内部的语法树(syntax tree)都会同步更新。这棵树会被频繁查询,以找出与语法高亮相关的位置区间。我们为这些区间命名,并最终映射到TextAreaTheme.syntax_styles中的 Rich 样式上。
为说明其工作原理,我们看看 "Monokai" 主题是如何高亮 Markdown 文件的。
当language属性被设置为"markdown"时,会使用类似下面的高亮查询(为简洁已裁剪):
(heading_content) @heading (link) @link这份高亮查询把 Markdown 解析器返回的heading_content节点映射到名字@heading,把link节点映射到名字@link。
在TextAreaTheme.syntax_styles字典中,我们把名字@heading映射为一个 Rich 样式。以下是 "Monokai" 主题中的相关片段:
TextAreaTheme( name="monokai", base_style=Style(color="#f8f8f2", bgcolor="#272822"), gutter_style=Style(color="#90908a", bgcolor="#272822"), # ... syntax_styles={ # Colorise @heading and make them bold "heading": Style(color="#F92672", bold=True), # Colorise and underline @link "link": Style(color="#66D9EF", underline=True), # ... }, )要弄清syntax_styles中可以映射哪些名字,建议查看 Textual 仓库中现有的主题和高亮查询(.scm文件)。高亮查询文件位于 src/textual/tree-sitter/highlights/,已内置 python、markdown、javascript、json、go、rust、bash、html、css、xml、yaml、toml、sql、regex、java 等语言。例如 python.scm 会把标识符映射为@variable、@type、@constant等,而 markdown.scm 则定义了@heading、@link.uri、@link.label、@list.marker等名字。
小技巧:你也可以查看活跃TextArea实例上的TextArea._highlights内容,看看当前打开的文档生成了哪些高亮。
添加自定义语言支持
要为TextArea添加一种语言支持,使用register_language方法。
注册语言需要两样东西:
- 一个 tree-sitter
Language对象,包含该语言的文法; - 一份用于语法高亮的高亮查询。
示例:添加 Java 支持
获取Language对象最简单的途径是使用py-tree-sitter-languages包。我们可以用它拿到表示 Java 的Language对象:
from tree_sitter_languages import get_language java_language = get_language("java")调用get_language时所用解析器的确切版本,可以通过所用py-tree-sitter-languages版本中的repos.txt文件查看。该文件包含各 tree-sitter 解析器 GitHub 仓库的链接与 commit 哈希。在这些仓库中,你通常可以在queries/highlights.scm找到现成的高亮查询,在src/node-types.json找到可用于高亮查询的全部节点类型。
由于我们要为 Java 添加支持,可以从仓库中获取 Java 的高亮查询,步骤如下:
- 打开
py-tree-sitter-languages仓库中的repos.txt文件; - 找到对应
tree-sitter-java的链接并前往该 GitHub 仓库(可能还需要定位到repos.txt中引用的特定 commit); - 打开
queries/highlights.scm查看 Java 的示例高亮查询。
请务必检查仓库中的许可证,确保可以自由复制。
警告:务必使用与当前解析器兼容的高亮查询,因此访问仓库时要注意
repos.txt中的 commit 哈希。
现在我们有了Language和高亮查询,就可以注册 Java 语言了(对应示例 docs/examples/widgets/text_area_custom_language.py,其查询文件为 docs/examples/widgets/java_highlights.scm):
from pathlib import Path from tree_sitter_languages import get_language from textual.app import App, ComposeResult from textual.widgets import TextArea java_language = get_language("java") java_highlight_query = (Path(__file__).parent / "java_highlights.scm").read_text() java_code = """\ class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } """ class TextAreaCustomLanguage(App): def compose(self) -> ComposeResult: text_area = TextArea.code_editor(text=java_code) text_area.cursor_blink = False # Register the Java language and highlight query text_area.register_language("java", java_language, java_highlight_query) # Switch to Java text_area.language = "java" yield text_area app = TextAreaCustomLanguage() if __name__ == "__main__": app.run()运行这个应用可以看到 Java 代码被高亮。你可以自由编辑文本,语法高亮会立即更新。
回顾一下:我们把 tree-sitter 高亮查询中的名字(如@heading)映射到TextAreaTheme.syntax_styles字典里的 Rich 样式对象。如果在注册语言后发现有部分高亮缺失,可能的原因有:
- 当前的
TextAreaTheme中没有对应高亮查询中名字的映射——给syntax_styles增加一个键值对即可解决; - 高亮查询没有给你期望高亮的模式分配名字——这时需要更新高亮查询,为它分配名字。
小技巧:tree-sitter 高亮查询中分配的名字通常跨多种语言复用。例如
@string在多种语言中都被用来高亮字符串。
导航与换行信息
如果你在TextArea之上构建功能,查看navigator和wrapped_document属性可能会很有用:
navigator是一个DocumentNavigator实例,可以提供关于光标在文档中位置的一般信息,以及执行某些操作时光标会移动到哪里。wrapped_document是一个WrappedDocument实例,可以结合换行情况把文档位置转换为视觉位置,还提供各种其他便捷方法与属性。
这些类的详细视图超出了本文范围,但请注意TextArea的很多功能都存在于它们之中,深入研究它们可能是值得的。
响应式属性
下表列出了TextArea的主要响应式属性(见 src/textual/widgets/_text_area.py):
| Name | Type | Default | Description |
|---|---|---|---|
language | str \| None | None | 用于语法高亮的语言。 |
theme | str | "css" | 使用的主题。 |
selection | Selection | Selection() | 当前选区。 |
show_line_numbers | bool | False | 显示或隐藏行号。 |
line_number_start | int | 1 | 槽中的起始行号。 |
indent_width | int | 4 | 缩进的空格数以及 Tab 的宽度。 |
match_cursor_bracket | bool | True | 是否在光标处高亮匹配的括号。 |
cursor_blink | bool | True | 组件获得焦点时光标是否闪烁。 |
soft_wrap | bool | True | 是否启用软换行。 |
read_only | bool | False | 是否启用只读模式。 |
消息
TextArea会发出以下消息:
TextArea.Changed:文本内容发生变更时触发。TextArea.SelectionChanged:选区发生变化时触发。
按键绑定
TextArea定义了一组丰富的默认按键绑定,覆盖光标移动(方向键、行首/行尾、词首/词尾、页面上下)、文本选择(配合 Shift 的方向键、按行/按词选择)、编辑操作(插入、删除、撤销/重做、剪切/复制/粘贴)以及全选(++f7++)、选行(++f6++)等功能。完整的绑定表可在TextArea.BINDINGS中查看(src/textual/widgets/_text_area.py)。
组件类
TextArea定义了若干组件类,用于样式化组件的各个方面,例如text-area--cursor(光标)、text-area--cursor-line(光标行)、text-area--selection(选区)、text-area--gutter(行号槽)等。来自theme属性的样式优先级更高,因此主题会覆盖组件类样式。完整列表见TextArea.COMPONENT_CLASSES。
补充说明
- 要移除
TextArea获得焦点时的描边效果,可以在 CSS 中设置border: none; padding: 0;。
相关资源
Input:单行文本输入组件。TextAreaTheme:为TextArea提供主题。DocumentNavigator:指导光标移动。WrappedDocument:管理文档的换行。EditHistory:管理撤销栈。- tree-sitter 官方文档网站以及 Python 绑定仓库、
py-tree-sitter-languages仓库(提供大量 tree-sitter 语言的二进制 wheel)。
【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考