-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/71 Syntax Highlighting #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
782cfc5
0f904b5
97db723
08ce38f
8cbe14e
59c477a
e349fc7
c42caad
1f598f8
517e941
a9ed0c1
30de205
2b0c6d3
b4f044a
050a4e9
68014cf
236b8f5
913de9f
dfe8e97
b17766b
9b1ddaf
df25499
48becf0
13dec61
5e44e63
18ce3ff
f00bc04
1221353
242f5ee
b3414ac
1c9ebee
c55881e
d89ec89
2098964
9cde542
4c9e5f2
b7275a6
4d89c41
e6a4289
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,82 +1,128 @@ | ||||||||||||||||||
| # clieditor | ||||||||||||||||||
|
|
||||||||||||||||||
| A simple, terminal-based text editor written in C from scratch. | ||||||||||||||||||
| This project is an exercise in building a terminal user interface (TUI) application, managing low-level terminal I/O, and structuring a C application in a modular way. | ||||||||||||||||||
| The goal was to use **no third-party libraries** like `ncurses`. | ||||||||||||||||||
| A terminal-based text editor written in C from scratch. This project serves primarily as a personal learning exercise in building a TUI application, exploring low-level terminal I/O, and structuring a C application in a modular way, all without dependencies like `ncurses`. | ||||||||||||||||||
|
|
||||||||||||||||||
| **Still absolutely unfinished and work in progress** | ||||||||||||||||||
| While it includes modern features, it is a hobby project with known limitations and areas for improvement. It is not intended to be a production-ready editor. | ||||||||||||||||||
|
|
||||||||||||||||||
|  | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Features | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Handwritten UI Toolkit**: A custom-built widget system from scratch, featuring a status bar, main menu, notifications, and an editor widget. | ||||||||||||||||||
| - **Syntax Highlighting**: A flexible, regex-based highlighting engine. Syntaxes are defined in simple `.ini` files, making it easy to add new languages. | ||||||||||||||||||
| - **Efficient UTF-8 Support**: A revised string library that handles UTF-8 characters correctly and efficiently, with optimized indexing for large strings. | ||||||||||||||||||
| - **Modern Text Editing**: A gap buffer implementation for the current line allows for efficient character insertion and deletion. | ||||||||||||||||||
| - **Advanced Text Layout**: Supports line wrapping that correctly handles wide characters and tab expansion. | ||||||||||||||||||
| - **Asynchronous Events**: A timer system for timed events like cursor blinking and auto-hiding notifications. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Building | ||||||||||||||||||
| Make sure to have `CMake` (you can also build it without though), a C compiler with the C standard library and POSIX C library available and run | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| Make sure to have `CMake`, a C compiler (like GCC or Clang) with the C standard library, and the POSIX C library available. | ||||||||||||||||||
|
|
||||||||||||||||||
| ```bash | ||||||||||||||||||
| cmake -B build | ||||||||||||||||||
| cmake --build build | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Features | ||||||||||||||||||
| - A handwritten widget system with a status bar, main menu, notifications, and an editor widget forming the core of the UI. | ||||||||||||||||||
| - UTF-8 support | ||||||||||||||||||
| - Line wrapping with correct handling of wide characters and tabs | ||||||||||||||||||
| - A timer system for timed events such as cursor blinking and notifications | ||||||||||||||||||
| ## Usage | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Design Overview | ||||||||||||||||||
| The code is organized into modules, each typically consisting of a data structure and a set of functions operating on it. | ||||||||||||||||||
| To open a file, simply pass its name as an argument: | ||||||||||||||||||
|
|
||||||||||||||||||
| ### User Interface | ||||||||||||||||||
| The UI is built as a tree of widgets, all inheriting from the base `Widget` module. | ||||||||||||||||||
| The root of the widget tree is the `App` widget. | ||||||||||||||||||
| If a widget has focus, all its ancestors also have focus. | ||||||||||||||||||
| User input is passed from the root down to the deepest focused widget until one handles it. | ||||||||||||||||||
| Widgets can define handlers for common events such as input, drawing, or parent resize events. | ||||||||||||||||||
| ```bash | ||||||||||||||||||
| ./build/clieditor src/main.c | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Drawing | ||||||||||||||||||
| Each widget draws itself onto a `Canvas`, which consists of `Cell`s. | ||||||||||||||||||
| Canvases are layered and clipped together, and finally compared to the special `Screen` canvas. | ||||||||||||||||||
| Only changed `Cell`s are written to the `Terminal`, the module that directly interacts with the terminal device. | ||||||||||||||||||
| ### Command-Line Options | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Editor | ||||||||||||||||||
| The editor itself is composed of several tightly integrated modules: | ||||||||||||||||||
| - **`TextBuffer`** — holds the complete text as a doubly-linked list of `Line`s, with a gap buffer for efficient editing. | ||||||||||||||||||
| - **`TextLayout`** — calculates which lines are visible on screen, where wrapping occurs, how tabs expand and where the cursor visually is on screen. It's also responsible also for scrolling. | ||||||||||||||||||
| - **`TextEdit`** — handles text modifications like inserting or deleting characters in the buffer and moving the cursor. | ||||||||||||||||||
| The `Editor` widget brings these parts together and manages user interaction and rendering. | ||||||||||||||||||
| - `clieditor <filename>`: Open a specific file. | ||||||||||||||||||
| - `clieditor -s <syntax> <filename>`: Open a file and force a specific syntax highlighting profile (e.g., `-s c ...`). The name corresponds to a `<syntax>.ini` file in the `data/syntax` directory. | ||||||||||||||||||
| - `clieditor -h`: Show the help message. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Directory Structure | ||||||||||||||||||
| ## Architecture Overview | ||||||||||||||||||
|
|
||||||||||||||||||
| ### `common/` | ||||||||||||||||||
| Fundamental, reusable modules with no dependencies on the rest of the editor, e.g., UTF-8 utilities and logging. | ||||||||||||||||||
| The editor is designed with a clean separation of concerns, organized into distinct modules. This makes the codebase easier to navigate, maintain, and extend. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### `io/` | ||||||||||||||||||
| Handles all direct interaction with the system — terminal I/O, file reading, etc. | ||||||||||||||||||
| This is the only layer with platform-specific code. | ||||||||||||||||||
| - `main.c`: The application's entry point. It handles initialization, argument parsing, and runs the main event loop. | ||||||||||||||||||
| - `common/`: A collection of fundamental, reusable data structures and utilities. This includes the core `String` library, a hash `Table`, a dynamic `Stack`, the `iniparser`, and the `Config` manager. | ||||||||||||||||||
| - `io/`: The layer for all direct interaction with the system, such as terminal I/O (`Terminal`, `Screen`), user `Input`, file operations (`File`), and the event `Timer`. | ||||||||||||||||||
| - `display/`: An abstract rendering layer. It defines *what* to draw through primitives like `Widget`, `Canvas`, and `Cell`, but not *how* it's rendered on screen. | ||||||||||||||||||
| - `document/`: The core data model of the editor, managing the text content independently of the UI. This includes the `TextBuffer`, `TextLayout` for visual calculation, and `TextEdit` for modification logic. | ||||||||||||||||||
| - `syntax/`: The syntax highlighting engine. It includes the `SyntaxDefinition` loader, the `SyntaxHighlighting` processor, and bindings to connect it to the editor's text layout. | ||||||||||||||||||
| - `widgets/`: A library of all concrete UI components, built upon the `display` primitives. It is divided into `primitives` (like `Label`, `Menu`) and `components` (like `EditorView`, `BottomBar`), with `App` serving as the root of the widget hierarchy. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### `display/` | ||||||||||||||||||
| Abstract rendering layer — defines *what* to draw (via `Canvas`, `Cell`, and `Widget`), but not *how* it’s shown on screen. | ||||||||||||||||||
| ## Core Concepts | ||||||||||||||||||
|
|
||||||||||||||||||
| ### `document/` | ||||||||||||||||||
| The core data model of the editor. Manages text independently of the UI. | ||||||||||||||||||
| The editor is built on several key concepts that are crucial to understanding its design and its current limitations. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### `widgets/` | ||||||||||||||||||
| All concrete UI components, like the editor view, status bar, or labels. | ||||||||||||||||||
| The `App` widget forms the root of the widget hierarchy. | ||||||||||||||||||
| ### The String System | ||||||||||||||||||
|
|
||||||||||||||||||
| ### `main.c` | ||||||||||||||||||
| Entry point of the application. Initializes all systems, loads the file, and runs the main loop. | ||||||||||||||||||
| The editor uses a custom, UTF-8 aware string library designed for a balance of performance and memory efficiency. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Questionable Design Choices | ||||||||||||||||||
| - **Representation**: A `String` is a standard `char*` byte buffer, keeping memory usage minimal. | ||||||||||||||||||
| - **Efficient Indexing**: To overcome the O(N) cost of character indexing in variable-length UTF-8 strings, the `String` struct maintains an auxiliary array (`multibytes`) that caches the byte offsets of multi-byte characters. This allows for O(log N) character-based indexing via binary search. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### UTF8 Support | ||||||||||||||||||
| I had no idea how UTF-8 actually worked in C, so I quickly threw together an `UTF8Char` struct and `UTF8String` containers built from it. | ||||||||||||||||||
| To be fair, they turned out to be quite robust and easy to use — which is the good part. | ||||||||||||||||||
| The bad part is that I now use at least 5 bytes per character, which blows up memory usage pretty fast. | ||||||||||||||||||
| For example, the release build needs around *600 MB* to load a *100 MB* file, which seems… a bit much. | ||||||||||||||||||
| ### The Widget System | ||||||||||||||||||
|
|
||||||||||||||||||
| ## License | ||||||||||||||||||
| The UI is a tree of widgets, with `App` at its root. This system was built entirely from scratch as a learning exercise. | ||||||||||||||||||
|
|
||||||||||||||||||
| This project is licensed under the GNU General Public License v3.0 (GPL-3.0). | ||||||||||||||||||
| - **Inheritance**: Polymorphism is achieved using a `WidgetOps` struct within the base `Widget`, which acts as a "vtable" pointing to a widget's specific implementations for `draw`, `on_input`, `on_resize`, etc. | ||||||||||||||||||
| - **Drawing**: The system uses a double-buffering strategy. Each widget draws to a `Canvas`, and the final canvas is compared against the previous frame to write only changed `Cell`s to the terminal. | ||||||||||||||||||
| - **Limitations**: As a from-scratch implementation, the widget, drawing, and focus systems are quite basic. They demonstrate core TUI concepts but could be significantly improved for more complex UI scenarios and are a known area of weakness. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### The Editor Engine | ||||||||||||||||||
|
|
||||||||||||||||||
| The core of the text editing functionality is split into three main parts: | ||||||||||||||||||
|
|
||||||||||||||||||
| 1. **`TextBuffer`**: This is the data model. It stores the document's text as a doubly-linked list of `Line` structs. For efficient editing, it employs a **gap buffer** within the `String` of the *current line*. Insertions and deletions happen at the gap, which is almost always an O(1) operation. | ||||||||||||||||||
| - **Note**: This is a good example of the project's learning nature. The performance benefit of the gap buffer is currently limited, as other features (namely the syntax highlighter) frequently force the gap to be merged back into the text. Optimizing this interaction is a known area for future work. | ||||||||||||||||||
| 2. **`TextLayout`**: This is the "view model". It is responsible for calculating how the text in the `TextBuffer` should be displayed, handling line wrapping, tab expansion, and scrolling. | ||||||||||||||||||
| 3. **`TextEdit`**: This is the "controller". It provides an API for all text manipulations (e.g., `TextEdit_InsertChar`, `TextEdit_MoveUp`), modifying the `TextBuffer` and marking the `TextLayout` as dirty. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Syntax Highlighting | ||||||||||||||||||
|
|
||||||||||||||||||
| The highlighting engine is designed to be extensible. | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Declarative Definitions**: Syntax rules are defined in simple `.ini` files, consisting of a `[meta]` section and multiple `[block:...]` sections. | ||||||||||||||||||
| - **Regex-Based Blocks**: Each block is defined by a `start` regex and an optional `end` regex. Blocks can be nested by specifying `child_blocks`. | ||||||||||||||||||
| - **Stateful Parsing**: The engine (`SyntaxHighlighting`) processes text line by line, maintaining a stack of open blocks. It uses the context from the end of the previous line to correctly highlight constructs that span multiple lines. | ||||||||||||||||||
|
|
||||||||||||||||||
| You may obtain a copy of the license at: | ||||||||||||||||||
| ## How to Read the Code | ||||||||||||||||||
|
|
||||||||||||||||||
| If you're new to the codebase, here's a recommended path: | ||||||||||||||||||
|
|
||||||||||||||||||
| 1. **Start at `src/main.c`**: Understand the initialization sequence and the main event loop. | ||||||||||||||||||
| 2. **Explore `common/`**: Look at `string.h` and `table.h`, as these are used everywhere. | ||||||||||||||||||
| 3. **Understand the UI**: `display/widget.h` is the base for all UI elements, and `widgets/components/editor.h` is the main editor widget. | ||||||||||||||||||
| 4. **Dive into the Editor Core**: `document/textbuffer.h` (storage), `document/textlayout.h` (display logic), and `syntax/highlighting.h` (highlighting logic). | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Coding Style | ||||||||||||||||||
|
|
||||||||||||||||||
| The project follows a consistent C coding style. | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Naming Conventions**: | ||||||||||||||||||
| - Types and structs are `PascalCase` (e.g., `TextBuffer`). | ||||||||||||||||||
| - Public functions are `Module_Function` (e.g., `TextBuffer_Init`). | ||||||||||||||||||
| - Enums and macros are `ALL_CAPS`. | ||||||||||||||||||
|
Comment on lines
+102
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix markdown list indentation. The nested list items under "Naming Conventions" are indented with 2 spaces, but markdown expects them to align with their parent list markers (0 indentation). This causes markdownlint to flag inconsistent indentation. - **Naming Conventions**:
- - Types and structs are `PascalCase` (e.g., `TextBuffer`).
- - Public functions are `Module_Function` (e.g., `TextBuffer_Init`).
- - Enums and macros are `ALL_CAPS`.
+ - Types and structs are `PascalCase` (e.g., `TextBuffer`).
+ - Public functions are `Module_Function` (e.g., `TextBuffer_Init`).
+ - Enums and macros are `ALL_CAPS`.📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.18.1)103-103: Inconsistent indentation for list items at the same level (MD005, list-indent) 103-103: Unordered list indentation (MD007, ul-indent) 104-104: Inconsistent indentation for list items at the same level (MD005, list-indent) 104-104: Unordered list indentation (MD007, ul-indent) 105-105: Inconsistent indentation for list items at the same level (MD005, list-indent) 105-105: Unordered list indentation (MD007, ul-indent) 🤖 Prompt for AI Agents |
||||||||||||||||||
| - **Object-Oriented C**: The code emulates object-oriented patterns. Modules are centered around a primary struct, and functions that operate on it take a pointer to that struct as their first argument (e.g., `void Widget_Draw(Widget *self, ...)`). | ||||||||||||||||||
| - **Memory Management**: All memory is managed manually. `_Create` functions `malloc` resources, and `_Destroy` functions `free` them. `_Init` and `_Deinit` pairs manage the contents of a struct without allocating/freeing the struct itself. | ||||||||||||||||||
| - **Formatting**: 4-space indentation. The opening brace `{` is always placed on the same line as the corresponding function declaration or control statement (`if`, `for`, `while`, etc.). | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Future Goals | ||||||||||||||||||
|
|
||||||||||||||||||
| This project is actively used for learning and experimentation. Here are some of the planned improvements and refactoring goals: | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Adopt a Rope Data Structure**: Replace the current `TextBuffer` implementation (a linked list of lines) with a Rope. This more advanced data structure should offer better performance for large files and complex editing operations. This will also require a corresponding redesign of the syntax highlighting engine to work efficiently with the new structure. | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Implement Common Editor Features**: While not the primary focus, implementing standard text editor functionalities such as searching, and find and replace, remains a long-term goal. | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Simplify the Widget System**: Refactor the core widget system, particularly the drawing and focus management logic. The goal is to create a simpler, more robust implementation that is easier to maintain, since the application's UI needs are relatively modest and centered around the main editor component. | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Refactor Application Setup**: Move the responsibility for creating and connecting widgets from the `main` function into the `App` module. This will centralize UI setup and make `App` the true root of the application, improving modularity. | ||||||||||||||||||
|
|
||||||||||||||||||
| - **Improve `const`-Correctness in the String Library**: Modify the `String` implementation to rebuild its internal UTF-8 indexing cache immediately after any modification. This change will allow read-only functions (like `String_GetChar`) to accept a `const String*`, enforcing better `const`-correctness throughout the API. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## License | ||||||||||||||||||
|
|
||||||||||||||||||
| This project is licensed under the GNU General Public License v3.0 (GPL-3.0). | ||||||||||||||||||
|
|
||||||||||||||||||
| https://www.gnu.org/licenses/gpl-3.0.html | ||||||||||||||||||
| You may obtain a copy of the license at: https://www.gnu.org/licenses/gpl-3.0.html | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrap bare URL in markdown link syntax. Line 128 contains a bare URL. Per markdownlint (MD034), URLs should be wrapped in markdown link syntax or angle brackets. - You may obtain a copy of the license at: https://www.gnu.org/licenses/gpl-3.0.html
+ You may obtain a copy of the license at: <https://www.gnu.org/licenses/gpl-3.0.html>📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.18.1)128-128: Bare URL used (MD034, no-bare-urls) 🤖 Prompt for AI Agents |
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # This is a simplified version of the INI file format supported by the projects IniParser module | ||
|
|
||
| [meta] | ||
| name = INI | ||
| file_extensions = ini | ||
|
|
||
| [block:root] | ||
| child_blocks = comment, section, assignment | ||
| color = 15 | ||
|
|
||
| [block:comment] | ||
| # comments start with ";" or "#" | ||
| start = "(;|#)" | ||
| end = "$" | ||
| color = 242 # gray | ||
| child_blocks = | ||
|
|
||
| [block:section] | ||
| # section: [section_name] | ||
| start = "^\\[.*\\]" | ||
| end = "$" | ||
| color = 33 # cyan | ||
| child_blocks = comment | ||
|
|
||
| [block:assignment] | ||
| start = [ \t]*[.\-_:a-zA-Z0-9]+[ \t]*=[ \t]* | ||
| end = $ | ||
| color=82 | ||
| child_blocks= number, string, bare_string, comment | ||
|
|
||
| [block:number] | ||
| start = [0-9]+ | ||
| color = 43 | ||
|
|
||
| [block:string] | ||
| start = "\"" | ||
| end = "\"|$" | ||
| color = 160 | ||
| child_blocks = escape_chars | ||
|
|
||
| [block:bare_string] | ||
| start = "." | ||
| end = $ | ||
| color = 67 | ||
| ends_on = comment | ||
|
|
||
| [block:escape_chars] | ||
| start = "\\\\[\\\\\\nt'\"]" | ||
| color = 217 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| [meta] | ||
| name = Markdown | ||
| file_extensions = md | ||
|
|
||
| [block:root] | ||
| child_blocks = title1, title2, title3, codeblock, code, emph, img | ||
| color = 15 | ||
|
|
||
| [block:title1] | ||
| start = "^# (.*)$" | ||
| color = 33 | ||
| child_blocks = | ||
|
|
||
| [block:title2] | ||
| start = "^## (.*)$" | ||
| color = 33 | ||
|
|
||
| [block:title3] | ||
| start = "^### (.*)$" | ||
| color = 33 | ||
|
|
||
| [block:code] | ||
| start = `[^`]*` # make pattern non-greedy by explicitly exclude ` from permitted characters | ||
| color = 64 | ||
|
|
||
|
defname marked this conversation as resolved.
|
||
| [block:emph] | ||
| start = \*\* | ||
| end = \*\* | ||
| color = 45 | ||
|
|
||
| [block:img] | ||
| start = !\[.*\]\([^\)]+\) | ||
| color = 92 | ||
|
|
||
| [block:codeblock] | ||
| start = "^```" | ||
| end = "^```" | ||
| color = 64 | ||
| child_blocks = | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hyphenate compound adjective "UTF-8-aware".
When "UTF-8-aware" precedes a noun, it should be hyphenated to form a compound adjective. This appears in two places: line 12 (Features section) and line 59 (Core Concepts section).
And on line 59:
Also applies to: 59-59
🤖 Prompt for AI Agents