flet-code-editor-dsl 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. flet_code_editor_dsl-0.3.2/PKG-INFO +1329 -0
  2. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/README.md +26 -23
  3. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/pyproject.toml +9 -5
  4. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flet_code_editor_dsl/__init__.py +3 -1
  5. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flet_code_editor_dsl/flet_code_editor_dsl.py +7 -0
  6. flet_code_editor_dsl-0.3.2/src/flet_code_editor_dsl.egg-info/PKG-INFO +1329 -0
  7. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/flet_code_editor_dsl/lib/src/code_editor.dart +65 -0
  8. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/flet_code_editor_dsl/lib/src/tokenizer.dart +11 -2
  9. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/flet_code_editor_dsl/pubspec.yaml +1 -1
  10. flet_code_editor_dsl-0.3.0/PKG-INFO +0 -8
  11. flet_code_editor_dsl-0.3.0/src/flet_code_editor_dsl.egg-info/PKG-INFO +0 -8
  12. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/LICENSE +0 -0
  13. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/MANIFEST.in +0 -0
  14. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/setup.cfg +0 -0
  15. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flet_code_editor_dsl/language.py +0 -0
  16. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flet_code_editor_dsl.egg-info/SOURCES.txt +0 -0
  17. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flet_code_editor_dsl.egg-info/dependency_links.txt +0 -0
  18. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flet_code_editor_dsl.egg-info/requires.txt +0 -0
  19. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flet_code_editor_dsl.egg-info/top_level.txt +0 -0
  20. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/__init__.py +0 -0
  21. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/flet_code_editor_dsl/__init__.py +0 -0
  22. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/flet_code_editor_dsl/lib/flet_code_editor_dsl.dart +0 -0
  23. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/flet_code_editor_dsl/lib/src/extension.dart +0 -0
  24. {flet_code_editor_dsl-0.3.0 → flet_code_editor_dsl-0.3.2}/src/flutter/flet_code_editor_dsl/lib/src/regex_highlighter.dart +0 -0
