gfx/code_editor.h
A source code editor: a document kept as lines, tokenisers that colour it, and the component that shows and edits it.
#include <sonore/gfx/code_editor.h>- 6types
- 1other declaration
- Guides: The native editor
On this page
Design notes
Why a plugin has one
A plugin that runs a user's script -- a Lua patch, a formula for a waveshaper, a live-coded sequencer -- or that lets somebody edit the XML of a preset or a mapping by hand needs a place to type that is not a text field. Code is long, indented, line-based and read by its colours; a TextArea that wraps it, eats its tabs and redraws all of it per keystroke fails at every one of those.
Lines, not a buffer
TextArea keeps one vector of characters and wraps it again on change, which is right for a paragraph. A source file is a hundred thousand lines: inserting a character in one flat buffer moves megabytes, and every question a caret asks becomes a search for line starts. CodeDocument keeps a string per line, so an edit touches its own line and a line number is an index. Absolute offsets -- what find and a listener speak -- come from line starts computed lazily and invalidated only from the edited line down: an edit near the end of a long file does not recount its beginning.
Line endings
Each line remembers how it ended -- LF, CR LF or a lone CR -- so a file with mixed endings saves byte for byte as it loaded, and a diff of an edit shows the line edited rather than every line. A line break is ONE character in every offset whatever bytes it is saved as, so a caret never stands between a CR and its LF. New breaks -- typed, pasted, inserted -- take the document's newline, which loading sets to the file's commonest ending; normaliseLineEndings() brings the rest into line, undoably.
Undo is commands here, not snapshots
undo.h keeps whole states, and says why that is right for a plugin's state. For a document of megabytes a snapshot per keystroke is the wrong trade, so every edit is recorded as the text it inserted or removed, grouped into transactions (newTransaction), adjacent typing merged into one record, and the history bounded by a budget of characters.
Colouring a line at a time
A tokeniser reads ONE line, given the state the line before it ended in -- inside a block comment, a raw string, a string continued with a backslash -- and returns the state this line ends in. The editor keeps those states, one per line, valid down to the first line edited since: painting the bottom of a file scans the lines above it once, without building tokens, and an edit re-scans from its own line. Tokens are only built for the lines on screen.
Monospaced, on a grid
Every character stands in a column -- a tab reaches the next stop, an East Asian wide character takes two, a combining mark none -- and is drawn at its column's x. A caret on a grid is arithmetic, and a font that is not monospaced still lines up, drawn a character at a time. The price is that no bidirectional reordering or Arabic joining happens: a grid of cells cannot show those, and a caret moving over the cells could not follow them.
Types 6
Text kept as lines, with positions that move with edits, transactions of undo, line endings kept per line, find, and listeners.
Text goes in and out as UTF-8 and is kept as code points. Offsets count code points with every line break as one, so they agree with the text getTextBetween returns, whose breaks are always LF.
Splits a line of code into coloured tokens.
A line at a time, with a state carried from the end of one line to the start of the next -- 0 at the top of a document -- so a comment or a string that spans lines is known on every line it spans, and a line can be coloured without the lines above it being coloured again: only their states are needed.
class CPlusPlusCodeTokeniser : public CodeTokeniser
C and C++: the reference's token types and colours. Keywords to C++20; integer and floating literals in decimal, hex, octal and binary, with digit separators, hex floats and suffixes; string and character literals with their prefixes, raw strings with delimiters, and a backslash at a line's end continuing a string, a line comment or a directive onto the next line, as the language's line splicing does.
class LuaTokeniser : public CodeTokeniser
Lua 5.4: the reference's token types and colours. Long brackets of any level for strings and comments, which span lines; short strings with their escapes, a backslash before the line's end and \z carrying them onto the next; decimal and hex numbers, hex floats among them.
class XmlTokeniser : public CodeTokeniser
XML: the reference's token types and colours. A tag's opening -- "<name" or "</name" -- and its closing ">" or "/>" are keywords, attribute names and text are identifiers, "=" an operator, attribute values strings; comments, CDATA sections, processing instructions and declarations span lines, and so do a tag's attributes and their values. Unlike the reference, text between tags is text: a quote in it starts nothing.
class CodeEditorComponent : public Widget, private CodeDocument::Listener
A monospaced editor over a CodeDocument, coloured by a CodeTokeniser.
Neither is owned: the document, as in the reference, outlives the editor and may be shown by several; the tokeniser may be null for plain text.
Functions 1
- fn
inline std::shared_ptr<Typeface> monospacedTypeface()A monospaced typeface from the usual places, or null: Consolas, Courier New or Lucida Console on Windows; Menlo, SF Mono or Monaco on macOS; DejaVu Sans Mono, Liberation Mono, Noto Sans Mono or FreeMono on Linux. Read once and kept, as systemTypeface() is. The editor below uses it when nobody has given it a font.gfx::monospacedTypeface · code_editor.h:323