├── .github ├── FUNDING.yml ├── labeler │ ├── awaiting.yml │ ├── contents.yml │ └── rewview.yml └── workflows │ ├── labeler.yml │ └── ci.yaml ├── docs └── lilly-banner.png ├── experiment ├── tui_render │ ├── lib │ │ └── utf8 │ │ │ ├── emoji_test_set.v │ │ │ ├── utf8.v │ │ │ └── utf8_test.v │ ├── v.mod │ ├── cp_utf8_module.vsh │ ├── immediate_grid.v │ └── emoji_grid.v ├── RobotoMono-Regular.ttf ├── clipboard │ ├── stdlib.v │ ├── wrap_pbpaste.v │ ├── r_x11.c │ └── x11.c.v ├── pattern_search │ ├── search_test.v │ ├── search.v │ └── search.c ├── gap_buffer │ ├── gap_buffer.v │ └── gap_buffer_test.v └── gui_render │ └── main.v ├── v.mod ├── .gitattributes ├── src ├── config │ └── default_lilly.conf ├── lib │ ├── buffer │ │ ├── distance.v │ │ ├── distance_test.v │ │ ├── range.v │ │ ├── position.v │ │ ├── range_test.v │ │ ├── position_test.v │ │ └── line_buffer.v │ ├── ui │ │ ├── buffer_editor.v │ │ ├── splash-logo.txt │ │ ├── buffer_cursor.v │ │ ├── status_line.v │ │ ├── todo_comments_picker_modal_test.v │ │ ├── todo_comments_picker_modal.v │ │ ├── file_picker_modal.v │ │ └── splash_screen.v │ ├── core │ │ ├── core_test.v │ │ ├── core.v │ │ ├── glyphs.v │ │ └── mode.v │ ├── clipboard │ │ ├── clipboard_darwin.v │ │ ├── clipboard.v │ │ ├── stdlib_backend.v │ │ ├── mock_clipboard.v │ │ ├── clipboard_linux.v │ │ ├── xclip_backend.v │ │ └── wayland_backend.v │ ├── utf8 │ │ ├── emoji_test_set.v │ │ ├── utf8.v │ │ └── utf8_test.v │ ├── clipboardv3 │ │ ├── clipboard_fallback.v │ │ ├── clipboard.v │ │ ├── clipboard_test.v │ │ ├── clipboard_darwin.c.v │ │ ├── clipboard_linux.v │ │ └── clipboard_darwin.m │ ├── draw │ │ ├── painting.v │ │ ├── ctx.v │ │ ├── gui_d_gui.v │ │ └── tui_immediate_ctx.v │ ├── history │ │ ├── history_test.v │ │ └── history.v │ ├── syntax │ │ ├── syntax_test.v │ │ ├── syntax.v │ │ └── parser.v │ ├── clipboardv2 │ │ └── clipboard.v │ ├── search │ │ ├── search.v │ │ └── search_test.v │ ├── chords │ │ ├── chords_test.v │ │ └── chords.v │ ├── workspace │ │ ├── workspace.v │ │ └── syntax_test.v │ └── theme │ │ └── theme.v ├── syntax │ ├── perl.syntax │ ├── c.syntax │ ├── rust.syntax │ ├── v.syntax │ ├── typescript.syntax │ ├── go.syntax │ ├── javascript.syntax │ └── python.syntax ├── cmd_args_test.v ├── panic_hook.c.v ├── debug_view.v └── main_test.v ├── feature-note.txt ├── .gitignore ├── windows-dev-notes.txt ├── README.md └── make.vsh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tauraamui 2 | ko_fi: tauraamui 3 | -------------------------------------------------------------------------------- /docs/lilly-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tauraamui/lilly/HEAD/docs/lilly-banner.png -------------------------------------------------------------------------------- /experiment/tui_render/lib/utf8/emoji_test_set.v: -------------------------------------------------------------------------------- 1 | module utf8 2 | 3 | pub const emoji_shark_char = '🦈' 4 | -------------------------------------------------------------------------------- /.github/labeler/awaiting.yml: -------------------------------------------------------------------------------- 1 | "Awaiting Review": 2 | - changed-files: 3 | - any-glob-to-any-file: "**/*" 4 | 5 | -------------------------------------------------------------------------------- /experiment/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tauraamui/lilly/HEAD/experiment/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /.github/labeler/contents.yml: -------------------------------------------------------------------------------- 1 | "Scope: Tests Only": 2 | - changed-files: 3 | - any-glob-to-all-files: "**/*_test.v" 4 | 5 | -------------------------------------------------------------------------------- /experiment/tui_render/v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'tui_render' 3 | description: '' 4 | version: '0.0.0' 5 | license: 'MIT' 6 | dependencies: [] 7 | } 8 | -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'lilly' 3 | description: 'TUI editor and VIM/Neovim alternative' 4 | version: '0.1.0' 5 | license: 'Apache-2.0' 6 | dependencies: [] 7 | } 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.bat eol=crlf 3 | 4 | **/*.v linguist-language=V 5 | **/*.vv linguist-language=V 6 | **/*.vsh linguist-language=V 7 | **/v.mod linguist-language=V 8 | -------------------------------------------------------------------------------- /experiment/clipboard/stdlib.v: -------------------------------------------------------------------------------- 1 | import src.lib.clipboardv3.x11 2 | 3 | fn main() { 4 | mut s_clip := x11.new_clipboard() 5 | defer { 6 | s_clip.shutdown_with_persistence() 7 | } 8 | s_clip.set_text('Some example text for stdlib clipboard copy!') 9 | } 10 | -------------------------------------------------------------------------------- /src/config/default_lilly.conf: -------------------------------------------------------------------------------- 1 | { 2 | "leader_key": " ", 3 | "relative_line_numbers": true, 4 | "insert_tabs_not_spaces": true, 5 | "selection_highlight_color": { "r": 96, "g": 138, "b": 143 }, 6 | "background_color": { "r": 59, "g": 34, "b": 76 }, 7 | "theme": "petal" 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/buffer/distance.v: -------------------------------------------------------------------------------- 1 | module buffer 2 | 3 | pub struct Distance { 4 | pub: 5 | lines int 6 | offset int 7 | } 8 | 9 | pub fn Distance.of_str(from string) Distance { 10 | return Distance{ 11 | lines: from.runes().count(it == lf) 12 | offset: from.split([lf].string()).last().len 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/labeler/rewview.yml: -------------------------------------------------------------------------------- 1 | # Label to be added when the PR is updated: 2 | "Coder Has Actioned Review": 3 | - changed-files: 4 | - any-glob-to-any-file: "**/*" 5 | 6 | # Condition is always false so the label is removed when the PR is updated: 7 | "Reviewed: Action Needed": 8 | - changed-files: 9 | - any-glob-to-any-file: "!**/*" 10 | 11 | -------------------------------------------------------------------------------- /feature-note.txt: -------------------------------------------------------------------------------- 1 | For this feature, we already acquire a list of contiguous tokens in the form of "identifiers", 2 | for the new buffer view document renderer. In order to treat keywords particularly and highlight 3 | them their own descrete colour we simply need to match if an identifier is found in the specific 4 | language's syntax codex. 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/buffer/distance_test.v: -------------------------------------------------------------------------------- 1 | module buffer 2 | 3 | fn test_distance_from_string_of_single_line_of_data() { 4 | assert Distance.of_str('line') == Distance{ 5 | lines: 0 6 | offset: 4 7 | } 8 | } 9 | 10 | fn test_distance_from_string_with_trailing_newline() { 11 | assert Distance.of_str('trailing newline\n') == Distance{ 12 | lines: 1 13 | offset: 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/buffer/range.v: -------------------------------------------------------------------------------- 1 | module buffer 2 | 3 | @[noinit] 4 | pub struct Range { 5 | pub: 6 | start Position 7 | end Position 8 | } 9 | 10 | pub fn Range.new(start Position, end Position) Range { 11 | if start > end { 12 | return Range{ 13 | start: end 14 | end: start 15 | } 16 | } 17 | return Range{start, end} 18 | } 19 | 20 | pub fn (range Range) includes(position Position) bool { 21 | return position >= range.start && position < range.end 22 | } 23 | -------------------------------------------------------------------------------- /experiment/tui_render/cp_utf8_module.vsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S v 2 | 3 | working_dir := getwd() 4 | utf8_module_path := join_path(working_dir, 'src', 'lib', 'utf8') 5 | tui_render_experiment_path := join_path(working_dir, 'experiment', 'tui_render') 6 | experiment_lib_dest_path := join_path(tui_render_experiment_path, 'lib', 'utf8') 7 | cp_all(utf8_module_path, experiment_lib_dest_path, true) or { 8 | println('unable to cp ${utf8_module_path} to ${experiment_lib_dest_path} -> ${err}') 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | lilly 4 | *.exe 5 | *.exe~ 6 | *.so 7 | *.dylib 8 | *.dll 9 | 10 | # Ignore binary output folders 11 | bin/ 12 | 13 | # Ignore common editor/system specific metadata 14 | .DS_Store 15 | .idea/ 16 | .vscode/ 17 | *.iml 18 | 19 | # ENV 20 | .env 21 | 22 | # vweb and database 23 | *.db 24 | *.js 25 | debug.log 26 | src/src 27 | windows.sess 28 | lilly.panic.log 29 | lillygui 30 | src/src.dSYM/Contents 31 | src/.githash 32 | experiment/tui_render/emoji_test_set.v 33 | -------------------------------------------------------------------------------- /src/syntax/perl.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Perl", 3 | "extensions": [".pl"], 4 | "keywords": [ 5 | "__DATA__", 6 | "else", 7 | "lock", 8 | "qw", 9 | "__END__", 10 | "elsif", 11 | "lt", 12 | "qx", 13 | "__FILE__", 14 | "eq", 15 | "m", 16 | "s", 17 | "__LINE__", 18 | "exp", 19 | "ne", 20 | "sub", 21 | "__PACKAGE__", 22 | "for", 23 | "no", 24 | "tr", 25 | "and", 26 | "foreach", 27 | "or", 28 | "unless", 29 | "use", 30 | "cmp", 31 | "ge", 32 | "package", 33 | "until", 34 | "continue", 35 | "gt", 36 | "q", 37 | "while", 38 | "CORE", 39 | "if", 40 | "qq", 41 | "xor", 42 | "do", 43 | "le", 44 | "qr", 45 | "y" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/syntax/c.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "C", 3 | "extensions": [".c", ".h", ".cpp", ".hh", ".hpp" ], 4 | "keywords": [ 5 | "auto", 6 | "break", 7 | "case", 8 | "const", 9 | "continue", 10 | "default", 11 | "do", 12 | "else", 13 | "enum", 14 | "extern", 15 | "for", 16 | "goto", 17 | "if", 18 | "register", 19 | "return", 20 | "sizeof", 21 | "static", 22 | "struct", 23 | "switch", 24 | "typedef", 25 | "union", 26 | "volatile", 27 | "while" 28 | ], 29 | "literals": [ 30 | "bool", 31 | "char", 32 | "double", 33 | "false", 34 | "float", 35 | "int", 36 | "long", 37 | "short", 38 | "signed", 39 | "true", 40 | "unsigned", 41 | "void" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/ui/buffer_editor.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module ui 16 | 17 | pub struct BufferEditor {} 18 | -------------------------------------------------------------------------------- /src/cmd_args_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module main 16 | 17 | fn test_options_matches_given_args_list_values() { 18 | options := resolve_options_from_args(['--debug']) 19 | assert options.debug_mode 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/core/core_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module core 16 | 17 | fn test_is_binary_file_checks_text_file_successfully() { 18 | assert is_binary_file('non-existent-file.txt') == true // if file not found, plays it safe 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/clipboard/clipboard_darwin.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboard 16 | 17 | import clipboard as stdlib_clipboard 18 | 19 | fn new_clipboard() Clipboard { 20 | return StdLibClipboard{ 21 | ref: stdlib_clipboard.new() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /experiment/pattern_search/search_test.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | fn test_compute_lps_buffer_from_pattern() { 4 | pattern := 'ABACDABAB' 5 | mut lsp := []int{len: pattern.len} 6 | compute_lps(pattern, mut lsp) 7 | assert lsp == [0, 0, 1, 0, 0, 1, 2, 3, 2] 8 | } 9 | 10 | fn test_kmp_search() { 11 | mut text := '// -x TODO(tauraamui) [29/01/25]: some comment contents' 12 | mut pattern := 'TODO' 13 | assert kmp(text, pattern) == 3 14 | 15 | text = 'ABABDABACDABABCABAB' 16 | pattern = 'ABACDABAB' 17 | assert kmp(text, pattern) == 5 18 | } 19 | 20 | fn test_kmp_rudimentary_attempt_select_full_comment() { 21 | mut text := '// -x TODO(tauraamui) [29/01/25]: some comment contents' 22 | mut pattern := 'TODO' 23 | start := kmp(text, pattern) 24 | end := kmp(text, ']:') + ']:'.len 25 | assert start == 3 26 | assert end == 30 27 | assert text[start..end].str() == 'TODO(tauraamui) [29/01/25]:' 28 | } 29 | -------------------------------------------------------------------------------- /src/panic_hook.c.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | 17 | fn C.open(const_pathname &char, flags int, mode int) int 18 | 19 | fn persist_stderr_to_disk() { 20 | fd := C.open(c'lilly.panic.log', C.O_CREAT | C.O_WRONLY | C.O_APPEND, 0o666) 21 | C.dup2(fd, C.STDERR_FILENO) 22 | } 23 | -------------------------------------------------------------------------------- /src/syntax/rust.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "extensions": [".rs"], 4 | "keywords": [ 5 | "as", 6 | "async", 7 | "await", 8 | "break", 9 | "const", 10 | "continue", 11 | "crate", 12 | "dyn", 13 | "else", 14 | "enum", 15 | "extern", 16 | "false", 17 | "fn", 18 | "for", 19 | "if", 20 | "impl", 21 | "in", 22 | "let", 23 | "loop", 24 | "match", 25 | "mod", 26 | "move", 27 | "mut", 28 | "pub", 29 | "ref", 30 | "return", 31 | "self", 32 | "Self", 33 | "static", 34 | "struct", 35 | "super", 36 | "trait", 37 | "true", 38 | "type", 39 | "unsafe", 40 | "use", 41 | "where", 42 | "while" 43 | ], 44 | "literals": [ 45 | "bool", 46 | "char", 47 | "f32", 48 | "f64", 49 | "i8", 50 | "i16", 51 | "i32", 52 | "i64", 53 | "i128", 54 | "isize", 55 | "str", 56 | "u8", 57 | "u16", 58 | "u32", 59 | "u64", 60 | "u128", 61 | "usize" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/clipboard/clipboard.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboard 16 | 17 | pub interface Clipboard { 18 | mut: 19 | copy(text string) bool 20 | paste() string 21 | } 22 | 23 | pub fn new() Clipboard { 24 | $if windows { 25 | return new() 26 | } $else { 27 | return new_clipboard() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/ui/splash-logo.txt: -------------------------------------------------------------------------------- 1 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⣼⣧⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 2 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣄⠀⠀⠀⣼⡿⢿⣧⠀⠀⠀⣠⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 3 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣆⠀⢠⣿⡇⢸⣿⡄⠀⣰⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 4 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣶⣤⡀⠀⠀⢸⣿⣿⣿⣿⣧⣾⣿⣿⣿⣿⣷⣼⣿⣿⣿⣿⡇⠀⠀⢀⣤⣶⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 5 | ⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠘⣿⣿⠿⣶⡀⢸⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⣿⡇⢀⣶⠿⣿⣿⠃⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀ 6 | ⠀⠀⠀⠀⠀⠀⣿⣿⣷⣦⠀⢿⣿⣷⣿⣇⠀⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⣿⠀⣸⣿⣾⣿⡿⠀⣴⣶⣿⣿⠀⠀⠀⠀⠀⠀ 7 | ⠀⠀⠀⠀⠀⠀⢹⣿⣿⢿⣇⠈⢿⣿⣿⣿⠀⢸⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⡇⠀⣿⣿⣿⡿⠁⣸⡿⣿⣿⡏⠀⠀⠀⠀⠀⠀ 8 | ⠀⠀⠀⢠⣤⣤⣤⣿⣿⣿⣿⣆⠈⢻⣿⣿⣧⠀⢿⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⣿⣿⡿⠀⣼⣿⣿⡟⠁⣰⣿⣿⣿⣿⣤⣤⣤⡄⠀⠀⠀ 9 | ⠀⠀⠀⠀⢻⣿⣿⡿⣿⣿⣿⣿⣧⡀⠹⢿⣿⡄⠈⢿⣿⣿⣿⣿⡇⢸⣿⣿⣿⣿⡿⠁⢠⣿⡿⠏⢀⣼⣿⣿⣿⣿⢿⣿⣿⡟⠀⠀⠀⠀ 10 | ⠀⠀⠀⠀⠀⠙⢿⣷⣌⠙⠻⢿⣿⣿⣦⡈⠹⢿⡄⠈⢿⣿⣿⣿⡇⢸⣿⣿⣿⡿⠁⢠⡿⠏⢁⣴⣿⣿⡿⠟⠋⣡⣾⡿⠋⠀⠀⠀⠀⠀ 11 | ⠀⠀⠀⠀⠀⠀⠀⠙⢿⣷⣦⣄⡈⠛⠿⣿⣦⡀⠙⠄⠀⠻⣿⣿⡇⢸⣿⣿⠟⠀⠠⠋⢀⣴⣿⠿⠛⢁⣠⣴⣾⡿⠋⠀⠀⠀⠀⠀⠀⠀ 12 | ⠀⠀⠀g⢀⠄⠀⠀p⠙⠻⣿⣿⣶⣦⣄⣈⠉⠀⠀⠀⠀⠈⠻⡇⢸⠟⠁⠀⠀⠀⠀⠉⣁⣠⣴⣶⣿⣿⠟⠋⠀⠀g⠠⡀⠀⠀⠀⠀ 13 | ⠀⠀⠀g⣼⠀⠀⠀⠀⠀⠀p⠉⠛⠻⠿⢿⣿⣷⣶⣦⠤⠄⠀⠀⠀⠀⠠⠤⣴⣶⣾⣿⡿⠿⠟⠛⠉⠀⠀⠀⠀⠀⠀g⣧⠀⠀⠀⠀ 14 | ⠀⠀⠀g⣿⣷⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⣿⠀⠀⠀⠀ 15 | ⠀⠀⠀g⠘⢿⣿⣿⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣿⣿⣿⡿⠃⠀⠀⠀⠀ 16 | ⠀⠀⠀⠀⠀g⠉⠛⠿⣿⣿⣿⣷⣶⣶⣶⣤⣤⣤⣤⣤⣤⣤⣤⣄⣠⣤⣤⣤⣤⣤⣤⣤⣤⣶⣶⣶⣾⣿⣿⣿⠿⠛⠉⠀⠀⠀⠀⠀⠀ 17 | ⠀⠀⠀⠀⠀⠀⠀⠀g⠈⠙⠻⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀ 18 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀g⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 19 | -------------------------------------------------------------------------------- /experiment/pattern_search/search.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | fn compute_lps(pattern string, mut lps []int) { 4 | mut i := 1 5 | mut j := 0 6 | 7 | for i < pattern.len { 8 | if pattern[i] == pattern[j] { 9 | j += 1 10 | lps[i] = j 11 | i += 1 12 | continue 13 | } 14 | if j > 0 { 15 | j = lps[j - 1] 16 | continue 17 | } 18 | lps[i] = 0 19 | i += 1 20 | } 21 | } 22 | 23 | fn kmp(text string, pattern string) int { 24 | mut lps := []int{len: pattern.len} 25 | compute_lps(pattern, mut lps) 26 | 27 | mut i := 0 28 | mut j := 0 29 | 30 | for i < text.len { 31 | if text[i] == pattern[j] { 32 | if j == pattern.len - 1 { 33 | return i - j 34 | } 35 | i += 1 36 | j += 1 37 | continue 38 | } 39 | // use lps to skip comps 40 | if j > 0 { 41 | j = lps[j - 1] 42 | } else { 43 | i += 1 44 | } 45 | } 46 | 47 | return -1 48 | } 49 | 50 | fn main() { 51 | text := 'ABABDABACDABABCABAB' 52 | pattern := 'ABABCABAB' 53 | kmp(text, pattern) 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /windows-dev-notes.txt: -------------------------------------------------------------------------------- 1 | Notes from trying to compile and run Lilly on Windows 2 | 3 | Requirements: git, tee 4 | 5 | 1. Download V for windows from vlang.io 6 | 2. Execute `v run install-clockwork.vsh` 7 | 8 | If you try and run Lilly on windows without using `.` it panics due to being able to understand the 9 | FQN paths on windows. 10 | 11 | When running the splash screen, for some reason the top line incorrectly positioned and wraps 12 | When in insert mode, it's unable to figure out that `CTRL+[` is actually equals, but this is only 13 | a problem when it is insert mode... 14 | 15 | When pressing any key, the last 8-9 lines randomly flicker on and off, even if no input is being 16 | processed, nor when the screen is actually changing per frame 17 | 18 | When the modal is open and covering lines, when pressing any key, that part of the view also starts to 19 | flicker randomly as well. This suggests that any part of the view flickers if rendering on top of something 20 | being already rendered is occurring. 21 | 22 | -------------------------------------------------------------------------------- /experiment/tui_render/lib/utf8/utf8.v: -------------------------------------------------------------------------------- 1 | module utf8 2 | 3 | pub fn str_clamp_to_visible_length(s string, max_width int) string { 4 | if max_width <= 0 { 5 | return '' 6 | } 7 | 8 | if utf8_str_visible_length(s) <= max_width { 9 | return s 10 | } 11 | 12 | mut result := []rune{} 13 | mut current_width := 0 14 | mut i := 0 15 | 16 | s_bytes := s.bytes() 17 | for i < s_bytes.len { 18 | // determine utf-8 sequence length for current char 19 | c_char := s_bytes[i] 20 | ul := ((0xe5000000 >> ((c_char >> 3) & 0x1e)) & 3) + 1 21 | 22 | if i + ul > s_bytes.len { 23 | break 24 | } 25 | 26 | prev_width := current_width 27 | // copy all bytes for current char into temporary slice to check visual len 28 | temp := s_bytes[i..(i + ul)].byterune() or { break } 29 | visual_width := utf8_str_visible_length([temp].string()) 30 | 31 | if current_width + visual_width > max_width { 32 | break 33 | } 34 | 35 | result << temp 36 | current_width += visual_width 37 | 38 | i += ul 39 | } 40 | 41 | return result.string() 42 | } 43 | -------------------------------------------------------------------------------- /src/syntax/v.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "V", 3 | "extensions": [ 4 | ".v", 5 | ".vv", 6 | ".vsh" 7 | ], 8 | "fmt_cmd": "v fmt -w ", 9 | "keywords": [ 10 | "as", 11 | "asm", 12 | "assert", 13 | "atomic", 14 | "break", 15 | "const", 16 | "continue", 17 | "defer", 18 | "else", 19 | "$else", 20 | "enum", 21 | "fn", 22 | "for", 23 | "go", 24 | "goto", 25 | "$if", 26 | "if", 27 | "import", 28 | "in", 29 | "interface", 30 | "is", 31 | "isreftype", 32 | "lock", 33 | "match", 34 | "module", 35 | "mut", 36 | "or", 37 | "pub", 38 | "return", 39 | "rlock", 40 | "select", 41 | "shared", 42 | "sizeof", 43 | "static", 44 | "struct", 45 | "spawn", 46 | "type", 47 | "typeof", 48 | "union", 49 | "unsafe", 50 | "volatile", 51 | "__offsetof" 52 | ], 53 | "literals": [ 54 | "none", 55 | "nil", 56 | "true", 57 | "false" 58 | ], 59 | "builtins": [ 60 | "print", 61 | "println", 62 | "eprint", 63 | "eprintln", 64 | "exit", 65 | "panic", 66 | "print_backtrace" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/clipboard/stdlib_backend.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboard 16 | 17 | import clipboard as stdlib_clipboard 18 | 19 | @[heap] 20 | struct StdLibClipboard { 21 | mut: 22 | ref &stdlib_clipboard.Clipboard 23 | } 24 | 25 | fn (mut stdlibclipboard StdLibClipboard) copy(text string) bool { 26 | return stdlibclipboard.ref.copy(text) 27 | } 28 | 29 | fn (mut stdlibclipboard StdLibClipboard) paste() string { 30 | return stdlibclipboard.ref.paste() 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/utf8/emoji_test_set.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module utf8 16 | 17 | pub const emoji_shark_char = '🦈' 18 | 19 | pub const emojis = { 20 | 'shark': '🦈' 21 | 'whale': '🐳' 22 | 'dolphin': '🐬' 23 | 'octopus': '🐙' 24 | 'crab': '🦀' 25 | 'squid': '🦑' 26 | 'turtle': '🐢' 27 | 'fish': '🐟' 28 | 'tropical_fish': '🐠' 29 | 'blowfish': '🐡' 30 | 'seal': '🦭' 31 | 'diving_mask': '🤿' 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/clipboard/mock_clipboard.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboard 16 | 17 | @[heap] 18 | struct MockClipboard { 19 | mut: 20 | copied_content string 21 | was_copy_unsuccessful bool 22 | } 23 | 24 | fn (mut mockclipboard MockClipboard) copy(text string) bool { 25 | mockclipboard.copied_content = text 26 | return !mockclipboard.was_copy_unsuccessful 27 | } 28 | 29 | fn (mut mockclipboard MockClipboard) paste() string { 30 | return mockclipboard.copied_content 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/clipboardv3/clipboard_fallback.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboardv3 16 | 17 | struct FallbackClipboard { 18 | mut: 19 | content ?ClipboardContent 20 | } 21 | 22 | fn new_fallback_clipboard() Clipboard { 23 | return FallbackClipboard{} 24 | } 25 | 26 | fn (mut clipboard FallbackClipboard) get_content() ?ClipboardContent { 27 | return clipboard.content 28 | } 29 | 30 | fn (mut clipboard FallbackClipboard) set_content(content ClipboardContent) { 31 | clipboard.content = content 32 | } 33 | -------------------------------------------------------------------------------- /experiment/tui_render/lib/utf8/utf8_test.v: -------------------------------------------------------------------------------- 1 | module utf8 2 | 3 | fn test_str_clamp_to_visible_length_match_max_to_str_len() { 4 | example_str := 'A1B2C3D4E5' 5 | assert str_clamp_to_visible_length(example_str, example_str.len) == 'A1B2C3D4E5' 6 | } 7 | 8 | fn test_str_clamp_to_visible_length_max_less_str_len() { 9 | example_str := 'A1B2C3D4E5' 10 | assert str_clamp_to_visible_length(example_str, 3) == 'A1B' 11 | } 12 | 13 | fn test_str_clamp_to_visible_length_shark_emoji_less_than_visual_size() { 14 | // shark emoji visually takes up 2 chars 15 | example_str := emoji_shark_char 16 | assert str_clamp_to_visible_length(example_str, 1) == '' 17 | } 18 | 19 | fn test_str_clamp_to_visible_length_shark_emoji_to_same_as_visual_size() { 20 | // shark emoji visually takes up 2 chars 21 | example_str := emoji_shark_char 22 | assert str_clamp_to_visible_length(example_str, 2) == emoji_shark_char 23 | } 24 | 25 | fn test_str_clamp_to_visible_length_shark_emoji_to_more_than_visual_size() { 26 | // shark emoji visually takes up 2 chars 27 | example_str := emoji_shark_char 28 | assert str_clamp_to_visible_length(example_str, 20) == emoji_shark_char 29 | } 30 | -------------------------------------------------------------------------------- /src/syntax/typescript.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TypeScript", 3 | "extensions": [".ts", ".tsx"], 4 | "keywords": [ 5 | "arguments", 6 | "await", 7 | "break", 8 | "case", 9 | "catch", 10 | "class", 11 | "const", 12 | "continue", 13 | "debugger", 14 | "default", 15 | "delete", 16 | "do", 17 | "else", 18 | "enum", 19 | "eval", 20 | "export", 21 | "extends", 22 | "finally", 23 | "for", 24 | "function", 25 | "if", 26 | "implements", 27 | "import", 28 | "in", 29 | "instanceof", 30 | "interface", 31 | "let", 32 | "new", 33 | "null", 34 | "of", 35 | "package", 36 | "private", 37 | "protected", 38 | "prototype", 39 | "public", 40 | "return", 41 | "static", 42 | "super", 43 | "switch", 44 | "this", 45 | "throw", 46 | "try", 47 | "typeof", 48 | "var", 49 | "void", 50 | "while", 51 | "with", 52 | "yield", 53 | "any", 54 | "boolean", 55 | "declare", 56 | "get", 57 | "module", 58 | "require", 59 | "number", 60 | "set", 61 | "string", 62 | "symbol", 63 | "type", 64 | "from" 65 | ], 66 | "literals": [ 67 | "NaN", 68 | "false", 69 | "true", 70 | "undefined" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/draw/painting.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module draw 16 | 17 | pub fn paint_shape_text(mut ctx Contextable, x int, y int, color Color, text string) { 18 | ctx.set_color(r: color.r, g: color.g, b: color.b) 19 | ctx.reset_bg_color() 20 | ctx.draw_text(x, y, text) 21 | } 22 | 23 | pub fn paint_text_on_background(mut ctx Contextable, x int, y int, bg_color Color, fg_color Color, text string) { 24 | ctx.set_bg_color(r: bg_color.r, g: bg_color.g, b: bg_color.b) 25 | ctx.set_color(r: fg_color.r, g: fg_color.g, b: fg_color.b) 26 | ctx.draw_text(x, y, text) 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/clipboard/clipboard_linux.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboard 16 | 17 | import clipboard as stdlib_clipboard 18 | import os 19 | 20 | fn new_clipboard() Clipboard { 21 | // NOTE(tauraamui): temp disable wayland clipboard support 22 | // if os_running_wayland() { 23 | // $if !test { return WaylandClipboard{} } 24 | // } 25 | $if test { 26 | return MockClipboard{} 27 | } 28 | return StdLibClipboard{ 29 | ref: stdlib_clipboard.new_primary() 30 | } 31 | } 32 | 33 | fn os_running_wayland() bool { 34 | return os.getenv('WAYLAND_DISPLAY').len > 0 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/history/history_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module history 16 | 17 | import lib.diff { Op } 18 | 19 | fn test_generate_diff_ops_twixt_two_file_versions() { 20 | fake_file_1 := [ 21 | '1. first existing line', 22 | ] 23 | 24 | fake_file_2 := [ 25 | '1. first existing line', 26 | '2. second new line which was added', 27 | ] 28 | 29 | mut his := History{} 30 | his.append_ops_to_undo(fake_file_1, fake_file_2) 31 | 32 | assert his.undos.array() == [ 33 | Op{ 34 | line_num: 0 35 | value: '2. second new line which was added' 36 | kind: 'ins' 37 | }, 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | labeler: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Content-Based 18 | uses: actions/labeler@v5 19 | with: 20 | configuration-path: ".github/labeler/contents.yml" 21 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 22 | 23 | - name: Awaiting Review 24 | uses: actions/labeler@v5 25 | if: github.event.action == 'opened' 26 | with: 27 | configuration-path: ".github/labeler/awaiting.yml" 28 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 29 | 30 | - name: Coder Has Actioned Review 31 | uses: actions/labeler@v5 32 | # https://github.community/t/do-something-if-a-particular-label-is-set/17149/4 33 | if: "contains(github.event.pull_request.labels.*.name, 'Reviewed: Action Needed')" 34 | with: 35 | configuration-path: ".github/labeler/review.yml" 36 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 37 | sync-labels: true 38 | 39 | -------------------------------------------------------------------------------- /src/syntax/go.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Go", 3 | "extensions": [".go"], 4 | "keywords": [ 5 | "break", 6 | "case", 7 | "chan", 8 | "const", 9 | "continue", 10 | "default", 11 | "defer", 12 | "else", 13 | "fallthrough", 14 | "for", 15 | "func", 16 | "go", 17 | "goto", 18 | "if", 19 | "import", 20 | "interface", 21 | "map", 22 | "package", 23 | "range", 24 | "return", 25 | "select", 26 | "struct", 27 | "switch", 28 | "type", 29 | "var" 30 | ], 31 | "literals": [ 32 | "nil", 33 | "true", 34 | "false", 35 | "error", 36 | "bool", 37 | "string", 38 | "int", 39 | "int8", 40 | "int16", 41 | "int32", 42 | "int64", 43 | "float32", 44 | "float64", 45 | "uint", 46 | "uint8", 47 | "uint16", 48 | "uint32", 49 | "uint64", 50 | "uintptr", 51 | "byte", 52 | "rune", 53 | "complex64", 54 | "complex128" 55 | ], 56 | "builtins": [ 57 | "append", 58 | "cap", 59 | "clear", 60 | "close", 61 | "complex", 62 | "copy", 63 | "delete", 64 | "imag", 65 | "len", 66 | "make", 67 | "max", 68 | "min", 69 | "new", 70 | "panic", 71 | "print", 72 | "println", 73 | "real", 74 | "recover" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/syntax/javascript.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JavaScript", 3 | "extensions": [".js", ".jsx", ".mjs"], 4 | "keywords": [ 5 | "arguments", 6 | "await", 7 | "break", 8 | "case", 9 | "catch", 10 | "class", 11 | "const", 12 | "continue", 13 | "debugger", 14 | "default", 15 | "delete", 16 | "do", 17 | "else", 18 | "enum", 19 | "eval", 20 | "export", 21 | "extends", 22 | "finally", 23 | "for", 24 | "function", 25 | "global", 26 | "if", 27 | "implements", 28 | "import", 29 | "in", 30 | "instanceof", 31 | "interface", 32 | "let", 33 | "new", 34 | "null", 35 | "package", 36 | "private", 37 | "protected", 38 | "prototype", 39 | "public", 40 | "return", 41 | "static", 42 | "super", 43 | "switch", 44 | "this", 45 | "throw", 46 | "try", 47 | "typeof", 48 | "var", 49 | "void", 50 | "while", 51 | "with", 52 | "yield" 53 | ], 54 | "literals": [ 55 | "Infinity", 56 | "NaN", 57 | "false", 58 | "globalThis", 59 | "true", 60 | "undefined" 61 | ], 62 | "builtins": [ 63 | "eval", 64 | "isFinite", 65 | "isNaN", 66 | "parseFloat", 67 | "parseInt", 68 | "decodeURI", 69 | "decodeURIComponent", 70 | "encodeURI", 71 | "encodeURIComponent", 72 | "escape", 73 | "unescape" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/syntax/syntax_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module syntax 16 | 17 | import os 18 | 19 | const t_lilly_config_root_dir_name = 'lilly' 20 | const t_lilly_syntaxes_dir_name = 'syntaxes' 21 | 22 | @[assert_continues] 23 | fn test_loads_builtin_syntax() { 24 | builtins := load_builtin_syntaxes() 25 | assert builtins.len == 8 26 | assert builtins[0].name == 'V' 27 | assert builtins[1].name == 'Go' 28 | assert builtins[2].name == 'C' 29 | assert builtins[3].name == 'Rust' 30 | assert builtins[4].name == 'JavaScript' 31 | assert builtins[5].name == 'TypeScript' 32 | assert builtins[6].name == 'Python' 33 | assert builtins[7].name == 'Perl' 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/clipboardv3/clipboard.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboardv3 16 | 17 | pub enum ContentType as u8 { 18 | @none 19 | inline 20 | block 21 | } 22 | 23 | pub struct ClipboardContent { 24 | pub mut: 25 | data string 26 | type ContentType 27 | } 28 | 29 | pub interface Clipboard { 30 | mut: 31 | get_content() ?ClipboardContent 32 | set_content(content ClipboardContent) 33 | } 34 | 35 | pub fn new() Clipboard { 36 | $if test { 37 | return new_fallback_clipboard() 38 | } 39 | 40 | $if darwin { 41 | return new_darwin_clipboard() 42 | } 43 | 44 | $if linux { 45 | return new_linux_clipboard() 46 | } 47 | return new_fallback_clipboard() 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/core/core.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module core 16 | 17 | import os 18 | 19 | pub fn is_binary_file(path string) bool { 20 | mut f := os.open(path) or { return true } 21 | mut buf := []u8{len: 1024} 22 | bytes_read := f.read_bytes_into(0, mut buf) or { return true } 23 | 24 | mut non_text_bytes := 0 25 | for i in 0 .. bytes_read { 26 | b := buf[i] 27 | // count bytes outside printable ASCII range 28 | if (b < 32 && b != 9 && b != 10 && b != 13) || b > 126 { 29 | non_text_bytes += 1 30 | } 31 | } 32 | 33 | // if more than 30% of read bytes are non-text, consider it to be a binary file 34 | return (f64(non_text_bytes) / f64(bytes_read)) > 0.3 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/clipboardv2/clipboard.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboardv2 16 | 17 | pub enum ContentType as u8 { 18 | @none 19 | inline 20 | block 21 | } 22 | 23 | pub struct ClipboardContent { 24 | pub: 25 | type ContentType 26 | data string 27 | } 28 | 29 | pub struct Clipboard { 30 | mut: 31 | content ClipboardContent 32 | } 33 | 34 | pub fn new() &Clipboard { 35 | return &Clipboard{ 36 | content: ClipboardContent{ 37 | type: .none 38 | } 39 | } 40 | } 41 | 42 | pub fn (mut clipboard Clipboard) get_content() ClipboardContent { 43 | return clipboard.content 44 | } 45 | 46 | pub fn (mut clipboard Clipboard) set_content(content ClipboardContent) { 47 | clipboard.content = content 48 | // update system clipboard here 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/clipboard/xclip_backend.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboard 16 | 17 | import os 18 | 19 | @[heap] 20 | struct XClipClipboard {} 21 | 22 | fn (mut xclipboard XClipClipboard) copy(text string) bool { 23 | mut cmd := os.Command{ 24 | path: "echo '${text}' | xclip -i -r" 25 | } 26 | defer { cmd.close() or {} } 27 | cmd.start() or { return false } 28 | return cmd.exit_code == 0 29 | } 30 | 31 | fn (xclipboard XClipClipboard) paste() []string { 32 | mut cmd := os.Command{ 33 | path: 'xclip -o' 34 | } 35 | defer { cmd.close() or {} } 36 | cmd.start() or { panic(err) } 37 | 38 | mut out := []string{} 39 | for { 40 | out << cmd.read_line() 41 | if cmd.eof { 42 | break 43 | } 44 | } 45 | 46 | if cmd.exit_code == 0 { 47 | return out 48 | } 49 | 50 | return [] 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/clipboard/wayland_backend.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboard 16 | 17 | import os 18 | 19 | @[heap] 20 | struct WaylandClipboard {} 21 | 22 | fn (mut wclipboard WaylandClipboard) copy(text string) bool { 23 | mut cmd := os.Command{ 24 | path: 'echo ${text} | wl-copy -n' 25 | } 26 | defer { cmd.close() or {} } 27 | 28 | cmd.start() or { panic(err) } 29 | 30 | return cmd.exit_code == 0 31 | } 32 | 33 | fn (wclipboard WaylandClipboard) paste() []string { 34 | mut cmd := os.Command{ 35 | path: 'wl-paste -n' 36 | } 37 | defer { cmd.close() or {} } 38 | 39 | cmd.start() or { panic(err) } 40 | 41 | mut out := []string{} 42 | for { 43 | out << cmd.read_line() 44 | if cmd.eof { 45 | break 46 | } 47 | } 48 | 49 | if cmd.exit_code == 0 { 50 | return out 51 | } 52 | return [] 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/history/history.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module history 16 | 17 | import datatypes 18 | import lib.diff { Op } 19 | 20 | pub struct History { 21 | mut: 22 | undos datatypes.Stack[Op] // will actually be type diff.Op 23 | redos datatypes.Stack[Op] 24 | } 25 | 26 | pub fn (mut history History) pop_undo() !Op { 27 | undo_op := history.undos.pop() or { return error('no pending undo operations remaining') } 28 | history.redos.push(undo_op) 29 | return undo_op 30 | } 31 | 32 | pub fn (mut history History) append_ops_to_undo(a []string, b []string) { 33 | mut ops := diff.diff(a, b) 34 | 35 | for i := ops.len - 1; i >= 0; i-- { 36 | ops[i].line_num = i - 1 37 | if ops[i].kind == 'same' { 38 | continue 39 | } 40 | } 41 | 42 | for op in ops { 43 | if op.kind == 'same' { 44 | continue 45 | } 46 | history.undos.push(op) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | permissions: 9 | contents: write # needed to push formatting commits 10 | checks: write 11 | 12 | jobs: 13 | format: 14 | if: github.event_name == 'pull_request' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | 23 | - uses: vlang/setup-v@v1.4 24 | with: 25 | check-latest: true 26 | 27 | - run: v fmt -w src/ 28 | 29 | - uses: EndBug/add-and-commit@v9 30 | with: 31 | add: 'src/' 32 | message: 'style: auto-format V code in src/' 33 | default_author: github_actions 34 | 35 | unit_tests: 36 | needs: format # waits for the formatter on PRs 37 | if: ${{ always() }} # still runs on push to master 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | with: 42 | ref: ${{ github.event.pull_request.head.ref || github.ref }} 43 | repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} 44 | 45 | - uses: vlang/setup-v@v1.4 46 | with: 47 | check-latest: true 48 | 49 | - run: v run ./make.vsh build 50 | - run: v run ./make.vsh verbose-test 51 | -------------------------------------------------------------------------------- /experiment/clipboard/wrap_pbpaste.v: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | fn set_content() { 5 | mut p := os.new_process('/usr/bin/pbcopy') 6 | p.set_redirect_stdio() 7 | p.run() 8 | 9 | p.stdin_write('set clipboard to me') 10 | os.fd_close(p.stdio_fd[0]) 11 | 12 | p.close() 13 | p.wait() 14 | println('ERR: ${p.err}, CODE: ${p.code}') 15 | } 16 | 17 | fn get_content() { 18 | mut out := []string{} 19 | mut er := []string{} 20 | mut rc := 0 21 | 22 | mut p := os.new_process('/usr/bin/pbpaste') 23 | p.set_redirect_stdio() 24 | p.run() 25 | 26 | for p.is_alive() { 27 | if data := p.pipe_read(.stderr) { 28 | eprintln('p.pipe_read .stderr, len: ${data.len:4} | data: `${data#[0..10]}`...') 29 | er << data 30 | } 31 | if data := p.pipe_read(.stdout) { 32 | eprintln('p.pipe_read .stdout, len: ${data.len:4} | data: `${data#[0..10]}`...') 33 | out << data 34 | } 35 | // avoid a busy loop, by sleeping a bit between each iteration 36 | time.sleep(2 * time.millisecond) 37 | } 38 | 39 | out << p.stdout_slurp() 40 | er << p.stderr_slurp() 41 | p.close() 42 | p.wait() 43 | 44 | if p.code > 0 { 45 | eprintln('----------------------------------------------------------') 46 | eprintln('COMMAND: pbcopy') 47 | eprintln('STDOUT:\n${out}') 48 | eprintln('STDERR:\n${er}') 49 | eprintln('----------------------------------------------------------') 50 | rc = 1 51 | } 52 | 53 | println('${out.join('')}, ${rc}, ${er.join('')}') 54 | } 55 | 56 | fn main() { 57 | // set_content() 58 | get_content() 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/search/search.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module search 16 | 17 | fn compute_lps(pattern []rune, mut lps []int) { 18 | mut i := 1 19 | mut j := 0 20 | 21 | for i < pattern.len { 22 | if pattern[i] == pattern[j] { 23 | j += 1 24 | lps[i] = j 25 | i += 1 26 | continue 27 | } 28 | if j > 0 { 29 | j = lps[j - 1] 30 | continue 31 | } 32 | lps[i] = 0 33 | i += 1 34 | } 35 | } 36 | 37 | pub fn kmp(text []rune, pattern []rune) int { 38 | mut lps := []int{len: pattern.len} 39 | compute_lps(pattern, mut lps) 40 | 41 | mut i := 0 42 | mut j := 0 43 | 44 | for i < text.len { 45 | if text[i] == pattern[j] { 46 | if j == pattern.len - 1 { 47 | return i - j 48 | } 49 | i += 1 50 | j += 1 51 | continue 52 | } 53 | // use lps to skip comps 54 | if j > 0 { 55 | j = lps[j - 1] 56 | } else { 57 | i += 1 58 | } 59 | } 60 | 61 | return -1 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/search/search_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module search 16 | 17 | fn test_compute_lps_buffer_from_pattern() { 18 | pattern := 'ABACDABAB'.runes() 19 | mut lsp := []int{len: pattern.len} 20 | compute_lps(pattern, mut lsp) 21 | assert lsp == [0, 0, 1, 0, 0, 1, 2, 3, 2] 22 | } 23 | 24 | fn test_kmp_search() { 25 | mut text := '// -x TODO(tauraamui) [29/01/25]: some comment contents'.runes() 26 | mut pattern := 'TODO'.runes() 27 | assert kmp(text, pattern) == 6 28 | 29 | text = 'ABABDABACDABABCABAB'.runes() 30 | pattern = 'ABACDABAB'.runes() 31 | assert kmp(text, pattern) == 5 32 | } 33 | 34 | fn test_kmp_rudimentary_attempt_select_full_comment() { 35 | mut text := '// -x TODO(tauraamui) [29/01/25]: some comment contents'.runes() 36 | mut pattern := 'TODO'.runes() 37 | start := kmp(text, pattern) 38 | end := kmp(text, ']:'.runes()) + ']:'.len 39 | assert start == 6 40 | assert end == 33 41 | assert text[start..end].string() == 'TODO(tauraamui) [29/01/25]:' 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/buffer/position.v: -------------------------------------------------------------------------------- 1 | module buffer 2 | 3 | @[noinit] 4 | pub struct Position { 5 | PositionFields 6 | } 7 | 8 | pub struct PositionArgs { 9 | PositionFields 10 | } 11 | 12 | @[params] 13 | struct PositionFields { 14 | pub: 15 | line int 16 | offset int 17 | } 18 | 19 | // pub fn Position.new(line int, offset int) Position { 20 | pub fn Position.new(args PositionArgs) Position { 21 | line := args.line 22 | offset := args.offset 23 | return Position{ 24 | line: if line < 0 { 0 } else { line } 25 | offset: if offset < 0 { 0 } else { offset } 26 | } 27 | } 28 | 29 | pub fn (p Position) add(d Distance) Position { 30 | return Position.new(line: p.line + d.lines, offset: p.offset + d.offset) 31 | } 32 | 33 | pub fn (p Position) sub(d Distance) Position { 34 | return Position.new(line: p.line - d.lines, offset: p.offset - d.offset) 35 | } 36 | 37 | pub fn (a Position) distance(b Position) Distance { 38 | return Distance{ 39 | lines: if a < b { b.line - a.line } else { a.line - b.line } 40 | offset: if a < b { b.offset - a.offset } else { a.offset - b.offset } 41 | } 42 | } 43 | 44 | pub fn (mut p Position) apply(d Distance) { 45 | p = p.add(d) 46 | } 47 | 48 | const less = true 49 | const greater = false 50 | 51 | fn (a Position) < (b Position) bool { 52 | if a.line < b.line { 53 | return less 54 | } else if a.line > b.line { 55 | return greater 56 | } else if a.offset < b.offset { 57 | return less 58 | } else if a.offset > b.offset { 59 | return greater 60 | } 61 | return greater 62 | } 63 | 64 | fn (a Position) == (b Position) bool { 65 | return a.line == b.line && a.offset == b.offset 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/utf8/utf8.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module utf8 16 | 17 | pub fn str_clamp_to_visible_length(s string, max_width int) string { 18 | if max_width <= 0 { 19 | return '' 20 | } 21 | 22 | if utf8_str_visible_length(s) <= max_width { 23 | return s 24 | } 25 | 26 | mut result := []rune{} 27 | mut current_width := 0 28 | mut i := 0 29 | 30 | s_bytes := s.bytes() 31 | for i < s_bytes.len { 32 | // determine utf-8 sequence length for current char 33 | c_char := s_bytes[i] 34 | ul := ((0xe5000000 >> ((c_char >> 3) & 0x1e)) & 3) + 1 35 | 36 | if i + ul > s_bytes.len { 37 | break 38 | } 39 | 40 | // copy all bytes for current char into temporary slice to check visual len 41 | temp := s_bytes[i..(i + ul)].byterune() or { break } 42 | visual_width := utf8_str_visible_length([temp].string()) 43 | 44 | if current_width + visual_width > max_width { 45 | break 46 | } 47 | 48 | result << temp 49 | current_width += visual_width 50 | 51 | i += ul 52 | } 53 | 54 | return result.string() 55 | } 56 | -------------------------------------------------------------------------------- /src/debug_view.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module main 16 | 17 | import lib.draw 18 | 19 | struct Debug { 20 | file_path string 21 | } 22 | 23 | const font_size = 16 24 | 25 | pub fn (mut debug Debug) jump_line_to_middle(y int) {} 26 | 27 | fn (mut debug Debug) set_from(from int) {} 28 | 29 | // It turns out that the TUI renderer actually considers 0 + 1 to be the same thing. 30 | // So technically we can say that the top left first possible to render position is 1, 1 instead of 0, 0. 31 | // Ok, for so for GUI rendering of text, or at least the invocation of "draw_text", it also seems to consider 32 | // single incrementations of Y as being a full char height span. 33 | fn (mut debug Debug) draw(mut ctx draw.Contextable) { 34 | for j in 0 .. ctx.window_height() { 35 | for i in 0 .. 10 { 36 | ctx.draw_text((font_size / 2) + i, j, '${i}') 37 | } 38 | } 39 | } 40 | 41 | fn (mut debug Debug) on_key_down(e draw.Event, mut r Root) { 42 | if e.code == .escape { 43 | r.quit() or {} 44 | } 45 | } 46 | 47 | fn (mut debug Debug) on_mouse_scroll(e draw.Event) {} 48 | -------------------------------------------------------------------------------- /experiment/pattern_search/search.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void computeLPSArray(const char *pattern, int patternLength, int *lps) { 5 | int length = 0; // length of the previous longest prefix suffix 6 | lps[0] = 0; // lps[0] is always 0 7 | int i = 1; 8 | 9 | while (i < patternLength) { 10 | if (pattern[i] == pattern[length]) { 11 | length++; 12 | lps[i] = length; 13 | i++; 14 | } else { 15 | if (length != 0) { 16 | length = lps[length - 1]; 17 | } else { 18 | lps[i] = 0; 19 | i++; 20 | } 21 | } 22 | } 23 | } 24 | 25 | void KMPSearch(const char *text, const char *pattern) { 26 | int textLength = strlen(text); 27 | int patternLength = strlen(pattern); 28 | int lps[patternLength]; // Preallocated memory for LPS array 29 | 30 | computeLPSArray(pattern, patternLength, lps); 31 | 32 | int i = 0; // index for text 33 | int j = 0; // index for pattern 34 | while (i < textLength) { 35 | if (pattern[j] == text[i]) { 36 | i++; 37 | j++; 38 | } 39 | 40 | if (j == patternLength) { 41 | printf("Pattern found at index %d\n", i - j); 42 | j = lps[j - 1]; 43 | } else if (i < textLength && pattern[j] != text[i]) { 44 | if (j != 0) { 45 | j = lps[j - 1]; 46 | } else { 47 | i++; 48 | } 49 | } 50 | } 51 | } 52 | 53 | int main() { 54 | const char *text = "ABABDABACDABABCABAB"; 55 | const char *pattern = "ABABCABAB"; 56 | KMPSearch(text, pattern); 57 | return 0; 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/utf8/utf8_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module utf8 16 | 17 | fn test_str_clamp_to_visible_length_match_max_to_str_len() { 18 | example_str := 'A1B2C3D4E5' 19 | assert str_clamp_to_visible_length(example_str, example_str.len) == 'A1B2C3D4E5' 20 | } 21 | 22 | fn test_str_clamp_to_visible_length_max_less_str_len() { 23 | example_str := 'A1B2C3D4E5' 24 | assert str_clamp_to_visible_length(example_str, 3) == 'A1B' 25 | } 26 | 27 | fn test_str_clamp_to_visible_length_shark_emoji_less_than_visual_size() { 28 | // shark emoji visually takes up 2 chars 29 | example_str := emoji_shark_char 30 | assert str_clamp_to_visible_length(example_str, 1) == '' 31 | } 32 | 33 | fn test_str_clamp_to_visible_length_shark_emoji_to_same_as_visual_size() { 34 | // shark emoji visually takes up 2 chars 35 | example_str := emoji_shark_char 36 | assert str_clamp_to_visible_length(example_str, 2) == emoji_shark_char 37 | } 38 | 39 | fn test_str_clamp_to_visible_length_shark_emoji_to_more_than_visual_size() { 40 | // shark emoji visually takes up 2 chars 41 | example_str := emoji_shark_char 42 | assert str_clamp_to_visible_length(example_str, 20) == emoji_shark_char 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/buffer/range_test.v: -------------------------------------------------------------------------------- 1 | module buffer 2 | 3 | fn test_new_range_returns_range() { 4 | assert Range.new(Position{}, Position{}) == Range{} 5 | } 6 | 7 | fn test_new_range_with_real_data_returns_range() { 8 | assert Range.new(Position{ line: 1, offset: 0 }, Position{ 9 | line: 8 10 | offset: 6 11 | }) == Range{Position{ 12 | line: 1 13 | offset: 0 14 | }, Position{ 15 | line: 8 16 | offset: 6 17 | }} 18 | } 19 | 20 | fn test_new_range_swaps_start_and_end_when_end_precedes_start() { 21 | start := Position{ 22 | line: 1 23 | offset: 4 24 | } 25 | end := Position{ 26 | line: 1 27 | offset: 1 28 | } 29 | range := Range.new(start, end) 30 | 31 | assert range.start == end 32 | assert range.end == start 33 | } 34 | 35 | fn test_new_range_swaps_start_and_end_when_end_precedes_start_on_same_line() { 36 | start := Position{ 37 | line: 0 38 | offset: 4 39 | } 40 | end := Position{ 41 | line: 0 42 | offset: 1 43 | } 44 | range := Range.new(start, end) 45 | 46 | assert range.start == end 47 | assert range.end == start 48 | } 49 | 50 | fn test_new_range_swaps_start_and_end_when_end_precedes_start_when_start_is_zeroed_out_on_same_line() { 51 | start := Position{ 52 | line: 0 53 | offset: 0 54 | } 55 | end := Position{ 56 | line: 0 57 | offset: 8 58 | } 59 | range := Range.new(start, end) 60 | 61 | assert range.start == Position.new(line: 0, offset: 0) 62 | assert range.end == Position.new(line: 0, offset: 8) 63 | } 64 | 65 | fn test_new_range_does_not_swap_start_and_end_if_end_does_not_precedes_start() { 66 | start := Position{ 67 | line: 0 68 | offset: 4 69 | } 70 | end := Position{ 71 | line: 1 72 | offset: 1 73 | } 74 | range := Range.new(start, end) 75 | 76 | assert range.start == start 77 | assert range.end == end 78 | } 79 | -------------------------------------------------------------------------------- /src/syntax/python.syntax: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python", 3 | "extensions": [ 4 | ".py" 5 | ], 6 | "keywords": [ 7 | "and", 8 | "as", 9 | "assert", 10 | "async", 11 | "await", 12 | "break", 13 | "class", 14 | "continue", 15 | "def", 16 | "del", 17 | "elif", 18 | "else", 19 | "except", 20 | "finally", 21 | "for", 22 | "from", 23 | "global", 24 | "if", 25 | "import", 26 | "in", 27 | "is", 28 | "lambda", 29 | "nonlocal", 30 | "not", 31 | "or", 32 | "pass", 33 | "raise", 34 | "return", 35 | "try", 36 | "while", 37 | "with", 38 | "yield" 39 | ], 40 | "literals": [ 41 | "False", 42 | "None", 43 | "True" 44 | ], 45 | "builtins": [ 46 | "abs", 47 | "aiter", 48 | "all", 49 | "anext", 50 | "any", 51 | "ascii", 52 | "bin", 53 | "bool", 54 | "breakpoint", 55 | "bytearray", 56 | "bytes", 57 | "callable", 58 | "chr", 59 | "classmethod", 60 | "compile", 61 | "complex", 62 | "delattr", 63 | "dict", 64 | "dir", 65 | "divmod", 66 | "enumerate", 67 | "eval", 68 | "exec", 69 | "filter", 70 | "float", 71 | "format", 72 | "frozenset", 73 | "getattr", 74 | "globals", 75 | "hasattr", 76 | "hash", 77 | "help", 78 | "hex", 79 | "id", 80 | "input", 81 | "int", 82 | "isinstance", 83 | "issubclass", 84 | "iter", 85 | "len", 86 | "list", 87 | "locals", 88 | "map", 89 | "max", 90 | "memoryview", 91 | "min", 92 | "next", 93 | "object", 94 | "oct", 95 | "open", 96 | "ord", 97 | "pow", 98 | "print", 99 | "property", 100 | "range", 101 | "repr", 102 | "reversed", 103 | "round", 104 | "set", 105 | "setattr", 106 | "slice", 107 | "sorted", 108 | "staticmethod", 109 | "str", 110 | "sum", 111 | "super", 112 | "tuple", 113 | "type", 114 | "vars", 115 | "zip", 116 | "__import__" 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /src/lib/core/glyphs.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module core 16 | 17 | pub const block = '█' 18 | pub const slant_left_flat_bottom = '' 19 | pub const left_rounded = '' 20 | pub const slant_left_flat_top = '' 21 | pub const slant_right_flat_bottom = '' 22 | pub const right_rounded = '' 23 | pub const slant_right_flat_top = '' 24 | 25 | pub const rune_digits = [`0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`] 26 | 27 | pub const zero_width_unicode = [ 28 | `\u034f`, // U+034F COMBINING GRAPHEME JOINER 29 | `\u061c`, // U+061C ARABIC LETTER MARK 30 | `\u17b4`, // U+17B4 KHMER VOWEL INHERENT AQ 31 | `\u17b5`, // U+17B5 KHMER VOWEL INHERENT AA 32 | `\u200a`, // U+200A HAIR SPACE 33 | `\u200b`, // U+200B ZERO WIDTH SPACE 34 | `\u200c`, // U+200C ZERO WIDTH NON-JOINER 35 | `\u200d`, // U+200D ZERO WIDTH JOINER 36 | `\u200e`, // U+200E LEFT-TO-RIGHT MARK 37 | `\u200f`, // U+200F RIGHT-TO-LEFT MARK 38 | `\u2060`, // U+2060 WORD JOINER 39 | `\u2061`, // U+2061 FUNCTION APPLICATION 40 | `\u2062`, // U+2062 INVISIBLE TIMES 41 | `\u2063`, // U+2063 INVISIBLE SEPARATOR 42 | `\u2064`, // U+2064 INVISIBLE PLUS 43 | `\u206a`, // U+206A INHIBIT SYMMETRIC SWAPPING 44 | `\u206b`, // U+206B ACTIVATE SYMMETRIC SWAPPING 45 | `\u206c`, // U+206C INHIBIT ARABIC FORM SHAPING 46 | `\u206d`, // U+206D ACTIVATE ARABIC FORM SHAPING 47 | `\u206e`, // U+206E NATIONAL DIGIT SHAPES 48 | `\u206f`, // U+206F NOMINAL DIGIT SHAPES 49 | `\ufeff`, // U+FEFF ZERO WIDTH NO-BREAK SPACE 50 | ] 51 | -------------------------------------------------------------------------------- /src/lib/draw/ctx.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module draw 16 | 17 | import term.ui as tui 18 | import lib.theme as themelib 19 | 20 | pub struct Event { 21 | tui.Event 22 | } 23 | 24 | pub struct Config { 25 | pub: 26 | render_debug bool 27 | default_bg_color ?tui.Color 28 | theme themelib.Theme 29 | user_data voidptr 30 | frame_fn fn (voidptr) @[required] 31 | event_fn fn (Event, voidptr) @[required] 32 | 33 | capture_events bool 34 | use_alternate_buffer bool = true 35 | } 36 | 37 | pub struct Color { 38 | pub: 39 | r u8 40 | g u8 41 | b u8 42 | } 43 | 44 | pub type Runner = fn () ! 45 | 46 | pub interface Drawer { 47 | mut: 48 | draw_text(x int, y int, text string) 49 | write(c string) 50 | draw_rect(x int, y int, width int, height int) 51 | draw_point(x int, y int) 52 | } 53 | 54 | pub interface Colorer { 55 | mut: 56 | set_color(c Color) 57 | set_bg_color(c Color) 58 | reset_color() 59 | reset_bg_color() 60 | } 61 | 62 | pub interface Renderer { 63 | Drawer 64 | Colorer 65 | } 66 | 67 | pub interface Contextable { 68 | Renderer 69 | theme() themelib.Theme 70 | mut: 71 | render_debug() bool 72 | rate_limit_draws() bool 73 | window_width() int 74 | window_height() int 75 | 76 | set_cursor_position(x int, y int) 77 | set_cursor_to_block() 78 | set_cursor_to_underline() 79 | set_cursor_to_vertical_bar() 80 | show_cursor() 81 | hide_cursor() 82 | 83 | bold() 84 | set_style(s Style) 85 | clear_style() 86 | 87 | reset() 88 | 89 | // run() ! 90 | clear() 91 | flush() 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/clipboardv3/clipboard_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboardv3 16 | 17 | fn test_clipboard_native_implementation() ! { 18 | mut clipboard := new() 19 | clipboard.set_content(ClipboardContent{ data: 'This is copied text!', type: .inline }) 20 | assert clipboard.get_content() or { return error('failed to get contents') } == ClipboardContent{ 21 | data: 'This is copied text!' 22 | type: .inline 23 | } 24 | } 25 | 26 | fn test_clipboard_native_implementation_sets_type_to_block() ! { 27 | mut clipboard := new() 28 | clipboard.set_content(ClipboardContent{ data: 'This is copied text!', type: .block }) 29 | assert clipboard.get_content() or { return error('failed to get contents') } == ClipboardContent{ 30 | data: 'This is copied text!' 31 | type: .block 32 | } 33 | } 34 | 35 | @[if linux ?] 36 | fn test_linux_clipboard_chooses_proc_to_invoke_depending_on_window_server() { 37 | mock_get_env_x11 := fn (key string) string { 38 | return 'x11' 39 | } 40 | mock_get_env_wayland := fn (key string) string { 41 | return 'wayland' 42 | } 43 | assert is_x11(mock_get_env_x11) 44 | assert is_x11(mock_get_env_wayland) == false 45 | } 46 | 47 | @[if darwin ?] 48 | fn test_clipboard_native_implementation_returns_no_content_type_from_plaintext_data() { 49 | $if darwin { 50 | C.clipboard_set_plaintext(c'A plain text sentence with no meta data!') 51 | mut clipboard := new() 52 | assert clipboard.get_content()! == ClipboardContent{ 53 | data: 'A plain text sentence with no meta data!' 54 | type: .block 55 | } 56 | } $else { 57 | assert true == true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/clipboardv3/clipboard_darwin.c.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboardv3 16 | 17 | #include 18 | #flag -framework AppKit 19 | 20 | #include "@VMODROOT/src/lib/clipboardv3/clipboard_darwin.m" 21 | 22 | fn C.clipboard_get_plaintext() &char 23 | fn C.clipboard_set_plaintext(text &char) 24 | fn C.clipboard_set_content(data &char, t_type u8) 25 | fn C.clipboard_get_content() &CClipboardContent 26 | 27 | struct CClipboardContent { 28 | data &char 29 | t_type u8 30 | } 31 | 32 | struct DarwinClipboard {} 33 | 34 | fn new_darwin_clipboard() Clipboard { 35 | return DarwinClipboard{} 36 | } 37 | 38 | fn (mut c DarwinClipboard) get_content() ?ClipboardContent { 39 | c_content_ptr := C.clipboard_get_content() 40 | if c_content_ptr == unsafe { nil } { 41 | return none 42 | } 43 | 44 | c_content := unsafe { &c_content_ptr } 45 | 46 | mut clipboard_content := ClipboardContent{ 47 | data: '' 48 | type: .block // NOTE(tauraamui) [20/05/2025]: default insert mode should be block really 49 | // cos it means its most likely coming from a non-lilly source 50 | } 51 | 52 | if c_content.data != 0 { 53 | clipboard_content.data = unsafe { cstring_to_vstring(c_content.data) } 54 | unsafe { C.free(c_content.data) } 55 | } 56 | 57 | clipboard_content.type = unsafe { ContentType(c_content.t_type) } 58 | if clipboard_content.type == .none { 59 | clipboard_content.type = .block 60 | } 61 | 62 | unsafe { C.free(c_content_ptr) } 63 | 64 | return clipboard_content 65 | } 66 | 67 | fn (mut c DarwinClipboard) set_content(content ClipboardContent) { 68 | C.clipboard_set_content(content.data.str, u8(content.type)) 69 | } 70 | -------------------------------------------------------------------------------- /experiment/tui_render/immediate_grid.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import lib.utf8 4 | import lib.draw 5 | import rand 6 | import strings 7 | 8 | struct CharGrid { 9 | mut: 10 | run_once bool 11 | width int 12 | height int 13 | } 14 | 15 | fn CharGrid.new() CharGrid { 16 | return CharGrid{ 17 | width: 100 18 | height: 100 19 | } 20 | } 21 | 22 | fn (mut grid CharGrid) update_bounds(width int, height int) { 23 | if grid.width == width && grid.height == height { 24 | return 25 | } 26 | grid.width = width 27 | grid.height = height 28 | } 29 | 30 | const char_values = ['X', 'A', 'C', 'Y', 'T'] 31 | 32 | fn (mut grid CharGrid) draw_chars(mut ctx draw.Contextable) { 33 | for y in 0 .. grid.height { 34 | for x in 0 .. grid.width { 35 | mut index := rand.int_in_range(0, char_values.len) or { 0 } 36 | r := rand.int_in_range(100, 255) or { 255 } 37 | g := rand.int_in_range(100, 255) or { 255 } 38 | b := rand.int_in_range(100, 255) or { 255 } 39 | ctx.set_color(draw.Color{u8(r), u8(g), u8(b)}) 40 | if (y - 4 <= 0 || y + 6 >= grid.height) || (x - 15 <= 0 || x + 15 >= grid.width) { 41 | index = 3 42 | ctx.reset_color() 43 | } 44 | mut char_to_render := char_values[index] 45 | if x == 0 { 46 | char_to_render = '${y}' 47 | } 48 | ctx.draw_text(x, y, char_to_render) 49 | } 50 | } 51 | ctx.set_bg_color(draw.Color{110, 150, 200}) 52 | ctx.draw_rect(15, 10, 15, 5) 53 | ctx.reset_bg_color() 54 | ctx.set_cursor_position(1, 1) 55 | } 56 | 57 | fn (mut grid CharGrid) draw(mut ctx draw.Contextable) { 58 | ctx.hide_cursor() 59 | ctx.clear() 60 | // if grid.run_once { return } 61 | // defer { grid.run_once = true } 62 | grid.update_bounds(ctx.window_width(), ctx.window_height()) 63 | // ctx.hide_cursor() 64 | grid.draw_chars(mut ctx) 65 | ctx.flush() 66 | } 67 | 68 | struct App { 69 | mut: 70 | ui &draw.Contextable = unsafe { nil } 71 | grid &CharGrid = unsafe { nil } 72 | } 73 | 74 | fn (app App) quit() ! { 75 | exit(0) 76 | } 77 | 78 | fn frame(mut app App) { 79 | app.grid.draw(mut app.ui) 80 | } 81 | 82 | fn event(e draw.Event, mut app App) { 83 | match e.typ { 84 | .key_down { 85 | app.quit() or { panic(err) } 86 | } 87 | else {} 88 | } 89 | } 90 | 91 | fn main() { 92 | mut grid := CharGrid.new() 93 | mut app := &App{ 94 | grid: &grid 95 | } 96 | 97 | ctx, run := draw.new_context( 98 | user_data: app 99 | event_fn: event 100 | frame_fn: frame 101 | capture_events: true 102 | use_alternate_buffer: true 103 | ) 104 | app.ui = ctx 105 | 106 | run()! 107 | } 108 | -------------------------------------------------------------------------------- /src/lib/chords/chords_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module chords 16 | 17 | fn test_chord_generates_single_op_from_directions_with_no_repeat_amount() { 18 | mut chord := Chord{} 19 | assert chord.h() == Op{ 20 | kind: .move 21 | direction: .left 22 | repeat: 1 23 | } 24 | assert chord.j() == Op{ 25 | kind: .move 26 | direction: .down 27 | repeat: 1 28 | } 29 | assert chord.k() == Op{ 30 | kind: .move 31 | direction: .up 32 | repeat: 1 33 | } 34 | assert chord.l() == Op{ 35 | kind: .move 36 | direction: .right 37 | repeat: 1 38 | } 39 | assert chord.w() == Op{ 40 | kind: .move 41 | direction: .word 42 | repeat: 1 43 | } 44 | assert chord.e() == Op{ 45 | kind: .move 46 | direction: .word_end 47 | repeat: 1 48 | } 49 | } 50 | 51 | fn test_chord_generates_single_op_from_invoking_i_alone() { 52 | mut chord := Chord{} 53 | assert chord.i() == Op{ 54 | kind: .mode 55 | mode: .insert 56 | } 57 | } 58 | 59 | fn test_chord_generates_single_nop_op_from_invoking_c_alone() { 60 | mut chord := Chord{} 61 | assert chord.c() == Op{ 62 | kind: .nop 63 | } 64 | } 65 | 66 | fn test_chord_generates_single_deletion_op_from_invoking_c_and_w() { 67 | mut chord := Chord{} 68 | assert chord.c() == Op{ 69 | kind: .nop 70 | } 71 | assert chord.w() == Op{ 72 | kind: .delete 73 | direction: .word 74 | repeat: 1 75 | } 76 | } 77 | 78 | fn test_chord_generates_single_deletion_op_from_invoking_c_i_and_w() { 79 | mut chord := Chord{} 80 | assert chord.c() == Op{ 81 | kind: .nop 82 | } 83 | assert chord.i() == Op{ 84 | kind: .nop 85 | } 86 | assert chord.w() == Op{ 87 | kind: .delete 88 | direction: .inside_word 89 | repeat: 1 90 | } 91 | } 92 | 93 | fn test_chord_generates_single_nop_op_from_invoking_c_i_and_i_again() { 94 | mut chord := Chord{} 95 | assert chord.c() == Op{ 96 | kind: .nop 97 | } 98 | assert chord.i() == Op{ 99 | kind: .nop 100 | } 101 | assert chord.i() == Op{ 102 | kind: .nop 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /experiment/tui_render/emoji_grid.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import lib.utf8 4 | import lib.draw 5 | import rand 6 | 7 | struct EmojiGrid { 8 | mut: 9 | run_once bool 10 | width int 11 | height int 12 | } 13 | 14 | fn EmojiGrid.new() EmojiGrid { 15 | return EmojiGrid{ 16 | width: 10 17 | height: 10 18 | } 19 | } 20 | 21 | fn (mut grid EmojiGrid) update_bounds(width int, height int) { 22 | if grid.width == width && grid.height == height { 23 | return 24 | } 25 | grid.width = width 26 | grid.height = height 27 | } 28 | 29 | fn (mut grid EmojiGrid) draw_chars(mut ctx draw.Contextable) { 30 | for y in 0 .. grid.height { 31 | for x in 0 .. grid.width { 32 | char_to_render := if (x == 0 || x == grid.width - 1) || (y == 0 || y == grid.height - 1) { 33 | 'X' 34 | } else { 35 | 'A' 36 | } 37 | ctx.draw_text(x, y, char_to_render) 38 | } 39 | } 40 | } 41 | 42 | fn (mut grid EmojiGrid) draw_emojis(mut ctx draw.Contextable) { 43 | emoji_chars := utf8.emojis.values() 44 | for y in 0 .. grid.height { 45 | // NOTE(tauraamui) [25/04/2025]: utf8 chars take up 2 grid cells not one 46 | for x in 0 .. (grid.width / 2) { 47 | mut index := rand.int_in_range(0, emoji_chars.len) or { 0 } 48 | if (y - 4 <= 0 || y + 5 >= grid.height) || (x - 15 <= 0 || x + 15 >= (grid.width / 2)) { 49 | index = 3 50 | } 51 | emoji := emoji_chars[index] 52 | ctx.draw_text((x * 2), y, emoji) 53 | } 54 | } 55 | } 56 | 57 | fn (mut grid EmojiGrid) draw(mut ctx draw.Contextable) { 58 | ctx.hide_cursor() 59 | grid.update_bounds(ctx.window_width(), ctx.window_height()) 60 | ctx.clear() 61 | grid.draw_emojis(mut ctx) 62 | ctx.flush() 63 | } 64 | 65 | fn (grid EmojiGrid) on_key_down(e draw.Event, mut root Root2) { 66 | match e.code { 67 | .escape { 68 | root.quit() or { panic('failed to quit via root: ${err}') } 69 | } 70 | else {} 71 | } 72 | } 73 | 74 | interface Root2 { 75 | quit() ! 76 | } 77 | 78 | struct App { 79 | mut: 80 | ui &draw.Contextable = unsafe { nil } 81 | grid &EmojiGrid = unsafe { nil } 82 | } 83 | 84 | fn (app App) quit() ! { 85 | exit(0) 86 | } 87 | 88 | fn frame(mut app App) { 89 | app.grid.draw(mut app.ui) 90 | } 91 | 92 | fn event(e draw.Event, mut app App) { 93 | match e.typ { 94 | .key_down { 95 | app.grid.on_key_down(e, mut app) 96 | } 97 | else {} 98 | } 99 | } 100 | 101 | fn main() { 102 | mut grid := EmojiGrid.new() 103 | mut app := &App{ 104 | grid: &grid 105 | } 106 | 107 | ctx, run := draw.new_context( 108 | user_data: app 109 | event_fn: event 110 | frame_fn: frame 111 | capture_events: true 112 | use_alternate_buffer: true 113 | ) 114 | app.ui = ctx 115 | 116 | run()! 117 | } 118 | -------------------------------------------------------------------------------- /src/lib/core/mode.v: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module core 16 | 17 | import lib.draw 18 | 19 | pub const status_green = draw.Color{145, 237, 145} 20 | pub const status_orange = draw.Color{237, 207, 123} 21 | pub const status_lilac = draw.Color{194, 110, 230} 22 | pub const status_dark_lilac = draw.Color{154, 119, 209} 23 | pub const status_cyan = draw.Color{138, 222, 237} 24 | pub const status_purple = draw.Color{130, 144, 250} 25 | 26 | pub enum Mode as u8 { 27 | normal 28 | visual 29 | visual_line 30 | insert 31 | command 32 | search 33 | leader 34 | pending_delete 35 | replace 36 | replacing 37 | pending_g 38 | pending_f 39 | pending_z 40 | } 41 | 42 | pub fn (mode Mode) draw(mut ctx draw.Contextable, x int, y int) int { 43 | defer { ctx.reset() } 44 | label := mode.str() 45 | status_line_y := y 46 | status_line_x := x 47 | status_color := mode.color() 48 | mut offset := 0 49 | draw.paint_shape_text(mut ctx, status_line_x + offset, status_line_y, status_color, 50 | '${left_rounded}${block}') 51 | offset += 2 52 | draw.paint_text_on_background(mut ctx, status_line_x + offset, status_line_y, status_color, 53 | draw.Color{0, 0, 0}, label) 54 | offset += label.len 55 | draw.paint_shape_text(mut ctx, status_line_x + offset, status_line_y, status_color, 56 | '${block}${slant_right_flat_bottom}') 57 | offset += 2 58 | return status_line_x + offset 59 | } 60 | 61 | pub fn (mode Mode) color() draw.Color { 62 | return match mode { 63 | .normal { status_green } 64 | .visual { status_lilac } 65 | .visual_line { status_lilac } 66 | .insert { status_orange } 67 | .command { status_cyan } 68 | .search { status_purple } 69 | .leader { status_purple } 70 | .pending_delete { status_green } 71 | .pending_g { status_green } 72 | .pending_f { status_purple } 73 | .replacing { status_green } 74 | .replace { status_green } 75 | .pending_z { status_green } 76 | } 77 | } 78 | 79 | pub fn (mode Mode) str() string { 80 | return match mode { 81 | .normal { 'NORMAL' } 82 | .visual { 'VISUAL' } 83 | .visual_line { 'VISUAL LINE' } 84 | .insert { 'INSERT' } 85 | .command { 'COMMAND' } 86 | .search { 'SEARCH' } 87 | .leader { 'LEADER' } 88 | .pending_delete { 'NORMAL' } 89 | .replace { 'NORMAL' } 90 | .pending_z { 'NORMAL' } 91 | .pending_g { 'NORMAL' } 92 | .pending_f { 'SEARCH' } 93 | .replacing { 'NORMAL' } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/lib/ui/buffer_cursor.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import lib.core 4 | 5 | pub struct CursorPos { 6 | pub mut: 7 | x int 8 | y int 9 | } 10 | 11 | pub struct SelectionSpan { 12 | pub: 13 | min_x int 14 | max_x int 15 | full bool 16 | } 17 | 18 | pub struct BufferCursor { 19 | mut: 20 | sel_start_pos ?CursorPos 21 | pub mut: 22 | pos CursorPos 23 | } 24 | 25 | // TODO(tauraamui) [11/06/2025]: make this private 26 | pub fn (cursor BufferCursor) y_within_selection(line_y int) bool { 27 | if sel_pos := cursor.sel_start_pos { 28 | // NOTE(tauraamui) [06/06/2025]: need to write a style guide for this project one day 29 | // as I seem to have invented a bunch of custom practices 30 | // for writing V code which I know is at odds with the standard 31 | // but anyway, even though technically this could be an if statement 32 | // I am finding I always much much prefer making it a match if it is 33 | // being used as an assignment result 34 | start := match true { 35 | sel_pos.y < cursor.pos.y { sel_pos.y } 36 | else { cursor.pos.y } 37 | } 38 | end := match true { 39 | cursor.pos.y > sel_pos.y { cursor.pos.y } 40 | else { sel_pos.y } 41 | } 42 | return line_y >= start && line_y <= end 43 | } 44 | return false 45 | } 46 | 47 | pub fn (cursor BufferCursor) resolve_line_selection_span(mode core.Mode, line_len int, line_y int) SelectionSpan { 48 | return match mode { 49 | .visual_line { 50 | SelectionSpan{ 51 | full: cursor.y_within_selection(line_y) 52 | } 53 | } 54 | .visual { 55 | start := cursor.sel_start() or { CursorPos{} } 56 | end := cursor.sel_end() or { CursorPos{} } 57 | should_be_considered_full_line := line_y > start.y && line_y < end.y 58 | min_x := if start.y == line_y { start.x } else { 0 } 59 | max_x := if end.y == line_y { end.x } else { line_len } 60 | SelectionSpan{ 61 | min_x: min_x 62 | max_x: max_x 63 | full: should_be_considered_full_line 64 | } 65 | } 66 | else { 67 | SelectionSpan{} 68 | } 69 | } 70 | } 71 | 72 | pub fn (cursor BufferCursor) sel_start() ?CursorPos { 73 | start_pos := cursor.sel_start_pos or { return none } 74 | if start_pos.y == cursor.pos.y { 75 | if start_pos.x < cursor.pos.x { 76 | return start_pos 77 | } 78 | return cursor.pos 79 | } 80 | if start_pos.y < cursor.pos.y { 81 | return start_pos 82 | } 83 | return cursor.pos 84 | } 85 | 86 | pub fn (cursor BufferCursor) sel_end() ?CursorPos { 87 | start_pos := cursor.sel_start_pos or { return none } 88 | if start_pos.y == cursor.pos.y { 89 | if start_pos.x < cursor.pos.x { 90 | return cursor.pos 91 | } 92 | return start_pos 93 | } 94 | if start_pos.y < cursor.pos.y { 95 | return cursor.pos 96 | } 97 | return start_pos 98 | } 99 | 100 | pub fn (cursor BufferCursor) sel_active() bool { 101 | return cursor.sel_start_pos != none 102 | } 103 | 104 | pub fn (mut cursor BufferCursor) set_selection(sel CursorPos) { 105 | cursor.sel_start_pos = sel 106 | } 107 | 108 | pub fn (mut cursor BufferCursor) clear_selection() { 109 | cursor.sel_start_pos = ?CursorPos(none) 110 | } 111 | -------------------------------------------------------------------------------- /src/lib/chords/chords.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module chords 16 | 17 | import strconv 18 | 19 | pub enum Kind as u8 { 20 | nop 21 | mode 22 | move 23 | delete 24 | paste 25 | } 26 | 27 | pub enum Direction as u8 { 28 | left 29 | up 30 | right 31 | down 32 | word 33 | word_end 34 | word_reverse 35 | inside_word 36 | } 37 | 38 | pub enum Mode as u8 { 39 | insert 40 | } 41 | 42 | pub struct Op { 43 | pub: 44 | kind Kind 45 | direction Direction 46 | mode Mode 47 | repeat int 48 | } 49 | 50 | pub struct Chord { 51 | mut: 52 | pending_motion string 53 | pending_repeat_amount string 54 | } 55 | 56 | pub fn (chord Chord) pending_repeat_amount() string { 57 | return chord.pending_repeat_amount 58 | } 59 | 60 | pub fn (mut chord Chord) reset() { 61 | chord.pending_repeat_amount = '' 62 | } 63 | 64 | pub fn (mut chord Chord) append_to_repeat_amount(n string) { 65 | chord.pending_repeat_amount = '${chord.pending_repeat_amount}${n}' 66 | } 67 | 68 | pub fn (mut chord Chord) b() Op { 69 | return chord.create_op(.move, .word_reverse) 70 | } 71 | 72 | pub fn (mut chord Chord) c() Op { 73 | chord.pending_motion = 'c' 74 | return Op{ 75 | kind: .nop 76 | } 77 | } 78 | 79 | pub fn (mut chord Chord) i() Op { 80 | if chord.pending_motion.len == 0 { 81 | return Op{ 82 | kind: .mode 83 | mode: .insert 84 | } 85 | } 86 | op := Op{ 87 | kind: .nop 88 | } 89 | if chord.pending_motion == 'ci' { 90 | chord.pending_motion = '' 91 | return op 92 | } 93 | chord.pending_motion = '${chord.pending_motion}i' 94 | return op 95 | } 96 | 97 | pub fn (mut chord Chord) h() Op { 98 | return chord.create_op(.move, .left) 99 | } 100 | 101 | pub fn (mut chord Chord) l() Op { 102 | return chord.create_op(.move, .right) 103 | } 104 | 105 | pub fn (mut chord Chord) j() Op { 106 | return chord.create_op(.move, .down) 107 | } 108 | 109 | pub fn (mut chord Chord) k() Op { 110 | return chord.create_op(.move, .up) 111 | } 112 | 113 | pub fn (mut chord Chord) e() Op { 114 | return chord.create_op(.move, .word_end) 115 | } 116 | 117 | pub fn (mut chord Chord) w() Op { 118 | if chord.pending_motion == 'c' { 119 | return chord.create_op(.delete, .word) 120 | } 121 | if chord.pending_motion == 'ci' { 122 | return chord.create_op(.delete, .inside_word) 123 | } 124 | return chord.create_op(.move, .word) 125 | } 126 | 127 | pub fn (mut chord Chord) p() Op { 128 | return chord.create_op(.paste, .down) 129 | } 130 | 131 | fn (mut chord Chord) create_op(kind Kind, direction Direction) Op { 132 | defer { 133 | chord.pending_motion = '' 134 | chord.pending_repeat_amount = '' 135 | } 136 | count := strconv.atoi(chord.pending_repeat_amount) or { 1 } 137 | return Op{ 138 | kind: kind 139 | direction: direction 140 | repeat: count 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/lib/clipboardv3/clipboard_linux.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module clipboardv3 16 | 17 | import os 18 | import time 19 | 20 | const xdg_session_type_env_name = 'XDG_SESSION_TYPE' 21 | const xclip_path = '/usr/bin/xclip' 22 | const xclip_paste_args = ['-selection', 'clipboard', '-out'] 23 | const xclip_copy_args = ['-selection', 'clipboard', '-in'] 24 | const wayland_copy_path = '/usr/bin/wl-copy' 25 | const wayland_paste_path = '/usr/bin/wl-paste' 26 | const wayland_copy_args = ['--type', 'text/plain'] 27 | const wayland_paste_args = ['--type', 'text/plain'] 28 | 29 | type Getenv = fn (key string) string 30 | 31 | struct Proc { 32 | copy_proc_path string 33 | paste_proc_path string 34 | paste_args []string 35 | copy_args []string 36 | } 37 | 38 | fn resolve_clipboard_proc(os_getenv Getenv) Proc { 39 | if is_x11(os_getenv) { 40 | return Proc{ 41 | copy_proc_path: xclip_path 42 | paste_proc_path: xclip_path // on x11 we use the same util for copy and paste 43 | paste_args: xclip_paste_args 44 | copy_args: xclip_copy_args 45 | } 46 | } 47 | return Proc{ 48 | copy_proc_path: wayland_copy_path 49 | paste_proc_path: wayland_paste_path 50 | paste_args: wayland_paste_args 51 | copy_args: wayland_copy_args 52 | } 53 | } 54 | 55 | fn is_x11(os_getenv Getenv) bool { 56 | return os_getenv(xdg_session_type_env_name) == 'x11' 57 | } 58 | 59 | struct LinuxClipboard { 60 | mut: 61 | proc Proc 62 | last_type ContentType 63 | } 64 | 65 | fn new_linux_clipboard() Clipboard { 66 | return LinuxClipboard{ 67 | proc: resolve_clipboard_proc(os.getenv) 68 | last_type: .block 69 | } 70 | } 71 | 72 | fn (c LinuxClipboard) get_content() ?ClipboardContent { 73 | mut out := []string{} 74 | mut er := []string{} 75 | 76 | mut p := os.new_process(c.proc.paste_proc_path) 77 | p.set_args(c.proc.paste_args) 78 | p.set_redirect_stdio() 79 | p.run() 80 | 81 | for p.is_alive() { 82 | if data := p.pipe_read(.stderr) { 83 | er << data 84 | } 85 | if data := p.pipe_read(.stdout) { 86 | out << data 87 | } 88 | time.sleep(2 * time.millisecond) 89 | } 90 | 91 | out << p.stdout_slurp() 92 | er << p.stderr_slurp() 93 | p.close() 94 | p.wait() 95 | 96 | return ClipboardContent{ 97 | data: out.join('') 98 | type: c.last_type 99 | } 100 | } 101 | 102 | fn (mut c LinuxClipboard) set_content(content ClipboardContent) { 103 | mut p := os.new_process(c.proc.copy_proc_path) 104 | p.set_args(c.proc.copy_args) 105 | p.set_redirect_stdio() 106 | p.run() 107 | 108 | content_to_set := content.data 109 | p.stdin_write(content_to_set) 110 | os.fd_close(p.stdio_fd[0]) 111 | 112 | p.close() 113 | p.wait() 114 | 115 | start := time.now() 116 | for { 117 | if (time.now() - start).milliseconds() >= 100 { 118 | break 119 | } 120 | current_clip_content := c.get_content() or { continue } 121 | if content_to_set == current_clip_content.data { 122 | break 123 | } 124 | } 125 | c.last_type = content.type 126 | } 127 | -------------------------------------------------------------------------------- /src/lib/draw/gui_d_gui.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module draw 16 | 17 | import gg 18 | import gx 19 | import math 20 | import os 21 | 22 | struct Context { 23 | user_data voidptr 24 | frame_cb fn (v voidptr) @[required] 25 | mut: 26 | gg &gg.Context = unsafe { nil } 27 | txt_cfg gx.TextCfg 28 | foreground_color Color 29 | background_color Color 30 | text_draws_since_last_pass int 31 | } 32 | 33 | pub fn new_context(cfg Config) (&Contextable, Runner) { 34 | mut ctx := &Context{ 35 | user_data: cfg.user_data 36 | frame_cb: cfg.frame_fn 37 | } 38 | ctx.gg = gg.new_context( 39 | width: 800 40 | height: 600 41 | create_window: true 42 | window_title: 'Lilly Editor' 43 | user_data: ctx 44 | bg_color: gx.white 45 | font_path: os.resource_abs_path('../experiment/RobotoMono-Regular.ttf') 46 | frame_fn: frame 47 | ) 48 | return ctx, unsafe { ctx.run_wrapper } 49 | } 50 | 51 | const font_size = 16 52 | 53 | fn (mut ctx Context) run_wrapper() ! { 54 | ctx.gg.run() 55 | } 56 | 57 | fn (mut ctx Context) render_debug() bool { 58 | return true 59 | } 60 | 61 | fn frame(mut ctx Context) { 62 | width := gg.window_size().width 63 | mut scale_factor := gg.dpi_scale() 64 | if scale_factor <= 0 { 65 | scale_factor = 1 66 | } 67 | ctx.txt_cfg = gx.TextCfg{ 68 | size: font_size * int(scale_factor) 69 | } 70 | ctx.frame_cb(ctx.user_data) 71 | if ctx.text_draws_since_last_pass < 1000 { 72 | ctx.text_draws_since_last_pass = 0 73 | ctx.gg.end() 74 | } 75 | } 76 | 77 | fn (mut ctx Context) rate_limit_draws() bool { 78 | return false 79 | } 80 | 81 | fn (mut ctx Context) window_width() int { 82 | return gg.window_size().width 83 | } 84 | 85 | fn (mut ctx Context) window_height() int { 86 | return gg.window_size().height 87 | } 88 | 89 | fn (mut ctx Context) set_cursor_position(x int, y int) {} 90 | 91 | fn (mut ctx Context) draw_text(x int, y int, text string) { 92 | // this offsetting stuff is a bit mental but seems to be correct 93 | if ctx.text_draws_since_last_pass == 0 { 94 | ctx.gg.begin() 95 | } 96 | ctx.gg.draw_text((font_size / 2) + x - (font_size / 2), (y * font_size) - font_size, 97 | text, ctx.txt_cfg) 98 | if ctx.text_draws_since_last_pass >= 1000 { 99 | ctx.gg.end(how: .passthru) 100 | ctx.text_draws_since_last_pass = 0 101 | return 102 | } 103 | ctx.text_draws_since_last_pass += 1 104 | } 105 | 106 | fn (mut ctx Context) write(c string) {} 107 | 108 | fn (mut ctx Context) draw_rect(x int, y int, width int, height int) { 109 | c := ctx.background_color 110 | ctx.gg.draw_rect_filled(x, y - 100, width, height / 16, gx.rgb(c.r, c.g, c.b)) 111 | } 112 | 113 | fn (mut ctx Context) draw_point(x int, y int) {} 114 | 115 | fn (mut ctx Context) bold() {} 116 | 117 | fn (mut ctx Context) set_color(c Color) { 118 | ctx.foreground_color = c 119 | } 120 | 121 | fn (mut ctx Context) set_bg_color(c Color) { 122 | ctx.background_color = c 123 | } 124 | 125 | fn (mut ctx Context) reset_color() { 126 | ctx.foreground_color = Color{} 127 | } 128 | 129 | fn (mut ctx Context) reset_bg_color() {} 130 | 131 | fn (mut ctx Context) reset() { 132 | ctx.foreground_color = Color{} 133 | ctx.background_color = Color{} 134 | } 135 | 136 | fn (mut ctx Context) run() ! { 137 | ctx.gg.run() 138 | } 139 | 140 | fn (mut ctx Context) clear() { 141 | ctx.gg.begin() 142 | ctx.gg.end() 143 | } 144 | 145 | fn (mut ctx Context) flush() {} 146 | -------------------------------------------------------------------------------- /src/lib/draw/tui_immediate_ctx.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module draw 16 | 17 | import term.ui as tui 18 | import lib.theme as themelib 19 | 20 | struct ImmediateContext { 21 | render_debug bool 22 | mut: 23 | ref &tui.Context 24 | } 25 | 26 | type Runner = fn () ! 27 | 28 | pub fn new_immediate_context(cfg Config) (&Contextable, Runner) { 29 | ctx := ImmediateContext{ 30 | render_debug: cfg.render_debug 31 | ref: tui.init( 32 | user_data: cfg.user_data 33 | event_fn: fn [cfg] (e &tui.Event, app voidptr) { 34 | cfg.event_fn(Event{e}, app) 35 | } 36 | frame_fn: cfg.frame_fn 37 | capture_events: cfg.capture_events 38 | use_alternate_buffer: cfg.use_alternate_buffer 39 | frame_rate: 30 40 | ) 41 | } 42 | return ctx, unsafe { ctx.run } 43 | } 44 | 45 | fn (ctx ImmediateContext) theme() themelib.Theme { 46 | return themelib.Theme.new('test') or { panic('error occured trying to resolve theme: ${err}') } 47 | } 48 | 49 | fn (mut ctx ImmediateContext) rate_limit_draws() bool { 50 | return true 51 | } 52 | 53 | fn (mut ctx ImmediateContext) render_debug() bool { 54 | return ctx.render_debug 55 | } 56 | 57 | fn (mut ctx ImmediateContext) window_width() int { 58 | return ctx.ref.window_width 59 | } 60 | 61 | fn (mut ctx ImmediateContext) window_height() int { 62 | return ctx.ref.window_height 63 | } 64 | 65 | fn (mut ctx ImmediateContext) set_cursor_position(x int, y int) { 66 | ctx.ref.set_cursor_position(x, y) 67 | } 68 | 69 | fn (mut ctx ImmediateContext) set_cursor_to_block() { 70 | ctx.ref.write('\x1b[0 q') 71 | } 72 | 73 | fn (mut ctx ImmediateContext) set_cursor_to_underline() { 74 | ctx.ref.write('\x1b[4 q') 75 | } 76 | 77 | fn (mut ctx ImmediateContext) set_cursor_to_vertical_bar() { 78 | ctx.ref.write('\x1b[6 q') 79 | } 80 | 81 | fn (mut ctx ImmediateContext) show_cursor() { 82 | ctx.ref.show_cursor() 83 | } 84 | 85 | fn (mut ctx ImmediateContext) hide_cursor() { 86 | ctx.ref.hide_cursor() 87 | } 88 | 89 | fn (mut ctx ImmediateContext) draw_text(x int, y int, text string) { 90 | ctx.ref.draw_text(x, y, text) 91 | } 92 | 93 | fn (mut ctx ImmediateContext) write(c string) { 94 | ctx.ref.write(c) 95 | } 96 | 97 | fn (mut ctx ImmediateContext) draw_rect(x int, y int, width int, height int) { 98 | ctx.ref.draw_rect(x, y, x + (width - 1), y + (height - 1)) 99 | } 100 | 101 | fn (mut ctx ImmediateContext) draw_point(x int, y int) { 102 | ctx.ref.draw_point(x, y) 103 | } 104 | 105 | fn (mut ctx ImmediateContext) bold() { 106 | ctx.ref.bold() 107 | } 108 | 109 | fn (mut ctx ImmediateContext) set_style(s Style) {} 110 | 111 | fn (mut ctx ImmediateContext) clear_style() {} 112 | 113 | fn (mut ctx ImmediateContext) set_color(c Color) { 114 | ctx.ref.set_color(tui.Color{ r: c.r, g: c.g, b: c.b }) 115 | } 116 | 117 | fn (mut ctx ImmediateContext) set_bg_color(c Color) { 118 | ctx.ref.set_bg_color(tui.Color{ r: c.r, g: c.g, b: c.b }) 119 | } 120 | 121 | fn (mut ctx ImmediateContext) reset_color() { 122 | ctx.ref.reset_color() 123 | } 124 | 125 | fn (mut ctx ImmediateContext) reset_bg_color() { 126 | ctx.ref.reset_bg_color() 127 | } 128 | 129 | fn (mut ctx ImmediateContext) reset() { 130 | ctx.ref.reset() 131 | } 132 | 133 | fn (mut ctx ImmediateContext) run() ! { 134 | return ctx.ref.run() 135 | } 136 | 137 | fn (mut ctx ImmediateContext) clear() { 138 | ctx.ref.clear() 139 | } 140 | 141 | fn (mut ctx ImmediateContext) flush() { 142 | ctx.ref.flush() 143 | } 144 | -------------------------------------------------------------------------------- /experiment/gap_buffer/gap_buffer.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import strings 4 | import arrays 5 | 6 | const gap_size = 6 7 | 8 | struct LineTracker { 9 | line_starts []int 10 | gap_start int 11 | gap_end int 12 | } 13 | 14 | struct Buffer { 15 | c_buffer GapBuffer 16 | line_tracker LineTracker 17 | cursor_line int 18 | cursor_column int 19 | } 20 | 21 | struct GapBuffer { 22 | mut: 23 | data []rune 24 | gap_start int 25 | gap_end int 26 | } 27 | 28 | fn GapBuffer.new() GapBuffer { 29 | return GapBuffer{ 30 | data: []rune{len: gap_size, cap: gap_size} 31 | gap_start: 0 32 | gap_end: gap_size 33 | } 34 | } 35 | 36 | pub fn (mut gap_buffer GapBuffer) insert(s string) { 37 | for r in s.runes() { 38 | gap_buffer.insert_rune(r) 39 | } 40 | } 41 | 42 | pub fn (mut gap_buffer GapBuffer) backspace() { 43 | if gap_buffer.gap_start == 0 { 44 | return 45 | } 46 | gap_buffer.gap_start -= 1 47 | } 48 | 49 | pub fn (mut gap_buffer GapBuffer) delete() { 50 | if gap_buffer.gap_end + 1 == gap_buffer.data.len { 51 | return 52 | } 53 | gap_buffer.gap_end += 1 54 | } 55 | 56 | fn (mut gap_buffer GapBuffer) insert_rune(r rune) { 57 | gap_buffer.data[gap_buffer.gap_start] = r 58 | gap_buffer.gap_start += 1 59 | gap_buffer.resize_if_full() 60 | } 61 | 62 | fn (mut gap_buffer GapBuffer) move_cursor_left(count int) { 63 | max_allowed_count := gap_buffer.gap_start 64 | to_move_count := int_min(count, max_allowed_count) 65 | 66 | for _ in 0 .. to_move_count { 67 | gap_buffer.data[gap_buffer.gap_end - 1] = gap_buffer.data[gap_buffer.gap_start - 1] 68 | gap_buffer.gap_start -= 1 69 | gap_buffer.gap_end -= 1 70 | } 71 | } 72 | 73 | fn (mut gap_buffer GapBuffer) move_cursor_right(count int) { 74 | max_allowed_count := gap_buffer.data.len - gap_buffer.gap_end 75 | to_move_count := int_min(count, max_allowed_count) 76 | 77 | for _ in 0 .. to_move_count { 78 | gap_buffer.data[gap_buffer.gap_start] = gap_buffer.data[gap_buffer.gap_end] 79 | gap_buffer.gap_start += 1 80 | gap_buffer.gap_end += 1 81 | } 82 | } 83 | 84 | fn (mut gap_buffer GapBuffer) resize_if_full() { 85 | if gap_buffer.empty_gap_space_size() != 0 { 86 | return 87 | } 88 | size := gap_buffer.data.len + gap_size 89 | mut data_dest := []rune{len: size, cap: size} 90 | 91 | arrays.copy(mut data_dest[..gap_buffer.gap_start], gap_buffer.data[..gap_buffer.gap_start]) 92 | arrays.copy(mut data_dest[gap_buffer.gap_end + (gap_size - (gap_buffer.empty_gap_space_size()))..], 93 | gap_buffer.data[gap_buffer.gap_end..]) 94 | gap_buffer.gap_end += gap_size 95 | 96 | gap_buffer.data = data_dest 97 | } 98 | 99 | struct LineIterator { 100 | data string 101 | mut: 102 | line_number int 103 | line_start int 104 | line_end int 105 | done bool 106 | } 107 | 108 | fn (mut line_iter LineIterator) next() ?string { 109 | if line_iter.done { 110 | line_iter.line_start = 0 111 | line_iter.line_end = 0 112 | line_iter.done = false 113 | return none 114 | } 115 | 116 | line_iter.line_start = line_iter.line_end 117 | 118 | mut trailing_newline := false 119 | for c in line_iter.data[line_iter.line_start..].runes() { 120 | line_iter.line_end += 1 121 | if c == `\n` { 122 | trailing_newline = true 123 | break 124 | } 125 | } 126 | 127 | line := line_iter.data[line_iter.line_start..line_iter.line_end - 1] 128 | if line_iter.line_end == line_iter.data.len && !trailing_newline { 129 | line_iter.done = true 130 | } else { 131 | line_iter.line_number += 1 132 | } 133 | 134 | return line 135 | } 136 | 137 | @[inline] 138 | fn (gap_buffer GapBuffer) empty_gap_space_size() int { 139 | return gap_buffer.gap_end - gap_buffer.gap_start 140 | } 141 | 142 | @[inline] 143 | fn (gap_buffer GapBuffer) str() string { 144 | return gap_buffer.data[..gap_buffer.gap_start].string() + 145 | gap_buffer.data[gap_buffer.gap_end..].string() 146 | } 147 | 148 | fn (gap_buffer GapBuffer) raw_str() string { 149 | mut sb := strings.new_builder(512) 150 | sb.write_runes(gap_buffer.data[..gap_buffer.gap_start]) 151 | sb.write_string(strings.repeat_string('_', gap_buffer.gap_end - gap_buffer.gap_start)) 152 | sb.write_runes(gap_buffer.data[gap_buffer.gap_end..]) 153 | return sb.str() 154 | } 155 | 156 | fn main() { 157 | println('Hello World!') 158 | mut gb := GapBuffer.new() 159 | gb.insert('Hello') 160 | gb.insert(' Wo') 161 | gb.insert('rld') 162 | gb.insert('!') 163 | println(gb.str()) 164 | } 165 | -------------------------------------------------------------------------------- /src/lib/syntax/syntax.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module syntax 16 | 17 | import json 18 | 19 | const builtin_v_syntax = $embed_file('../../syntax/v.syntax').to_string() 20 | const builtin_go_syntax = $embed_file('../../syntax/go.syntax').to_string() 21 | const builtin_c_syntax = $embed_file('../../syntax/c.syntax').to_string() 22 | const builtin_rust_syntax = $embed_file('../../syntax/rust.syntax').to_string() 23 | const builtin_js_syntax = $embed_file('../../syntax/javascript.syntax').to_string() 24 | const builtin_ts_syntax = $embed_file('../../syntax/typescript.syntax').to_string() 25 | const builtin_python_syntax = $embed_file('../../syntax/python.syntax').to_string() 26 | const builtin_perl_syntax = $embed_file('../../syntax/perl.syntax').to_string() 27 | 28 | pub struct Syntax { 29 | pub: 30 | name string 31 | extensions []string 32 | keywords []string 33 | literals []string 34 | builtins []string 35 | } 36 | 37 | pub fn load_builtin_syntaxes() []Syntax { 38 | v_syntax := json.decode(Syntax, builtin_v_syntax) or { 39 | panic('builtin V syntax file failed to decode: ${err}') 40 | } 41 | go_syntax := json.decode(Syntax, builtin_go_syntax) or { 42 | panic('builtin Go syntax file failed to decode: ${err}') 43 | } 44 | c_syntax := json.decode(Syntax, builtin_c_syntax) or { 45 | panic('builtin C syntax file failed to decode: ${err}') 46 | } 47 | rust_syntax := json.decode(Syntax, builtin_rust_syntax) or { 48 | panic('builtin Rust syntax file failed to decode: ${err}') 49 | } 50 | js_syntax := json.decode(Syntax, builtin_js_syntax) or { 51 | panic('builtin JavaScript syntax file failed to decode: ${err}') 52 | } 53 | ts_syntax := json.decode(Syntax, builtin_ts_syntax) or { 54 | panic('builtin TypeScript syntax file failed to decode: ${err}') 55 | } 56 | python_syntax := json.decode(Syntax, builtin_python_syntax) or { 57 | panic('builtin Python syntax file failed to decode: ${err}') 58 | } 59 | perl_syntax := json.decode(Syntax, builtin_perl_syntax) or { 60 | panic('builting Perl syntax file failed to decode: ${err}') 61 | } 62 | 63 | return [v_syntax, go_syntax, c_syntax, rust_syntax, js_syntax, ts_syntax, python_syntax, 64 | perl_syntax] 65 | } 66 | 67 | fn load_syntaxes_from_disk(syntax_config_dir fn () !string, 68 | dir_walker fn (path string, f fn (string)), 69 | read_file fn (path string) !string) ![]Syntax { 70 | syntax_dir_full_path := syntax_config_dir() or { return err } 71 | mut syns := []Syntax{} 72 | dir_walker(syntax_dir_full_path, fn [mut syns, read_file] (file_path string) { 73 | if !file_path.ends_with('.syntax') { 74 | return 75 | } 76 | contents := read_file(file_path) or { 77 | panic('${err.msg()}') 78 | '{}' 79 | } // TODO(tauraamui): log out to a file here probably 80 | mut syn := json.decode(Syntax, contents) or { Syntax{} } 81 | if file_path.ends_with('v.syntax') { 82 | unsafe { 83 | syns[0] = syn 84 | } 85 | return 86 | } 87 | if file_path.ends_with('go.syntax') { 88 | unsafe { 89 | syns[1] = syn 90 | } 91 | return 92 | } 93 | if file_path.ends_with('c.syntax') { 94 | unsafe { 95 | syns[2] = syn 96 | } 97 | return 98 | } 99 | if file_path.ends_with('rust.syntax') { 100 | unsafe { 101 | syns[3] = syn 102 | } 103 | return 104 | } 105 | if file_path.ends_with('js.syntax') { 106 | unsafe { 107 | syns[4] = syn 108 | } 109 | return 110 | } 111 | if file_path.ends_with('ts.syntax') { 112 | unsafe { 113 | syns[5] = syn 114 | } 115 | return 116 | } 117 | if file_path.ends_with('python.syntax') { 118 | unsafe { 119 | syns[6] = syn 120 | } 121 | return 122 | } 123 | if file_path.ends_with('perl.syntax') { 124 | unsafe { 125 | syns[7] = syn 126 | } 127 | return 128 | } 129 | syns << syn 130 | }) 131 | return syns 132 | } 133 | -------------------------------------------------------------------------------- /src/lib/buffer/position_test.v: -------------------------------------------------------------------------------- 1 | module buffer 2 | 3 | fn test_add_with_zero_line_distance() { 4 | mut pos := Position{ 5 | line: 1 6 | offset: 3 7 | } 8 | dist := Distance{ 9 | lines: 0 10 | offset: 4 11 | } 12 | 13 | assert pos.add(dist) == Position{ 14 | line: 1 15 | offset: 7 16 | } 17 | } 18 | 19 | fn test_apply_with_zero_line_distance() { 20 | mut pos := Position{ 21 | line: 1 22 | offset: 3 23 | } 24 | dist := Distance{ 25 | lines: 0 26 | offset: 4 27 | } 28 | 29 | pos.apply(dist) 30 | assert pos == Position{ 31 | line: 1 32 | offset: 7 33 | } 34 | } 35 | 36 | fn test_add_with_zero_line_distance_diff_offset() { 37 | mut pos := Position{ 38 | line: 1 39 | offset: 3 40 | } 41 | dist := Distance{ 42 | lines: 0 43 | offset: 19 44 | } 45 | 46 | assert pos.add(dist) == Position{ 47 | line: 1 48 | offset: 22 49 | } 50 | } 51 | 52 | fn test_add_with_some_line_distance() { 53 | mut pos := Position{ 54 | line: 1 55 | offset: 3 56 | } 57 | dist := Distance{ 58 | lines: 12 59 | offset: 4 60 | } 61 | 62 | assert pos.add(dist) == Position{ 63 | line: 13 64 | offset: 7 65 | } 66 | } 67 | 68 | fn test_sub_with_zero_line_distance() { 69 | mut pos := Position{ 70 | line: 1 71 | offset: 3 72 | } 73 | dist := Distance{ 74 | lines: 0 75 | offset: 4 76 | } 77 | 78 | assert pos.sub(dist) == Position{ 79 | line: 1 80 | offset: 0 81 | } 82 | } 83 | 84 | fn test_sub_with_zero_line_distance_diff_offset() { 85 | mut pos := Position{ 86 | line: 1 87 | offset: 19 88 | } 89 | dist := Distance{ 90 | lines: 0 91 | offset: 3 92 | } 93 | 94 | assert pos.sub(dist) == Position{ 95 | line: 1 96 | offset: 16 97 | } 98 | } 99 | 100 | fn test_sub_with_some_line_distance() { 101 | mut pos := Position{ 102 | line: 12 103 | offset: 4 104 | } 105 | dist := Distance{ 106 | lines: 1 107 | offset: 3 108 | } 109 | 110 | assert pos.sub(dist) == Position{ 111 | line: 11 112 | offset: 1 113 | } 114 | } 115 | 116 | fn test_less_than_works_when_position_a_less_then_b() { 117 | earlier_position := Position{ 118 | line: 2 119 | offset: 20 120 | } 121 | later_position := Position{ 122 | line: 3 123 | offset: 10 124 | } 125 | 126 | assert earlier_position < later_position 127 | } 128 | 129 | fn test_less_than_works_when_position_b_more_then_a() { 130 | earlier_position := Position{ 131 | line: 2 132 | offset: 20 133 | } 134 | later_position := Position{ 135 | line: 3 136 | offset: 10 137 | } 138 | 139 | assert later_position < earlier_position == false 140 | } 141 | 142 | fn test_more_than_works_when_position_b_more_then_a() { 143 | earlier_position := Position{ 144 | line: 2 145 | offset: 20 146 | } 147 | later_position := Position{ 148 | line: 3 149 | offset: 10 150 | } 151 | 152 | assert later_position > earlier_position 153 | } 154 | 155 | fn test_more_than_works_when_position_a_less_then_b() { 156 | earlier_position := Position{ 157 | line: 2 158 | offset: 20 159 | } 160 | later_position := Position{ 161 | line: 3 162 | offset: 10 163 | } 164 | 165 | assert earlier_position > later_position == false 166 | } 167 | 168 | fn test_compare_works_when_lines_and_offsets_are_equal() { 169 | position_a := Position{ 170 | line: 11 171 | offset: 3 172 | } 173 | position_b := Position{ 174 | line: 11 175 | offset: 3 176 | } 177 | 178 | assert position_a <= position_b 179 | assert position_a >= position_b 180 | assert position_a == position_b 181 | } 182 | 183 | fn test_compare_works_when_lines_and_offsets_are_not_equal() { 184 | position_a := Position{ 185 | line: 11 186 | offset: 3 187 | } 188 | position_b := Position{ 189 | line: 8 190 | offset: 3 191 | } 192 | 193 | assert position_a != position_b 194 | } 195 | 196 | fn test_distance_between_a_lines_larger() { 197 | position_a := Position{ 198 | line: 11 199 | offset: 3 200 | } 201 | position_b := Position{ 202 | line: 8 203 | offset: 3 204 | } 205 | 206 | assert position_a.distance(position_b) == Distance{ 207 | lines: 3 208 | } 209 | assert position_b.distance(position_a) == Distance{ 210 | lines: 3 211 | } 212 | } 213 | 214 | fn test_distance_between_b_lines_larger() { 215 | position_a := Position{ 216 | line: 11 217 | offset: 3 218 | } 219 | position_b := Position{ 220 | line: 85 221 | offset: 9 222 | } 223 | 224 | assert position_a.distance(position_b) == Distance{ 225 | lines: 74 226 | offset: 6 227 | } 228 | assert position_b.distance(position_a) == Distance{ 229 | lines: 74 230 | offset: 6 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /experiment/clipboard/r_x11.c: -------------------------------------------------------------------------------- 1 | // compile with: 2 | // gcc x11.c -lX11 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | int main(void) { 13 | Display* display = XOpenDisplay(NULL); 14 | 15 | Window window = XCreateSimpleWindow(display, RootWindow(display, DefaultScreen(display)), 10, 10, 200, 200, 1, 16 | BlackPixel(display, DefaultScreen(display)), WhitePixel(display, DefaultScreen(display))); 17 | 18 | XSelectInput(display, window, ExposureMask | KeyPressMask); 19 | 20 | const Atom UTF8_STRING = XInternAtom(display, "UTF8_STRING", True); 21 | const Atom CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0); 22 | const Atom XSEL_DATA = XInternAtom(display, "XSEL_DATA", 0); 23 | 24 | const Atom SAVE_TARGETS = XInternAtom((Display*) display, "SAVE_TARGETS", False); 25 | const Atom TARGETS = XInternAtom((Display*) display, "TARGETS", False); 26 | const Atom MULTIPLE = XInternAtom((Display*) display, "MULTIPLE", False); 27 | const Atom ATOM_PAIR = XInternAtom((Display*) display, "ATOM_PAIR", False); 28 | const Atom CLIPBOARD_MANAGER = XInternAtom((Display*) display, "CLIPBOARD_MANAGER", False); 29 | 30 | // input 31 | XConvertSelection(display, CLIPBOARD, UTF8_STRING, XSEL_DATA, window, CurrentTime); 32 | XSync(display, 0); 33 | 34 | XEvent event; 35 | XNextEvent(display, &event); 36 | 37 | if (event.type == SelectionNotify && event.xselection.selection == CLIPBOARD && event.xselection.property != 0) { 38 | 39 | int format; 40 | unsigned long N, size; 41 | char* data, * s = NULL; 42 | Atom target; 43 | 44 | XGetWindowProperty(event.xselection.display, event.xselection.requestor, 45 | event.xselection.property, 0L, (~0L), 0, AnyPropertyType, &target, 46 | &format, &size, &N, (unsigned char**) &data); 47 | 48 | if (target == UTF8_STRING || target == XA_STRING) { 49 | printf("paste: %s\n", data); 50 | XFree(data); 51 | } 52 | 53 | XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property); 54 | } 55 | 56 | // output 57 | char text[] = "new string\0"; 58 | 59 | XSetSelectionOwner((Display*) display, CLIPBOARD, (Window) window, CurrentTime); 60 | 61 | XConvertSelection((Display*) display, CLIPBOARD_MANAGER, SAVE_TARGETS, None, (Window) window, CurrentTime); 62 | 63 | Bool running = True; 64 | while (running) { 65 | XNextEvent(display, &event); 66 | if (event.type == SelectionRequest) { 67 | const XSelectionRequestEvent* request = &event.xselectionrequest; 68 | 69 | XEvent reply = { SelectionNotify }; 70 | reply.xselection.property = 0; 71 | 72 | if (request->target == TARGETS) { 73 | const Atom targets[] = { TARGETS, 74 | MULTIPLE, 75 | UTF8_STRING, 76 | XA_STRING }; 77 | 78 | XChangeProperty(display, 79 | request->requestor, 80 | request->property, 81 | 4, 82 | 32, 83 | PropModeReplace, 84 | (unsigned char*) targets, 85 | sizeof(targets) / sizeof(targets[0])); 86 | 87 | reply.xselection.property = request->property; 88 | } 89 | 90 | if (request->target == MULTIPLE) { 91 | Atom* targets = NULL; 92 | 93 | Atom actualType = 0; 94 | int actualFormat = 0; 95 | unsigned long count = 0, bytesAfter = 0; 96 | 97 | XGetWindowProperty(display, request->requestor, request->property, 0, LONG_MAX, False, ATOM_PAIR, &actualType, &actualFormat, &count, &bytesAfter, (unsigned char **) &targets); 98 | 99 | unsigned long i; 100 | for (i = 0; i < count; i += 2) { 101 | Bool found = False; 102 | 103 | if (targets[i] == UTF8_STRING || targets[i] == XA_STRING) { 104 | XChangeProperty((Display*) display, 105 | request->requestor, 106 | targets[i + 1], 107 | targets[i], 108 | 8, 109 | PropModeReplace, 110 | (unsigned char*) text, 111 | sizeof(text)); 112 | XFlush(display); 113 | running = False; 114 | } else { 115 | targets[i + 1] = None; 116 | } 117 | } 118 | 119 | XChangeProperty((Display*) display, 120 | request->requestor, 121 | request->property, 122 | ATOM_PAIR, 123 | 32, 124 | PropModeReplace, 125 | (unsigned char*) targets, 126 | count); 127 | 128 | XFlush(display); 129 | XFree(targets); 130 | 131 | reply.xselection.property = request->property; 132 | } 133 | 134 | reply.xselection.display = request->display; 135 | reply.xselection.requestor = request->requestor; 136 | reply.xselection.selection = request->selection; 137 | reply.xselection.target = request->target; 138 | reply.xselection.time = request->time; 139 | 140 | XSendEvent((Display*) display, request->requestor, False, 0, &reply); 141 | XFlush(display); 142 | } 143 | } 144 | 145 | XCloseDisplay(display); 146 | } 147 | 148 | -------------------------------------------------------------------------------- /experiment/gui_render/main.v: -------------------------------------------------------------------------------- 1 | import sokol 2 | import sokol.sapp 3 | import sokol.gfx 4 | import sokol.sgl 5 | import fontstash 6 | import sokol.sfons 7 | import os 8 | 9 | const text = ' 10 | Once upon a midnight dreary, while I pondered, weak and weary, 11 | Over many a quaint and curious volume of forgotten lore— 12 | While I nodded, nearly napping, suddenly there came a tapping, 13 | As of some one gently rapping, rapping at my chamber door. 14 | “’Tis some visitor,” I muttered, “tapping at my chamber door— 15 | Only this and nothing more.” 16 | 17 | Ah, distinctly I remember it was in the bleak December; 18 | And each separate dying ember wrought its ghost upon the floor. 19 | Eagerly I wished the morrow;—vainly I had sought to borrow 20 | From my books surcease of sorrow—sorrow for the lost Lenore— 21 | For the rare and radiant maiden whom the angels name Lenore— 22 | Nameless here for evermore. 23 | 24 | And the silken, sad, uncertain rustling of each purple curtain 25 | Thrilled me—filled me with fantastic terrors never felt before; 26 | So that now, to still the beating of my heart, I stood repeating 27 | “’Tis some visitor entreating entrance at my chamber door— 28 | Some late visitor entreating entrance at my chamber door;— 29 | This it is and nothing more.” 30 | 31 | Presently my soul grew stronger; hesitating then no longer, 32 | “Sir,” said I, “or Madam, truly your forgiveness I implore; 33 | But the fact is I was napping, and so gently you came rapping, 34 | And so faintly you came tapping, tapping at my chamber door, 35 | That I scarce was sure I heard you”—here I opened wide the door;— 36 | Darkness there and nothing more. 37 | 38 | Deep into that darkness peering, long I stood there wondering, fearing, 39 | Doubting, dreaming dreams no mortal ever dared to dream before; 40 | But the silence was unbroken, and the stillness gave no token, 41 | And the only word there spoken was the whispered word, “Lenore?” 42 | This I whispered, and an echo murmured back the word, “Lenore!”— 43 | Merely this and nothing more. 44 | 45 | Back into the chamber turning, all my soul within me burning, 46 | Soon again I heard a tapping somewhat louder than before. 47 | “Surely,” said I, “surely that is something at my window lattice; 48 | Let me see, then, what thereat is, and this mystery explore— 49 | Let my heart be still a moment and this mystery explore;— 50 | ’Tis the wind and nothing more!” 51 | ' 52 | 53 | const lines = text.split('\n') 54 | 55 | struct AppState { 56 | mut: 57 | pass_action gfx.PassAction 58 | fons &fontstash.Context = unsafe { nil } 59 | font_normal int 60 | inited bool 61 | } 62 | 63 | fn main() { 64 | mut color_action := gfx.ColorAttachmentAction{ 65 | load_action: .clear 66 | clear_value: gfx.Color{ 67 | r: 1.0 68 | g: 1.0 69 | b: 1.0 70 | a: 1.0 71 | } 72 | } 73 | mut pass_action := gfx.PassAction{} 74 | pass_action.colors[0] = color_action 75 | state := &AppState{ 76 | pass_action: pass_action 77 | fons: unsafe { nil } // &fontstash.Context(0) 78 | } 79 | title := 'V Metal/GL Text Rendering' 80 | desc := sapp.Desc{ 81 | user_data: state 82 | init_userdata_cb: init 83 | frame_userdata_cb: frame 84 | window_title: title.str 85 | html5_canvas_name: title.str 86 | width: 600 87 | height: 700 88 | high_dpi: true 89 | } 90 | sapp.run(&desc) 91 | } 92 | 93 | fn init(mut state AppState) { 94 | desc := sapp.create_desc() 95 | gfx.setup(&desc) 96 | s := &sgl.Desc{} 97 | sgl.setup(s) 98 | state.fons = sfons.create(512, 512, 1) 99 | // or use DroidSerif-Regular.ttf 100 | if bytes := os.read_bytes(os.resource_abs_path('RobotoMono-Regular.ttf')) { 101 | println('loaded font: ${bytes.len}') 102 | state.font_normal = state.fons.add_font_mem('sans', bytes, false) 103 | } 104 | } 105 | 106 | fn frame(mut state AppState) { 107 | state.render_font() 108 | gfx.begin_default_pass(&state.pass_action, sapp.width(), sapp.height()) 109 | sgl.draw() 110 | gfx.end_pass() 111 | gfx.commit() 112 | } 113 | 114 | const black = sfons.rgba(0, 0, 0, 255) 115 | const font_height = 18 116 | 117 | fn (mut state AppState) render_font() { 118 | mut dy := font_height 119 | mut fons := state.fons 120 | if !state.inited { 121 | fons.clear_state() 122 | sgl.defaults() 123 | sgl.matrix_mode_projection() 124 | sgl.ortho(0.0, f32(sapp.width()), f32(sapp.height()), 0.0, -1.0, 1.0) 125 | fons.set_font(state.font_normal) 126 | // fons.set_size(100.0) 127 | fons.set_color(black) 128 | fons.set_font(state.font_normal) 129 | fons.set_size(font_height) 130 | state.inited = true 131 | } 132 | 133 | for line in lines { 134 | fons.draw_text(1, f32(dy), line) 135 | dy += font_height - 4 136 | } 137 | sfons.flush(fons) 138 | } 139 | 140 | fn line(sx f32, sy f32, ex f32, ey f32) { 141 | sgl.begin_lines() 142 | sgl.c4b(255, 255, 0, 128) 143 | sgl.v2f(sx, sy) 144 | sgl.v2f(ex, ey) 145 | sgl.end() 146 | } 147 | -------------------------------------------------------------------------------- /src/lib/ui/status_line.v: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module ui 16 | 17 | import lib.draw 18 | import lib.core 19 | 20 | pub struct SearchSelection { 21 | pub: 22 | active bool 23 | total int 24 | current int 25 | } 26 | 27 | @[params] 28 | pub struct Status { 29 | pub: 30 | mode core.Mode 31 | cursor_x int 32 | cursor_y int 33 | file_name string 34 | selection SearchSelection 35 | git_branch string 36 | dirty bool 37 | } 38 | 39 | pub fn draw_status_line(mut ctx draw.Contextable, status Status) { 40 | defer { ctx.reset() } 41 | 42 | y := ctx.window_height() - 2 43 | 44 | // invoke the mode indicator draw 45 | mut offset := status.mode.draw(mut ctx, 0, y) 46 | 47 | // if filename provided, render its segment next 48 | if status.file_name.len > 0 { 49 | dirty_indicator := if status.dirty { ' [+]' } else { '' } 50 | offset += draw_file_name_segment(mut ctx, offset, y, status.file_name + dirty_indicator) 51 | } 52 | 53 | // if search selection active/provided, render it's segment next 54 | if status.selection.active { 55 | offset += draw_search_selection_info_segment(mut ctx, offset, y, status.selection) 56 | } 57 | 58 | // if git branch active/provided, render it's segment next 59 | if status.git_branch.len > 0 { 60 | offset += draw_git_branch_section(mut ctx, offset, y, status.git_branch) 61 | } 62 | 63 | // draw leaning end of base status line bar 64 | draw.paint_shape_text(mut ctx, offset, y, draw.Color{25, 25, 25}, '${core.slant_left_flat_top}') 65 | offset += 1 66 | 67 | // render the cursor status as a right trailing segment 68 | draw_cursor_position_segment(mut ctx, 0, y, offset, status.cursor_x, status.cursor_y) 69 | } 70 | 71 | fn draw_file_name_segment(mut ctx draw.Contextable, x int, y int, file_name string) int { 72 | draw.paint_shape_text(mut ctx, x, y, draw.Color{86, 86, 86}, '${core.slant_left_flat_top}${core.block}') 73 | mut offset := 2 74 | ctx.bold() 75 | draw.paint_text_on_background(mut ctx, x + offset, y, draw.Color{86, 86, 86}, draw.Color{230, 230, 230}, 76 | file_name) 77 | offset += file_name.len 78 | draw.paint_shape_text(mut ctx, x + offset, y, draw.Color{86, 86, 86}, '${core.block}${core.slant_right_flat_bottom}') 79 | offset += 2 80 | return offset 81 | } 82 | 83 | fn draw_search_selection_info_segment(mut ctx draw.Contextable, x int, y int, selection SearchSelection) int { 84 | selection_info_label := '${selection.current}/${selection.total}' 85 | mut offset := 2 86 | draw.paint_shape_text(mut ctx, x, y, core.status_purple, '${core.slant_left_flat_top}${core.block}') 87 | draw.paint_text_on_background(mut ctx, x + offset, y, core.status_purple, draw.Color{230, 230, 230}, 88 | selection_info_label) 89 | offset += selection_info_label.len 90 | draw.paint_shape_text(mut ctx, x + offset, y, core.status_purple, '${core.block}${core.slant_right_flat_bottom}') 91 | offset += 2 92 | return offset 93 | } 94 | 95 | fn draw_git_branch_section(mut ctx draw.Contextable, x int, y int, git_branch string) int { 96 | draw.paint_shape_text(mut ctx, x, y, core.status_dark_lilac, '${core.slant_left_flat_top}${core.block}') 97 | mut offset := 2 98 | draw.paint_text_on_background(mut ctx, x + offset, y, core.status_dark_lilac, draw.Color{230, 230, 230}, 99 | git_branch) 100 | offset += git_branch.runes().len - 1 101 | draw.paint_shape_text(mut ctx, x + offset, y, draw.Color{154, 119, 209}, '${core.block}${core.slant_right_flat_bottom}') 102 | offset += 2 103 | return offset 104 | } 105 | 106 | fn draw_cursor_position_segment(mut ctx draw.Contextable, x int, y int, last_segment_offset int, cursor_x int, cursor_y int) int { 107 | cursor_info_label := '${cursor_y + 1}:${cursor_x + 1}' 108 | draw.paint_shape_text(mut ctx, ctx.window_width() - 1, y, draw.Color{245, 42, 42}, 109 | '${core.block}${core.block}') 110 | ctx.bold() 111 | draw.paint_text_on_background(mut ctx, ctx.window_width() - 1 - cursor_info_label.len, 112 | y, draw.Color{245, 42, 42}, draw.Color{255, 255, 255}, cursor_info_label) 113 | draw.paint_shape_text(mut ctx, ctx.window_width() - 2 - cursor_info_label.len - 2, 114 | y, draw.Color{245, 42, 42}, '${core.slant_right_flat_top}${core.slant_left_flat_bottom}${core.block}') 115 | draw.paint_shape_text(mut ctx, ctx.window_width() - 2 - cursor_info_label.len - 2, 116 | y, draw.Color{25, 25, 25}, '${core.slant_right_flat_top}') 117 | ctx.set_bg_color(draw.Color{25, 25, 25}) 118 | ctx.draw_rect(last_segment_offset, y, (ctx.window_width() - 2 - cursor_info_label.len - 2) - last_segment_offset, 119 | 1) 120 | ctx.reset_bg_color() 121 | return 0 122 | } 123 | -------------------------------------------------------------------------------- /src/lib/workspace/workspace.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module workspace 16 | 17 | import os 18 | import json 19 | import term.ui as tui 20 | import lib.syntax as syntaxlib 21 | 22 | const builtin_lilly_config_file_content = $embed_file('../../config/default_lilly.conf').to_string() 23 | pub const lilly_config_root_dir_name = 'lilly' 24 | const lilly_syntaxes_dir_name = 'syntaxes' 25 | 26 | pub struct Workspace { 27 | pub: 28 | config Config 29 | mut: 30 | files []string 31 | syntaxes []syntaxlib.Syntax 32 | git_branch string 33 | } 34 | 35 | pub interface Logger { 36 | mut: 37 | error(msg string) 38 | } 39 | 40 | pub struct Config { 41 | pub: 42 | theme ?string 43 | pub mut: 44 | leader_key string 45 | relative_line_numbers bool 46 | insert_tabs_not_spaces bool 47 | } 48 | 49 | pub fn open_workspace(mut _log Logger, 50 | root_path string, 51 | is_dir fn (path string) bool, 52 | dir_walker fn (path string, f fn (string)), 53 | config Config, 54 | config_dir fn () !string, 55 | read_file fn (path string) !string, 56 | execute fn (cmd string) os.Result) !Workspace { 57 | path := root_path 58 | if !is_dir(path) { 59 | return error('${path} is not a directory') 60 | } 61 | mut wrkspace := Workspace{ 62 | config: config 63 | } 64 | 65 | wrkspace.resolve_files(path, is_dir, dir_walker) 66 | wrkspace.resolve_git_branch_name(execute) 67 | wrkspace.syntaxes = syntaxlib.load_builtin_syntaxes() 68 | return wrkspace 69 | } 70 | 71 | fn (mut workspace Workspace) resolve_git_branch_name(execute fn (cmd string) os.Result) { 72 | prefix := '\uE0A0' // the git branch symbol rune 73 | wt := spawn currently_in_worktree(execute) 74 | in_wt := wt.wait() 75 | if in_wt { 76 | gb := spawn get_branch(execute) 77 | branch := gb.wait() 78 | workspace.git_branch = '${prefix} ${branch}' 79 | } 80 | } 81 | 82 | fn currently_in_worktree(execute fn (cmd string) os.Result) bool { 83 | res := execute('git rev-parse --is-inside-work-tree') 84 | return res.exit_code == 0 85 | } 86 | 87 | fn get_branch(execute fn (cmd string) os.Result) string { 88 | res := execute('git branch --show-current') 89 | return res.output 90 | } 91 | 92 | fn (mut workspace Workspace) resolve_files(path string, 93 | is_dir fn (path string) bool, 94 | dir_walker fn (path string, f fn (string))) { 95 | mut files_ref := &workspace.files 96 | dir_walker(path, fn [mut files_ref, is_dir] (file_path string) { 97 | if file_path.contains('.git') { 98 | return 99 | } 100 | // FIX(tauraamui): this doesn't actually work if the passed path isn't just '.' 101 | if is_dir(file_path) { 102 | return 103 | } 104 | files_ref << file_path 105 | }) 106 | } 107 | 108 | pub fn (workspace Workspace) branch() string { 109 | return workspace.git_branch 110 | } 111 | 112 | pub fn (workspace Workspace) get_files() []string { 113 | return workspace.files 114 | } 115 | 116 | pub fn (workspace Workspace) syntaxes() []syntaxlib.Syntax { 117 | return workspace.syntaxes 118 | } 119 | 120 | pub fn resolve_config(mut _log Logger, config_dir fn () !string, read_file fn (path string) !string) Config { 121 | loaded_config := attempt_to_load_from_disk(config_dir, read_file) or { 122 | _log.error('failed to resolve config: ${err}') 123 | return fallback_to_bundled_default_config() 124 | } 125 | // loaded_config := attempt_to_load_from_disk(config_dir, read_file) or { fallback_to_bundled_default_config() } 126 | return loaded_config 127 | } 128 | 129 | // NOTE(tauraamui): 130 | // Whilst technically json decode can fail, this should only be the case in this instance 131 | // if we the editor authors have fucked up the default config file format, this kind of 132 | // issue should never make it out to production, hence the acceptable panic here. 133 | fn fallback_to_bundled_default_config() Config { 134 | return json.decode(Config, builtin_lilly_config_file_content) or { 135 | panic('decoding bundled config failed: ${err}') 136 | } 137 | } 138 | 139 | fn attempt_to_load_from_disk(config_dir fn () !string, read_file fn (path string) !string) !Config { 140 | config_root_dir := config_dir() or { 141 | return error('unable to resolve local config root directory') 142 | } 143 | config_file_full_path := os.join_path(config_root_dir, lilly_config_root_dir_name, 144 | 'lilly.conf') 145 | config_file_contents := read_file(config_file_full_path) or { 146 | return error('local config file ${config_file_full_path} not found: ${err}') 147 | } 148 | return json.decode(Config, config_file_contents) or { 149 | return error('unable to parse config ${config_file_full_path}: ${err}') 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/lib/ui/todo_comments_picker_modal_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module ui 16 | 17 | import time 18 | import lib.buffer 19 | import lib.draw 20 | import lib.theme as themelib 21 | 22 | struct TestDrawer { 23 | draw_text_callback fn (x int, y int, text string) @[required] 24 | } 25 | 26 | fn (drawer TestDrawer) theme() themelib.Theme { 27 | return themelib.Theme.new('test') or { panic('error occurred loading theme: ${err}') } 28 | } 29 | 30 | fn (mut drawer TestDrawer) draw_text(x int, y int, text string) { 31 | if drawer.draw_text_callback == unsafe { nil } { 32 | return 33 | } 34 | drawer.draw_text_callback(x, y, text) 35 | } 36 | 37 | fn (mut drawer TestDrawer) write(text string) { 38 | time.sleep(1 * time.millisecond) 39 | } 40 | 41 | fn (mut drawer TestDrawer) draw_rect(x int, y int, width int, height int) { 42 | time.sleep(1 * time.millisecond) 43 | } 44 | 45 | fn (mut drawer TestDrawer) draw_point(x int, y int) { 46 | time.sleep(1 * time.millisecond) 47 | } 48 | 49 | fn (mut drawer TestDrawer) render_debug() bool { 50 | return false 51 | } 52 | 53 | fn (mut drawer TestDrawer) set_color(c draw.Color) {} 54 | 55 | fn (mut drawer TestDrawer) set_bg_color(c draw.Color) {} 56 | 57 | fn (mut drawer TestDrawer) reset_color() {} 58 | 59 | fn (mut drawer TestDrawer) reset_bg_color() {} 60 | 61 | fn (mut drawer TestDrawer) rate_limit_draws() bool { 62 | return false 63 | } 64 | 65 | fn (mut drawer TestDrawer) window_width() int { 66 | return 500 67 | } 68 | 69 | fn (mut drawer TestDrawer) window_height() int { 70 | return 500 71 | } 72 | 73 | fn (mut drawer TestDrawer) set_cursor_position(x int, y int) {} 74 | 75 | fn (mut drawer TestDrawer) set_cursor_to_block() {} 76 | 77 | fn (mut drawer TestDrawer) set_cursor_to_underline() {} 78 | 79 | fn (mut drawer TestDrawer) set_cursor_to_vertical_bar() {} 80 | 81 | fn (mut drawer TestDrawer) show_cursor() {} 82 | 83 | fn (mut drawer TestDrawer) hide_cursor() {} 84 | 85 | fn (mut drawer TestDrawer) set_style(s draw.Style) {} 86 | 87 | fn (mut drawer TestDrawer) clear_style() {} 88 | 89 | fn (mut drawer TestDrawer) bold() {} 90 | 91 | fn (mut drawer TestDrawer) reset() {} 92 | 93 | fn (mut drawer TestDrawer) clear() {} 94 | 95 | fn (mut drawer TestDrawer) flush() {} 96 | 97 | fn test_todo_comment_modal_rendering_with_match_list_entries() { 98 | mut drawn_text := []string{} 99 | mut ref := &drawn_text 100 | 101 | mut mock_drawer := TestDrawer{ 102 | draw_text_callback: fn [mut ref] (x int, y int, text string) { 103 | ref << text 104 | } 105 | } 106 | 107 | // NOTE(tauraamui) [07/03/2025]: despite the below example comments having the '-x' exclusion 108 | // flag to exclude them from the match list, it doesn't prevent 109 | // this render test working correctly as the match list is manuallly 110 | // populated here 111 | mut mock_modal := TodoCommentPickerModal.new([ 112 | buffer.Match{ 113 | file_path: 'example-file.txt' 114 | pos: buffer.Position.new( 115 | line: 38 116 | offset: 11 117 | ) 118 | contents: 'TODO(tauraamui) [28/02/2025] random comment' 119 | keyword_len: 4 120 | }, 121 | buffer.Match{ 122 | file_path: 'test-file.txt' 123 | pos: buffer.Position.new( 124 | line: 112 125 | offset: 3 126 | ) 127 | contents: 'TODO(tauraamui) [11/01/2025] blah blah blah blah...!' 128 | keyword_len: 4 129 | }, 130 | ]) 131 | 132 | mock_modal.draw(mut mock_drawer) 133 | assert drawn_text.len > 0 134 | cleaned_list := drawn_text[1..drawn_text.len - 2].clone() 135 | assert cleaned_list == [ 136 | 'example-file.txt:38:11 ', 137 | 'TODO', 138 | '(tauraamui) [28/02/2025] random comment', 139 | 'test-file.txt:112:3 ', 140 | 'TODO', 141 | '(tauraamui) [11/01/2025] blah blah blah blah...!', 142 | ] 143 | } 144 | 145 | fn test_todo_comment_modal_enter_returns_currently_selected_match_entry() { 146 | mut drawn_text := []string{} 147 | mut ref := &drawn_text 148 | 149 | mut mock_drawer := TestDrawer{ 150 | draw_text_callback: fn [mut ref] (x int, y int, text string) { 151 | ref << text 152 | } 153 | } 154 | 155 | mut mock_modal := TodoCommentPickerModal.new([ 156 | buffer.Match{ 157 | file_path: 'example-file.txt' 158 | pos: buffer.Position.new( 159 | line: 38 160 | offset: 11 161 | ) 162 | contents: 'A fake l // -x TODO(tauraamui) [28/02/2025] random comment' 163 | }, 164 | buffer.Match{ 165 | file_path: 'test-file.txt' 166 | pos: buffer.Position.new( 167 | line: 112 168 | offset: 3 169 | ) 170 | contents: '// -x TODO(tauraamui) [11/01/2025] blah blah blah blah...!' 171 | }, 172 | ]) 173 | } 174 | -------------------------------------------------------------------------------- /src/main_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module main 16 | 17 | import log 18 | 19 | fn wd_resolver() string { 20 | return 'test-workspace' 21 | } 22 | 23 | fn test_resolve_file_and_workspace_dir_paths() { 24 | assert resolve_options_from_args([]).log_level == log.Level.disabled 25 | mut file_path, mut workspace_path := resolve_file_and_workspace_dir_paths([], wd_resolver)! 26 | assert file_path == '' 27 | assert workspace_path == 'test-workspace' 28 | 29 | file_path, workspace_path = resolve_file_and_workspace_dir_paths([ 30 | './random-dir/test-file.txt', 31 | ], wd_resolver)! 32 | assert file_path == './random-dir/test-file.txt' 33 | assert workspace_path == './random-dir' 34 | } 35 | 36 | fn test_resolve_file_and_workspace_dir_paths_with_args() { 37 | mock_args := ['--log-level', 'debug', '.'] 38 | 39 | assert resolve_options_from_args(mock_args).log_level == log.Level.debug 40 | 41 | mut file_path, mut workspace_path := resolve_file_and_workspace_dir_paths(mock_args, 42 | wd_resolver)! 43 | assert file_path == '' 44 | assert workspace_path == '.' 45 | 46 | file_path, workspace_path = resolve_file_and_workspace_dir_paths([ 47 | './random-dir/test-file.txt', 48 | ], wd_resolver)! 49 | assert file_path == './random-dir/test-file.txt' 50 | assert workspace_path == './random-dir' 51 | } 52 | 53 | fn test_resolve_options_from_args_no_show_version_flag() { 54 | mock_args := []string{} 55 | assert resolve_options_from_args(mock_args).show_version == false 56 | } 57 | 58 | fn test_resolve_options_from_args_show_version_long_flag() { 59 | mock_args := ['--version'] 60 | assert resolve_options_from_args(mock_args).show_version 61 | } 62 | 63 | fn test_resolve_options_from_args_show_version_short_flag() { 64 | mock_args := ['-v'] 65 | assert resolve_options_from_args(mock_args).show_version 66 | } 67 | 68 | fn test_resolve_options_from_args_no_show_help_flag() { 69 | mock_args := []string{} 70 | assert resolve_options_from_args(mock_args).show_help == false 71 | } 72 | 73 | fn test_resolve_options_from_args_show_help_long_flag() { 74 | mock_args := ['--help'] 75 | assert resolve_options_from_args(mock_args).show_help 76 | } 77 | 78 | fn test_resolve_options_from_args_show_help_short_flag() { 79 | mock_args := ['-h'] 80 | assert resolve_options_from_args(mock_args).show_help 81 | } 82 | 83 | fn test_resolve_options_from_args_no_debug_mode_flag() { 84 | mock_args := []string{} 85 | assert resolve_options_from_args(mock_args).debug_mode == false 86 | } 87 | 88 | fn test_resolve_options_from_args_debug_mode_long_flag() { 89 | mock_args := ['--debug'] 90 | assert resolve_options_from_args(mock_args).debug_mode 91 | } 92 | 93 | fn test_resolve_options_from_args_debug_mode_short_flag() { 94 | mock_args := ['-d'] 95 | assert resolve_options_from_args(mock_args).debug_mode 96 | } 97 | 98 | fn test_resolve_options_from_args_no_capture_panics_flag() { 99 | mock_args := []string{} 100 | assert resolve_options_from_args(mock_args).capture_panics == false 101 | } 102 | 103 | fn test_resolve_options_from_args_capture_panics_long_flag() { 104 | mock_args := ['--capture-panics'] 105 | assert resolve_options_from_args(mock_args).capture_panics == true 106 | } 107 | 108 | fn test_resolve_options_from_args_capture_panics_short_flag() { 109 | mock_args := ['-cp'] 110 | assert resolve_options_from_args(mock_args).capture_panics == true 111 | } 112 | 113 | fn test_resolve_options_from_args_no_disable_capture_panics_flag() { 114 | mock_args := []string{} 115 | assert resolve_options_from_args(mock_args).capture_panics == false 116 | } 117 | 118 | fn test_resolve_options_from_args_disable_capture_panics_long_flag() { 119 | mock_args := ['--disable-capture-panics'] 120 | assert resolve_options_from_args(mock_args).capture_panics == false 121 | } 122 | 123 | fn test_resolve_options_from_args_disable_capture_panics_short_flag() { 124 | mock_args := ['-dpc'] 125 | assert resolve_options_from_args(mock_args).capture_panics == false 126 | } 127 | 128 | fn test_resolve_options_from_args_no_log_level_label_long_flag() { 129 | mock_args := []string{} 130 | assert resolve_options_from_args(mock_args).log_level == log.Level.disabled 131 | } 132 | 133 | fn test_resolve_options_from_args_log_level_label_long_flag() { 134 | mock_args := ['--log-level', 'debug'] 135 | assert resolve_options_from_args(mock_args).log_level == log.Level.debug 136 | } 137 | 138 | fn test_resolve_options_from_args_log_level_label_short_flag() { 139 | mock_args := ['-ll', 'warn'] 140 | assert resolve_options_from_args(mock_args).log_level == log.Level.warn 141 | } 142 | 143 | fn test_resolve_options_from_args_log_level_label_short_flag_with_invalid_level() { 144 | mock_args := ['-ll', 'smoked-sausage'] 145 | assert resolve_options_from_args(mock_args).log_level == log.Level.disabled 146 | } 147 | -------------------------------------------------------------------------------- /src/lib/clipboardv3/clipboard_darwin.m: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | #import 17 | #import 18 | 19 | static NSString *const ClipboardContentType = @"com.lilly.ClipboardContent"; 20 | 21 | static NSString *getPasteboardTextInternal(void) { 22 | NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; 23 | NSString *text = [pasteboard stringForType:NSPasteboardTypeString]; 24 | return text; 25 | } 26 | 27 | static void setPasteboardTextInternal(NSString *text) { 28 | if (text) { 29 | NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; 30 | [pasteboard clearContents]; 31 | [pasteboard setString:text forType:NSPasteboardTypeString]; 32 | } 33 | } 34 | 35 | src__lib__clipboardv3__CClipboardContent* clipboard_get_content(void) { 36 | src__lib__clipboardv3__CClipboardContent* clipboard_content = NULL; 37 | @autoreleasepool { 38 | NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; 39 | 40 | NSData *archivedData = [pasteboard dataForType:ClipboardContentType]; 41 | if (archivedData) { 42 | NSError *error = nil; 43 | NSDictionary *contentDict = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSDictionary class], [NSString class], [NSNumber class], nil] fromData:archivedData error:&error]; 44 | 45 | if (contentDict && !error && [contentDict isKindOfClass:[NSDictionary class]]) { 46 | NSString *text = contentDict[@"data"]; 47 | NSNumber *t_type = contentDict[@"type"]; 48 | 49 | if (text && t_type) { 50 | clipboard_content = malloc(sizeof(src__lib__clipboardv3__CClipboardContent)); 51 | if (clipboard_content) { 52 | clipboard_content->data = NULL; 53 | const char *utf8String = [text UTF8String]; 54 | if (utf8String) { 55 | clipboard_content->data = malloc(strlen(utf8String) + 1); 56 | if (clipboard_content->data) { 57 | strcpy(clipboard_content->data, utf8String); 58 | clipboard_content->t_type = [t_type unsignedCharValue]; 59 | } else { 60 | free(clipboard_content); 61 | clipboard_content = NULL; 62 | } 63 | } else { 64 | free(clipboard_content); 65 | clipboard_content = NULL; 66 | } 67 | } else { 68 | clipboard_content = NULL; 69 | } 70 | } 71 | } 72 | return clipboard_content; 73 | } 74 | 75 | NSString *plainText = getPasteboardTextInternal(); 76 | if (plainText) { 77 | clipboard_content = malloc(sizeof(src__lib__clipboardv3__CClipboardContent)); 78 | if (clipboard_content) { 79 | clipboard_content->data = NULL; 80 | const char *utf8String = [plainText UTF8String]; 81 | if (utf8String) { 82 | clipboard_content->data = malloc(strlen(utf8String) + 1); 83 | if (clipboard_content->data) { 84 | strcpy(clipboard_content->data, utf8String); 85 | clipboard_content->t_type = 0; 86 | } else { 87 | free(clipboard_content); 88 | clipboard_content = NULL; 89 | } 90 | } else { 91 | free(clipboard_content); 92 | clipboard_content = NULL; 93 | } 94 | } else { 95 | clipboard_content = NULL; 96 | } 97 | } 98 | return clipboard_content; 99 | } 100 | } 101 | 102 | void clipboard_set_content(const char* data, unsigned char contentType) { 103 | @autoreleasepool { 104 | if (!data) { 105 | return; 106 | } 107 | NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; 108 | [pasteboard clearContents]; 109 | 110 | NSMutableDictionary *contentDict = [NSMutableDictionary dictionary]; 111 | 112 | NSString *textString = [NSString stringWithUTF8String:data]; 113 | if (textString) { 114 | [contentDict setObject:textString forKey:@"data"]; 115 | } 116 | 117 | [contentDict setObject:@(contentType) forKey:@"type"]; 118 | 119 | NSError *error = nil; 120 | NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:contentDict requiringSecureCoding:NO error:&error]; 121 | 122 | if (archivedData && !error) { 123 | [pasteboard setData:archivedData forType:ClipboardContentType]; 124 | if (textString) { 125 | [pasteboard setString:textString forType:NSPasteboardTypeString]; 126 | } 127 | } 128 | } 129 | } 130 | 131 | char* clipboard_get_plaintext(void) { 132 | char* text = NULL; 133 | @autoreleasepool { 134 | NSString *clipboard_text = getPasteboardTextInternal(); 135 | if (clipboard_text) { 136 | const char *utf8_clipboard_text = [clipboard_text UTF8String]; 137 | if (utf8_clipboard_text) { 138 | text = malloc(strlen(utf8_clipboard_text) + 1); 139 | if (text) { 140 | strcpy(text, utf8_clipboard_text); 141 | } 142 | } 143 | } 144 | } 145 | return text; 146 | } 147 | 148 | void clipboard_set_plaintext(const char* text) { 149 | @autoreleasepool { 150 | if (text) { 151 | NSString *utf8_text = [NSString stringWithUTF8String:text]; 152 | if (utf8_text) { 153 | setPasteboardTextInternal(utf8_text); 154 | } 155 | } 156 | } 157 | } 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/lib/workspace/syntax_test.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module workspace 16 | 17 | import os 18 | 19 | struct MockLogger { 20 | mut: 21 | error_msgs []string 22 | } 23 | 24 | fn (mut mock_log MockLogger) error(msg string) { 25 | mock_log.error_msgs << msg 26 | } 27 | 28 | struct MockOS {} 29 | 30 | fn (mock_os MockOS) execute(cmd string) os.Result { 31 | return os.Result{ 32 | exit_code: 0 33 | output: 'branch' 34 | } 35 | } 36 | 37 | struct MockFS { 38 | pwd string 39 | dirs map[string][]string 40 | files map[string][]string 41 | file_contents map[string]string 42 | } 43 | 44 | fn (mock_fs MockFS) is_dir(path string) bool { 45 | mut expanded_path := path.replace('./', mock_fs.pwd) 46 | if mock_fs.pwd == expanded_path { 47 | return true 48 | } 49 | _ := mock_fs.dirs[expanded_path] or { return false } 50 | return true 51 | } 52 | 53 | fn (mock_fs MockFS) dir_walker(path string, f fn (string)) { 54 | mut expanded_path := path.replace('./', mock_fs.pwd) 55 | sub_dirs := mock_fs.dirs[expanded_path] or { return } 56 | for sub_dir in sub_dirs { 57 | full_dir := '${expanded_path}/${sub_dir}' 58 | sub_dir_files := mock_fs.files[full_dir] or { continue } 59 | for sub_dir_file in sub_dir_files { 60 | f('${full_dir}/${sub_dir_file}') 61 | } 62 | } 63 | for file in mock_fs.files[expanded_path] or { return } { 64 | f('${expanded_path}/${file}') 65 | } 66 | } 67 | 68 | fn (mock_fs MockFS) read_file(path string) !string { 69 | if v := mock_fs.file_contents[path] { 70 | return v 71 | } 72 | return error('file ${path} does not exist') 73 | } 74 | 75 | fn (mock_fs MockFS) config_dir() !string { 76 | return '/home/test-user/.config' 77 | } 78 | 79 | fn test_open_workspace_loads_builtin_syntax() { 80 | mock_fs := MockFS{ 81 | pwd: '/home/test-user/dev/fakeproject' 82 | dirs: { 83 | '/home/test-user/dev/fakeproject': [] 84 | } 85 | files: {} 86 | file_contents: {} 87 | } 88 | 89 | mock_os := MockOS{} 90 | 91 | mut mock_log := MockLogger{} 92 | cfg := resolve_config(mut mock_log, mock_fs.config_dir, mock_fs.read_file) 93 | wrkspace := open_workspace(mut mock_log, './', mock_fs.is_dir, mock_fs.dir_walker, 94 | cfg, mock_fs.config_dir, mock_fs.read_file, mock_os.execute) or { panic('${err.msg()}') } 95 | assert wrkspace.syntaxes.len == 8 96 | assert wrkspace.syntaxes[0].name == 'V' 97 | assert wrkspace.syntaxes[1].name == 'Go' 98 | assert wrkspace.syntaxes[2].name == 'C' 99 | assert wrkspace.syntaxes[3].name == 'Rust' 100 | assert wrkspace.syntaxes[4].name == 'JavaScript' 101 | assert wrkspace.syntaxes[5].name == 'TypeScript' 102 | assert wrkspace.syntaxes[6].name == 'Python' 103 | assert wrkspace.syntaxes[7].name == 'Perl' 104 | } 105 | 106 | /* 107 | fn test_open_workspace_overrides_builtin_syntax() { 108 | mock_fs := MockFS{ 109 | pwd: '/home/test-user/dev/fakeproject' 110 | dirs: { 111 | '/home/test-user/.config/lilly/syntaxes': [] 112 | '/home/test-user/dev/fakeproject': [] 113 | } 114 | files: { 115 | '/home/test-user/.config/lilly/syntaxes': ['go.syntax'] 116 | } 117 | file_contents: { 118 | '/home/test-user/.config/lilly/syntaxes/go.syntax': '{ "name": "GoTest"}' 119 | } 120 | } 121 | 122 | mock_os := MockOS{} 123 | 124 | mut mock_log := MockLogger{} 125 | cfg := resolve_config(mut mock_log, mock_fs.config_dir, mock_fs.read_file) 126 | wrkspace := open_workspace(mut mock_log, './', mock_fs.is_dir, mock_fs.dir_walker, cfg, 127 | mock_fs.config_dir, mock_fs.read_file, mock_os.execute) or { panic('${err.msg()}') } 128 | assert wrkspace.syntaxes.len == 8 129 | assert wrkspace.syntaxes[0].name == 'V' 130 | assert wrkspace.syntaxes[1].name == 'GoTest' 131 | } 132 | 133 | fn test_open_workspace_loads_custom_syntax() { 134 | mock_fs := MockFS{ 135 | pwd: '/home/test-user/dev/fakeproject' 136 | dirs: { 137 | '/home/test-user/.config/lilly/syntaxes': [] 138 | '/home/test-user/dev/fakeproject': [] 139 | } 140 | files: { 141 | '/home/test-user/.config/lilly/syntaxes': ['brainfuck.syntax'] 142 | } 143 | file_contents: { 144 | '/home/test-user/.config/lilly/syntaxes/brainfuck.syntax': '{ "name": "Brainfuck"}' 145 | } 146 | } 147 | 148 | mock_os := MockOS{} 149 | 150 | mut mock_log := MockLogger{} 151 | cfg := resolve_config(mut mock_log, mock_fs.config_dir, mock_fs.read_file) 152 | wrkspace := open_workspace(mut mock_log, './', mock_fs.is_dir, mock_fs.dir_walker, cfg, 153 | mock_fs.config_dir, mock_fs.read_file, mock_os.execute) or { panic('${err.msg()}') } 154 | assert mock_log.error_msgs.len == 1 155 | assert mock_log.error_msgs[0] == 'failed to resolve config: local config file /home/test-user/.config/lilly/lilly.conf not found: file /home/test-user/.config/lilly/lilly.conf does not exist' 156 | assert wrkspace.syntaxes.len == 9 157 | assert wrkspace.syntaxes[0].name == 'V' 158 | assert wrkspace.syntaxes[1].name == 'Go' 159 | assert wrkspace.syntaxes[8].name == 'Brainfuck' 160 | } 161 | */ 162 | -------------------------------------------------------------------------------- /experiment/gap_buffer/gap_buffer_test.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | fn test_inserting_into_gap_buffer() { 4 | mut gb := GapBuffer.new() 5 | assert gb.raw_str() == '_'.repeat(gap_size) // if the buffer is empty, str shows just the gap 6 | 7 | gb.insert('12345') // insert a string which is 1 char less than the gap size 8 | assert gb.empty_gap_space_size() == 1 9 | assert gb.raw_str() == '12345_' // so we can see the gap is "nearly full", but one space is left 10 | 11 | gb.insert('6') 12 | assert gb.empty_gap_space_size() == gap_size 13 | assert gb.raw_str() == '123456${'_'.repeat(gap_size)}' // thanks to resizing gap is now back to "gap size" post cursor loc 14 | } 15 | 16 | fn test_inserting_into_gap_buffer_and_then_backspacing() { 17 | mut gb := GapBuffer.new() 18 | assert gb.raw_str() == '_'.repeat(gap_size) // if the buffer is empty, str shows just the gap 19 | 20 | gb.insert('This is a full sentence!') // insert a string which is 1 char less than the gap size 21 | assert gb.empty_gap_space_size() == 6 22 | assert gb.raw_str() == 'This is a full sentence!${'_'.repeat(gap_size)}' // so we can see the gap is "nearly full", but one space is left 23 | 24 | gb.backspace() 25 | assert gb.empty_gap_space_size() == 7 26 | assert gb.raw_str() == 'This is a full sentence${'_'.repeat(gap_size + 1)}' // so we can see the gap is "nearly full", but one space is left 27 | 28 | gb.backspace() 29 | gb.backspace() 30 | gb.backspace() 31 | gb.backspace() 32 | 33 | assert gb.empty_gap_space_size() == 11 34 | assert gb.raw_str() == 'This is a full sent${'_'.repeat(gap_size + 5)}' // so we can see the gap is "nearly full", but one space is left 35 | 36 | gb.insert('A') 37 | assert gb.empty_gap_space_size() == 10 38 | assert gb.raw_str() == 'This is a full sentA${'_'.repeat(gap_size + 4)}' // so we can see the gap is "nearly full", but one space is left 39 | } 40 | 41 | fn test_inserting_into_gap_buffer_and_then_deleting() { 42 | mut gb := GapBuffer.new() 43 | assert gb.raw_str() == '_'.repeat(gap_size) // if the buffer is empty, str shows just the gap 44 | 45 | gb.insert('This is a full sentence!') // insert a string which is 1 char less than the gap size 46 | assert gb.empty_gap_space_size() == 6 47 | assert gb.raw_str() == 'This is a full sentence!${'_'.repeat(gap_size)}' // so we can see the gap is "nearly full", but one space is left 48 | 49 | gb.move_cursor_left(10) 50 | assert gb.raw_str() == 'This is a full${'_'.repeat(gap_size)} sentence!' // so we can see the gap is "nearly full", but one space is left 51 | 52 | gb.delete() 53 | assert gb.raw_str() == 'This is a full${'_'.repeat(gap_size + 1)}sentence!' // so we can see the gap is "nearly full", but one space is left 54 | 55 | gb.delete() 56 | gb.delete() 57 | gb.delete() 58 | gb.delete() 59 | 60 | assert gb.raw_str() == 'This is a full${'_'.repeat(gap_size + 5)}ence!' // so we can see the gap is "nearly full", but one space is left 61 | } 62 | 63 | fn test_moving_cursor_left() { 64 | mut gb := GapBuffer.new() 65 | 66 | gb.insert('Some test text, here we go!') 67 | assert gb.empty_gap_space_size() == 3 68 | assert gb.raw_str() == 'Some test text, here we go!${'_'.repeat(gap_size / 2)}' 69 | 70 | gb.move_cursor_left(1) 71 | assert gb.raw_str() == 'Some test text, here we go${'_'.repeat(gap_size / 2)}!' 72 | } 73 | 74 | fn test_moving_cursor_right() { 75 | mut gb := GapBuffer.new() 76 | 77 | gb.insert('Some test text, here we go!') 78 | assert gb.empty_gap_space_size() == 3 79 | assert gb.raw_str() == 'Some test text, here we go!${'_'.repeat(gap_size / 2)}' 80 | 81 | gb.move_cursor_right(1) 82 | assert gb.raw_str() == 'Some test text, here we go!${'_'.repeat(gap_size / 2)}' 83 | 84 | gb.move_cursor_left(3) 85 | assert gb.raw_str() == 'Some test text, here we ${'_'.repeat(gap_size / 2)}go!' 86 | 87 | gb.move_cursor_right(1) 88 | assert gb.raw_str() == 'Some test text, here we g${'_'.repeat(gap_size / 2)}o!' 89 | } 90 | 91 | fn test_moving_cursor_left_and_then_insert() { 92 | mut gb := GapBuffer.new() 93 | 94 | gb.insert('Some test text, here we go!') 95 | assert gb.empty_gap_space_size() == 3 96 | assert gb.raw_str() == 'Some test text, here we go!${'_'.repeat(gap_size / 2)}' 97 | 98 | gb.move_cursor_left(8) 99 | assert gb.raw_str() == 'Some test text, her${'_'.repeat(gap_size / 2)}e we go!' 100 | 101 | gb.insert('??') 102 | assert gb.raw_str() == 'Some test text, her??_e we go!' 103 | 104 | gb.insert('+') 105 | assert gb.raw_str() == 'Some test text, her??+${'_'.repeat(gap_size)}e we go!' 106 | 107 | assert gb.str() == 'Some test text, her??+e we go!' 108 | } 109 | 110 | fn test_line_iterator() { 111 | mut gb := GapBuffer.new() 112 | gb.insert('1. This is the first line\n2. This is the second line\n3. This is the third line.') 113 | 114 | iter := LineIterator{ 115 | data: gb.str() 116 | } 117 | for i, line in iter { 118 | match i { 119 | 0 { assert line == '1. This is the first line' } 120 | 1 { assert line == '2. This is the second line' } 121 | 2 { assert line == '3. This is the third line' } 122 | else {} 123 | } 124 | } 125 | } 126 | 127 | fn test_line_iterator_with_lots_of_blank_lines() { 128 | mut gb := GapBuffer.new() 129 | gb.insert('1. This is the first line\n\n\n\n2. This is the second line\n3. This is the third line.') 130 | 131 | iter := LineIterator{ 132 | data: gb.str() 133 | } 134 | for i, line in iter { 135 | match i { 136 | 0 { assert line == '1. This is the first line' } 137 | 1 { assert line == '' } 138 | 2 { assert line == '' } 139 | 3 { assert line == '' } 140 | 4 { assert line == '2. This is the second line' } 141 | 5 { assert line == '3. This is the third line' } 142 | else {} 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/lib/ui/todo_comments_picker_modal.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module ui 16 | 17 | import lib.draw 18 | import lib.buffer 19 | 20 | pub struct TodoCommentPickerModal { 21 | mut: 22 | open bool 23 | from int 24 | current_sel_id int 25 | pub: 26 | matches []buffer.Match 27 | } 28 | 29 | pub fn TodoCommentPickerModal.new(matches []buffer.Match) TodoCommentPickerModal { 30 | return TodoCommentPickerModal{ 31 | matches: matches 32 | } 33 | } 34 | 35 | pub fn (mut tc_picker TodoCommentPickerModal) open() { 36 | tc_picker.open = true 37 | } 38 | 39 | pub fn (tc_picker TodoCommentPickerModal) is_open() bool { 40 | return tc_picker.open 41 | } 42 | 43 | pub fn (mut tc_picker TodoCommentPickerModal) close() { 44 | tc_picker.open = false 45 | } 46 | 47 | pub fn (mut tc_picker TodoCommentPickerModal) draw(mut ctx draw.Contextable) { 48 | defer { ctx.reset_bg_color() } 49 | ctx.set_color(r: 245, g: 245, b: 245) // set font colour 50 | ctx.set_bg_color(r: 15, g: 15, b: 15) 51 | 52 | mut y_offset := 0 53 | debug_mode_str := if ctx.render_debug() { ' ***RENDER DEBUG MODE ***' } else { '' } 54 | 55 | ctx.draw_text(1, y_offset, '=== ${debug_mode_str} TODO COMMENTS PICKER ${debug_mode_str} ===') // draw header 56 | y_offset += 1 57 | ctx.set_cursor_position(0, y_offset + tc_picker.current_sel_id - tc_picker.from) 58 | y_offset += tc_picker.draw_scrollable_list(mut ctx, y_offset, tc_picker.matches) 59 | ctx.set_bg_color(r: 153, g: 95, b: 146) 60 | ctx.draw_rect(1, y_offset, ctx.window_width(), 1) 61 | search_label := 'SEARCH:' 62 | ctx.draw_text(0, y_offset, search_label) 63 | // ctx.draw_text(1 + utf8_str_visible_length(search_label) + 1, y_offset, tc_picker.search.query) 64 | ctx.draw_text(utf8_str_visible_length(search_label) + 1, y_offset, '') 65 | } 66 | 67 | fn (mut tc_picker TodoCommentPickerModal) resolve_to() int { 68 | matches := tc_picker.matches 69 | mut to := tc_picker.from + max_height 70 | if to > matches.len { 71 | to = matches.len 72 | } 73 | return to 74 | } 75 | 76 | pub fn (mut tc_picker TodoCommentPickerModal) draw_scrollable_list(mut ctx draw.Contextable, y_offset int, list []buffer.Match) int { 77 | ctx.reset_bg_color() 78 | ctx.set_bg_color(r: 15, g: 15, b: 15) 79 | ctx.draw_rect(0, y_offset, ctx.window_width(), max_height - 1) 80 | to := tc_picker.resolve_to() 81 | for i := tc_picker.from; i < to; i++ { 82 | mut iter_y_offset := y_offset + (i - tc_picker.from) 83 | match_item := list[i] 84 | ctx.set_bg_color(r: 15, g: 15, b: 15) 85 | if tc_picker.current_sel_id == i { 86 | ctx.set_bg_color(r: 53, g: 53, b: 53) 87 | ctx.draw_rect(0, iter_y_offset, ctx.window_width(), 1) 88 | } 89 | 90 | list_item_file_path := '${match_item.file_path}:${match_item.pos.line}:${match_item.pos.offset} ' 91 | ctx.draw_text(1, iter_y_offset, list_item_file_path) 92 | 93 | mut x_offset := utf8_str_visible_length(list_item_file_path) 94 | keyword := match_item.contents[..match_item.keyword_len] 95 | 96 | ctx.bold() 97 | draw.paint_text_on_background(mut ctx, x_offset, iter_y_offset, draw.Color{245, 42, 42}, 98 | draw.Color{255, 255, 255}, keyword) 99 | ctx.reset() 100 | 101 | x_offset += utf8_str_visible_length(keyword) 102 | post_keyword := match_item.contents[match_item.keyword_len..] 103 | if tc_picker.current_sel_id == i { 104 | ctx.set_bg_color(r: 53, g: 53, b: 53) 105 | } 106 | ctx.draw_text(x_offset, iter_y_offset, post_keyword) 107 | } 108 | return y_offset + (max_height - 2) 109 | } 110 | 111 | pub fn (mut tc_picker TodoCommentPickerModal) on_key_down(e draw.Event) Action { 112 | match e.code { 113 | .escape { 114 | return Action{ 115 | op: .close_op 116 | } 117 | } 118 | .down { 119 | tc_picker.move_selection_down() 120 | } 121 | .up { 122 | tc_picker.move_selection_up() 123 | } 124 | .enter { 125 | return tc_picker.match_selected() 126 | } 127 | else {} 128 | } 129 | return Action{ 130 | op: .no_op 131 | } 132 | } 133 | 134 | fn (mut tc_picker TodoCommentPickerModal) move_selection_down() { 135 | matches := tc_picker.matches 136 | tc_picker.current_sel_id += 1 137 | to := tc_picker.resolve_to() 138 | if tc_picker.current_sel_id >= to { 139 | if matches.len - to > 0 { 140 | tc_picker.from += 1 141 | } 142 | } 143 | if tc_picker.current_sel_id >= matches.len { 144 | tc_picker.current_sel_id = matches.len - 1 145 | } 146 | } 147 | 148 | fn (mut tc_picker TodoCommentPickerModal) move_selection_up() { 149 | tc_picker.current_sel_id -= 1 150 | if tc_picker.current_sel_id < tc_picker.from { 151 | tc_picker.from -= 1 152 | } 153 | if tc_picker.from < 0 { 154 | tc_picker.from = 0 155 | } 156 | if tc_picker.current_sel_id < 0 { 157 | tc_picker.current_sel_id = 0 158 | } 159 | } 160 | 161 | fn (mut tc_picker TodoCommentPickerModal) match_selected() Action { 162 | matches := tc_picker.matches 163 | selected_match := matches[tc_picker.current_sel_id] 164 | return Action{ 165 | op: .open_file_op 166 | file_path: '${selected_match.file_path}:${selected_match.pos.line}:${selected_match.pos.offset}' 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Lilly 3 | 4 |
5 | 6 | ## A VIM-Like editor for your terminal (chat on Discord) 7 | 8 | > [!IMPORTANT] 9 | > Lilly is in a pre-alpha state, and only suitable for use by developers. 10 | > This editor is technically usable, it is the exclusive editor used to work on itself, 11 | > however many features are missing, and there is no guarantee of stable features or a lack of bugs. 12 | > Features, bug fixes and issues are welcome. 13 | 14 | > [!WARNING] 15 | > (the master branch is currently in a slightly broken state, checkout commit bee9e01 for last known stable version) 16 | 17 | ![Screenshot 2023-11-17 20 07 13](https://github.com/tauraamui/lilly/assets/3159648/12e893ce-0120-4eb4-9d54-71b1a076832c) 18 | 19 | ![Screenshot 2023-12-01 21 01 45](https://github.com/tauraamui/lilly/assets/3159648/e9023db2-0214-49e1-baad-9a75aa22d291) 20 | 21 | An editor designed as a batteries included experience, eliminating the need for plugins. So, basically Helix but for VIM 22 | motions. The end vision is a one to one replacement/equivalent functionality for all VIM features, macros, motions, and more. 23 | 24 | ## Current project state (as of Nov 2025) 25 | 26 | ### Announcing PROJECT PETAL. 27 | 28 | Good news or bad news? 29 | The bad news is that the refactoring efforts to migrate from line to gap buffer have stalled. 30 | The good news is this is because thanks to the new [bobatea](https://github.com/tauraamui/bobatea) 31 | library I have been working on, it has made a full from scratch re-write of the editor necessary and 32 | totally worth it. 33 | 34 | Bobatea is built ontop of Lilly's immediate mode TUI rendering backend which internally uses a double buffer 35 | backing grid to provide efficient cell by cell rendering to the host terminal emulator. The only cells which 36 | are re-drawn by bobatea are ones that have changed. The underlying V standard library term.ui module has been 37 | modified to provide separate loops for rendering and user input, and re-draws have been unchained to whiz up 38 | to whatever target FPS you want (Lilly asks for 60fps by default). 39 | 40 | The libraries API has been intentionally designed to be a as close as possible experience to using the Go 41 | based [bubbletea](https://github.com/charmbracelet/bubbletea), from both end users and the devs building with it. 42 | 43 | ### tldr: 44 | Lilly is being re-written from scratch in conjunction with a new TUI library intended to bring bubbletea to 45 | the V community. If you would like to track this new implementation all the work is happening on the `petal` branch. 46 | 47 | ## Milestone 1: A pre-alpha release 48 | 49 | ### Targets: 50 | 51 | - [ ] Gap buffer to replace string array 52 | - [x] Within line visual mode (kind of) 53 | - [ ] Fix found search result highlighting 54 | - [ ] Horizontal scrolling 55 | - [ ] Splits (horizontal + vertical) 56 | - [ ] Goto def 57 | - [x] List of active but not open buffers 58 | - [x] Search/Find files 59 | - [ ] Workspace wide search (ripgrep + roll your own) 60 | 61 | ## How to build (requires the V compiler https://vlang.io) 62 | 63 | #### Build lilly by executing 64 | ./make.vsh build-prod 65 | 66 | #### or run with no binary build with 67 | ./make.vsh run 68 | 69 | You can see what other tasks are available to run with `./make.vsh --tasks` 70 | 71 | (you can compile make.vsh into a binary to make executing tasks as fast as possible, use `./make.vsh compile-make` or `v -prod -skip-running make.vsh`) 72 | 73 | ### Not convinced? 74 | 75 | Not a problem, Neovim/VIM are fantastic existing projects and are freely available for you to use today. 76 | 77 | ### misc + extra information 78 | 79 | ### radicle.xyz remote 80 | 81 | The Lilly project is also hosted by (approx minimum 20 seeds) on the decentralised peer-to-peer git host network "Radicle". 82 | If you would like to contribute using that instead of Github then please clone with: 83 | `rad clone rad:zENt7TUiNcnJSf9H371PZ66XdgxE` and then submit a patch in the usual git way but using the rad toolchain (see https://radicle.xyz/guides/user#working-with-patches) 84 | 85 | Feel free to also raise issues here, I will hopefully remember to check the inbox frequently. 86 | 87 | 88 | ### memleak checks 89 | 90 | On macOS we get this output from running: 91 | 92 | `leaks --atExit -- ./lilly .` 93 | 94 | ``` 95 | lilly(53176) MallocStackLogging: could not tag MSL-related memory as no_footprint, so those pages will be included in process footprint - (null) 96 | lilly(53176) MallocStackLogging: recording malloc and VM allocation stacks using lite mode 97 | Process 53176 is not debuggable. Due to security restrictions, leaks can only show or save contents of readonly memory of restricted processes. 98 | 99 | Process: lilly [53176] 100 | Path: /Users/USER/*/lilly 101 | Load Address: 0x10294c000 102 | Identifier: lilly 103 | Version: 0 104 | Code Type: ARM64 105 | Platform: macOS 106 | Parent Process: leaks [53172] 107 | 108 | Date/Time: 2024-12-05 11:07:02.409 +0000 109 | Launch Time: 2024-12-05 11:06:46.429 +0000 110 | OS Version: macOS 13.2.1 (22D68) 111 | Report Version: 7 112 | Analysis Tool: /usr/bin/leaks 113 | 114 | Physical footprint: 4513K 115 | Physical footprint (peak): 4529K 116 | Idle exit: untracked 117 | ---- 118 | 119 | leaks Report Version: 4.0, multi-line stacks 120 | Process 53176: 226 nodes malloced for 22 KB 121 | Process 53176: 0 leaks for 0 total leaked bytes. 122 | ``` 123 | 124 | Look at that. 0 memory leaks. 125 | 126 | (experimental GUI render target) 127 | 128 | ![Screenshot 2023-12-13 21 10 40](https://github.com/tauraamui/lilly/assets/3159648/17ec7286-ecc2-4e68-addd-9c503afd45ee) 129 | -------------------------------------------------------------------------------- /src/lib/syntax/parser.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Edtior contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module syntax 16 | 17 | enum State { 18 | default 19 | in_comment 20 | in_block_comment 21 | in_double_quote 22 | in_single_quote 23 | } 24 | 25 | pub enum TokenType { 26 | identifier 27 | operator 28 | string 29 | comment 30 | comment_start 31 | comment_end 32 | block_start 33 | block_end 34 | number 35 | whitespace 36 | keyword 37 | literal 38 | builtin 39 | other 40 | } 41 | 42 | pub struct Token { 43 | t_type TokenType 44 | mut: 45 | start int 46 | end int 47 | } 48 | 49 | pub fn (t Token) start() int { 50 | return t.start 51 | } 52 | 53 | pub fn (t Token) end() int { 54 | return t.end 55 | } 56 | 57 | pub fn (t Token) t_type() TokenType { 58 | return t.t_type 59 | } 60 | 61 | struct LineInfo { 62 | start_token_index int 63 | token_count int 64 | } 65 | 66 | pub struct Parser { 67 | l_syntax []Syntax 68 | mut: 69 | state State 70 | pending_token ?Token 71 | tokens []Token 72 | line_info []LineInfo 73 | } 74 | 75 | pub fn Parser.new(syn []Syntax) Parser { 76 | return Parser{ 77 | l_syntax: syn 78 | } 79 | } 80 | 81 | pub fn (mut parser Parser) reset() { 82 | parser.state = .default 83 | parser.pending_token = none 84 | parser.tokens.clear() 85 | parser.line_info.clear() 86 | } 87 | 88 | pub fn (parser Parser) get_line_tokens(line_num int) []Token { 89 | if line_num < 0 || line_num >= parser.line_info.len { 90 | return []Token{} 91 | } 92 | line_info := parser.line_info[line_num] 93 | start_index := line_info.start_token_index 94 | end_index := start_index + line_info.token_count 95 | return parser.tokens[start_index..end_index] 96 | } 97 | 98 | pub fn (mut parser Parser) parse_lines(lines []string) { 99 | for i, line in lines { 100 | parser.parse_line(i, line) 101 | } 102 | } 103 | 104 | fn resolve_char_type(c_char rune) TokenType { 105 | // Default classification 106 | return match c_char { 107 | ` `, `\t` { .whitespace } 108 | `a`...`z`, `A`...`Z` { .identifier } 109 | `0`...`9` { .number } 110 | `"`, `'` { .string } // quotes should be string tokens 111 | else { .other } 112 | } 113 | } 114 | 115 | fn for_each_char(index int, 116 | l_char rune, c_char rune, 117 | mut rune_count &int, 118 | mut token_count &int, 119 | mut tokens []Token, 120 | parser_state State) TokenType { 121 | current_char_type := resolve_char_type(c_char) 122 | if l_char != rune(0) { 123 | last_char_type := resolve_char_type(l_char) 124 | 125 | mut token_type := last_char_type 126 | if last_char_type != .whitespace { 127 | token_type = match parser_state { 128 | .in_comment { TokenType.comment } 129 | .in_block_comment { TokenType.comment } 130 | .in_double_quote { TokenType.string } 131 | .in_single_quote { TokenType.string } 132 | .default { last_char_type } 133 | } 134 | } 135 | 136 | transition_occurred := last_char_type != current_char_type 137 | if transition_occurred { 138 | token := Token{ 139 | t_type: token_type 140 | start: index - rune_count 141 | end: index 142 | } 143 | tokens << token 144 | token_count += 1 145 | rune_count = 0 146 | } 147 | } 148 | 149 | rune_count += 1 150 | return current_char_type 151 | } 152 | 153 | pub fn (mut parser Parser) parse_line(index int, line string) []Token { 154 | mut start_token_index := parser.tokens.len 155 | mut token_count := 0 156 | mut rune_count := 0 157 | runes := line.runes() 158 | if parser.state == .in_comment { 159 | parser.state = .default 160 | } 161 | // single line comments terminate at the end of the line 162 | 163 | mut token_type := TokenType.other 164 | for i, c_char in runes { 165 | mut l_char := rune(0) 166 | if i > 0 { 167 | l_char = runes[i - 1] 168 | } 169 | 170 | // store the previous state before updating 171 | previous_state := parser.state 172 | 173 | parser.state = match parser.state { 174 | .default { 175 | match true { 176 | l_char == `/` && c_char == `/` { .in_comment } 177 | l_char == `/` && c_char == `*` { .in_block_comment } 178 | c_char == `"` { .in_double_quote } 179 | c_char == `'` { .in_single_quote } 180 | else { State.default } 181 | } 182 | } 183 | .in_double_quote { 184 | if c_char == `"` { State.default } else { State.in_double_quote } 185 | } 186 | .in_single_quote { 187 | if c_char == `'` { State.default } else { State.in_single_quote } 188 | } 189 | .in_block_comment { 190 | match true { 191 | l_char == `*` && c_char == `/` { State.default } 192 | else { State.in_block_comment } 193 | } 194 | } 195 | else { 196 | parser.state 197 | } 198 | } 199 | 200 | // use previous_state for classifying the previous character 201 | token_type = for_each_char(i, l_char, c_char, mut &rune_count, mut &token_count, mut 202 | parser.tokens, previous_state) 203 | } 204 | 205 | token_type = match parser.state { 206 | .in_comment { TokenType.comment } 207 | .in_block_comment { TokenType.comment } 208 | .in_double_quote { TokenType.string } 209 | .in_single_quote { TokenType.string } 210 | else { token_type } 211 | } 212 | 213 | if rune_count > 0 { 214 | token := Token{ 215 | t_type: token_type 216 | start: runes.len - rune_count 217 | end: runes.len 218 | } 219 | parser.tokens << token 220 | token_count += 1 221 | } 222 | 223 | line_info := LineInfo{ 224 | start_token_index: start_token_index 225 | token_count: token_count 226 | } 227 | parser.line_info << line_info 228 | return parser.get_line_tokens(index) 229 | } 230 | -------------------------------------------------------------------------------- /make.vsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S v run 2 | 3 | import build 4 | import strconv 5 | import math 6 | 7 | const app_name = 'lilly' 8 | 9 | mut context := build.context( 10 | default: 'run' 11 | ) 12 | 13 | // BUILD TASKS 14 | context.task(name: 'build', depends: ['_generate-git-hash'], run: |self| system('v ./src -o lilly')) 15 | context.task( 16 | name: 'build-prod' 17 | depends: ['_generate-git-hash'] 18 | run: |self| system('v ./src -o ${app_name}') 19 | ) 20 | context.task(name: 'run', depends: ['_generate-git-hash'], run: |self| system('v -g run ./src .')) 21 | context.task( 22 | name: 'run-with-gap' 23 | depends: ['_generate-git-hash'] 24 | run: |self| system('v -g run ./src -ugb .') 25 | ) 26 | context.task( 27 | name: 'run-debug-log' 28 | depends: ['_generate-git-hash'] 29 | run: |self| system('v -g run ./src --log-level debug .') 30 | ) 31 | context.task( 32 | name: 'run-gui' 33 | depends: ['_generate-git-hash'] 34 | run: |self| system('v -g -d gui run ./src .') 35 | ) 36 | context.task(name: 'compile-make', run: |self| system('v -prod -skip-running make.vsh')) 37 | 38 | // TEST TASKS 39 | context.task( 40 | name: 'test' 41 | depends: ['_generate-git-hash'] 42 | run: |self| exit(system('v -g test ./src')) 43 | ) 44 | context.task( 45 | name: 'verbose-test' 46 | depends: ['_generate-git-hash'] 47 | run: |self| exit(system('v -g -stats test ./src')) 48 | ) 49 | 50 | // EXPERIMENTS 51 | context.task( 52 | name: 'linux-clipboard' 53 | help: 'runs experiment to test linux C code clipboard integration' 54 | run: fn (self build.Task) ! { 55 | system('v -g run ./experiment/clipboard/x11.c.v') 56 | } 57 | ) 58 | 59 | context.task( 60 | name: 'emoji-grid' 61 | help: 'runs experiment to test emoji grid rendering' 62 | depends: ['_copy-emoji-grid-code'] 63 | run: fn (self build.Task) ! { 64 | system('v -g run ./src/emoji_grid.v') 65 | rm('./src/emoji_grid.v')! 66 | } 67 | ) 68 | 69 | context.task( 70 | name: 'immediate-grid' 71 | help: 'runs experiment to test immediate grid rendering' 72 | depends: ['_copy-immediate-grid-code'] 73 | run: fn (self build.Task) ! { 74 | system('v -g run ./src/immediate_grid.v') 75 | rm('./src/immediate_grid.v')! 76 | } 77 | ) 78 | 79 | // UTIL TASKS 80 | context.task(name: 'format', run: |self| system('v fmt -w .')) 81 | 82 | context.task(name: 'git', depends: ['format'], run: |self| system('lazygit')) 83 | 84 | context.task( 85 | name: 'ansi-colour-codes' 86 | help: 'displays ansi colour code chart' 87 | run: fn (self build.Task) ! { 88 | print('\n + ') 89 | for i := 0; i < 36; i++ { 90 | print('${i:2} ') 91 | } 92 | 93 | print('\n\n ${0:3} ') 94 | for i := 0; i < 16; i++ { 95 | print('\033[48;5;${i}m \033[m ') 96 | } 97 | 98 | for i := 0; i < 7; i++ { 99 | real_i := (i * 36) + 16 100 | print('\n\n ${real_i:3} ') 101 | for j := 0; j < 36; j++ { 102 | print('\033[48;5;${real_i + j}m \033[m ') 103 | } 104 | } 105 | println('') 106 | } 107 | ) 108 | context.task( 109 | name: 'ansi-to-rgb' 110 | help: 'prompts for single ansi colour code and outputs the RGB components' 111 | run: fn (self build.Task) ! { 112 | ansi_num_to_convert := input('ANSI colour to convert to RGB: ') 113 | c := strconv.atoi(ansi_num_to_convert) or { panic('invalid num: ${err}') } 114 | 115 | if !(c >= 16 && c <= 231) { 116 | println('${c} -> is outside the 6x6x6 colour cube (16-231).') 117 | return 118 | } 119 | 120 | c_prime := c - 16 121 | r := c_prime / 36 122 | g := (c_prime % 36) / 6 123 | b := c_prime % 6 124 | 125 | levels := [int(0), 95, 135, 175, 215, 255] 126 | rr := levels[r] 127 | gg := levels[g] 128 | bb := levels[b] 129 | 130 | println('${c} -> RGB(${rr}, ${gg}, ${bb})') 131 | } 132 | ) 133 | 134 | context.task( 135 | name: 'rgb-to-ansi' 136 | help: 'prompts three times for rgb values and produces single ansi colour code' 137 | run: fn (self build.Task) ! { 138 | find_nearest_level := fn (levels []int, value int) int { 139 | mut nearest_index := 0 140 | mut min_diff := math.max_f64 141 | for i, level in levels { 142 | diff := math.abs(f64(value - level)) 143 | if diff < min_diff { 144 | min_diff = diff 145 | nearest_index = i 146 | } 147 | } 148 | return nearest_index 149 | } 150 | 151 | levels := [int(0), 95, 135, 175, 215, 255] 152 | 153 | rr_num_to_convert := input('R: ') 154 | rr := strconv.atoi(rr_num_to_convert) or { panic('invalid num for R: ${err}') } 155 | gg_num_to_convert := input('G: ') 156 | gg := strconv.atoi(gg_num_to_convert) or { panic('invalid num for G: ${err}') } 157 | bb_num_to_convert := input('B: ') 158 | bb := strconv.atoi(bb_num_to_convert) or { panic('invalid num for B: ${err}') } 159 | 160 | r := find_nearest_level(levels, rr) 161 | g := find_nearest_level(levels, gg) 162 | b := find_nearest_level(levels, bb) 163 | println('RGB(${rr}, ${gg}, ${bb}) -> ${16 + (36 * r) + (6 * g) + b}') 164 | } 165 | ) 166 | context.task(name: 'git-prune', run: |self| system('git remote prune origin')) 167 | // NOTE(tauraamui) [27/05/2025]: unsure whether this util should really live here 168 | // since it's only really for me as it's unlikely 169 | // anyone else will be using radical but oh well?' 170 | context.task(name: 'rad-push', run: |self| system('git push rad master')) 171 | context.task( 172 | name: 'apply-license-header' 173 | help: 'executes addlicense tool to insert license headers into files without one' 174 | run: |self| system('addlicense -v -c "The Lilly Edtior contributors" -y "2025" ./src/*') 175 | ) 176 | context.task( 177 | name: 'install-license-tool' 178 | help: 'REQUIRES GO: installs a tool used to insert the license header into source files' 179 | run: |self| system('go install github.com/google/addlicense@latest') 180 | ) 181 | 182 | // ARTIFACTS 183 | context.artifact( 184 | name: '_generate-git-hash' 185 | help: 'generate .githash to contain latest commit of current branch to embed in builds' 186 | run: |self| system('git log -n 1 --pretty=format:"%h" | tee ./src/.githash') 187 | ) 188 | context.artifact( 189 | name: '_copy-emoji-grid-code' 190 | help: 'internal tool, do not run this directly' 191 | run: |self| cp('./experiment/tui_render/emoji_grid.v', './src/emoji_grid.v')! 192 | ) 193 | context.artifact( 194 | name: '_copy-immediate-grid-code' 195 | help: 'internal tool, do not run this directly' 196 | run: |self| cp('./experiment/tui_render/immediate_grid.v', './src/immediate_grid.v')! 197 | ) 198 | 199 | context.run() 200 | -------------------------------------------------------------------------------- /src/lib/ui/file_picker_modal.v: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module ui 16 | 17 | import strings 18 | import lib.draw 19 | import lib.core 20 | 21 | const max_height = 20 22 | 23 | @[noinit] 24 | pub struct FilePickerModal { 25 | title string 26 | pub mut: 27 | special_mode bool // NOTE(tsauraamui) [14/02/2025] will likely deprecate or change this for now 28 | mut: 29 | file_paths []string 30 | search FileSearch 31 | open bool 32 | current_sel_id int 33 | from int 34 | } 35 | 36 | struct FileSearch { 37 | mut: 38 | query string 39 | cursor_x int 40 | } 41 | 42 | fn (mut file_search FileSearch) put_char(c string) { 43 | first := file_search.query[..file_search.cursor_x] 44 | last := file_search.query[file_search.cursor_x..] 45 | file_search.query = '${first}${c}${last}' 46 | file_search.cursor_x += 1 47 | } 48 | 49 | fn (mut file_search FileSearch) backspace() { 50 | if file_search.cursor_x == 0 { 51 | return 52 | } 53 | first := file_search.query[..file_search.cursor_x - 1] 54 | last := file_search.query[file_search.cursor_x..] 55 | file_search.query = '${first}${last}' 56 | file_search.cursor_x -= 1 57 | if file_search.cursor_x < 0 { 58 | file_search.cursor_x = 0 59 | } 60 | } 61 | 62 | pub fn FilePickerModal.new(title string, file_paths []string, special_mode bool) FilePickerModal { 63 | return FilePickerModal{ 64 | title: if title.len == 0 { 'FILE PICKER' } else { title } 65 | file_paths: file_paths 66 | special_mode: special_mode 67 | search: FileSearch{} 68 | } 69 | } 70 | 71 | pub fn (mut f_picker FilePickerModal) open() { 72 | f_picker.open = true 73 | } 74 | 75 | pub fn (mut f_picker FilePickerModal) draw(mut ctx draw.Contextable) { 76 | defer { ctx.reset_bg_color() } 77 | ctx.set_color(r: 245, g: 245, b: 245) 78 | ctx.set_bg_color(r: 15, g: 15, b: 15) 79 | mut y_offset := 0 80 | debug_mode_str := if ctx.render_debug() { ' *** RENDER DEBUG MODE ***' } else { '' } 81 | special_mode_str := if f_picker.special_mode { ' - SPECIAL MODE' } else { '' } 82 | ctx.draw_text(0, y_offset, '=== ${debug_mode_str} ${f_picker.title}${special_mode_str} ${debug_mode_str} ===') 83 | y_offset += 1 84 | ctx.set_cursor_position(0, y_offset + f_picker.current_sel_id - f_picker.from) 85 | y_offset += f_picker.draw_scrollable_list(mut ctx, y_offset, f_picker.file_paths) 86 | ctx.set_bg_color(r: 153, g: 95, b: 146) 87 | ctx.draw_rect(0, y_offset, ctx.window_width(), 1) 88 | search_label := 'SEARCH:' 89 | ctx.draw_text(0, y_offset, search_label) 90 | ctx.draw_text(utf8_str_visible_length(search_label) + 1, y_offset, f_picker.search.query) 91 | } 92 | 93 | fn (mut f_picker FilePickerModal) draw_scrollable_list(mut ctx draw.Contextable, y_offset int, list []string) int { 94 | ctx.reset_bg_color() 95 | ctx.set_bg_color(r: 15, g: 15, b: 15) 96 | ctx.draw_rect(0, y_offset, ctx.window_width(), max_height) 97 | to := f_picker.resolve_to() 98 | for i := f_picker.from; i < to; i++ { 99 | ctx.set_bg_color(r: 15, g: 15, b: 15) 100 | if f_picker.current_sel_id == i { 101 | ctx.set_bg_color(r: 53, g: 53, b: 53) 102 | ctx.draw_rect(0, y_offset + (i - f_picker.from), ctx.window_width(), 1) 103 | } 104 | ctx.draw_text(0, y_offset + (i - f_picker.from), list[i]) 105 | } 106 | return y_offset + (max_height - 2) 107 | } 108 | 109 | pub struct Action { 110 | pub: 111 | op ActionOp 112 | file_path string 113 | } 114 | 115 | pub enum ActionOp as u8 { 116 | no_op 117 | close_op 118 | open_file_op 119 | } 120 | 121 | pub fn (mut f_picker FilePickerModal) on_key_down(e draw.Event) Action { 122 | match e.code { 123 | .escape { 124 | return Action{ 125 | op: .close_op 126 | } 127 | } 128 | 48...57, 97...122 { 129 | f_picker.search.put_char(e.ascii.ascii_str()) 130 | f_picker.current_sel_id = 0 131 | f_picker.reorder_file_paths() 132 | } 133 | .down { 134 | f_picker.move_selection_down() 135 | } 136 | .up { 137 | f_picker.move_selection_up() 138 | } 139 | .enter { 140 | skip_byte_check := f_picker.special_mode 141 | return f_picker.file_selected(skip_byte_check) 142 | } 143 | .backspace { 144 | f_picker.search.backspace() 145 | f_picker.current_sel_id = 0 146 | f_picker.reorder_file_paths() 147 | } 148 | else { 149 | f_picker.search.put_char(e.ascii.ascii_str()) 150 | f_picker.current_sel_id = 0 151 | f_picker.reorder_file_paths() 152 | } 153 | } 154 | return Action{ 155 | op: .no_op 156 | } 157 | } 158 | 159 | fn (mut f_picker FilePickerModal) move_selection_down() { 160 | file_paths := f_picker.file_paths 161 | f_picker.current_sel_id += 1 162 | to := f_picker.resolve_to() 163 | if f_picker.current_sel_id >= to { 164 | if file_paths.len - to > 0 { 165 | f_picker.from += 1 166 | } 167 | } 168 | if f_picker.current_sel_id >= file_paths.len { 169 | f_picker.current_sel_id = file_paths.len - 1 170 | } 171 | } 172 | 173 | fn (mut f_picker FilePickerModal) move_selection_up() { 174 | f_picker.current_sel_id -= 1 175 | if f_picker.current_sel_id < f_picker.from { 176 | f_picker.from -= 1 177 | } 178 | if f_picker.from < 0 { 179 | f_picker.from = 0 180 | } 181 | if f_picker.current_sel_id < 0 { 182 | f_picker.current_sel_id = 0 183 | } 184 | } 185 | 186 | fn (mut f_picker FilePickerModal) file_selected(skip_byte_check bool) Action { 187 | file_paths := f_picker.file_paths 188 | selected_path := file_paths[f_picker.current_sel_id] 189 | if !skip_byte_check && core.is_binary_file(selected_path) { 190 | return Action{ 191 | op: .no_op 192 | } 193 | } 194 | return Action{ 195 | op: .open_file_op 196 | file_path: selected_path 197 | } 198 | } 199 | 200 | fn (mut f_picker FilePickerModal) reorder_file_paths() { 201 | query := f_picker.search.query 202 | f_picker.file_paths.sort_with_compare(fn [query] (a &string, b &string) int { 203 | a_score := score_value_by_query(query, a) 204 | b_score := score_value_by_query(query, b) 205 | if b_score > a_score { 206 | return 1 207 | } 208 | if a_score == b_score { 209 | return 0 210 | } 211 | return -1 212 | }) 213 | } 214 | 215 | fn (mut f_picker FilePickerModal) resolve_to() int { 216 | file_paths := f_picker.file_paths 217 | mut to := f_picker.from + max_height 218 | if to > file_paths.len { 219 | to = file_paths.len 220 | } 221 | return to 222 | } 223 | 224 | pub fn (f_picker FilePickerModal) is_open() bool { 225 | return f_picker.open 226 | } 227 | 228 | pub fn (mut f_picker FilePickerModal) close() { 229 | f_picker.open = false 230 | } 231 | 232 | @[inline] 233 | fn score_value_by_query(query string, value string) f32 { 234 | return f32(int(strings.dice_coefficient(query, value) * 1000)) / 1000 235 | } 236 | -------------------------------------------------------------------------------- /src/lib/ui/splash_screen.v: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Lilly Editor contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module ui 16 | 17 | import math 18 | import lib.draw 19 | import lib.utf8 20 | 21 | const logo_contents = $embed_file('./splash-logo.txt') 22 | 23 | struct SplashLogo { 24 | mut: 25 | data []string 26 | width int 27 | } 28 | 29 | pub struct SplashScreen { 30 | commit_hash string 31 | pub: 32 | file_path string 33 | mut: 34 | logo SplashLogo 35 | leader_state LeaderState 36 | leader_key string 37 | } 38 | 39 | struct LeaderState { 40 | mut: 41 | special bool 42 | normal bool 43 | x_count int 44 | f_count int 45 | b_count int 46 | leader_mode bool 47 | } 48 | 49 | fn reset_leader_state(mut state LeaderState) { 50 | state.leader_mode = false 51 | state.f_count = 0 52 | state.b_count = 0 53 | state.special = false 54 | state.normal = false 55 | } 56 | 57 | pub fn SplashScreen.new(commit_hash string, leader_key string) SplashScreen { 58 | assert commit_hash.len > 0 59 | assert leader_key.len == 1 60 | 61 | mut splash := SplashScreen{ 62 | commit_hash: commit_hash 63 | file_path: '**lss**' 64 | logo: SplashLogo{ 65 | data: logo_contents.to_string().split_into_lines() 66 | } 67 | leader_key: leader_key 68 | } 69 | 70 | for l in splash.logo.data { 71 | assert l.len >= 1 72 | if l.len > splash.logo.width { 73 | splash.logo.width = l.len 74 | } 75 | } 76 | 77 | return splash 78 | } 79 | 80 | pub fn (splash SplashScreen) jump_line_to_middle(y int) {} 81 | 82 | pub fn (splash SplashScreen) set_from(from int) {} 83 | 84 | pub fn (splash SplashScreen) draw(mut ctx draw.Contextable) { 85 | offset_x := 0 86 | mut offset_y := 0 + f64(ctx.window_height()) * 0.1 87 | assert offset_y >= 1 88 | ctx.set_color(r: 245, g: 191, b: 243) 89 | for i, l in splash.logo.data { 90 | start_x := offset_x + (ctx.window_width() / 2) - (l.runes().len / 2) 91 | assert start_x > 2 92 | if has_colouring_directives(l) { 93 | for j, c in l.runes() { 94 | mut to_draw := '${c}' 95 | if to_draw == 'g' { 96 | to_draw = ' ' 97 | ctx.set_color(r: 97, g: 242, b: 136) 98 | } 99 | if to_draw == 'p' { 100 | to_draw = ' ' 101 | ctx.set_color(r: 245, g: 191, b: 243) 102 | } 103 | ctx.draw_text(start_x + j, int(math.floor(offset_y)) + i, to_draw) 104 | } 105 | continue 106 | } 107 | ctx.draw_text(offset_x + (ctx.window_width() / 2) - (l.runes().len / 2), 108 | int(math.floor(offset_y)) + i, l) 109 | } 110 | ctx.reset_color() 111 | 112 | offset_y += splash.logo.data.len 113 | offset_y += (ctx.window_height() - offset_y) * 0.05 114 | 115 | fg_color := ctx.theme().pallete[.identifier] 116 | ctx.set_color(r: fg_color.r, g: fg_color.g, b: fg_color.b) 117 | version_label := 'lilly - dev version ${utf8.emoji_shark_char} (#${splash.commit_hash}) leader = ${resolve_whitespace_to_name(splash.leader_key)}' 118 | // version_label := "lilly - dev version (#${gitcommit_hash})" 119 | ctx.draw_text(offset_x + (ctx.window_width() / 2) - (version_label.len / 2), int(math.floor(offset_y)), 120 | version_label) 121 | 122 | offset_y += 2 123 | 124 | basic_command_help := [ 125 | ' Find File ff', 126 | ] 127 | 128 | disabled_command_help := [ 129 | ' Find Word fg', 130 | ' Recent Files fo', 131 | ' File Browser fv', 132 | ' Colorschemes cs', 133 | ' New File nf', 134 | ] 135 | 136 | for h in basic_command_help { 137 | ctx.draw_text(offset_x + (ctx.window_width() / 2) - (h.len / 2), int(math.floor(offset_y)), 138 | h) 139 | offset_y += 2 140 | } 141 | 142 | for dh in disabled_command_help { 143 | ctx.set_style(.strikethrough) 144 | ctx.draw_text(offset_x + (ctx.window_width() / 2) - (dh.len / 2), int(math.floor(offset_y)), 145 | dh) 146 | offset_y += 2 147 | ctx.clear_style() 148 | } 149 | 150 | exit_label_str := 'Exit/Quit ESC' 151 | ctx.draw_text(offset_x + (ctx.window_width() / 2) - (exit_label_str.len / 2), int(math.floor(offset_y)), 152 | exit_label_str) 153 | offset_y += 2 154 | 155 | copyright_footer := 'the lilly editor authors ©' 156 | ctx.draw_text(offset_x + (ctx.window_width() / 2) - (copyright_footer.len / 2), int(math.floor(offset_y)), 157 | copyright_footer) 158 | } 159 | 160 | fn resolve_whitespace_to_name(leader_key string) string { 161 | match leader_key { 162 | ' ' { return 'space' } 163 | else { return leader_key } 164 | } 165 | } 166 | 167 | fn has_colouring_directives(line string) bool { 168 | for c in line.split('') { 169 | if c == 'g' || c == 'p' { 170 | return true 171 | } 172 | } 173 | return false 174 | } 175 | 176 | pub fn (mut splash SplashScreen) on_mouse_scroll(e draw.Event) {} 177 | 178 | pub enum SplashScreenAction as u8 { 179 | no_op 180 | quit 181 | open_file_picker 182 | open_inactive_buffer_picker 183 | open_file_picker_special 184 | open_inactive_buffer_picker_special 185 | } 186 | 187 | pub fn (mut splash SplashScreen) on_key_down(e draw.Event) SplashScreenAction { 188 | match e.utf8 { 189 | splash.leader_key { splash.leader_state.leader_mode = true } 190 | else {} 191 | } 192 | match e.code { 193 | .escape { 194 | if splash.leader_state.leader_mode { 195 | reset_leader_state(mut splash.leader_state) 196 | } 197 | return .quit 198 | } 199 | .x { 200 | if splash.leader_state.leader_mode { 201 | splash.leader_state.x_count += 1 202 | if !splash.leader_state.normal { 203 | splash.leader_state.special = true 204 | } 205 | } 206 | } 207 | .f { 208 | if splash.leader_state.leader_mode { 209 | splash.leader_state.f_count += 1 210 | if !splash.leader_state.special { 211 | splash.leader_state.normal = true 212 | } 213 | } 214 | if splash.leader_state.f_count == 2 { 215 | defer { reset_leader_state(mut splash.leader_state) } 216 | return if !splash.leader_state.special { 217 | .open_file_picker 218 | } else { 219 | .open_file_picker_special 220 | } 221 | } 222 | } 223 | .b { 224 | if splash.leader_state.leader_mode { 225 | splash.leader_state.b_count += 1 226 | if !splash.leader_state.special { 227 | splash.leader_state.normal = true 228 | } 229 | if splash.leader_state.f_count == 1 && splash.leader_state.b_count >= 1 { 230 | defer { reset_leader_state(mut splash.leader_state) } 231 | return if !splash.leader_state.special { 232 | .open_inactive_buffer_picker 233 | } else { 234 | .open_inactive_buffer_picker_special 235 | } 236 | } 237 | } 238 | } 239 | else {} 240 | } 241 | return .no_op 242 | } 243 | -------------------------------------------------------------------------------- /src/lib/theme/theme.v: -------------------------------------------------------------------------------- 1 | module theme 2 | 3 | import term.ui as tui 4 | import lib.syntax as syntaxlib 5 | 6 | const acme_pallete = { 7 | syntaxlib.TokenType.identifier: tui.Color{15, 12, 0} 8 | .operator: tui.Color{15, 12, 0} 9 | .string: tui.Color{146, 100, 25} 10 | .comment: tui.Color{22, 78, 15} 11 | .comment_start: tui.Color{22, 78, 15} 12 | .comment_end: tui.Color{22, 78, 15} 13 | .block_start: tui.Color{15, 12, 0} 14 | .block_end: tui.Color{15, 12, 0} 15 | .number: tui.Color{15, 12, 0} 16 | .whitespace: tui.Color{15, 12, 0} 17 | .keyword: tui.Color{0, 200, 215} 18 | .literal: tui.Color{15, 12, 0} 19 | .builtin: tui.Color{15, 12, 0} 20 | .other: tui.Color{15, 12, 0} 21 | } 22 | 23 | const black_astra_pallete = { 24 | syntaxlib.TokenType.identifier: tui.Color{255, 255, 255} 25 | .operator: tui.Color{15, 12, 0} 26 | .string: tui.Color{255, 95, 135} 27 | .comment: tui.Color{192, 192, 192} 28 | .comment_start: tui.Color{192, 192, 192} 29 | .comment_end: tui.Color{192, 192, 192} 30 | .block_start: tui.Color{15, 12, 0} 31 | .block_end: tui.Color{15, 12, 0} 32 | .number: tui.Color{255, 0, 175} 33 | .whitespace: tui.Color{15, 12, 0} 34 | .keyword: tui.Color{255, 0, 95} 35 | .literal: tui.Color{255, 135, 0} 36 | .builtin: tui.Color{255, 255, 255} 37 | .other: tui.Color{255, 255, 255} 38 | } 39 | 40 | const bloo_pallete = { 41 | syntaxlib.TokenType.identifier: tui.Color{255, 255, 255} 42 | .operator: tui.Color{15, 12, 0} 43 | .string: tui.Color{175, 255, 255} 44 | .comment: tui.Color{192, 192, 192} 45 | .comment_start: tui.Color{192, 192, 192} 46 | .comment_end: tui.Color{192, 192, 192} 47 | .block_start: tui.Color{15, 12, 0} 48 | .block_end: tui.Color{15, 12, 0} 49 | .number: tui.Color{175, 215, 255} 50 | .whitespace: tui.Color{15, 12, 0} 51 | .keyword: tui.Color{0, 255, 255} 52 | .literal: tui.Color{255, 255, 255} 53 | .builtin: tui.Color{255, 255, 255} 54 | .other: tui.Color{255, 255, 255} 55 | } 56 | 57 | const petal_pallete = { 58 | syntaxlib.TokenType.identifier: tui.Color{200, 200, 235} 59 | .operator: tui.Color{200, 200, 235} 60 | .string: tui.Color{87, 215, 217} 61 | .comment: tui.Color{130, 130, 130} 62 | .comment_start: tui.Color{200, 200, 235} 63 | .comment_end: tui.Color{200, 200, 235} 64 | .block_start: tui.Color{200, 200, 235} 65 | .block_end: tui.Color{200, 200, 235} 66 | .number: tui.Color{215, 135, 215} 67 | .whitespace: tui.Color{200, 200, 235} 68 | .keyword: tui.Color{255, 95, 175} 69 | .literal: tui.Color{0, 215, 255} 70 | .builtin: tui.Color{130, 144, 250} 71 | .other: tui.Color{200, 200, 235} 72 | } 73 | 74 | // NOTE(tauraamui) [10/06/2025]: these colors don't need to be valid at all they're only 75 | // here to ensure that colour lookups in tests provide 76 | // unique results 77 | const test_pallete = { 78 | syntaxlib.TokenType.identifier: tui.Color{99, 99, 99} 79 | .operator: tui.Color{87, 87, 87} 80 | .string: tui.Color{50, 50, 50} 81 | .comment: tui.Color{43, 43, 43} 82 | .comment_start: tui.Color{32, 32, 32} 83 | .comment_end: tui.Color{20, 20, 20} 84 | .block_start: tui.Color{19, 19, 19} 85 | .block_end: tui.Color{15, 15, 15} 86 | .number: tui.Color{49, 49, 49} 87 | .whitespace: tui.Color{75, 45, 79} 88 | .keyword: tui.Color{5, 21, 5} 89 | .literal: tui.Color{15, 15, 15} 90 | .builtin: tui.Color{102, 102, 102} 91 | .other: tui.Color{215, 0, 0} 92 | } 93 | 94 | pub fn color_to_type(color tui.Color) ?syntaxlib.TokenType { 95 | index := test_pallete.values().index(color) 96 | if index < 0 { 97 | return none 98 | } 99 | return test_pallete.keys()[index] 100 | } 101 | 102 | pub type Pallete = map[syntaxlib.TokenType]tui.Color 103 | 104 | pub struct Theme { 105 | pub: 106 | pallete Pallete 107 | cursor_line_color tui.Color 108 | selection_highlight_color tui.Color 109 | background_color ?tui.Color 110 | line_number_color tui.Color 111 | } 112 | 113 | pub fn Theme.new(name string) !Theme { 114 | $if test { 115 | return Theme{ 116 | pallete: test_pallete 117 | cursor_line_color: tui.Color{53, 53, 53} 118 | selection_highlight_color: tui.Color{111, 111, 111} 119 | background_color: tui.Color{59, 34, 76} 120 | line_number_color: test_pallete[.number] 121 | } 122 | } 123 | return match name { 124 | 'acme' { 125 | Theme{ 126 | pallete: acme_pallete 127 | cursor_line_color: tui.Color{174, 255, 254} 128 | selection_highlight_color: tui.Color{96, 138, 143} 129 | background_color: tui.Color{255, 255, 215} 130 | line_number_color: acme_pallete[.number] 131 | } 132 | } 133 | 'bloo' { // boris johnson reference. "I like to paint them .. BLOO!" (if you know you know) 134 | Theme{ 135 | pallete: bloo_pallete 136 | cursor_line_color: tui.Color{0, 0, 175} 137 | selection_highlight_color: tui.Color{96, 138, 143} 138 | background_color: tui.Color{0, 95, 255} 139 | line_number_color: bloo_pallete[.number] 140 | } 141 | } 142 | 'petal' { 143 | Theme{ 144 | pallete: petal_pallete 145 | cursor_line_color: tui.Color{53, 53, 53} 146 | selection_highlight_color: tui.Color{96, 138, 143} 147 | background_color: tui.Color{59, 34, 76} 148 | line_number_color: petal_pallete[.number] 149 | } 150 | } 151 | 'black-astra' { // black clover reference 152 | Theme{ 153 | pallete: black_astra_pallete 154 | cursor_line_color: tui.Color{53, 53, 53} 155 | selection_highlight_color: tui.Color{135, 0, 175} 156 | background_color: tui.Color{20, 20, 20} 157 | line_number_color: tui.Color{175, 0, 0} 158 | } 159 | } 160 | 'space' { // this theme has a transparent background, if your terminal emulator 161 | // is also transparent this will enable you to see your wifu desktop pic 162 | // under your code :) 163 | Theme{ 164 | pallete: petal_pallete 165 | cursor_line_color: tui.Color{53, 53, 53} 166 | selection_highlight_color: tui.Color{96, 138, 143} 167 | line_number_color: petal_pallete[.number] 168 | } 169 | } 170 | else { 171 | error("unable to find theme '${name}'") 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /experiment/clipboard/x11.c.v: -------------------------------------------------------------------------------- 1 | #flag -lX11 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | @[typedef] 11 | pub struct C.Display {} 12 | 13 | fn (d &C.Display) str() string { 14 | return 'C.Display{}' 15 | } 16 | 17 | @[typedef] 18 | pub struct C.XSelectionEvent { 19 | mut: 20 | type int 21 | display &C.Display = unsafe { nil } 22 | requestor Window 23 | selection Atom 24 | target Atom 25 | property Atom 26 | time int 27 | } 28 | 29 | type Window = u64 30 | type Atom = u64 31 | type EventMask = u64 32 | 33 | fn C.XOpenDisplay(name &u8) &C.Display 34 | 35 | fn C.XCloseDisplay(d &C.Display) 36 | 37 | fn C.XFlush(display &C.Display) 38 | 39 | fn C.XNextEvent(display &C.Display, event &C.XEvent) 40 | 41 | fn C.XSetSelectionOwner(display &C.Display, atom Atom, window Window, time int) 42 | 43 | fn C.XCreateSimpleWindow(d &C.Display, root Window, 44 | x int, y int, width u32, height u32, 45 | border_width u32, border u64, 46 | background u64) Window 47 | 48 | fn C.XSelectInput(d &C.Display, window Window, EventMask) 49 | 50 | fn C.XInternAtom(display &C.Display, atom_name &u8, only_if_exists int) Atom 51 | 52 | fn C.XConvertSelection(display &C.Display, selection Atom, target Atom, property Atom, requestor Window, time int) int 53 | 54 | fn C.XSync(display &C.Display, discard int) int 55 | 56 | fn C.XGetWindowProperty(display &C.Display, window Window, 57 | property Atom, long_offset i64, long_length i64, 58 | delete int, req_type Atom, actual_type_return &Atom, 59 | actual_format_return &int, nitems_return &u64, 60 | bytes_after_return &u64, prop_return &&u8) int 61 | 62 | fn C.XChangeProperty(display &C.Display, 63 | window Window, 64 | property Atom, 65 | typ Atom, 66 | format int, 67 | mode int, 68 | data voidptr, 69 | nelements int) int 70 | 71 | fn C.XSendEvent(display &C.Display, requestor Window, propegate int, mask i64, event &C.XEvent) 72 | 73 | fn C.RootWindow(display &C.Display, screen_number int) Window 74 | 75 | fn C.XDeleteProperty(display &C.Display, window Window, property Atom) int 76 | 77 | fn C.DefaultScreen(display &C.Display) int 78 | 79 | fn C.BlackPixel(display &C.Display, screen_number int) u32 80 | 81 | fn C.WhitePixel(display &C.Display, screen_number int) u32 82 | 83 | fn C.XFree(data voidptr) 84 | 85 | @[typedef] 86 | pub struct C.XSelectionRequestEvent { 87 | mut: 88 | display &C.Display = unsafe { nil } 89 | owner Window 90 | requestor Window 91 | selection Atom 92 | target Atom 93 | property Atom 94 | time int 95 | } 96 | 97 | @[typedef] 98 | pub struct C.XSelectionEvent { 99 | mut: 100 | type int 101 | display &C.Display = unsafe { nil } 102 | requestor Window 103 | selection Atom 104 | target Atom 105 | property Atom 106 | time int 107 | } 108 | 109 | @[typedef] 110 | pub struct C.XSelectionClearEvent { 111 | mut: 112 | window Window 113 | selection Atom 114 | } 115 | 116 | @[typedef] 117 | pub struct C.XDestroyWindowEvent { 118 | mut: 119 | window Window 120 | } 121 | 122 | @[typedef] 123 | union C.XEvent { 124 | mut: 125 | type int 126 | xdestroywindow C.XDestroyWindowEvent 127 | xselectionclear C.XSelectionClearEvent 128 | xselectionrequest C.XSelectionRequestEvent 129 | xselection C.XSelectionEvent 130 | } 131 | 132 | fn main() { 133 | display := C.XOpenDisplay(C.NULL) 134 | defer { C.XCloseDisplay(display) } 135 | 136 | window := C.XCreateSimpleWindow(display, C.RootWindow(display, C.DefaultScreen(display)), 137 | 10, 10, 200, 200, 1, C.BlackPixel(display, C.DefaultScreen(display)), C.WhitePixel(display, 138 | C.DefaultScreen(display))) 139 | 140 | C.XSelectInput(display, window, C.ExposureMask | C.KeyPressMask) 141 | 142 | utf8_string := C.XInternAtom(display, &char(c'UTF8_STRING'), 1) 143 | clipboard := C.XInternAtom(display, &char(c'CLIPBOARD'), 0) 144 | xsel_data := C.XInternAtom(display, &char(c'XSEL_DATA'), 0) 145 | 146 | save_targets := C.XInternAtom(display, &char(c'SAVE_TARGETS'), 0) 147 | targets := C.XInternAtom(display, &char(c'TARGETS'), 0) 148 | multiple := C.XInternAtom(display, &char(c'MULTIPLE'), 0) 149 | atom_pair := C.XInternAtom(display, &char(c'ATOM_PAIR'), 0) 150 | clipboard_manager := C.XInternAtom(display, &char(c'CLIPBOARD_MANAGER'), 0) 151 | 152 | C.XConvertSelection(display, clipboard, utf8_string, xsel_data, window, C.CurrentTime) 153 | C.XSync(display, 0) 154 | 155 | event := C.XEvent{} 156 | C.XNextEvent(display, &event) 157 | 158 | xa_string := Atom(31) 159 | if unsafe { 160 | event.type == C.SelectionNotify && event.xselection.selection == clipboard 161 | && event.xselection.property != 0 162 | } { 163 | format := 0 164 | n := u64(0) 165 | size := u64(0) 166 | data := &u8(unsafe { nil }) 167 | target := Atom(0) 168 | 169 | C.XGetWindowProperty(event.xselection.display, event.xselection.requestor, event.xselection.property, 170 | 0, 1024, 0, C.AnyPropertyType, &target, &format, &size, &n, &data) 171 | 172 | if target == utf8_string || target == xa_string { 173 | println('CURRENT CLIPBOARD CONTENT: ${cstring_to_vstring(data)}') 174 | C.XFree(data) 175 | } 176 | 177 | C.XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property) 178 | } 179 | 180 | text_to_insert_to_clipboard := 'an example string to copy' 181 | 182 | C.XSetSelectionOwner(display, clipboard, window, C.CurrentTime) 183 | C.XConvertSelection(display, clipboard_manager, save_targets, C.None, window, C.CurrentTime) 184 | 185 | mut running := true 186 | for running { 187 | C.XNextEvent(display, &event) 188 | if unsafe { event.type == C.SelectionRequest } { 189 | request := unsafe { &event.xselectionrequest } 190 | 191 | mut reply := C.XEvent{ 192 | type: C.SelectionNotify 193 | } 194 | reply.xselection = C.XSelectionEvent{ 195 | property: Atom(0) 196 | } 197 | 198 | if request.target == targets { 199 | target_atoms := [targets, multiple, utf8_string, xa_string] 200 | C.XChangeProperty(display, request.requestor, request.property, Atom(4), 201 | Atom(32), C.PropModeReplace, target_atoms.data, target_atoms.len / int(sizeof(target_atoms[0]))) 202 | 203 | reply.xselection.property = request.property 204 | } 205 | 206 | if request.target == multiple { 207 | mut target_atoms := []Atom{} 208 | 209 | actual_type := Atom(0) 210 | actual_format := 0 211 | count := u64(0) 212 | bytes_after := u64(0) 213 | 214 | C.XGetWindowProperty(display, request.requestor, request.property, 0, 215 | C.LONG_MAX, 0, atom_pair, &actual_type, &actual_format, &count, &bytes_after, 216 | target_atoms.data) 217 | 218 | for i := 0; i < count; i += 2 { 219 | if target_atoms[i] == utf8_string || target_atoms[i] == xa_string { 220 | C.XChangeProperty(display, request.requestor, target_atoms[i + 1], 221 | target_atoms[i], Atom(8), C.PropModeReplace, text_to_insert_to_clipboard.str, 222 | text_to_insert_to_clipboard.len) 223 | C.XFlush(display) 224 | running = false 225 | continue 226 | } 227 | target_atoms[i + 1] = C.None 228 | } 229 | 230 | C.XChangeProperty(display, request.requestor, request.property, atom_pair, 231 | Atom(32), C.PropModeReplace, target_atoms.data, count) 232 | C.XFlush(display) 233 | C.XFree(voidptr(&target_atoms)) 234 | 235 | reply.xselection.property = request.property 236 | } 237 | 238 | reply.xselection.display = request.display 239 | reply.xselection.requestor = request.requestor 240 | reply.xselection.selection = request.selection 241 | reply.xselection.target = request.target 242 | reply.xselection.time = request.time 243 | 244 | C.XSendEvent(display, request.requestor, 0, 0, voidptr(&reply)) 245 | C.XFlush(display) 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/lib/buffer/line_buffer.v: -------------------------------------------------------------------------------- 1 | module buffer 2 | 3 | import arrays 4 | 5 | struct LineBuffer { 6 | mut: 7 | lines []string 8 | } 9 | 10 | fn LineBuffer.new(d []string) LineBuffer { 11 | return LineBuffer{ 12 | lines: d 13 | } 14 | } 15 | 16 | pub fn (mut l_buffer LineBuffer) insert_text(pos Position, s string) ?Position { 17 | // handle if set of lines up to position don't exist 18 | if l_buffer.expansion_required(pos) { 19 | return grow_and_set(mut l_buffer.lines, pos.line, s) 20 | } 21 | 22 | line_content := l_buffer.lines[pos.line] 23 | mut clamped_offset := if pos.offset > line_content.len { line_content.len } else { pos.offset } 24 | if clamped_offset > line_content.runes().len { 25 | return Position.new(line: pos.line, offset: clamped_offset) 26 | } 27 | 28 | pre_line_content := line_content.runes()[..clamped_offset].string() 29 | post_line_content := line_content.runes()[clamped_offset..line_content.runes().len].string() 30 | 31 | l_buffer.lines[pos.line] = '${pre_line_content}${s}${post_line_content}' 32 | 33 | clamped_pos := Position.new(line: pos.line, offset: clamped_offset) 34 | 35 | return clamped_pos.add(Distance{ lines: 0, offset: s.runes().len }) 36 | } 37 | 38 | pub fn (mut l_buffer LineBuffer) insert_tab(pos Position, tabs_not_spaces bool) ?Position { 39 | prefix := if tabs_not_spaces { '\t' } else { ' '.repeat(4) } 40 | return l_buffer.insert_text(pos, prefix) 41 | } 42 | 43 | pub fn (mut l_buffer LineBuffer) newline(pos Position) ?Position { 44 | // handle if set of lines up to position don't exist 45 | if l_buffer.expansion_required(pos) { 46 | post_expand_pos := grow_and_set(mut l_buffer.lines, pos.line, '') 47 | l_buffer.lines << [''] 48 | return post_expand_pos.add(Distance{ lines: 1, offset: 0 }) 49 | } 50 | 51 | line_at_pos := l_buffer.lines[pos.line] 52 | clamped_offset := if pos.offset > line_at_pos.runes().len { 53 | line_at_pos.runes().len 54 | } else { 55 | pos.offset 56 | } 57 | content_after_cursor := line_at_pos[clamped_offset..] 58 | content_before_cursor := line_at_pos[..clamped_offset] 59 | 60 | whitespace_prefix := resolve_whitespace_prefix_from_line(content_before_cursor) 61 | l_buffer.lines[pos.line] = content_before_cursor 62 | l_buffer.lines.insert(pos.line + 1, '${whitespace_prefix}${content_after_cursor}') 63 | return Position.new(line: pos.line, offset: 0).add(Distance{ 64 | lines: 1 65 | offset: whitespace_prefix.runes().len 66 | }) 67 | } 68 | 69 | fn resolve_whitespace_prefix_from_line(line string) string { 70 | // when mapping over rune data, we acquire the first index for which a 71 | // char is not empty/whitespace. first visible char we encounter, we return the index! :) 72 | pre_prefix_index := arrays.index_of_first(line.runes(), fn (idx int, cchar rune) bool { 73 | return !is_whitespace(cchar) 74 | }) 75 | return match pre_prefix_index { 76 | -1 { '' } 77 | 0 { '' } 78 | else { line.runes()[..pre_prefix_index].string() } 79 | } 80 | } 81 | 82 | pub fn (mut l_buffer LineBuffer) x(pos Position) Position { 83 | if l_buffer.is_oob(pos) { 84 | return pos 85 | } 86 | 87 | line_at_pos := l_buffer.lines[pos.line] 88 | if line_at_pos.len == 0 { 89 | return pos 90 | } 91 | 92 | clamped_offset := if pos.offset >= line_at_pos.runes().len { 93 | line_at_pos.runes().len - 1 94 | } else { 95 | pos.offset 96 | } 97 | mut line_content := l_buffer.lines[pos.line].runes() 98 | line_content.delete(clamped_offset) 99 | l_buffer.lines[pos.line] = line_content.string() 100 | 101 | return Position.new(line: pos.line, offset: clamped_offset) 102 | } 103 | 104 | pub fn (mut l_buffer LineBuffer) backspace(pos Position) ?Position { 105 | if pos.line == 0 && pos.offset == 0 { 106 | return pos 107 | } 108 | 109 | clamped_pos := if l_buffer.is_oob(pos) { 110 | Position.new( 111 | line: l_buffer.lines.len - 1 112 | offset: l_buffer.lines[l_buffer.lines.len - 1].runes().len - 1 113 | ) 114 | } else { 115 | pos 116 | } 117 | 118 | if clamped_pos.offset > 0 { 119 | mut line_content := l_buffer.lines[clamped_pos.line].runes() 120 | line_content.delete(clamped_pos.offset) 121 | l_buffer.lines[clamped_pos.line] = line_content.string() 122 | return clamped_pos.add(Distance{0, -1}) 123 | } 124 | 125 | line_content := l_buffer.lines[clamped_pos.line] 126 | if clamped_pos.line - 1 >= 0 { 127 | length_pre_append := l_buffer.lines[clamped_pos.line - 1].runes().len 128 | l_buffer.lines[clamped_pos.line - 1] = '${l_buffer.lines[clamped_pos.line - 1]}${line_content}' 129 | l_buffer.lines.delete(clamped_pos.line) 130 | return clamped_pos.add(Distance{-1, length_pre_append - 1}) 131 | } 132 | 133 | return none 134 | } 135 | 136 | pub fn (l_buffer LineBuffer) delete(ignore_newlines bool) bool { 137 | return false 138 | } 139 | 140 | pub fn (mut l_buffer LineBuffer) o(pos Position) ?Position { 141 | if l_buffer.expansion_required(pos) { 142 | post_expand_pos := grow_and_set(mut l_buffer.lines, pos.line, '') 143 | l_buffer.lines << [''] 144 | return post_expand_pos.add(Distance{ lines: 1, offset: 0 }) 145 | } 146 | l_buffer.lines.insert(pos.line + 1, '') 147 | return Position.new(line: pos.line, offset: 0).add(Distance{ lines: 1 }) 148 | } 149 | 150 | pub fn (l_buffer LineBuffer) left(pos Position) ?Position { 151 | // the add method auto clamps indexes of less than 0 152 | return pos.add(Distance{ lines: 0, offset: -1 }) 153 | } 154 | 155 | pub fn (l_buffer LineBuffer) right(pos Position, insert_mode bool) ?Position { 156 | if l_buffer.is_oob(pos) { 157 | return pos 158 | } 159 | return l_buffer.clamp_cursor_x_pos(pos.add(Distance{ offset: 1 }), insert_mode) 160 | } 161 | 162 | pub fn (l_buffer LineBuffer) down(pos Position, insert_mode bool) ?Position { 163 | pos_one_line_down := pos.add(Distance{ lines: 1 }) 164 | if pos_one_line_down.line >= l_buffer.lines.len { 165 | return pos 166 | } 167 | return l_buffer.clamp_cursor_x_pos(pos_one_line_down, insert_mode) 168 | } 169 | 170 | pub fn (l_buffer LineBuffer) up(pos Position, insert_mode bool) ?Position { 171 | pos_one_line_down := pos.add(Distance{ lines: -1 }) 172 | return l_buffer.clamp_cursor_x_pos(pos_one_line_down, insert_mode) 173 | } 174 | 175 | pub fn (l_buffer LineBuffer) up_to_next_blank_line(pos Position) ?Position { 176 | if pos.line >= l_buffer.lines.len { 177 | return none 178 | } 179 | from := pos.line 180 | for i := from; i >= 0; i-- { 181 | line_is_empty := l_buffer.lines[i].len == 0 182 | if i != from && line_is_empty { 183 | return Position.new(line: from, offset: 0).add(Distance{ lines: (from - i) * -1 }) 184 | } 185 | } 186 | return none 187 | } 188 | 189 | pub fn (l_buffer LineBuffer) down_to_next_blank_line(pos Position) ?Position { 190 | if pos.line >= l_buffer.lines.len { 191 | return none 192 | } 193 | from := pos.line 194 | for i := from; i < l_buffer.lines.len; i++ { 195 | line_is_empty := l_buffer.lines[i].len == 0 196 | if i != from && line_is_empty { 197 | return Position.new(line: from, offset: 0).add(Distance{ lines: i }) 198 | } 199 | } 200 | return none 201 | } 202 | 203 | pub fn (l_buffer LineBuffer) num_of_lines() int { 204 | return l_buffer.lines.len 205 | } 206 | 207 | pub fn (l_buffer LineBuffer) str() string { 208 | return l_buffer.lines.join('\n') 209 | } 210 | 211 | fn (l_buffer LineBuffer) is_oob(pos Position) bool { 212 | return l_buffer.lines.len - 1 < pos.line 213 | } 214 | 215 | fn (l_buffer LineBuffer) expansion_required(pos Position) bool { 216 | return l_buffer.is_oob(pos) 217 | } 218 | 219 | fn (l_buffer LineBuffer) clamp_cursor_x_pos(pos Position, insert_mode bool) Position { 220 | mut clamped_offset := pos.offset 221 | if clamped_offset < 0 { 222 | clamped_offset = 0 223 | } 224 | 225 | if l_buffer.lines.len == 0 { 226 | return Position.new(line: 0, offset: 0) 227 | } 228 | current_line_len := l_buffer.lines[pos.line].runes().len 229 | 230 | if insert_mode { 231 | if clamped_offset > current_line_len { 232 | clamped_offset = current_line_len 233 | } 234 | } else { 235 | diff := pos.offset - (current_line_len - 1) 236 | if diff > 0 { 237 | clamped_offset = current_line_len - 1 238 | } 239 | } 240 | if clamped_offset < 0 { 241 | clamped_offset = 0 242 | } 243 | return Position.new(line: pos.line, offset: clamped_offset) 244 | } 245 | 246 | fn grow_and_set(mut lines []string, pos_line int, data_to_set string) Position { 247 | s := data_to_set 248 | lines << []string{len: pos_line - lines.len + 1} 249 | lines[pos_line] = s 250 | return Position.new(line: pos_line, offset: s.runes().len) 251 | } 252 | --------------------------------------------------------------------------------