@@ -0,0 +1,1329 @@
1
+ Metadata-Version: 2.4
2
+ Name: flet-code-editor-dsl
3
+ Version: 0.3.2
4
+ Summary: Flet code editors with syntax highlighting: EditorTokens (tokenizer) and EditorRegex (regex)
5
+ Author-email: obgwew <obgwew@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/obgwew/flet-code-editor-dsl
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: flet>=0.85.2
12
+ Dynamic: license-file
13
+
14
+ # flet-code-editor-dsl
15
+
16
+ > Two native Flutter code-editing controls for [Flet](https://flet.dev) — **`EditorTokens`** (hand-written tokenizer) and **`EditorRegex`** (regular expressions) — plus a small Python DSL (`Language`) that describes a language declaratively: keywords, strings, comments, numbers, sigils, error underlines and code folding, without writing a single regular expression. Includes touch-screen helpers: a cut/copy/paste menu at the caret, a magnifier, a cursor pad and page/editor scroll hand-off.
17
+
18
+ ![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)
19
+ ![Flet](https://img.shields.io/badge/flet-%3E%3D0.85.2-informational)
20
+ [![PyPI](https://img.shields.io/pypi/v/flet-code-editor-dsl)](https://pypi.org/project/flet-code-editor-dsl/)
21
+ ![License](https://img.shields.io/badge/license-Apache%202.0-blue)
22
+
23
+ ---
24
+
25
+ ## Table of Contents
26
+
27
+ 1. [Overview](#overview)
28
+ 2. [What's New in 0.3.2 / Migration](#whats-new-in-032--migration)
29
+ 3. [Features](#features)
30
+ 4. [Requirements](#requirements)
31
+ 5. [Installation](#installation)
32
+ 6. [Quick Start](#quick-start)
33
+ 7. [Choosing a Control: `EditorTokens` vs `EditorRegex`](#choosing-a-control-editortokens-vs-editorregex)
34
+ 8. [Editor Reference (both controls)](#editor-reference-both-controls)
35
+ - [Common properties](#common-properties)
36
+ - [Engine properties](#engine-properties)
37
+ - [Sizing the editor (important)](#sizing-the-editor-important)
38
+ - [Vertical modes](#vertical-modes)
39
+ - [Reading and writing text, `on_change` and debounce](#reading-and-writing-text-on_change-and-debounce)
40
+ - [Colors](#colors)
41
+ - [Changing things at runtime](#changing-things-at-runtime)
42
+ 9. [Mobile / Touch Features](#mobile--touch-features)
43
+ - [Selection menu (cut / copy / paste at the caret)](#selection-menu-cut--copy--paste-at-the-caret)
44
+ - [Extra toolbar and cursor pad](#extra-toolbar-and-cursor-pad)
45
+ - [Magnifier](#magnifier)
46
+ - [Scroll hand-off (`scroll_mode`)](#scroll-hand-off-scroll_mode)
47
+ - [Paste behavior](#paste-behavior)
48
+ 10. [The `Language` DSL (for `EditorTokens`)](#the-language-dsl-for-editortokens)
49
+ - [`Language`](#language)
50
+ - [`WordGroup`](#wordgroup)
51
+ - [`StringRule`](#stringrule)
52
+ - [`BlockComment` and `line_comments`](#blockcomment-and-line_comments)
53
+ - [`Sigil`](#sigil)
54
+ - [Numbers, operators, punctuation, identifiers](#numbers-operators-punctuation-identifiers)
55
+ - [`TokenColors`](#tokencolors)
56
+ - [`ErrorRules` (error underlines)](#errorrules-error-underlines)
57
+ - [`FoldRule` and `fold_by_indent` (code folding)](#foldrule-and-fold_by_indent-code-folding)
58
+ - [`case_insensitive`](#case_insensitive)
59
+ - [`Language.to_json()`](#languageto_json)
60
+ 11. [How the Lexer Works](#how-the-lexer-works)
61
+ 12. [`EditorRegex` Reference (regex engine)](#editorregex-reference-regex-engine)
62
+ 13. [Performance Notes](#performance-notes)
63
+ 14. [Complete Examples](#complete-examples)
64
+ 15. [Important: First Build Required](#important-first-build-required)
65
+ 16. [Building Your App](#building-your-app)
66
+ 17. [Developing and Testing Locally](#developing-and-testing-locally)
67
+ 18. [Project Structure](#project-structure)
68
+ 19. [Known Limitations](#known-limitations)
69
+ 20. [Troubleshooting](#troubleshooting)
70
+ 21. [Contributing](#contributing)
71
+ 22. [Sponsors](#sponsors)
72
+ 23. [License](#license)
73
+
74
+ ---
75
+
76
+ ## Overview
77
+
78
+ `flet-code-editor-dsl` is a Flet extension that ships **two** Flutter controls. Both are built on [`re_editor`](https://pub.dev/packages/re_editor) (a fully custom editor: each line is laid out and painted independently and only visible lines are built), and both offer the same features. **The only difference is the highlighting engine:**
79
+
80
+ | Control | Highlighting engine | Configured with | Best for |
81
+ |---------|---------------------|-----------------|----------|
82
+ | **`EditorTokens`** | Hand-written, stateful **tokenizer** (no regex) | A `Language` object (`language=lang.to_json()`) | Large documents, multi-line strings/comments, languages with real structure |
83
+ | **`EditorRegex`** | **Regular expressions**, matched per line and cached | `rules`, `error_rules`, `folds` | Quick regex-driven coloring when you already have patterns |
84
+
85
+ Instead of shipping fixed grammars, you describe your language in Python:
86
+
87
+ ```python
88
+ import flet_code_editor_dsl as ce
89
+
90
+ lang = ce.Language(
91
+ words=[ce.WordGroup(["let", "if", "else"], "#C586C0")],
92
+ line_comments=["#"],
93
+ strings=[ce.StringRule('"')],
94
+ colors=ce.TokenColors(comment="#6A9955", string="#CE9178", number="#B5CEA8"),
95
+ )
96
+ ```
97
+
98
+ and hand `lang.to_json()` to `EditorTokens`.
99
+
100
+ ## What's New in 0.3.2 / Migration
101
+
102
+ ### 0.3.2
103
+
104
+ | Change | Details |
105
+ |--------|---------|
106
+ | **Controls renamed** | `FletCodeEditor` → **`EditorTokens`**, `FletCodeField` → **`EditorRegex`**. The old names still work as deprecated aliases in `__init__.py` (delete them if you do not need them). |
107
+ | **`EditorRegex` rebuilt** | It now runs on `re_editor` instead of `code_text_field` (that dependency was removed). It has the **same feature set** as `EditorTokens`: line numbers, error underlines (`error_rules`), code folding (`folds`, `fold_by_indent`), read-only, fonts, colors, size levels, debounce. |
108
+ | **`incremental_highlighting` removed** | `EditorRegex` always highlights line by line. Passing the old `incremental_highlighting=` argument raises a `TypeError`. |
109
+ | **Touch support** | New selection menu at the caret/handles, optional toolbar and cursor pad, magnifier, `scroll_mode`. See [Mobile / Touch Features](#mobile--touch-features). |
110
+ | **Paste fix** | Cut/copy/paste from the new menu/toolbar read the clipboard directly and normalize line endings and control characters before inserting. |
111
+ | **New Dart files** | `regex_highlighter.dart` (regex engine). `code_editor.dart` is now shared by both controls. The old `src/flet_code_editor_dsl.dart` (`CodeField` widget) was deleted. |
112
+ | **Number literals** | `0b…` now accepts only `0`/`1` and `0o…` only `0`–`7` (before, they accepted any hex digit, so `0b1010f` was one number). `0x…` is unchanged. |
113
+ | **Sigils** | If the longest matching prefix has no name after it, shorter prefixes are tried next (for example `$$` and `$` defined together: a lone `$$ ` now falls back to `$`). |
114
+
115
+ Rename in your code:
116
+
117
+ ```python
118
+ # before # after
119
+ ce.FletCodeEditor(language=...) ce.EditorTokens(language=...)
120
+ ce.FletCodeField(rules=[...]) ce.EditorRegex(rules=[...])
121
+ ```
122
+
123
+ Because the Dart side changed, **rebuild your app** after upgrading.
124
+
125
+ ### Migrating from the original `CodeEditor` API
126
+
127
+ The earlier regex-based `CodeEditor` control and its `rule()` / `group()` / `pair()` helpers no longer exist.
128
+
129
+ | Original API | 0.3.2 |
130
+ |-------|-------|
131
+ | Control `CodeEditor` | `EditorTokens` (tokenizer) and `EditorRegex` (regex) |
132
+ | `rule()`, `group()` + `rules_json()` | `Language(words=[WordGroup(...)], strings=..., ...)` + `Language.to_json()`; `HighlightRule` list for `EditorRegex` |
133
+ | `pair()` + `pairs_json()` | `ErrorRules(brackets=[BracketPair(...)])` and `StringRule` |
134
+ | `strict` / `error_color` | `ErrorRules(unknown_words=True, allowed_words=[...], forbidden_words=[...])` |
135
+ | `before` / `after` / `ignore_if` | Not needed: the lexer understands strings, comments, calls, sigils |
136
+ | Flet `>= 0.80` | Flet `>= 0.85.2` |
137
+
138
+ ## Features
139
+
140
+ - Native Flutter rendering through the Flet extension mechanism.
141
+ - Two engines with the same features: declarative `Language` tokenizer (**no regex**) or regex rules.
142
+ - Virtualized rendering: only visible lines are built and highlighted.
143
+ - Per-line caching; correct multi-line state (block comments, multi-line strings) in the tokenizer engine.
144
+ - Strings with escapes, custom delimiters and optional multi-line support.
145
+ - Line and block comments, sigils (`$name`, `@user`), numbers (decimal, float, exponent, `_`, `0x`/`0b`/`0o`), function-call detection.
146
+ - Optional error underlines (wavy or straight) and user-defined folding regions.
147
+ - Line numbers, word wrap, read-only, font family/size, text and background colors.
148
+ - **Touch helpers:** cut/copy/paste menu at the caret, optional toolbar with a cursor pad, long-press magnifier, page/editor scroll hand-off.
149
+ - Standard `ft.LayoutControl` properties (`width`, `height`, `expand`, `margin`, `visible`, `opacity`, ...).
150
+
151
+ ## Requirements
152
+
153
+ | Component | Version |
154
+ |-----------|---------|
155
+ | Python | `>= 3.10` |
156
+ | Flet (Python) | `>= 0.85.2` |
157
+ | Flet (Flutter side) | `flet: ^0.85.2` |
158
+ | Dart SDK | `>= 3.3.0 < 4.0.0` (the tokenizer uses Dart 3 records) |
159
+ | Flutter packages | `re_editor ^0.10.0` (pulled in automatically) |
160
+ | Git | Only for installing from GitHub |
161
+
162
+ > The package contains a Flutter extension, so your app must declare it in `pyproject.toml` and be built with `flet build`. See [Important: First Build Required](#important-first-build-required).
163
+
164
+ ---
165
+
166
+ ## Installation
167
+
168
+ ### Option 1: PyPI (recommended)
169
+
170
+ ```bash
171
+ pip install flet-code-editor-dsl
172
+ ```
173
+
174
+ Declare it in your app's `pyproject.toml`:
175
+
176
+ ```toml
177
+ [project]
178
+ name = "my-flet-app"
179
+ version = "1.0.0"
180
+ requires-python = ">=3.10"
181
+ dependencies = [
182
+ "flet>=0.85.2",
183
+ "flet-code-editor-dsl>=0.3.2",
184
+ ]
185
+ ```
186
+
187
+ Pin an exact version for reproducible builds: `"flet-code-editor-dsl==0.3.2"`. Upgrade with `pip install --upgrade flet-code-editor-dsl`.
188
+
189
+ ### Option 2: GitHub
190
+
191
+ ```bash
192
+ pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git"
193
+ pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git@v0.3.2" # pinned
194
+ ```
195
+
196
+ Inside `pyproject.toml`:
197
+
198
+ ```toml
199
+ dependencies = [
200
+ "flet>=0.85.2",
201
+ "flet-code-editor-dsl @ git+https://github.com/obgwew/flet-code-editor-dsl.git",
202
+ ]
203
+ ```
204
+
205
+ ### Option 3: Local checkout or wheel
206
+
207
+ See [Developing and Testing Locally](#developing-and-testing-locally). Note that `flet build` does **not** read `[tool.uv.sources]`; point `dependencies` at a wheel file instead.
208
+
209
+ ### Verifying the installation
210
+
211
+ ```bash
212
+ python -c "import flet_code_editor_dsl as m; print(m.__all__)"
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Quick Start
218
+
219
+ ```python
220
+ import flet as ft
221
+ import flet_code_editor_dsl as ce
222
+
223
+ LANG = ce.Language(
224
+ words=[
225
+ ce.WordGroup(["let", "if", "else", "while", "return"], "#C586C0"), # keywords
226
+ ce.WordGroup(["true", "false", "null"], "#569CD6"), # constants
227
+ ],
228
+ line_comments=["#"],
229
+ strings=[ce.StringRule('"')],
230
+ colors=ce.TokenColors(
231
+ comment="#6A9955", string="#CE9178", escape="#D7BA7D", number="#B5CEA8",
232
+ function="#DCDCAA", identifier="#9CDCFE", operator="#D4D4D4", punctuation="#D4D4D4",
233
+ ),
234
+ )
235
+
236
+
237
+ def main(page: ft.Page):
238
+ page.title = "Code Editor"
239
+ page.theme_mode = ft.ThemeMode.DARK
240
+
241
+ editor = ce.EditorTokens(
242
+ value='let x = add(1, 2) # sum\nprint("done\\n")',
243
+ language=LANG.to_json(), # <-- JSON string, not the object
244
+ default_color="#D4D4D4",
245
+ background_color="#1E1E1E",
246
+ expand=True,
247
+ )
248
+ page.add(editor)
249
+
250
+
251
+ ft.run(main)
252
+ ```
253
+
254
+ Same thing with the regex engine:
255
+
256
+ ```python
257
+ editor = ce.EditorRegex(
258
+ value='let x = add(1, 2) # sum',
259
+ rules=[
260
+ ce.HighlightRule(r"#.*", "#6A9955"), # comments first
261
+ ce.HighlightRule(r'"[^"\n]*"', "#CE9178"), # strings
262
+ ce.HighlightRule(r"\b(?:let|if|else)\b", "#C586C0"), # keywords
263
+ ce.HighlightRule(r"\b\d+(?:\.\d+)?\b", "#B5CEA8"), # numbers
264
+ ],
265
+ default_color="#D4D4D4",
266
+ background_color="#1E1E1E",
267
+ expand=True,
268
+ )
269
+ ```
270
+
271
+ > **Two things that surprise people on first use:**
272
+ > 1. For `EditorTokens`, `language` expects the **JSON string** returned by `Language(...).to_json()`, not the `Language` object.
273
+ > 2. By default the editor box is only `13/30` of the available space. Use `expand=True` (or `width`/`height`) to control the size. See [Sizing the editor](#sizing-the-editor-important).
274
+
275
+ ---
276
+
277
+ ## Choosing a Control: `EditorTokens` vs `EditorRegex`
278
+
279
+ Both controls share the same widget, so everything outside highlighting is identical.
280
+
281
+ | Feature | `EditorTokens` | `EditorRegex` |
282
+ |---------|:--------------:|:-------------:|
283
+ | Highlighting engine | Tokenizer (`Language`) | Regex (`HighlightRule`) |
284
+ | Multi-line strings / block comments | Yes (state carried across lines) | No (each line is highlighted alone) |
285
+ | Error underlines | Yes (`Language.errors`) | Yes (`error_rules`, regex based) |
286
+ | Code folding | Yes (`Language.folds`, `fold_by_indent`) | Yes (`folds`, `fold_by_indent`) |
287
+ | Line numbers, `wrap`, `read_only` | Yes | Yes |
288
+ | `font_family`, `font_size`, `default_color`, `background_color` | Yes | Yes |
289
+ | `width_level` / `height_level`, `vertical_mode`, `min_lines`, `max_lines` | Yes | Yes |
290
+ | `debounce_ms`, `on_change` | Yes | Yes |
291
+ | Mobile: selection menu, toolbar, magnifier, `scroll_mode` | Yes | Yes |
292
+
293
+ Rule of thumb: use **`EditorTokens`** for real languages (especially with multi-line constructs); use **`EditorRegex`** when you already have regular expressions or only need a few colors.
294
+
295
+ ---
296
+
297
+ ## Editor Reference (both controls)
298
+
299
+ ```python
300
+ @ft.control("EditorTokens")
301
+ class EditorTokens(ft.LayoutControl): ...
302
+
303
+ @ft.control("EditorRegex")
304
+ class EditorRegex(ft.LayoutControl): ...
305
+ ```
306
+
307
+ ### Common properties
308
+
309
+ | Property | Type | Default | Description |
310
+ |----------|------|---------|-------------|
311
+ | `value` | `str` | `""` | Editor text. Assigning from Python replaces the content. |
312
+ | `line_numbers` | `bool` | `True` | Show the line-number gutter. |
313
+ | `syntax_highlighting` | `bool` | `True` | Master switch for highlighting (and the error underlines that depend on it). |
314
+ | `show_errors` | `bool` | `True` | Draw error underlines (needs `Language.errors` or `error_rules`). |
315
+ | `code_folding` | `bool` | `True` | Show fold markers (needs folds or `fold_by_indent`). |
316
+ | `wrap` | `bool` | `False` | Soft-wrap long lines. |
317
+ | `read_only` | `bool` | `False` | Disable editing (selection and scrolling still work). |
318
+ | `width_level` | `int \| None` | `13` | Visible width as `level/30` of the available width. See [Sizing](#sizing-the-editor-important). |
319
+ | `height_level` | `int \| None` | `13` | Visible height as `level/30` of the available height. |
320
+ | `vertical_mode` | `VerticalMode` | `FIXED` | `FIXED`, `BOUNDED` or `UNLIMITED`. |
321
+ | `min_lines` / `max_lines` | `int \| None` | `None` | Bounds in `BOUNDED` mode (fall back to `1` / `10`). |
322
+ | `font_family` | `str` | `"Consolas"` | Font family (use a monospace font). |
323
+ | `font_size` | `float` | `12.0` | Font size in logical pixels. |
324
+ | `default_color` | `str \| None` | `None` | Color for text with no token color. |
325
+ | `background_color` | `str \| None` | `None` | Editor background. |
326
+ | `debounce_ms` | `int` | `150` | Delay before the text is pushed to Python and `on_change` fires. |
327
+ | `on_change` | handler | `None` | Called after the debounce delay; `e.data` is the full text. |
328
+
329
+ Touch-related properties are listed in [Mobile / Touch Features](#mobile--touch-features). All `ft.LayoutControl` properties are available as well.
330
+
331
+ ### Engine properties
332
+
333
+ **`EditorTokens`**
334
+
335
+ | Property | Type | Default | Description |
336
+ |----------|------|---------|-------------|
337
+ | `language` | `str` | `""` | JSON from `Language(...).to_json()`. `""` means no highlighting. Invalid JSON is silently treated as "no highlighting". |
338
+
339
+ Error underlines and folding for this control are configured **inside** the `Language` (`errors=`, `folds=`, `fold_by_indent=`).
340
+
341
+ **`EditorRegex`**
342
+
343
+ | Property | Type | Default | Description |
344
+ |----------|------|---------|-------------|
345
+ | `rules` | `list[HighlightRule]` | `[]` | Regex highlighting rules. First matching rule wins per character. |
346
+ | `error_rules` | `list[ErrorPattern]` | `[]` | Regexes whose matches are underlined as errors. |
347
+ | `error_color` | `str` | `"#F44747"` | Underline color. |
348
+ | `wavy` | `bool` | `True` | Wavy underline (`False` = straight). |
349
+ | `folds` | `list[FoldRule]` | `[]` | Folding regions (same `FoldRule` as the DSL). |
350
+ | `fold_by_indent` | `bool` | `False` | Also fold by indentation. |
351
+
352
+ ### Sizing the editor (important)
353
+
354
+ There are three independent ways to size the control, evaluated in this order:
355
+
356
+ 1. **Explicit layout** — `width=`, `height=` or `expand=`. When given, the level properties are ignored for that axis:
357
+ - `width` or `expand` set → `width_level` is ignored.
358
+ - `height`, `expand` set, or `vertical_mode != FIXED` → `height_level` is ignored.
359
+ 2. **Line-count height** — `vertical_mode="bounded"` / `"unlimited"` computes the height from the number of lines.
360
+ 3. **Levels** — otherwise the box is `level / 30` of the available space on that axis, with a **minimum of 48 px**. Level `30` covers the available space. If the parent gives unbounded constraints, the screen size is used.
361
+
362
+ Because both levels default to `13`, an editor placed in a layout **without** `expand`, `width` or `height` will look small. This is the most common "why is my editor tiny?" question:
363
+
364
+ ```python
365
+ ce.EditorTokens(...) # ~13/30 x 13/30 of the space
366
+ ce.EditorTokens(..., width_level=30, height_level=30) # fills the space
367
+ ce.EditorTokens(..., expand=True) # fills the parent (levels ignored)
368
+ ce.EditorTokens(..., width=600, height=400) # fixed size (levels ignored)
369
+ ```
370
+
371
+ Values outside `1..30` are clamped.
372
+
373
+ > When the optional toolbar is shown (`mobile_controls=True`) in `FIXED` mode, the editor and the toolbar share the given height (the toolbar takes 44 px).
374
+
375
+ ### Vertical modes
376
+
377
+ ```python
378
+ class VerticalMode(str, Enum):
379
+ FIXED = "fixed" # fills the given height, scrolls internally
380
+ BOUNDED = "bounded" # grows between min_lines and max_lines, then scrolls
381
+ UNLIMITED = "unlimited" # grows with the text, no limit
382
+ ```
383
+
384
+ | Mode | Behavior |
385
+ |------|----------|
386
+ | `FIXED` | Fills the height it is given (explicit height, `expand`, or `height_level`) and scrolls internally. |
387
+ | `BOUNDED` | Height follows the line count, clamped to `[min_lines, max_lines]` (defaults `1` and `10`), then scrolls. |
388
+ | `UNLIMITED` | Height follows the line count with no limit. Put it inside a scrollable parent. |
389
+
390
+ ```python
391
+ # A chat-input style box: starts at 3 lines, grows to 12, then scrolls
392
+ ce.EditorTokens(
393
+ language=LANG.to_json(),
394
+ vertical_mode=ce.VerticalMode.BOUNDED,
395
+ min_lines=3,
396
+ max_lines=12,
397
+ width=500,
398
+ )
399
+ ```
400
+
401
+ ### Reading and writing text, `on_change` and debounce
402
+
403
+ ```python
404
+ def on_change(e: ft.ControlEvent):
405
+ print("Current text:", e.data)
406
+
407
+ editor = ce.EditorTokens(on_change=on_change, debounce_ms=300)
408
+ ```
409
+
410
+ - Typing does **not** send every keystroke. After the user stops typing for `debounce_ms` milliseconds, the full text is written to `editor.value` and `on_change` fires (only if you assigned a handler).
411
+ - `debounce_ms=0` sends immediately; negative values are treated as `0`.
412
+ - If nothing really changed (only the cursor moved, or the text equals what was already sent), nothing is sent.
413
+ - `editor.value = "..."` followed by `editor.update()` replaces the content. Your own echo is ignored, so there is no feedback loop.
414
+ - Because of the debounce, `editor.value` may be slightly stale right after typing. Use `on_change` for the latest value, or lower `debounce_ms`.
415
+
416
+ ### Colors
417
+
418
+ Colors for `default_color`, `background_color`, `error_color` and everything in the DSL are **hex strings**:
419
+
420
+ | Format | Example | Meaning |
421
+ |--------|---------|---------|
422
+ | `#RRGGBB` | `#FF8800` | Opaque (alpha `FF` added automatically) |
423
+ | `#AARRGGBB` | `#80FF8800` | Explicit alpha |
424
+
425
+ - Plain strings, **not** `ft.Colors.*` values or color names.
426
+ - Invalid or empty values are treated as "not set" instead of raising an error.
427
+ - Set `default_color` explicitly when you set a dark `background_color`, otherwise un-styled text may be unreadable.
428
+
429
+ ### Changing things at runtime
430
+
431
+ ```python
432
+ editor.language = OTHER_LANG.to_json() # EditorTokens
433
+ editor.rules = NEW_RULES # EditorRegex
434
+ editor.show_errors = False
435
+ editor.code_folding = False
436
+ editor.syntax_highlighting = False
437
+ editor.read_only = True
438
+ editor.wrap = True
439
+ editor.font_size = 16
440
+ editor.background_color = "#0D1117"
441
+ editor.update()
442
+ ```
443
+
444
+ Changing the language/rules, `syntax_highlighting`, `show_errors` or `code_folding` rebuilds the highlighter; the internal editor widget is re-created so every line repaints. The text is kept, but the scroll position may reset. No rebuild of the Flutter client is needed for any of these changes.
445
+
446
+ ---
447
+
448
+ ## Mobile / Touch Features
449
+
450
+ New in 0.3.2. They apply to both controls. Properties that take `None` mean **automatic**: enabled on Android/iOS, disabled elsewhere.
451
+
452
+ | Property | Type | Default | Description |
453
+ |----------|------|---------|-------------|
454
+ | `selection_menu` | `bool` | `True` | Show the cut/copy/paste menu at the caret and selection handles (Android/iOS only). |
455
+ | `selection_menu_items` | `list[str]` | `["cut","copy","paste","select_all"]` | Buttons of that menu, in order (see `ce.DEFAULT_SELECTION_MENU_ITEMS`). Also accepts `"undo"`, `"redo"`. Unknown ids are ignored. |
456
+ | `mobile_controls` | `bool \| None` | `None` (auto) | Show the extra toolbar. Auto = shown on Android/iOS, hidden elsewhere; `True` / `False` force it. |
457
+ | `toolbar_items` | `list[str]` | all items | Toolbar buttons, see below. |
458
+ | `toolbar_position` | `ToolbarPosition` | `BOTTOM` | `BOTTOM` or `TOP`. |
459
+ | `cursor_pad` | `bool \| None` | `None` (auto) | Draggable pad inside the toolbar. Only visible when the toolbar is shown. |
460
+ | `magnifier` | `bool \| None` | `None` (auto) | Magnifier while a finger is held. |
461
+ | `magnifier_size` | `float` | `110.0` | Magnifier diameter. |
462
+ | `magnifier_zoom` | `float` | `1.6` | Magnification factor. |
463
+ | `scroll_mode` | `ScrollMode` | `PAGE_FIRST` | Page/editor scroll hand-off. |
464
+
465
+ ### Selection menu (cut / copy / paste at the caret)
466
+
467
+ On Android and iOS, `re_editor` shows no menu unless it is given a toolbar controller. The library provides one built on Flutter's `AdaptiveTextSelectionToolbar`, so the menu looks native to the platform and appears when you tap the caret, long-press, or select text.
468
+
469
+ ```python
470
+ ce.EditorTokens(
471
+ language=LANG.to_json(),
472
+ selection_menu=True,
473
+ selection_menu_items=["cut", "copy", "paste", "select_all", "undo", "redo"],
474
+ )
475
+ ```
476
+
477
+ - *Cut* and *Copy* appear only when something is selected. *Cut*, *Paste*, *Undo* and *Redo* are hidden in `read_only` mode.
478
+ - The menu is shown on **Android and iOS only**. On desktop, the usual keyboard shortcuts apply.
479
+ - Set `selection_menu=False` to disable it. The menu is rebuilt every time it opens, so changing `selection_menu_items` or `read_only` at runtime takes effect immediately.
480
+ - It works together with the optional toolbar: both use the same cut/copy/paste implementation (see [Paste behavior](#paste-behavior)).
481
+
482
+ ### Extra toolbar and cursor pad
483
+
484
+ An extra bar next to the editor (arrows, undo/redo, home/end, tab, cursor pad). With the default `mobile_controls=None` it is **shown automatically on Android/iOS** and hidden on desktop; pass `True` or `False` to force it:
485
+
486
+ ```python
487
+ ce.EditorTokens(
488
+ language=LANG.to_json(),
489
+ mobile_controls=True,
490
+ toolbar_position=ce.ToolbarPosition.BOTTOM,
491
+ toolbar_items=["undo", "redo", "cut", "copy", "paste", "select_all", "left", "right"],
492
+ cursor_pad=True,
493
+ )
494
+ ```
495
+
496
+ Available `toolbar_items` (default is all of them, in this order; see `ce.DEFAULT_TOOLBAR_ITEMS`):
497
+
498
+ | Item | Action |
499
+ |------|--------|
500
+ | `undo`, `redo` | Undo / redo |
501
+ | `cut`, `copy`, `paste`, `select_all` | Clipboard and selection |
502
+ | `select_mode` | Toggle: arrows, pad and line start/end **extend the selection** instead of moving the caret |
503
+ | `left`, `right`, `up`, `down` | Move the caret |
504
+ | `home`, `end` | Start / end of the line |
505
+ | `tab` | Insert two spaces |
506
+
507
+ **Cursor pad.** A small pad at the left of the toolbar: drag horizontally to move the caret one character per ~9 px, drag vertically to move one line per ~22 px. With `select_mode` on, dragging extends the selection.
508
+
509
+ ### Magnifier
510
+
511
+ While a finger is held on the text (after ~0.4 s) a round magnifier appears above it and follows the finger until it is released.
512
+
513
+ - **Touch only** — it does not appear with a mouse.
514
+ - It magnifies the area under the finger; it is a visual aid and does not move the caret by itself.
515
+ - It is shown for a long press **inside the text area**. It is not shown while dragging the selection handles, because those live in a separate layer owned by `re_editor`.
516
+ - Disable with `magnifier=False`; tune with `magnifier_size` and `magnifier_zoom`.
517
+
518
+ ### Scroll hand-off (`scroll_mode`)
519
+
520
+ When the editor sits inside a scrollable page, a drag on the editor normally scrolls only the editor and the page stays still. `scroll_mode` decides who moves:
521
+
522
+ ```python
523
+ class ScrollMode(str, Enum):
524
+ PAGE_FIRST = "page_first" # the page scrolls first; the editor moves once the page reached its limit
525
+ EDITOR_FIRST = "editor_first" # the editor scrolls first; the rest of the drag goes to the page
526
+ EDITOR_ONLY = "editor_only" # no hand-off
527
+ ```
528
+
529
+ - `PAGE_FIRST` (default): a vertical drag moves the nearest vertical scrollable above the editor until it reaches its end in that direction; only then does the editor scroll. Reversing the drag moves the page first again.
530
+ - `EDITOR_FIRST`: once the editor hits its top/bottom, the remaining drag distance is passed to the page.
531
+ - If the editor is not inside a vertical scrollable, all modes behave like `EDITOR_ONLY`.
532
+ - Only direct drags are handed off; fling (inertia) is not forwarded. In `PAGE_FIRST` mode the editor uses clamping scroll physics (no iOS bounce).
533
+
534
+ ### Paste behavior
535
+
536
+ Cut, copy and paste from the selection menu and the toolbar use the library's own implementation: the clipboard is read directly, `\r\n` / `\r` / Unicode line separators become `\n`, and NUL characters are removed before the text is inserted. Empty clipboards insert nothing. Keyboard shortcuts on desktop still use `re_editor`'s built-in handling.
537
+
538
+ ---
539
+
540
+ ## The `Language` DSL (for `EditorTokens`)
541
+
542
+ All classes are plain Python `dataclasses` importable from the top-level package:
543
+
544
+ ```python
545
+ from flet_code_editor_dsl import (
546
+ Language, WordGroup, StringRule, BlockComment, Sigil,
547
+ TokenColors, FoldRule, ErrorRules, BracketPair,
548
+ )
549
+ ```
550
+
551
+ ### `Language`
552
+
553
+ | Field | Type | Default | Description |
554
+ |-------|------|---------|-------------|
555
+ | `words` | `list[WordGroup]` | `[]` | Exact words grouped by color (keywords, constants, builtins...). |
556
+ | `line_comments` | `list[str]` | `[]` | Markers that start a comment running to the end of the line (`"//"`, `"#"`, `"--"`). |
557
+ | `block_comments` | `list[BlockComment]` | `[]` | Multi-line comments (`/* ... */`). |
558
+ | `strings` | `list[StringRule]` | `[]` | String literal styles. |
559
+ | `sigils` | `list[Sigil]` | `[]` | Prefix + name tokens (`$var`, `@user`). |
560
+ | `numbers` | `bool` | `True` | Recognize numeric literals. |
561
+ | `operators` | `str` | `"+-*/%=<>!&\|^~?:"` | Each character in this string is an operator character. |
562
+ | `punctuation` | `str` | `"(){}[],;."` | Each character is a punctuation character. |
563
+ | `identifier_extra` | `str` | `""` | Extra characters allowed **inside** names (for example `"-"`). |
564
+ | `case_insensitive` | `bool` | `False` | Match words (and error word lists) ignoring case. |
565
+ | `colors` | `TokenColors` | `TokenColors()` | Colors for the built-in token kinds. |
566
+ | `folds` | `list[FoldRule]` | `[]` | User-defined folding regions. |
567
+ | `fold_by_indent` | `bool` | `False` | Also fold by indentation. |
568
+ | `errors` | `ErrorRules \| None` | `None` | Error underlines. `None` disables all error checking. |
569
+
570
+ ### `WordGroup`
571
+
572
+ ```python
573
+ WordGroup(words: list[str], color: str)
574
+ ```
575
+
576
+ A set of **whole words** sharing one color. Matching is exact against the full identifier, so `end` does **not** match `endpoint`, and `if` does not match inside `iffy`. A word is only matched if it consists of identifier characters (letters, digits, `_`, non-ASCII characters and anything in `identifier_extra`); a "word" such as `"->"` never matches — use `operators` for symbols.
577
+
578
+ ```python
579
+ words=[
580
+ ce.WordGroup(["if", "else", "while", "for", "return"], "#C586C0"),
581
+ ce.WordGroup(["True", "False", "None"], "#569CD6"),
582
+ ce.WordGroup(["print", "len", "range"], "#4EC9B0"),
583
+ ]
584
+ ```
585
+
586
+ If the same word appears in several groups, the **last** group wins. Entries with empty text or an invalid color are ignored.
587
+
588
+ ### `StringRule`
589
+
590
+ ```python
591
+ StringRule(open: str, close: str | None = None, escape: str | None = "\\", multiline: bool = False)
592
+ ```
593
+
594
+ | Field | Description |
595
+ |-------|-------------|
596
+ | `open` | Opening delimiter (any length). |
597
+ | `close` | Closing delimiter. Defaults to `open`. |
598
+ | `escape` | Escape character (default backslash) or `None`. The escape character and the **one character after it** form an escape sequence, colored with `TokenColors.escape` (falls back to the string color). A closing delimiter preceded by the escape does not end the string. |
599
+ | `multiline` | If `True`, an unclosed string continues on the following lines. If `False`, it ends at the end of the line (and is reported as an error if `ErrorRules.unterminated_strings` is on). |
600
+
601
+ Rules are tried **longest `open` first**, so `'"""'` wins over `'"'` regardless of list order.
602
+
603
+ ```python
604
+ strings=[
605
+ ce.StringRule('"'),
606
+ ce.StringRule("'"),
607
+ ce.StringRule('"""', multiline=True), # Python docstrings
608
+ ce.StringRule("`", multiline=True, escape=None), # JS template / SQL identifiers
609
+ ce.StringRule("[[", "]]", escape=None, multiline=True), # Lua long strings
610
+ ce.StringRule("'", escape="'"), # SQL-style '' escaping
611
+ ]
612
+ ```
613
+
614
+ The delimiters themselves are part of the colored string token.
615
+
616
+ ### `BlockComment` and `line_comments`
617
+
618
+ ```python
619
+ BlockComment(start: str, end: str)
620
+ ```
621
+
622
+ ```python
623
+ line_comments=["//", "#"],
624
+ block_comments=[ce.BlockComment("/*", "*/"), ce.BlockComment("<!--", "-->")]
625
+ ```
626
+
627
+ - Line comments run to the end of the line. Longer markers are tried first.
628
+ - Block comments carry their state across lines: a line that starts inside a block comment is colored correctly even if the opening marker is far above.
629
+ - Block comments are **not nested**: the first end marker closes the comment. The end marker is searched only after the start marker, so `/*/` does not close itself.
630
+ - Comments are recognized before strings, so a comment marker inside a string is **not** a comment, and a quote inside a comment is **not** a string (the lexer reads left to right).
631
+
632
+ ### `Sigil`
633
+
634
+ ```python
635
+ Sigil(prefix: str, color: str)
636
+ ```
637
+
638
+ A prefix followed by a name, e.g. `Sigil("$", "#9CDCFE")` colors `$name`, `$_x1`. The whole token (prefix + name) gets the color. A prefix with **no** name after it (a lone `$`) is not a sigil and falls through to the other rules.
639
+
640
+ ```python
641
+ sigils=[ce.Sigil("$", "#9CDCFE"), ce.Sigil("@", "#4FC1FF"), ce.Sigil("%", "#D7BA7D")]
642
+ ```
643
+
644
+ ### Numbers, operators, punctuation, identifiers
645
+
646
+ **Numbers** (`numbers=True`): integers and decimals (`42`, `3.14`, `.5`), digit separators (`1_000_000`), exponents (`1e10`, `2.5E-3`), prefixed literals (`0x1F` hex, `0b1010` binary, `0o755` octal; each prefix accepts only its own digits and `_`). A number ends where it ends: `10px` is the number `10` followed by the identifier `px`. Identifiers cannot start with a digit. Set `numbers=False` if digits should not be treated specially.
647
+
648
+ **Operators** — each character in `Language.operators` is an operator character; a *run* of operator characters (`==`, `<=`, `&&`) becomes **one** token with `TokenColors.operator`.
649
+
650
+ **Punctuation** — each character in `Language.punctuation` becomes its own token with `TokenColors.punctuation`. Characters in neither set are drawn with the default color.
651
+
652
+ > Because comments and strings are recognized first, `//` is a comment, not two operators, as long as you listed `//` in `line_comments`.
653
+
654
+ **Identifiers** — a name starts with an ASCII letter, `_`, or **any non-ASCII character** (Arabic, Chinese, accented letters and emoji count as name characters), and continues with letters, digits, `_`, non-ASCII characters and `identifier_extra`.
655
+
656
+ - An identifier found in `words` gets that group's color.
657
+ - Otherwise, if it is directly followed by `(` (spaces and tabs allowed in between) **and** `TokenColors.function` is set, it is a **function call**.
658
+ - Otherwise it gets `TokenColors.identifier`.
659
+
660
+ ### `TokenColors`
661
+
662
+ ```python
663
+ TokenColors(
664
+ comment=None, string=None, escape=None, number=None,
665
+ function=None, identifier=None, operator=None, punctuation=None,
666
+ )
667
+ ```
668
+
669
+ | Field | Applies to |
670
+ |-------|------------|
671
+ | `comment` | Line and block comments |
672
+ | `string` | String literals (including delimiters) |
673
+ | `escape` | Escape sequences inside strings; falls back to `string` |
674
+ | `number` | Numeric literals |
675
+ | `function` | Identifiers followed by `(` that are not in `words` |
676
+ | `identifier` | All other names |
677
+ | `operator` | Operator runs |
678
+ | `punctuation` | Punctuation characters and the bracket characters used in `ErrorRules.brackets` |
679
+
680
+ `None` = default text color (`default_color`). If you leave `function` as `None`, function calls look like ordinary identifiers.
681
+
682
+ ### `ErrorRules` (error underlines)
683
+
684
+ ```python
685
+ ErrorRules(
686
+ unterminated_strings=True,
687
+ brackets=[], # list[BracketPair]
688
+ unknown_words=False,
689
+ allowed_words=[],
690
+ forbidden_words=[],
691
+ color="#F44747",
692
+ wavy=True,
693
+ )
694
+ ```
695
+
696
+ Enable by passing an instance: `Language(errors=ce.ErrorRules(...))`. The default `errors=None` turns every check off. The underlines can be hidden at runtime with `show_errors=False`. **All checks work inside a single line.**
697
+
698
+ | Check | Behavior |
699
+ |-------|----------|
700
+ | `unterminated_strings` | A non-multiline string not closed before the end of the line is underlined from its opening delimiter. Multi-line strings are never flagged. |
701
+ | `brackets` | Each `BracketPair(open, close)` must be **single characters**. An unmatched closer, or an opener never closed on the same line, is underlined. Brackets inside strings and comments are not counted. |
702
+ | `unknown_words` | Every identifier **not** in `words` and not in `allowed_words` is underlined. Function-call names count as identifiers too. |
703
+ | `forbidden_words` | Words that are **always** underlined, even if they are in `words`. |
704
+
705
+ ```python
706
+ errors=ce.ErrorRules(
707
+ unterminated_strings=True,
708
+ brackets=[ce.BracketPair("(", ")"), ce.BracketPair("[", "]"), ce.BracketPair("{", "}")],
709
+ unknown_words=True,
710
+ allowed_words=["x", "y", "total", "price", "print"],
711
+ forbidden_words=["goto"],
712
+ color="#F44747",
713
+ wavy=True,
714
+ )
715
+ ```
716
+
717
+ > The lexer builds a message for each error (*Unterminated string*, *Unknown word: foo*, *Not allowed: goto*, *Unmatched ')'*, *Unclosed '('*), but the editor only draws the underline; messages are not shown as tooltips yet. Brackets spanning several lines are **not** matched across lines; for a language where `{` and `}` regularly sit on different lines, leave the `{}` pair out.
718
+
719
+ ### `FoldRule` and `fold_by_indent` (code folding)
720
+
721
+ ```python
722
+ FoldRule(start: str, end: str | None = None, length: int | None = None)
723
+ ```
724
+
725
+ Folding adds fold markers in the gutter. It is active when `code_folding=True` **and** at least one `FoldRule` is defined or `fold_by_indent=True`. `FoldRule` and `fold_by_indent` are used the same way by `EditorRegex` (as the `folds=` / `fold_by_indent=` properties of the control).
726
+
727
+ 1. **Start/end markers** — a line starting with `start` opens a region and a line starting with `end` closes it. Nesting is supported (each rule keeps its own stack).
728
+
729
+ ```python
730
+ folds=[ce.FoldRule("class", "end"), ce.FoldRule("def", "end"), ce.FoldRule("if", "end")]
731
+ ```
732
+
733
+ 2. **Fixed length** — fold the next `length` lines after a line starting with `start` (clamped to the last line). If both `end` and `length` are given, `end` is used.
734
+
735
+ ```python
736
+ folds=[ce.FoldRule("on", length=5)]
737
+ ```
738
+
739
+ Matching rules:
740
+
741
+ - Markers match at the **beginning of a line, after indentation**.
742
+ - If the marker ends with a letter, digit or `_`, it matches **whole words only**: `end` does not match `endpoint`.
743
+ - If the marker ends with a symbol (`{`, `#region`...), it matches as a plain prefix.
744
+ - Matching is case-sensitive and ignores `case_insensitive`.
745
+ - A region is foldable only if it has at least one line inside it. An opener without a closer folds nothing; a bare `FoldRule("{")` with neither `end` nor `length` does nothing.
746
+ - Folding is purely line-based and **not aware of strings or comments**.
747
+ - If `start == end`, the same marker toggles open/closed.
748
+
749
+ **`fold_by_indent=True`** additionally creates a region for every line followed by more-indented lines (Python/YAML style). A tab counts as 4 spaces, blank lines are skipped, and a region ends at the last non-empty line still indented deeper. It can be combined with `folds`.
750
+
751
+ ### `case_insensitive`
752
+
753
+ When `True`, words in `WordGroup`, `allowed_words` and `forbidden_words` are compared in lower case, and the identifier found in the text is lower-cased before lookup (ideal for SQL, BASIC, Pascal). It does **not** affect comment markers, string delimiters, sigil prefixes or `FoldRule` markers.
754
+
755
+ ```python
756
+ sql = ce.Language(
757
+ case_insensitive=True,
758
+ words=[ce.WordGroup(["select", "from", "where", "and", "or", "insert", "into"], "#C586C0")],
759
+ )
760
+ ```
761
+
762
+ ### `Language.to_json()`
763
+
764
+ ```python
765
+ def to_json(self) -> str
766
+ ```
767
+
768
+ Serializes the whole description into a compact JSON string. Pass it to `EditorTokens(language=...)`. The editor compares the string, so **re-assigning an identical JSON string does not rebuild anything**; build it once at module level and reuse it:
769
+
770
+ ```python
771
+ PY_LANG = ce.Language(...).to_json()
772
+ a = ce.EditorTokens(language=PY_LANG)
773
+ b = ce.EditorTokens(language=PY_LANG, read_only=True)
774
+ ```
775
+
776
+ ---
777
+
778
+ ## How the Lexer Works
779
+
780
+ For every line the lexer scans left to right and at each position tries the following, **stopping at the first match**:
781
+
782
+ | # | Step | Notes |
783
+ |---|------|-------|
784
+ | 0 | Continue a multi-line construct | If the line starts inside a block comment or multi-line string, finish it first. |
785
+ | 1 | Whitespace | Spaces, tabs, `\r` and non-breaking space. Never colored. |
786
+ | 2 | Line comments | Longest marker first. Consumes the rest of the line. |
787
+ | 3 | Block comments | Longest start marker first. May stay open to the next line. |
788
+ | 4 | Strings | Longest `open` first. May stay open for `multiline=True`. |
789
+ | 5 | Sigils | Prefix + at least one name character. |
790
+ | 6 | Numbers | Only if `numbers=True`. |
791
+ | 7 | Identifiers / words / calls | Also applies `forbidden_words` and `unknown_words`. |
792
+ | 8 | Bracket check | Only for characters listed in `ErrorRules.brackets`. |
793
+ | 9 | Operators | A run of operator characters is one token. |
794
+ | 10 | Punctuation | One character per token. |
795
+ | 11 | Anything else | Default color. |
796
+
797
+ Practical consequences:
798
+
799
+ - **Order inside lists does not matter** for strings, comments and sigils (the longest marker wins). It matters only for duplicate words (last group wins).
800
+ - Comments beat strings and strings beat everything else, because these are tested first while scanning left to right.
801
+ - There is **no rule-priority system** like in regex highlighters; the structure of the language decides the color.
802
+ - Bracket characters listed in `ErrorRules.brackets` always get the punctuation color.
803
+ - Adjacent tokens with the same color are merged into one span.
804
+
805
+ **Multi-line state.** The lexer keeps a small integer state at the start of each line: `0` = normal, `1..N` = inside block comment *N*, then one state per multi-line string rule. A line's start state is derived from the lines above it and cached, so editing a line only re-lexes the lines below it **when they become visible**.
806
+
807
+ ---
808
+
809
+ ## `EditorRegex` Reference (regex engine)
810
+
811
+ ```python
812
+ @dataclass
813
+ class HighlightRule:
814
+ pattern: str # Dart RegExp pattern
815
+ color: str # "#RRGGBB" or "#AARRGGBB"
816
+
817
+ @dataclass
818
+ class ErrorPattern:
819
+ pattern: str # Dart RegExp pattern; every match is underlined
820
+ message: str = ""
821
+ ```
822
+
823
+ Properties: see [Engine properties](#engine-properties) and [Common properties](#common-properties).
824
+
825
+ How it works:
826
+
827
+ - Each line is matched separately against every rule; the result is cached by line text (the cache is cleared when it exceeds 5000 entries).
828
+ - **The first matching rule wins** for each character: a character already colored by an earlier rule is not recolored by later ones. Put your most specific rules (comments, strings) **first** and general rules (identifiers) **last**. (This is the opposite of the original `CodeEditor` engine, where later rules overrode earlier ones.)
829
+ - Zero-length matches are ignored.
830
+ - `error_rules` matches are underlined with `error_color` (wavy by default) on top of the token color. They can be hidden with `show_errors=False`.
831
+ - Patterns use the **Dart `RegExp`** engine. Use `(?<name>...)` instead of Python's `(?P<name>...)`, and raw strings (`r"..."`) in Python. Patterns or colors that fail to parse are silently skipped.
832
+ - Rules are re-compiled only when the lists change; ordinary typing never recompiles regexes.
833
+ - Because every line is matched alone, patterns cannot span lines: multi-line comments and triple-quoted strings **cannot** be highlighted. Use `EditorTokens` for those.
834
+
835
+ ```python
836
+ import flet as ft
837
+ import flet_code_editor_dsl as ce
838
+
839
+ RULES = [
840
+ ce.HighlightRule(r"//.*$", "#6A9955"), # comments first
841
+ ce.HighlightRule(r'"(?:[^"\\]|\\.)*"?', "#CE9178"), # strings
842
+ ce.HighlightRule(r"\b(?:func|let|if|else|return)\b", "#C586C0"),
843
+ ce.HighlightRule(r"\b\d+(?:\.\d+)?\b", "#B5CEA8"),
844
+ ce.HighlightRule(r"\b[A-Za-z_]\w*(?=\()", "#DCDCAA"), # function calls
845
+ ]
846
+ ERRORS = [
847
+ ce.ErrorPattern(r'"[^"\\]*(?:\\.[^"\\]*)*$', "Unterminated string"),
848
+ ]
849
+
850
+ def main(page: ft.Page):
851
+ page.add(
852
+ ce.EditorRegex(
853
+ value='func main() {\n print("hi\n}',
854
+ rules=RULES,
855
+ error_rules=ERRORS,
856
+ folds=[ce.FoldRule("func", length=20)],
857
+ fold_by_indent=True,
858
+ vertical_mode=ce.VerticalMode.BOUNDED,
859
+ min_lines=3,
860
+ max_lines=10,
861
+ width=500,
862
+ )
863
+ )
864
+
865
+ ft.run(main)
866
+ ```
867
+
868
+ ---
869
+
870
+ ## Performance Notes
871
+
872
+ - Only the lines that are actually painted are highlighted (the editor is virtualized).
873
+ - **Tokenizer:** each line's result is cached by `(index, text, start state)`. For languages **without** multi-line constructs an edit invalidates only that line; with multi-line constructs it invalidates the chain from the edited line downward, lazily. The cache is cleared beyond 20 000 cached lines.
874
+ - **Regex:** each line's result is cached by its text; cleared beyond 5000 entries.
875
+ - Changing the language/rules, `syntax_highlighting`, `show_errors` or `code_folding` forces a repaint of all visible lines; changing colors/fonts of the control itself does not re-highlight.
876
+ - Folding analysis runs on the entire document when the text changes; it is cheap line-prefix checking but O(lines × fold rules).
877
+
878
+ ---
879
+
880
+ ## Complete Examples
881
+
882
+ ### 1. A tiny scripting language with errors and folding
883
+
884
+ ```python
885
+ import flet as ft
886
+ import flet_code_editor_dsl as ce
887
+
888
+ SAMPLE = '''\
889
+ # Example program
890
+ let total = (price * 3) + 12
891
+ if (total > 100) {
892
+ print("expensive")
893
+ } else {
894
+ print("cheap")
895
+ }
896
+ /* multi-line
897
+ comment */
898
+ let broken = (1 + 2
899
+ let s = "unterminated
900
+ '''
901
+
902
+ LANG = ce.Language(
903
+ words=[
904
+ ce.WordGroup(["let", "if", "else"], "#C586C0"),
905
+ ce.WordGroup(["true", "false"], "#569CD6"),
906
+ ],
907
+ line_comments=["#"],
908
+ block_comments=[ce.BlockComment("/*", "*/")],
909
+ strings=[ce.StringRule('"')],
910
+ colors=ce.TokenColors(
911
+ comment="#6A9955", string="#CE9178", escape="#D7BA7D",
912
+ number="#B5CEA8", function="#DCDCAA", identifier="#9CDCFE",
913
+ operator="#D4D4D4", punctuation="#FFD700",
914
+ ),
915
+ folds=[ce.FoldRule("if", "}")],
916
+ errors=ce.ErrorRules(
917
+ unterminated_strings=True,
918
+ brackets=[ce.BracketPair("(", ")")],
919
+ unknown_words=True,
920
+ allowed_words=["total", "price", "print", "broken", "s"],
921
+ ),
922
+ )
923
+
924
+
925
+ def main(page: ft.Page):
926
+ page.title = "flet-code-editor-dsl demo"
927
+ page.theme_mode = ft.ThemeMode.DARK
928
+
929
+ status = ft.Text(f"Characters: {len(SAMPLE)}")
930
+
931
+ def on_change(e: ft.ControlEvent):
932
+ status.value = f"Characters: {len(e.data or '')}"
933
+ status.update()
934
+
935
+ def toggle_errors(e: ft.ControlEvent):
936
+ editor.show_errors = e.control.value
937
+ editor.update()
938
+
939
+ editor = ce.EditorTokens(
940
+ value=SAMPLE,
941
+ language=LANG.to_json(),
942
+ default_color="#D4D4D4",
943
+ background_color="#1E1E1E",
944
+ font_family="Consolas",
945
+ font_size=15,
946
+ expand=True,
947
+ on_change=on_change,
948
+ debounce_ms=200,
949
+ )
950
+
951
+ page.add(
952
+ ft.Row([ft.Switch(label="Show errors", value=True, on_change=toggle_errors), status]),
953
+ editor,
954
+ )
955
+
956
+
957
+ ft.run(main)
958
+ ```
959
+
960
+ ### 2. Python-like language (docstrings, `fold_by_indent`)
961
+
962
+ ```python
963
+ PYTHON_LIKE = ce.Language(
964
+ words=[
965
+ ce.WordGroup(["def", "class", "if", "elif", "else", "for", "while", "return",
966
+ "import", "from", "as", "in", "not", "and", "or", "with", "try",
967
+ "except", "finally", "raise", "lambda"], "#C586C0"),
968
+ ce.WordGroup(["True", "False", "None", "self"], "#569CD6"),
969
+ ],
970
+ line_comments=["#"],
971
+ strings=[
972
+ ce.StringRule('"""', multiline=True),
973
+ ce.StringRule("'''", multiline=True),
974
+ ce.StringRule('"'),
975
+ ce.StringRule("'"),
976
+ ],
977
+ colors=ce.TokenColors(comment="#6A9955", string="#CE9178", escape="#D7BA7D",
978
+ number="#B5CEA8", function="#DCDCAA", identifier="#9CDCFE"),
979
+ fold_by_indent=True,
980
+ )
981
+ ```
982
+
983
+ ### 3. SQL (case-insensitive, `--` and `/* */` comments, `''` escaping)
984
+
985
+ ```python
986
+ SQL = ce.Language(
987
+ case_insensitive=True,
988
+ words=[
989
+ ce.WordGroup(["select", "from", "where", "join", "left", "right", "inner", "on",
990
+ "group", "by", "order", "having", "insert", "into", "values",
991
+ "update", "set", "delete", "create", "table", "and", "or", "not",
992
+ "null", "as", "limit"], "#C586C0"),
993
+ ce.WordGroup(["count", "sum", "avg", "min", "max"], "#DCDCAA"),
994
+ ],
995
+ line_comments=["--"],
996
+ block_comments=[ce.BlockComment("/*", "*/")],
997
+ strings=[ce.StringRule("'", escape="'"), ce.StringRule('"', escape=None)],
998
+ colors=ce.TokenColors(comment="#6A9955", string="#CE9178", number="#B5CEA8",
999
+ identifier="#9CDCFE"),
1000
+ )
1001
+ ```
1002
+
1003
+ ### 4. Config / templating language with sigils and dashed names
1004
+
1005
+ ```python
1006
+ TEMPLATE = ce.Language(
1007
+ identifier_extra="-", # allows names like font-size
1008
+ sigils=[ce.Sigil("$", "#4FC1FF"), ce.Sigil("@", "#D7BA7D")],
1009
+ line_comments=["//"],
1010
+ strings=[ce.StringRule('"'), ce.StringRule("'")],
1011
+ words=[ce.WordGroup(["include", "define", "if", "endif"], "#C586C0")],
1012
+ colors=ce.TokenColors(comment="#6A9955", string="#CE9178", number="#B5CEA8",
1013
+ identifier="#9CDCFE"),
1014
+ folds=[ce.FoldRule("if", "endif"), ce.FoldRule("define", length=3)],
1015
+ )
1016
+ ```
1017
+
1018
+ ### 5. Read-only viewer that grows with its content
1019
+
1020
+ ```python
1021
+ log_view = ce.EditorTokens(
1022
+ value=log_text,
1023
+ language=LOG_LANG.to_json(),
1024
+ read_only=True,
1025
+ line_numbers=False,
1026
+ vertical_mode=ce.VerticalMode.BOUNDED,
1027
+ min_lines=1,
1028
+ max_lines=20,
1029
+ width=700,
1030
+ )
1031
+ ```
1032
+
1033
+ ### 6. Phone-friendly editor inside a scrolling page
1034
+
1035
+ ```python
1036
+ page.add(
1037
+ ft.ListView(
1038
+ expand=True,
1039
+ controls=[
1040
+ ft.Text("Notes"),
1041
+ ce.EditorTokens(
1042
+ value=text,
1043
+ language=LANG.to_json(),
1044
+ height=360,
1045
+ width_level=30,
1046
+ height_level=None,
1047
+ # touch helpers
1048
+ selection_menu=True,
1049
+ selection_menu_items=["cut", "copy", "paste", "select_all"],
1050
+ magnifier=True,
1051
+ mobile_controls=True, # optional toolbar + cursor pad
1052
+ cursor_pad=True,
1053
+ scroll_mode=ce.ScrollMode.PAGE_FIRST,
1054
+ ),
1055
+ ft.Text("More page content below..."),
1056
+ ],
1057
+ )
1058
+ )
1059
+ ```
1060
+
1061
+ ### 7. Switching languages from a dropdown
1062
+
1063
+ ```python
1064
+ LANGUAGES = {"Python": PYTHON_LIKE.to_json(), "SQL": SQL.to_json(), "Plain": ""}
1065
+
1066
+ def on_pick(e):
1067
+ editor.language = LANGUAGES[e.control.value]
1068
+ editor.update()
1069
+
1070
+ page.add(
1071
+ ft.Dropdown(options=[ft.dropdown.Option(k) for k in LANGUAGES],
1072
+ value="Python", on_select=on_pick), # on_select / on_change depends on your Flet version
1073
+ editor,
1074
+ )
1075
+ ```
1076
+
1077
+ ---
1078
+
1079
+ ## Important: First Build Required
1080
+
1081
+ `flet-code-editor-dsl` is not a pure-Python package. Its widgets are written in Dart/Flutter and must be **compiled into the Flet client**. The stock Flet client does not include them, so running an app against the stock client fails with `Unknown control: EditorTokens` / `Unknown control: EditorRegex`.
1082
+
1083
+ 1. **Declare the library in your app's `pyproject.toml`** under `[project] dependencies`.
1084
+ 2. **Build your app** with `flet build <platform>`.
1085
+ 3. **Run the built app** (or the build output on a device/emulator).
1086
+
1087
+ > If the library is only installed with `pip` and not declared in `pyproject.toml`, Flet will not include the extension and the controls will not appear.
1088
+
1089
+ | Change | Rebuild needed? |
1090
+ |--------|-----------------|
1091
+ | Editing Python code, `value`, `language` / `Language(...)`, `rules`, colors, fonts, flags, sizes, layout, mobile options | No — sent to the client at runtime. |
1092
+ | Updating `flet-code-editor-dsl` to a version whose Dart code changed (for example when upgrading to 0.3.2) | **Yes** |
1093
+ | Upgrading Flet itself to a new major/minor version | Yes |
1094
+ | Adding or removing other Flet extensions | Yes |
1095
+
1096
+ ## Building Your App
1097
+
1098
+ ```bash
1099
+ # Desktop
1100
+ flet build windows
1101
+ flet build macos
1102
+ flet build linux
1103
+
1104
+ # Mobile
1105
+ flet build apk
1106
+ flet build ipa
1107
+
1108
+ # Web
1109
+ flet build web
1110
+ ```
1111
+
1112
+ `flet build` detects the Flutter package bundled in `flutter/flet_code_editor_dsl/` and adds it to the generated Flutter project. A Flutter SDK is required; Flet downloads one if missing. The extension registers two control types — `EditorTokens` and `EditorRegex` — in `extension.dart`.
1113
+
1114
+ > The touch features (selection menu, magnifier, scroll hand-off) only matter on a touch device, so test them with an Android/iOS build rather than a desktop build.
1115
+
1116
+ ---
1117
+
1118
+ ## Developing and Testing Locally
1119
+
1120
+ To test the library **before publishing it**:
1121
+
1122
+ 1. Build a wheel from the repository root (the version number can stay as is):
1123
+
1124
+ ```bash
1125
+ pip wheel . --no-deps -w dist
1126
+ ```
1127
+
1128
+ Check that it contains both the Python code and the Flutter files (`flutter/...` entries):
1129
+
1130
+ ```bash
1131
+ python -c "import zipfile,glob; z=zipfile.ZipFile(glob.glob('dist/*.whl')[0]); n=z.namelist(); print(b'EditorRegex' in z.read('flet_code_editor_dsl/__init__.py'), len([x for x in n if x.startswith('flutter/')]))"
1132
+ ```
1133
+
1134
+ 2. In your test app's `pyproject.toml`, reference the wheel file directly:
1135
+
1136
+ ```toml
1137
+ dependencies = [
1138
+ "flet>=0.85.2",
1139
+ "flet-code-editor-dsl @ file:///C:/path/to/repo/dist/flet_code_editor_dsl-0.3.2-py3-none-any.whl",
1140
+ ]
1141
+ ```
1142
+
1143
+ Do **not** rely on `[tool.uv.sources]`: `flet build` passes the dependency names to its packaging step and ignores that table, so it would install the version published on PyPI instead of your local one.
1144
+
1145
+ 3. Delete the old build output and build again:
1146
+
1147
+ ```bash
1148
+ rm -rf build # PowerShell: Remove-Item -Recurse -Force .\build
1149
+ flet build apk -v
1150
+ ```
1151
+
1152
+ 4. Confirm the right version was packaged before running:
1153
+
1154
+ ```bash
1155
+ grep EditorRegex build/site-packages/flet_code_editor_dsl/__init__.py
1156
+ ```
1157
+
1158
+ Repeat step 1 (and delete `build`) after every change to the library.
1159
+
1160
+ **Publishing:** bump `version` in `pyproject.toml` (PyPI never accepts the same version twice), then:
1161
+
1162
+ ```bash
1163
+ pip install -U build twine
1164
+ python -m build
1165
+ twine check dist/*
1166
+ twine upload dist/* # try `--repository testpypi` first
1167
+ ```
1168
+
1169
+ ---
1170
+
1171
+ ## Project Structure
1172
+
1173
+ ```text
1174
+ flet-code-editor-dsl/
1175
+ ├── README.md
1176
+ ├── LICENSE
1177
+ ├── FUNDING.yml
1178
+ ├── .gitignore
1179
+ ├── MANIFEST.in
1180
+ ├── pyproject.toml
1181
+ ├── example/ # test app (see "Developing and Testing Locally")
1182
+ │ ├── pyproject.toml
1183
+ │ └── src/main.py
1184
+ └── src/
1185
+ ├── flet_code_editor_dsl/
1186
+ │ ├── __init__.py # Public exports (+ deprecated aliases)
1187
+ │ ├── flet_code_editor_dsl.py # EditorTokens, EditorRegex, HighlightRule, ErrorPattern, VerticalMode, ScrollMode, ToolbarPosition
1188
+ │ └── language.py # Language DSL dataclasses + to_json()
1189
+ └── flutter/
1190
+ ├── __init__.py
1191
+ └── flet_code_editor_dsl/
1192
+ ├── __init__.py
1193
+ ├── pubspec.yaml # Flutter package (v0.3.2)
1194
+ └── lib/
1195
+ ├── flet_code_editor_dsl.dart # Exports Extension
1196
+ └── src/
1197
+ ├── extension.dart # Maps control types to the shared widget
1198
+ ├── code_editor.dart # Shared editor widget (re_editor): sync, sizing, selection menu, toolbar, magnifier, scroll hand-off
1199
+ ├── regex_highlighter.dart # Regex engine (EditorRegex)
1200
+ └── tokenizer.dart # Tokenizer engine (EditorTokens): CodeLanguage, LineHighlighter, FoldAnalyzer
1201
+ ```
1202
+
1203
+ | File | Responsibility |
1204
+ |------|----------------|
1205
+ | `flet_code_editor_dsl.py` | Declares the two Flet controls and their helper types. |
1206
+ | `language.py` | Declarative language description; produces the JSON consumed by Dart. |
1207
+ | `extension.dart` | Registers `EditorRegex` and `EditorTokens` with Flet. |
1208
+ | `code_editor.dart` | The widget both controls share: value sync and debounce, engine selection, sizing, line numbers, fold indicator, selection menu, toolbar/cursor pad, magnifier, scroll hand-off, clipboard actions. |
1209
+ | `regex_highlighter.dart` | `RegexSpec` / `RegexHighlighter`: per-line regex highlighting and error underlines. |
1210
+ | `tokenizer.dart` | `CodeLanguage.fromJson`, the stateful lexer, the cached `LineHighlighter`, the `FoldAnalyzer`, and the `CodeHighlighter` interface both engines implement. |
1211
+ | `pyproject.toml` | Packaging metadata (setuptools). Includes `flutter.flet_code_editor_dsl` (`**/*`) as package data so `flet build` can find the Flutter code. |
1212
+
1213
+ > Keep generated files out of the repository:
1214
+ >
1215
+ > ```gitignore
1216
+ > __pycache__/
1217
+ > *.pyc
1218
+ > *.egg-info/
1219
+ > build/
1220
+ > dist/
1221
+ > ```
1222
+
1223
+ ---
1224
+
1225
+ ## Known Limitations
1226
+
1227
+ - **Touch features are new in 0.3.2** and have had limited testing on real devices; please report problems with your device, OS version and Flet version.
1228
+ - **The magnifier does not follow the selection handles** (they are owned by `re_editor`); it appears on a long press inside the text and follows the finger, not the caret.
1229
+ - **The selection menu is Android/iOS only.** Desktop keeps `re_editor`'s keyboard shortcuts.
1230
+ - **Pasting with the platform's built-in shortcut/menu inside `re_editor`** still uses its own code path; the library's paste normalization applies to its own menu and toolbar.
1231
+ - **`scroll_mode` forwards drags only**, not fling; `PAGE_FIRST` uses clamping physics inside the editor.
1232
+ - **No nested block comments.** The first end marker closes a block comment.
1233
+ - **Error checks are per line.** Unmatched brackets are not tracked across lines; the unterminated-string check does not apply to multi-line strings.
1234
+ - **Error messages are not shown yet.** Errors are drawn as underlines only.
1235
+ - **Folding is line-based.** It is not aware of strings and comments; markers are recognized only at the start of a line. A `FoldRule` needs `end` or `length` to produce a region.
1236
+ - **`EditorRegex` highlights line by line**: no multi-line constructs, first-rule-wins.
1237
+ - **No autocompletion, auto-indent or search UI** is provided by the DSL.
1238
+ - **Colors are hex strings** (`#RRGGBB` / `#AARRGGBB`), not `ft.Colors.*` values.
1239
+ - **Debounced value.** `editor.value` on the Python side may lag by up to `debounce_ms` behind what is on screen.
1240
+ - **First build required.** As with all Flet extensions, the controls are not available in the stock Flet client.
1241
+
1242
+ ## Troubleshooting
1243
+
1244
+ | Symptom | Likely cause and fix |
1245
+ |---------|----------------------|
1246
+ | `Unknown control: EditorRegex` / `EditorTokens` | The app is not running with the extension compiled in. Declare the library in `pyproject.toml` and run a build made with `flet build`. |
1247
+ | `ImportError: cannot import name 'EditorRegex'` in the built app | The build packaged an older version (an older release from PyPI). See [Developing and Testing Locally](#developing-and-testing-locally): use a wheel file in `dependencies`, delete `build/`, rebuild, and check `build/site-packages/flet_code_editor_dsl/__init__.py`. |
1248
+ | `TypeError` about `incremental_highlighting` | The argument no longer exists; `EditorRegex` always highlights per line. Remove it. |
1249
+ | `FletCodeEditor` / `FletCodeField` not found | They remain as deprecated aliases of `EditorTokens` / `EditorRegex`. If you removed the aliases, use the new names. |
1250
+ | No cut/copy/paste menu on the phone | Make sure `selection_menu=True` (default), `selection_menu_items` is not empty, the app was **rebuilt** with 0.3.2 or newer, and the app runs on Android/iOS. The menu appears when you tap the caret, long-press, or select text. |
1251
+ | Pasted text does not appear | Use *Paste* from the selection menu or the toolbar (they sanitize the clipboard text). Check that the editor is not `read_only`. |
1252
+ | Magnifier does not appear | It is touch-only, needs a ~0.4 s long press inside the text area, and `magnifier` must not be `False`. |
1253
+ | Cursor pad is missing | It lives in the toolbar: the toolbar must be visible (automatic on Android/iOS, or `mobile_controls=True`) and `cursor_pad` must not be `False`. |
1254
+ | The page does not scroll when dragging over the editor | Check `scroll_mode`. `PAGE_FIRST` (default) moves the page first; `EDITOR_ONLY` disables hand-off. The editor must be inside a vertical scrollable (`ListView`, `Column(scroll=...)`). |
1255
+ | The editor is tiny | `width_level` / `height_level` default to 13 (of 30). Set `expand=True`, `width`/`height`, or levels up to 30. |
1256
+ | No colors at all | `EditorTokens`: `language` is empty or not valid JSON — pass `Language(...).to_json()` (a string). `EditorRegex`: `rules` is empty or the patterns are invalid for Dart. Also check `syntax_highlighting=True`. |
1257
+ | Text is hard to see on a dark background | Set `default_color` (for example `"#D4D4D4"`) together with `background_color`. |
1258
+ | Function calls look like normal names | `TokenColors.function` is not set. |
1259
+ | A word is not highlighted as a keyword | Not an exact whole-word match (case, or it contains non-identifier characters). Use `case_insensitive=True`, or `identifier_extra` for dashed words. |
1260
+ | Error underlines do not appear | `EditorTokens`: `Language.errors` is `None`. `EditorRegex`: `error_rules` is empty. Or `show_errors=False` / `syntax_highlighting=False`. |
1261
+ | Red underline under every variable | `unknown_words=True` flags everything not in `words`/`allowed_words`. Add the names to `allowed_words` or turn it off. |
1262
+ | No fold markers | `code_folding=False`, or no `FoldRule` (with `end` or `length`) and `fold_by_indent=False`. |
1263
+ | `on_change` seems late | It is debounced by `debounce_ms` (default 150 ms). Lower it, or set `0`. |
1264
+ | An `EditorRegex` rule has no effect | An earlier rule already colored those characters (first match wins), or the regex is invalid for Dart. |
1265
+ | Changes made from Python are not visible | Call `editor.update()` after modifying a property. |
1266
+ | `pip install` from GitHub fails | Confirm Git is installed, the URL is correct, and (private repos) that you are authenticated. |
1267
+
1268
+ ## Contributing
1269
+
1270
+ Contributions are welcome.
1271
+
1272
+ 1. Fork the repository.
1273
+ 2. Create a feature branch: `git checkout -b feature/my-feature`.
1274
+ 3. Commit your changes with clear messages.
1275
+ 4. Push the branch and open a Pull Request describing the change and its motivation.
1276
+
1277
+ Please open an issue first for larger changes or new features so the design can be discussed. See [Developing and Testing Locally](#developing-and-testing-locally) for how to try your changes in an app.
1278
+
1279
+ ## Sponsors
1280
+
1281
+ Development of `flet-code-editor-dsl` is supported by its sponsors. Their contributions help cover maintenance time, testing across platforms, and new features.
1282
+
1283
+ ### Our Sponsors
1284
+
1285
+ <!--
1286
+ Add sponsors below. Suggested format:
1287
+
1288
+ <a href="https://example.com"><img src="https://example.com/logo.png" alt="Sponsor Name" height="60"></a>
1289
+ -->
1290
+
1291
+ *Your logo could be here. [Become the first sponsor!](https://github.com/sponsors/obgwew)*
1292
+
1293
+ ### Become a Sponsor
1294
+
1295
+ If this library saves you time, please consider supporting it:
1296
+
1297
+ - [Sponsor on GitHub](https://github.com/sponsors/obgwew)
1298
+
1299
+ | Tier | Benefit |
1300
+ |------|---------|
1301
+ | **Supporter** | Your name listed in this section. |
1302
+ | **Silver** | Your name and link listed in this section. |
1303
+ | **Gold** | Your logo and link displayed prominently at the top of this section. |
1304
+
1305
+ Sponsorship is entirely optional and does not affect the [license](#license) or the features available to anyone. Thank you to everyone who supports open-source work.
1306
+
1307
+ ---
1308
+
1309
+ ## License
1310
+
1311
+ This project is licensed under the **Apache License, Version 2.0**. See the [LICENSE](LICENSE) file for the full text.
1312
+
1313
+ ```text
1314
+ Copyright 2026 obgwew
1315
+
1316
+ Licensed under the Apache License, Version 2.0 (the "License");
1317
+ you may not use this file except in compliance with the License.
1318
+ You may obtain a copy of the License at
1319
+
1320
+ http://www.apache.org/licenses/LICENSE-2.0
1321
+
1322
+ Unless required by applicable law or agreed to in writing, software
1323
+ distributed under the License is distributed on an "AS IS" BASIS,
1324
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1325
+ See the License for the specific language governing permissions and
1326
+ limitations under the License.
1327
+ ```
1328
+
1329
+ By contributing to this repository, you agree that your contributions will be licensed under the same Apache 2.0 license.