flet-code-editor-dsl
Two native Flutter code-editing controls for Flet, 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.
Table of Contents
- Overview
- What's New in 0.2.0 / Migration from the Previous API
- Features
- Requirements
- Installation
- Quick Start
- Choosing a Control:
FletCodeEditorvsFletCodeField FletCodeEditorReference- The
LanguageDSL - How the Lexer Works
- Performance Notes
FletCodeFieldReference (regex based)- Complete Examples
- Important: First Build Required
- Building Your App
- Project Structure
- Known Limitations
- Troubleshooting
- Contributing
- Sponsors
- License
Overview
flet-code-editor-dsl is a Flet extension that ships two Flutter controls:
| Control | Built on | Highlighting | Best for |
|---|---|---|---|
FletCodeEditor |
re_editor (fully custom editor; each line is laid out and painted independently, only visible lines are built) |
Hand-written, stateful tokenizer configured by a Language object |
Large documents, custom languages, error underlines, code folding |
FletCodeField |
code_text_field (TextField based) |
Regular expressions (HighlightRule), cached per line |
Small snippets, quick regex-driven coloring |
Instead of shipping fixed grammars, you describe your language in Python:
import flet_code_editor_dsl as ce
lang = ce.Language(
words=[ce.WordGroup(["let", "if", "else"], "#C586C0")],
line_comments=["#"],
strings=[ce.StringRule('"')],
colors=ce.TokenColors(comment="#6A9955", string="#CE9178", number="#B5CEA8"),
)
and hand lang.to_json() to the editor.
What's New in 0.2.0 / Migration from the Previous API
Version 0.2.0 is a rewrite of the public API. The earlier regex-based CodeEditor control and its rule() / group() / pair() helpers no longer exist.
| Previous API | 0.2.0 |
|---|---|
Control CodeEditor (type flet_code_editor_dsl) |
FletCodeEditor (new, tokenizer based) and FletCodeField (regex based) |
rule(), group() + rules_json() |
Language(words=[WordGroup(...)], strings=..., ...) + Language.to_json() for the editor; HighlightRule list for FletCodeField |
pair() + pairs_json() (bracket/quote coloring) |
ErrorRules(brackets=[BracketPair(...)]) (unmatched brackets get an underline) and StringRule (quotes are part of the string token) |
strict / error_color (paint unknown text red) |
ErrorRules(unknown_words=True, allowed_words=[...], forbidden_words=[...]) with a colored (wavy) underline |
before / after / ignore_if context regexes |
Not needed: the lexer natively understands strings, comments, calls (name(), sigils, etc. |
Dart RegExp for everything |
No regex in FletCodeEditor at all. Regex is only used by FletCodeField |
Flet >= 0.80 |
Flet >= 0.85.2 |
New in 0.2.0:
- Line numbers, code folding, read-only mode, word wrap.
- Multi-line strings and block comments with state carried across lines.
- Per-token-kind colors (
TokenColors) including escape sequences inside strings. - Error underlines (unterminated strings, unmatched brackets, unknown/forbidden words).
- Size control by level (
width_level/height_level) or by line count (vertical_mode). - Debounced synchronization of the text back to Python (
debounce_ms).
Features
- Native Flutter rendering through the Flet extension mechanism.
- Declarative
Languagedescription — no regular expressions. - Virtualized rendering: only visible lines are built and lexed.
- Incremental, per-line caching with correct multi-line state (block comments, multi-line strings).
- Strings with escape characters, custom delimiters and optional multi-line support.
- Line comments and block comments (multiple styles at once).
- Sigils such as
$nameor@user. - Numbers: decimal, floats, exponents,
_separators,0x/0b/0oprefixes. - Function-call detection (
name() with its own color. - Optional error underlines (wavy or straight) and user-defined folding regions.
- Configurable font, size, text color, background, line numbers, wrapping, read-only.
- Standard
ft.LayoutControlproperties (width,height,expand,margin,visible,opacity, ...).
Requirements
| Component | Version |
|---|---|
| Python | >= 3.10 (the dataclasses use str | None syntax) |
| Flet (Python) | >= 0.85.2 (see requires.txt) |
| Flet (Flutter side) | flet: ^0.85.2 |
| Dart SDK | >= 3.3.0 < 4.0.0 (the tokenizer uses Dart 3 records) |
| Flutter | >= 1.17.0 (practically: the version Flet 0.85 uses) |
| Flutter packages | re_editor ^0.10.0, code_text_field ^1.1.0 (pulled in automatically) |
| Git | Only for installing from GitHub |
Note: the package contains a Flutter extension, so your app must declare it in
pyproject.tomland be built once withflet build. See Important: First Build Required.
Installation
Option 1: PyPI (recommended)
pip install flet-code-editor-dsl
Declare it in your app's pyproject.toml:
[project]
name = "my-flet-app"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = [
"flet>=0.85.2",
"flet-code-editor-dsl",
]
Pin an exact version for reproducible builds:
dependencies = ["flet>=0.85.2", "flet-code-editor-dsl==0.2.0"]
Upgrade:
pip install --upgrade flet-code-editor-dsl
Option 2: GitHub
pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git"
# pinned:
pip install "git+https://github.com/obgwew/flet-code-editor-dsl.git@v0.2.0"
Inside pyproject.toml:
dependencies = [
"flet>=0.85.2",
"flet-code-editor-dsl @ git+https://github.com/obgwew/flet-code-editor-dsl.git",
]
Option 3: Local editable install
git clone https://github.com/obgwew/flet-code-editor-dsl.git
cd flet-code-editor-dsl
pip install -e .
Verifying the installation
python -c "import flet_code_editor_dsl as m; print(m.__all__)"
Expected output:
['FletCodeEditor', 'FletCodeField', 'HighlightRule', 'VerticalMode', 'Language', 'WordGroup', 'StringRule', 'BlockComment', 'Sigil', 'TokenColors', 'FoldRule', 'ErrorRules', 'BracketPair']
Quick Start
import flet as ft
import flet_code_editor_dsl as ce
LANG = ce.Language(
words=[
ce.WordGroup(["let", "if", "else", "while", "return"], "#C586C0"), # keywords
ce.WordGroup(["true", "false", "null"], "#569CD6"), # constants
],
line_comments=["#"],
strings=[ce.StringRule('"')],
colors=ce.TokenColors(
comment="#6A9955",
string="#CE9178",
escape="#D7BA7D",
number="#B5CEA8",
function="#DCDCAA",
identifier="#9CDCFE",
operator="#D4D4D4",
punctuation="#D4D4D4",
),
)
def main(page: ft.Page):
page.title = "Code Editor"
page.theme_mode = ft.ThemeMode.DARK
editor = ce.FletCodeEditor(
value='let x = add(1, 2) # sum\nprint("done\\n")',
language=LANG.to_json(), # <-- JSON string, not the object
default_color="#D4D4D4",
background_color="#1E1E1E",
expand=True,
)
page.add(editor)
ft.run(main)
On Flet versions that still use the older entry point, replace
ft.run(main)withft.app(main).
Two things that surprise people on first use:
languageexpects the JSON string returned byLanguage(...).to_json(), not theLanguageobject itself.- By default the editor box is only
13/30of the available space. Useexpand=True(orwidth/height) to control the size. See Sizing the editor.
Choosing a Control: FletCodeEditor vs FletCodeField
| Feature | FletCodeEditor |
FletCodeField |
|---|---|---|
| Highlighting engine | Tokenizer (Language) |
Regex (HighlightRule) |
| Multi-line strings / block comments | Yes (state carried across lines) | No (each line is highlighted alone in incremental mode) |
| Only visible lines are built | Yes | No |
| Error underlines | Yes (ErrorRules) |
No |
| Code folding | Yes (FoldRule, fold_by_indent) |
No |
| Read-only mode | Yes | No |
font_family, font_size |
Yes | No (fixed to Consolas 12) |
default_color, background_color |
Yes | No |
width_level / height_level |
Yes | No |
debounce_ms |
Yes | No (text is sent on every change) |
Line numbers, wrap, vertical_mode, min_lines, max_lines |
Yes | Yes |
on_change |
Yes | Yes |
Rule of thumb: use FletCodeEditor unless you only need a few regex-based colors.
FletCodeEditor Reference
@ft.control("FletCodeEditor")
class FletCodeEditor(ft.LayoutControl): ...
Properties
| Property | Type | Default | Description |
|---|---|---|---|
value |
str |
"" |
Editor text. Assigning from Python replaces the content. |
language |
str |
"" |
JSON produced by Language(...).to_json(). "" means no highlighting. Invalid JSON is silently treated as "no highlighting". |
line_numbers |
bool |
True |
Show the line-number gutter. |
syntax_highlighting |
bool |
True |
Master switch for highlighting (and the error underlines that depend on it). |
show_errors |
bool |
True |
Draw error underlines. Requires Language.errors to be set. |
code_folding |
bool |
True |
Show fold markers. Requires Language.folds or fold_by_indent=True. |
wrap |
bool |
False |
Soft-wrap long lines. |
read_only |
bool |
False |
Disable editing (selection and scrolling still work). |
width_level |
int | None |
13 |
Visible width as level/30 of the available width. See Sizing. |
height_level |
int | None |
13 |
Visible height as level/30 of the available height. |
vertical_mode |
VerticalMode |
FIXED |
FIXED, BOUNDED or UNLIMITED. See Vertical modes. |
min_lines |
int | None |
None |
Lower bound in BOUNDED mode (falls back to 1). |
max_lines |
int | None |
None |
Upper bound in BOUNDED mode (falls back to 10). |
font_family |
str |
"Consolas" |
Font family. Use a monospace font for correct alignment. |
font_size |
float |
12.0 |
Font size in logical pixels. |
default_color |
str | None |
None |
Color for text with no token color (see Colors). |
background_color |
str | None |
None |
Editor background. |
debounce_ms |
int |
150 |
Delay before the text is pushed to Python and on_change fires. |
on_change |
ft.ControlEventHandler | None |
None |
Called after the debounce delay; e.data is the full text. |
All ft.LayoutControl properties are available as well.
Sizing the editor (important)
There are three independent ways to size the control. They are evaluated in this order:
- Explicit layout —
width=,height=orexpand=(any truthy / positive value). When given, the level properties are ignored for that axis:widthorexpandset →width_levelis ignored.height,expandset, orvertical_mode != FIXED→height_levelis ignored.
- Line-count height —
vertical_mode="bounded"/"unlimited"computes the height from the number of lines (see below). - Levels — otherwise the box is
level / 30of the available space on that axis, with a minimum of 48 px. Level30covers the available space, level1gives a small box. If the parent gives unbounded constraints, the screen size is used as the reference.
Because both levels default to 13, an editor placed in a layout without expand, width or height will look small (about 43% of the space). This is by design but is the most common "why is my editor tiny?" question:
ce.FletCodeEditor(...) # ~13/30 x 13/30 of the space
ce.FletCodeEditor(..., width_level=30, height_level=30) # fills the space
ce.FletCodeEditor(..., expand=True) # fills the parent (levels ignored)
ce.FletCodeEditor(..., width=600, height=400) # fixed size (levels ignored)
Values outside 1..30 are clamped.
Vertical modes
class VerticalMode(str, Enum):
FIXED = "fixed" # fills the given height, scrolls internally
BOUNDED = "bounded" # grows between min_lines and max_lines, then scrolls
UNLIMITED = "unlimited" # grows with the text, no limit
| Mode | Behavior |
|---|---|
FIXED |
The editor fills the height it is given (explicit height, expand, or height_level) and scrolls internally. |
BOUNDED |
Height follows the line count, clamped to [min_lines, max_lines] (defaults 1 and 10), then scrolls. |
UNLIMITED |
Height follows the line count with no limit. Put it inside a scrollable parent. |
In BOUNDED and UNLIMITED modes the height is measured from the font (line height × lines + padding), so changing font_size or font_family updates it automatically. Use UNLIMITED only inside a scrollable container, otherwise very long text can overflow.
# A chat-input style box: starts at 3 lines, grows to 12, then scrolls
ce.FletCodeEditor(
language=LANG.to_json(),
vertical_mode=ce.VerticalMode.BOUNDED,
min_lines=3,
max_lines=12,
width=500,
)
Reading and writing text, on_change and debounce
def on_change(e: ft.ControlEvent):
print("Current text:", e.data)
editor = ce.FletCodeEditor(on_change=on_change, debounce_ms=300)
How synchronization works:
- Typing inside the editor does not send every keystroke. After the user stops typing for
debounce_msmilliseconds, the full text is written toeditor.valueandon_changefires (only if you assigned a handler). debounce_ms=0sends immediately; negative values are treated as0.- If nothing really changed (for example, only the cursor moved, or the text equals what was already sent), nothing is sent.
- Setting
editor.value = "..."and callingeditor.update()replaces the editor content. Your own echo is ignored, so there is no feedback loop. - Because of the debounce, reading
editor.valueright after the user types may return slightly stale text. Useon_changefor the latest value, or lowerdebounce_ms.
editor.value = "new content"
editor.update()
Colors
Colors for default_color, background_color and everything in the DSL are hex strings:
| Format | Example | Meaning |
|---|---|---|
#RRGGBB |
#FF8800 |
Opaque (alpha FF added automatically) |
#AARRGGBB |
#80FF8800 |
Explicit alpha |
- They are plain strings, not
ft.Colors.*values or color names. - Invalid or empty values are treated as "not set" instead of raising an error.
- For tokens,
Nonemeans "usedefault_color". Ifdefault_coloris alsoNone, Flutter's own theme text color is used. - Always set
default_colorexplicitly when you set a darkbackground_color, otherwise un-styled text may be unreadable.
Changing things at runtime
# Swap the language (e.g. when the user picks another file type)
editor.language = OTHER_LANG.to_json()
editor.update()
# Toggle features
editor.show_errors = False
editor.code_folding = False
editor.syntax_highlighting = False
editor.read_only = True
editor.wrap = True
editor.update()
# Appearance
editor.font_size = 16
editor.font_family = "Courier New"
editor.background_color = "#0D1117"
editor.default_color = "#C9D1D9"
editor.line_numbers = False
editor.update()
Only language, syntax_highlighting, show_errors and code_folding rebuild the highlighter. When any of them changes, 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.
The Language DSL
All classes are plain Python dataclasses importable from the top-level package:
from flet_code_editor_dsl import (
Language, WordGroup, StringRule, BlockComment, Sigil,
TokenColors, FoldRule, ErrorRules, BracketPair,
)
Language
| Field | Type | Default | Description |
|---|---|---|---|
words |
list[WordGroup] |
[] |
Exact words grouped by color (keywords, constants, builtins...). |
line_comments |
list[str] |
[] |
Markers that start a comment running to the end of the line ("//", "#", "--"). |
block_comments |
list[BlockComment] |
[] |
Multi-line comments (/* ... */). |
strings |
list[StringRule] |
[] |
String literal styles. |
sigils |
list[Sigil] |
[] |
Prefix + name tokens ($var, @user). |
numbers |
bool |
True |
Recognize numeric literals. |
operators |
str |
"+-*/%=<>!&|^~?:" |
Each character in this string is an operator character. |
punctuation |
str |
"(){}[],;." |
Each character is a punctuation character. |
identifier_extra |
str |
"" |
Extra characters allowed inside names (for example "-" for CSS/Lisp-style names). |
case_insensitive |
bool |
False |
Match words (and error word lists) ignoring case. |
colors |
TokenColors |
TokenColors() |
Colors for the built-in token kinds. |
folds |
list[FoldRule] |
[] |
User-defined folding regions. |
fold_by_indent |
bool |
False |
Also fold by indentation. |
errors |
ErrorRules | None |
None |
Error underlines. None disables all error checking. |
WordGroup
WordGroup(words: list[str], color: str)
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 ever matched if it consists of identifier characters (letters, digits, _, non-ASCII characters and anything listed in identifier_extra); a "word" such as "->" will never match — use operators for symbols.
words=[
ce.WordGroup(["if", "else", "while", "for", "return"], "#C586C0"),
ce.WordGroup(["True", "False", "None"], "#569CD6"),
ce.WordGroup(["print", "len", "range"], "#4EC9B0"),
]
If the same word appears in several groups, the last group wins. Entries with empty text or an invalid color are ignored.
StringRule
StringRule(open: str, close: str | None = None, escape: str | None = "\\", multiline: bool = False)
| Field | Description |
|---|---|
open |
Opening delimiter (any length). |
close |
Closing delimiter. Defaults to open. |
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. |
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). |
Rules are tried longest open first, so '"""' wins over '"' regardless of list order.
strings=[
ce.StringRule('"'),
ce.StringRule("'"),
ce.StringRule('"""', multiline=True), # Python docstrings
ce.StringRule("`", multiline=True, escape=None), # JS template / SQL identifiers
ce.StringRule("[[", "]]", escape=None, multiline=True), # Lua long strings
ce.StringRule("'", escape="'"), # SQL-style '' escaping
]
The delimiters themselves are part of the colored string token.
BlockComment and line_comments
BlockComment(start: str, end: str)
line_comments=["//", "#"],
block_comments=[ce.BlockComment("/*", "*/"), ce.BlockComment("<!--", "-->")]
- Line comments run to the end of the line. Longer markers are tried first (so
///can win over//— they share the same color, so this only matters for correctness of matching). - 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.
- 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. - 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).
Sigil
Sigil(prefix: str, color: str)
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. Characters allowed in the name are the usual identifier characters plus identifier_extra.
sigils=[ce.Sigil("$", "#9CDCFE"), ce.Sigil("@", "#4FC1FF"), ce.Sigil("%", "#D7BA7D")]
Numbers, operators, punctuation, identifiers
Numbers (numbers=True):
- Integers and decimals:
42,3.14,.5 - Digit separators:
1_000_000 - Exponents:
1e10,2.5E-3 - Prefixed literals:
0x1F,0b1010,0o755(prefix charactersx,b,oin either case; hex digits and_follow) - A number ends where it ends:
10pxis the number10followed by the identifierpx. - Identifiers cannot start with a digit.
Set numbers=False if digits should not be treated specially (they are then colored as part of identifiers or "anything else").
Operators — each character in Language.operators is an operator character; a run of operator characters (==, <=, &&, -> if both - and > are listed) becomes one token with TokenColors.operator.
Punctuation — each character in Language.punctuation becomes its own token with TokenColors.punctuation.
Characters belonging to neither set (and not part of anything else, e.g. @, #, \, `) are drawn with the default color.
Because comments and strings are recognized first, a character such as
/in//is a comment, not an operator, as long as you listed//inline_comments.
Identifiers — a name starts with an ASCII letter, _, or any non-ASCII character (so Arabic, Chinese, accented letters and emoji are treated as name characters), and continues with letters, digits, _, non-ASCII characters and identifier_extra.
- An identifier found in
wordsgets that group's color. - Otherwise, if it is directly followed by
((spaces and tabs allowed in between) andTokenColors.functionis set, it is a function call. - Otherwise it gets
TokenColors.identifier.
TokenColors
TokenColors(
comment=None, string=None, escape=None, number=None,
function=None, identifier=None, operator=None, punctuation=None,
)
| Field | Applies to |
|---|---|
comment |
Line and block comments |
string |
String literals (including delimiters) |
escape |
Escape sequences inside strings (\n, \"); falls back to string |
number |
Numeric literals |
function |
Identifiers followed by ( that are not in words |
identifier |
All other names |
operator |
Operator runs |
punctuation |
Punctuation characters and bracket characters used in ErrorRules.brackets |
None = default text color (FletCodeEditor.default_color). Sigils and word groups carry their own colors. If you leave function as None, function calls look like ordinary identifiers.
ErrorRules (error underlines)
ErrorRules(
unterminated_strings=True,
brackets=[], # list[BracketPair]
unknown_words=False,
allowed_words=[],
forbidden_words=[],
color="#F44747",
wavy=True,
)
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. Errors are drawn as an underline (wavy by default, wavy=False for straight) in color; the token keeps its own text color.
| Check | Behavior |
|---|---|
unterminated_strings |
A non-multiline string that is not closed before the end of the line is underlined from its opening delimiter to the end of the line. Multi-line strings are never flagged. |
brackets |
Each BracketPair(open, close) must be single characters. An unmatched closer, or an opener never closed on the same line, is underlined. Mismatched nesting ((]) is detected. Brackets inside strings and comments are not counted because the lexer consumes those first. Bracket characters are colored with TokenColors.punctuation. |
unknown_words |
Every identifier that is not in words and not in allowed_words is underlined. Function-call names and sigil-less variables count as identifiers too, so list the names you want to accept in allowed_words. |
forbidden_words |
Words that are always underlined, even if they are in words. |
errors=ce.ErrorRules(
unterminated_strings=True,
brackets=[
ce.BracketPair("(", ")"),
ce.BracketPair("[", "]"),
ce.BracketPair("{", "}"),
],
unknown_words=True,
allowed_words=["x", "y", "total", "price", "print"],
forbidden_words=["goto"],
color="#F44747",
wavy=True,
)
The lexer builds a human-readable message for each error (for example Unterminated string, Unknown word: foo, Unmatched ')', Unclosed '('), but the current editor only draws the underline; messages are not displayed as tooltips yet.
Brackets spanning several lines are not matched across lines; each line is checked on its own. For a language where
{and}regularly sit on different lines, leave the{}pair out ofbrackets.
FoldRule and fold_by_indent (code folding)
FoldRule(start: str, end: str | None = None, length: int | None = None)
Folding adds fold markers in the gutter (a 20 px column next to the line numbers). It is active when code_folding=True and the language defines at least one FoldRule or sets fold_by_indent=True.
Two kinds of rules:
-
Start/end markers — a line starting with
startopens a region and a line starting withendcloses it. Nesting is supported (each rule keeps its own stack).folds=[ce.FoldRule("class", "end"), ce.FoldRule("def", "end"), ce.FoldRule("if", "end")]
-
Fixed length — fold the next
lengthlines after a line starting withstart.folds=[ce.FoldRule("on", length=5)] # fold the 5 lines after every line starting with "on"
If the region would run past the end of the document it is clamped to the last line. If both
endandlengthare given,endis used.
Matching rules:
- Markers are matched at the beginning of a line, after indentation.
- If the marker ends with a letter, digit or
_, it matches whole words only:enddoes not matchendpoint,classdoes not matchclassify. - If the marker ends with a symbol (
{,#region...), it matches as a plain prefix. - Marker matching is case-sensitive and ignores
case_insensitive. - A region is only foldable if it has at least one line inside it. An opener without a closer produces no fold.
- Folding analysis is purely line-based and is not aware of strings or comments: a line inside a block comment that begins with
endwill still close a region. - If
start == end(for example"```"to"```"), the same marker toggles open/closed.
folds=[
ce.FoldRule("{"), # NOTE: without end/length this does nothing
ce.FoldRule("#region", "#endregion"),
ce.FoldRule("function", "end"),
]
fold_by_indent=True additionally creates a region for every line that is 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 that is still indented deeper. It can be combined with folds.
case_insensitive
When True, the words in WordGroup, ErrorRules.allowed_words and ErrorRules.forbidden_words are compared in lower case, and the identifier found in the text is lower-cased before lookup. This is ideal for SQL, BASIC, Pascal and similar languages:
sql = ce.Language(
case_insensitive=True,
words=[ce.WordGroup(["select", "from", "where", "and", "or", "insert", "into"], "#C586C0")],
)
# SELECT, Select and select are all keywords
It does not affect comment markers, string delimiters, sigil prefixes or FoldRule markers.
Language.to_json()
def to_json(self) -> str
Serializes the whole description (nested dataclasses included) into a compact JSON string (ensure_ascii=False, no extra spaces). Pass it to FletCodeEditor(language=...). Because the editor compares the string, re-assigning an identical JSON string does not cause a rebuild, so it is safe to build it once at module level and reuse it for many editors:
PY_LANG = ce.Language(...).to_json()
a = ce.FletCodeEditor(language=PY_LANG)
b = ce.FletCodeEditor(language=PY_LANG, read_only=True)
How the Lexer Works
Understanding the order helps when two features seem to "fight". For every line the lexer scans left to right and at each position tries the following, stopping at the first match:
| # | Step | Notes |
|---|---|---|
| 0 | Continue a multi-line construct | If the line starts inside a block comment or multi-line string, finish it first. |
| 1 | Whitespace | Spaces, tabs, \r and non-breaking space. Never colored. |
| 2 | Line comments | Longest marker first. Consumes the rest of the line. |
| 3 | Block comments | Longest start marker first. May stay open to the next line. |
| 4 | Strings | Longest open first. May stay open for multiline=True. |
| 5 | Sigils | Prefix + at least one name character. |
| 6 | Numbers | Only if numbers=True. |
| 7 | Identifiers / words / calls | Also applies forbidden_words and unknown_words. |
| 8 | Bracket check | Only for characters listed in ErrorRules.brackets. |
| 9 | Operators | A run of operator characters is one token. |
| 10 | Punctuation | One character per token. |
| 11 | Anything else | Default color. |
Practical consequences:
- Order inside lists does not matter for strings, comments and sigils (the longest marker wins automatically). It only matters for duplicate words (last group wins).
- Comments beat strings and strings beat everything else, because the scan is left-to-right and these are tested first.
- There is no rule-priority/override system like in regex highlighters; the structure of the language decides the color.
- Bracket characters listed in
ErrorRules.bracketsalways get the punctuation color, even if they are also inoperators. - Adjacent tokens with the same color are merged into one span.
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.
Performance Notes
- Only the lines that are actually painted are lexed (the editor is virtualized).
- Each line's result is cached by
(index, text, start state). Unchanged lines are never lexed again. - For languages without multi-line constructs (no block comments and no
multiline=Truestrings) there is no dependency between lines, so an edit invalidates only that line. - For languages with multi-line constructs, an edit invalidates the chain from the edited line downward, lazily.
- The cache is cleared when it grows beyond 20 000 cached lines.
- Changing
language,syntax_highlighting,show_errorsorcode_foldingforces a repaint of all visible lines; changing colors/fonts of the control itself does not re-lex. - Folding analysis (
FoldAnalyzer) runs on the entire document when the text changes; it is cheap line-prefix checking but is O(lines × fold rules).
FletCodeField Reference (regex based)
A lighter control based on a TextField-style code field. Highlighting is regex based and cached per line.
@dataclass
class HighlightRule:
pattern: str # Dart RegExp pattern
color: str # "#RRGGBB" or "#AARRGGBB"
@ft.control("FletCodeField")
class FletCodeField(ft.LayoutControl): ...
| Property | Type | Default | Description |
|---|---|---|---|
value |
str |
"" |
Text. |
rules |
list[HighlightRule] |
[] |
Regex highlighting rules. |
line_numbers |
bool |
True |
Show line numbers. |
syntax_highlighting |
bool |
True |
Master switch. |
incremental_highlighting |
bool |
True |
Highlight line by line with a per-line cache. If False, the whole text is highlighted as a single chunk (patterns can then span several lines, but every edit re-highlights everything). |
wrap |
bool |
False |
Soft-wrap long lines. |
vertical_mode |
VerticalMode |
FIXED |
FIXED expands to fill the given height; BOUNDED uses min_lines/max_lines (defaults 1 / 10); UNLIMITED grows with the text. |
min_lines / max_lines |
int | None |
None |
Line limits for BOUNDED. |
on_change |
handler | None |
Fired on every text change; e.data is the text. |
Behavior and gotchas:
- Patterns use the Dart
RegExpengine (ECMAScript-style,multiLineon). Use(?<name>...)instead of Python's(?P<name>...), and raw strings (r"...") in Python. - Patterns or colors that fail to parse are silently skipped.
- 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 (strings, comments) first and general rules (identifiers) last. This is the opposite of the previous
CodeEditorengine, where later rules overrode earlier ones. - Rules are re-compiled only when the list changes; ordinary typing never recompiles regexes.
- In incremental mode every line is highlighted independently, so multi-line constructs (block comments, triple-quoted strings) cannot be highlighted. Use
FletCodeEditorfor those. - The font is fixed to Consolas, size 12. There are no
font_*or color properties.
import flet as ft
import flet_code_editor_dsl as ce
RULES = [
ce.HighlightRule(r"#.*", "#6A9955"), # comments first
ce.HighlightRule(r'"[^"\n]*"', "#CE9178"), # strings
ce.HighlightRule(r"\b(?:let|if|else)\b", "#C586C0"), # keywords
ce.HighlightRule(r"\b\d+(?:\.\d+)?\b", "#B5CEA8"), # numbers
]
def main(page: ft.Page):
page.add(
ce.FletCodeField(
value='let x = 1 # one',
rules=RULES,
vertical_mode=ce.VerticalMode.BOUNDED,
min_lines=3,
max_lines=10,
width=500,
)
)
ft.run(main)
Complete Examples
1. A tiny scripting language with errors and folding
import flet as ft
import flet_code_editor_dsl as ce
SAMPLE = '''\
# Example program
let total = (price * 3) + 12
if (total > 100) {
print("expensive")
} else {
print("cheap")
}
/* multi-line
comment */
let broken = (1 + 2
let s = "unterminated
'''
LANG = ce.Language(
words=[
ce.WordGroup(["let", "if", "else"], "#C586C0"),
ce.WordGroup(["true", "false"], "#569CD6"),
],
line_comments=["#"],
block_comments=[ce.BlockComment("/*", "*/")],
strings=[ce.StringRule('"')],
colors=ce.TokenColors(
comment="#6A9955", string="#CE9178", escape="#D7BA7D",
number="#B5CEA8", function="#DCDCAA", identifier="#9CDCFE",
operator="#D4D4D4", punctuation="#FFD700",
),
folds=[ce.FoldRule("if", "}")], # fold from `if` to a line starting with `}`
errors=ce.ErrorRules(
unterminated_strings=True,
brackets=[ce.BracketPair("(", ")")],
unknown_words=True,
allowed_words=["total", "price", "print", "broken", "s"],
),
)
def main(page: ft.Page):
page.title = "flet-code-editor-dsl demo"
page.theme_mode = ft.ThemeMode.DARK
status = ft.Text(f"Characters: {len(SAMPLE)}")
def on_change(e: ft.ControlEvent):
status.value = f"Characters: {len(e.data or '')}"
status.update()
def toggle_errors(e: ft.ControlEvent):
editor.show_errors = e.control.value
editor.update()
editor = ce.FletCodeEditor(
value=SAMPLE,
language=LANG.to_json(),
default_color="#D4D4D4",
background_color="#1E1E1E",
font_family="Consolas",
font_size=15,
line_numbers=True,
expand=True,
on_change=on_change,
debounce_ms=200,
)
page.add(
ft.Row([ft.Switch(label="Show errors", value=True, on_change=toggle_errors), status]),
editor,
)
ft.run(main)
2. Python-like language (docstrings, fold_by_indent)
PYTHON_LIKE = ce.Language(
words=[
ce.WordGroup(["def", "class", "if", "elif", "else", "for", "while", "return",
"import", "from", "as", "in", "not", "and", "or", "with", "try",
"except", "finally", "raise", "lambda"], "#C586C0"),
ce.WordGroup(["True", "False", "None", "self"], "#569CD6"),
],
line_comments=["#"],
strings=[
ce.StringRule('"""', multiline=True),
ce.StringRule("'''", multiline=True),
ce.StringRule('"'),
ce.StringRule("'"),
],
colors=ce.TokenColors(comment="#6A9955", string="#CE9178", escape="#D7BA7D",
number="#B5CEA8", function="#DCDCAA", identifier="#9CDCFE"),
fold_by_indent=True,
)
3. SQL (case-insensitive, -- and /* */ comments, '' escaping)
SQL = ce.Language(
case_insensitive=True,
words=[
ce.WordGroup(["select", "from", "where", "join", "left", "right", "inner", "on",
"group", "by", "order", "having", "insert", "into", "values",
"update", "set", "delete", "create", "table", "and", "or", "not",
"null", "as", "limit"], "#C586C0"),
ce.WordGroup(["count", "sum", "avg", "min", "max"], "#DCDCAA"),
],
line_comments=["--"],
block_comments=[ce.BlockComment("/*", "*/")],
strings=[ce.StringRule("'", escape="'"), ce.StringRule('"', escape=None)],
colors=ce.TokenColors(comment="#6A9955", string="#CE9178", number="#B5CEA8",
identifier="#9CDCFE"),
)
4. Config / templating language with sigils and dashed names
TEMPLATE = ce.Language(
identifier_extra="-", # allows names like font-size
sigils=[ce.Sigil("$", "#4FC1FF"), ce.Sigil("@", "#D7BA7D")],
line_comments=["//"],
strings=[ce.StringRule('"'), ce.StringRule("'")],
words=[ce.WordGroup(["include", "define", "if", "endif"], "#C586C0")],
colors=ce.TokenColors(comment="#6A9955", string="#CE9178", number="#B5CEA8",
identifier="#9CDCFE"),
folds=[ce.FoldRule("if", "endif"), ce.FoldRule("define", length=3)],
)
5. Read-only viewer that grows with its content
log_view = ce.FletCodeEditor(
value=log_text,
language=LOG_LANG.to_json(),
read_only=True,
line_numbers=False,
vertical_mode=ce.VerticalMode.BOUNDED,
min_lines=1,
max_lines=20,
width=700,
)
6. Switching languages from a dropdown
LANGUAGES = {"Python": PYTHON_LIKE.to_json(), "SQL": SQL.to_json(), "Plain": ""}
def on_pick(e):
editor.language = LANGUAGES[e.control.value]
editor.update()
page.add(
ft.Dropdown(options=[ft.dropdown.Option(k) for k in LANGUAGES],
value="Python", on_select=on_pick), # on_select / on_change depends on your Flet version
editor,
)
Important: First Build Required
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.
- Declare the library in your app's
pyproject.tomlunder[project] dependencies. - Build your app once with
flet build <platform>. - Run normally afterwards. After the first build, the controls also work with
flet runas long aspyproject.tomlstill declares the library.
Key requirement: if the library is only installed with
pipand not declared inpyproject.toml, Flet will not include the extension and the controls will not appear.
| Change | Rebuild needed? |
|---|---|
Editing Python code, value, language / Language(...), colors, fonts, flags, sizes, layout |
No — sent to the client at runtime. |
Updating flet-code-editor-dsl to a version whose Dart code changed (for example when upgrading to 0.2.0 from the previous API) |
Yes |
| Upgrading Flet itself to a new major/minor version | Yes |
| Adding or removing other Flet extensions | Yes |
Building Your App
# Desktop
flet build windows
flet build macos
flet build linux
# Mobile
flet build apk
flet build ipa
# Web
flet build web
flet build detects the Flutter package bundled in flutter/flet_code_editor_dsl/ and adds it to the generated Flutter project automatically. A Flutter SDK is required; Flet downloads one if missing. The extension registers two control types — FletCodeEditor and FletCodeField — in extension.dart.
Project Structure
flet-code-editor-dsl/
├── README.md
├── LICENSE
├── FUNDING.yml
├── .gitignore
├── pyproject.toml
└── src/
├── flet_code_editor_dsl/
│ ├── __init__.py # Public exports
│ ├── flet_code_editor_dsl.py # FletCodeEditor, FletCodeField, HighlightRule, VerticalMode
│ └── language.py # Language DSL dataclasses + to_json()
└── flutter/
├── __init__.py
└── flet_code_editor_dsl/
├── pubspec.yaml # Flutter package (v0.2.0)
└── lib/
├── flet_code_editor_dsl.dart # Exports Extension
└── src/
├── extension.dart # Maps control types to widgets
├── code_editor.dart # FletCodeEditor widget (re_editor)
├── flet_code_editor_dsl.dart # FletCodeField widget (regex, per-line cache)
└── tokenizer.dart # CodeLanguage lexer, LineHighlighter, FoldAnalyzer
| File | Responsibility |
|---|---|
flet_code_editor_dsl.py |
Declares the two Flet controls, HighlightRule and VerticalMode. |
language.py |
Declarative description of a language; produces the JSON consumed by Dart. |
extension.dart |
Registers FletCodeField and FletCodeEditor with Flet. |
code_editor.dart |
Editor widget: value sync and debounce, language parsing, sizing logic, line numbers, fold indicator. |
tokenizer.dart |
CodeLanguage.fromJson, the stateful lexer (lexLine), the cached LineHighlighter and the FoldAnalyzer. |
flet_code_editor_dsl.dart (Flutter) |
FletCodeField widget and its per-line cached regex controller. |
pyproject.toml |
Packaging metadata (setuptools). Includes everything under the flutter.flet_code_editor_dsl package (**/*, i.e. pubspec.yaml and lib/) as package data so flet build can find the Flutter code. |
__pycache__/,*.pycand*.egg-info/are generated automatically (by Python and bypip install -e ./ the build) and are not part of the source. Keep them out of the repository with a.gitignore:__pycache__/ *.pyc *.egg-info/ build/ dist/
Known Limitations
- No nested block comments. The first end marker closes a block comment.
- Error checks are per line. Unmatched brackets are not tracked across lines; unterminated-string checking does not apply to multi-line strings.
- Error messages are not shown yet. Errors are drawn as underlines only (no tooltips).
- Folding is line-based. It is not aware of strings and comments, and markers are only recognized at the start of a line.
FoldRuleneedsendorlengthto produce a region (a bare start marker folds nothing).- No autocompletion, auto-indent or search UI is provided by the DSL.
FletCodeFieldis regex-based: no multi-line constructs in incremental mode, first-rule-wins, fixed font.- Colors are hex strings (
#RRGGBB/#AARRGGBB), notft.Colors.*values. - Debounced value.
editor.valueon the Python side may lag by up todebounce_msbehind what is on screen. - First build required. As with all Flet extensions, the controls are not available in the stock Flet client.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
| "Unknown control" error or the editor does not appear | The library is not declared in pyproject.toml, or the app was not built with it. Add it to dependencies and run flet build. |
Error after upgrading (CodeEditor, rule, rules_json not found) |
These were removed in 0.2.0. See Migration. Rebuild the app. |
| The editor is tiny | width_level / height_level default to 13 (of 30). Set expand=True, width/height, or levels up to 30. |
| No colors at all | language is empty or not valid JSON — make sure you passed Language(...).to_json() (a string), not the object. Also check syntax_highlighting=True. |
| Text is hard to see on a dark background | Set default_color (for example "#D4D4D4") together with background_color. |
| Function calls look like normal names | TokenColors.function is not set. |
| A word is not highlighted as a keyword | The word was not an exact whole-word match (case, or it contains non-identifier characters). Use case_insensitive=True, or identifier_extra for dashed words. |
| Error underlines do not appear | Language.errors is None, or show_errors=False, or syntax_highlighting=False. |
| 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. |
| No fold markers | code_folding=False, or the language has no FoldRule (with end or length) and fold_by_indent=False. |
on_change seems late |
It is debounced by debounce_ms (default 150 ms). Lower it, or set 0. |
| Colors look different from what I typed | Colors are #RRGGBB / #AARRGGBB; AA is the first pair. Named colors are not supported. |
FletCodeField rule has no effect |
An earlier rule already colored those characters (first match wins), or the regex is invalid for Dart. |
| Changes made from Python are not visible | Call editor.update() after modifying a property. |
pip install from GitHub fails |
Confirm Git is installed, the URL is correct, and (private repos) that you are authenticated. |
Contributing
Contributions are welcome.
- Fork the repository.
- Create a feature branch:
git checkout -b feature/my-feature. - Commit your changes with clear messages.
- Push the branch and open a Pull Request describing the change and its motivation.
Please open an issue first for larger changes or new features so the design can be discussed.
Sponsors
Development of flet-code-editor-dsl is supported by its sponsors. Their contributions help cover maintenance time, testing across platforms, and new features.
Our Sponsors
Your logo could be here. Become the first sponsor!
Become a Sponsor
If this library saves you time, please consider supporting it:
| Tier | Benefit |
|---|---|
| Supporter | Your name listed in this section. |
| Silver | Your name and link listed in this section. |
| Gold | Your logo and link displayed prominently at the top of this section. |
Sponsorship is entirely optional and does not affect the license or the features available to anyone. Thank you to everyone who supports open-source work.
License
This project is licensed under the Apache License, Version 2.0. See the LICENSE file for the full text.
Copyright 2026 obgwew
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
By contributing to this repository, you agree that your contributions will be licensed under the same Apache 2.0 license.
Metadata
Release files for flet-code-editor-dsl 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| flet_code_editor_dsl-0.2.1.tar.gz | 70.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flet_code_editor_dsl-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 111.9 kB
Release files / flet_code_editor_dsl-0.2.1.tar.gz
| Download URL | flet_code_editor_dsl-0.2.1.tar.gz |
|---|---|
| Size | 70.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
63db6588c05adb94392636d87f98893767a869fc4ab91e0cf57faf3ff0969705
|
|
BLAKE2b-256 checksum How to use checksums |
df3f7630c8b8831951c4f405476b401f90713db94032d6d659161fe658845e4d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / flet_code_editor_dsl-0.2.1-py3-none-any.whl
| Download URL | flet_code_editor_dsl-0.2.1-py3-none-any.whl |
|---|---|
| Size | 41.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ec3bc0f869bed9b9ea87d4f000ca88bd1890b9a4411324714e4f1555c547e48b
|
|
BLAKE2b-256 checksum How to use checksums |
1065c640a95ed1dbd956c728b1c5ca8cea057b8cb81bed124168fee825feb421
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|