diff --git a/.gitignore b/.gitignore index a0c115a..9a8ad3e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,7 @@ build bin .thinlto-cache/ +Gemfile* +.ruby-lsp/ __old__ diff --git a/Makefile b/Makefile index f50695c..ab96538 100644 --- a/Makefile +++ b/Makefile @@ -13,14 +13,14 @@ CCACHE := ccache CXX := $(CCACHE) clang++ CFLAGS_DEBUG :=\ - -std=c++20 -Wall -Wextra \ + -std=c++23 -Wall -Wextra -Wno-c23-extensions \ -O0 -fno-inline -gsplit-dwarf\ -g -fno-omit-frame-pointer\ -Wno-unused-command-line-argument \ -I./include -I./libs \ -I/usr/include/ruby-3.4.0 -I/usr/include/ruby-3.4.0/x86_64-linux CFLAGS_RELEASE :=\ - -std=c++20 -O3 -march=native \ + -std=c++23 -O3 -march=native \ -fno-rtti -fstrict-aliasing \ -ffast-math -flto=thin \ -fvisibility=hidden -fuse-ld=lld \ @@ -28,6 +28,7 @@ CFLAGS_RELEASE :=\ -mllvm -vectorize-loops \ -fno-unwind-tables -fno-asynchronous-unwind-tables\ -Wno-unused-command-line-argument \ + -Wno-c23-extensions \ -I./include -I./libs \ -I/usr/include/ruby-3.4.0 -I/usr/include/ruby-3.4.0/x86_64-linux @@ -41,7 +42,7 @@ UNICODE_OBJ_RELEASE := $(patsubst libs/unicode_width/%.c,$(OBJ_DIR)/release/unic LIBS := \ libs/libgrapheme/libgrapheme.a \ - -lpcre2-8 -lmagic -lruby + -Wl,-Bstatic -lpcre2-8 -Wl,-Bdynamic -lmagic -lruby SRC := $(wildcard $(SRC_DIR)/**/*.cc) $(wildcard $(SRC_DIR)/*.cc) OBJ_DEBUG := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/debug/%.o,$(SRC)) diff --git a/README.md b/README.md index 769ce9d..ff01845 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,19 @@ And any modern terminal should work fine - preferably `kitty` or `wezterm`.
#### `./libs` folder -Some other dependancies like `libgrapheme` and `unicode_width` are added as submodules or copied.
- `unicode_width` is compiled by the makefile so nothing to do there.
- `libgrapheme` needs to be compiled using `make` in it's folder.
+Some other dependancies are added as submodules or copied.
+ - `unicode_width` is compiled by the makefile so nothing to do there.
+ - `libgrapheme` needs to be compiled using `make` in it's folder.
+ - `` #### LSPs -The following lsp's are supported and can be installed anywhere in your `$PATH`
+Lsp's are defined in the `libcrib.rb` file and you can use your `config/main.rb` file to add more.
+ +The following lsp's are added by default and can be installed anywhere in your `$PATH`
* [clangd](https://clangd.llvm.org/) -* [solargraph](https://solargraph.org/) +* [ruby-lsp](https://shopify.github.io/ruby-lsp/) * [bash-language-server](https://github.com/bash-lsp/bash-language-server) * [vscode-css-language-server](https://github.com/hrsh7th/vscode-langservers-extracted) * [vscode-json-language-server](https://github.com/hrsh7th/vscode-langservers-extracted) @@ -81,14 +84,11 @@ The following lsp's are supported and can be installed anywhere in your `$PATH`< * [make-language-server](https://github.com/Freed-Wu/autotools-language-server) > As it is still in development, some of these may not work as expected or that well.
-> But for c/ruby/lua/python it should work fine (I test more with these).
> It should work even if the lsp is not installed but lsp features will not work.
-> See `include/config.h` & `include/ts/decl.h` if you want to add your own lsp and/or tree-sitter grammar.
#### Compiler -`g++` and `clang++` should both work fine but `c++20+` is required. -The makefile uses `clang++` by default.
+`clang++` should work fine but `c++23+` is required.
Can remove `ccache` if you want from the makefile.
#### Compliling @@ -100,9 +100,6 @@ make release ### Running Preferably add the `bin` folder to PATH or move `bin/crib` to somewhere in PATH.
-But make sure that `scripts/` are at `../` relative to the binary or it will crash.
-`scripts/init.sh` and `scripts/exit.sh` can be used to add hooks to the editor on startup and exit -(Make sure to remove my `kitty` hooks from them if you want).
For some LSP's to work properly `crib` needs to be run from the root folder of the project. *To be fixed*
then do -
@@ -110,127 +107,12 @@ then do -
crib ./filename.ext ``` -*If `filename.ext` does not exist, it will fail to load the editor - use `touch filename.ext` to create it - to be fixed*
-*Try out with files in `samples/`*
+*If `filename.ext` does not exist, it will be created*
## Keybindings -### Mouse Interactions - -These interactions work globally or generally across the editor canvas. - -| Action | Function | -| --- | --- | -| **Scroll Up/Down** | Scrolls the view. | -| **Scroll Left/Right** | Moves the cursor left or right. | -| **Left Click (Press)** | Moves cursor to position; resets selection. | -| **Left Click (Double)** | Selects the **word** under the cursor (enters SELECT mode). | -| **Left Click (Triple)** | Selects the **line** under the cursor (enters SELECT mode). | -| **Left Click (Drag)** | Selects text (Character, Word, or Line based on initial click type). | -| **Left Click (Release)** | If cursor and selection start are the same, returns to NORMAL mode. | - -### Navigation (Global / Special Keys) - -These keys work primarily in Normal mode but handle movement logic. - -| Key | Modifier | Function | -| --- | --- | --- | -| **Arrows** (Up/Down/Left/Right) | None | Move cursor 1 step in that direction. | -| **Arrows** (Up/Down) | `CTRL` | Move cursor **5 steps** in that direction. | -| **Arrows** (Left/Right) | `CTRL` | Jump to the previous/next **word boundary**. | -| **Arrows** (Up/Down) | `ALT` | **Move the current line** Up or Down. | -| **Arrows** (Left/Right) | `ALT` | Move cursor **8 steps** in that direction. | - -### NORMAL Mode - -This is the default navigation and command mode. - -| Key | Function | -| --- | --- | -| **i** | Enter **INSERT** mode (at current position). | -| **a** | Enter **INSERT** mode (append: moves cursor right by 1 first). | -| **s** or **v** | Enter **SELECT** mode (start character selection). | -| **:** or **;** | Enter **RUNNER** mode (Command Bar). | -| **u** | Select the **last line** of the file (Jumps to bottom). | -| **h** | Trigger **LSP Hover** information for the symbol under cursor. | -| **Ctrl + h** | Scroll the hover window **Up**. | -| **Ctrl + l** | Scroll the hover window **Down**. | -| **Ctrl + s** | **Save** the file. | -| **Ctrl + d** | Scroll Page **Down** (1 unit). | -| **Ctrl + u** | Scroll Page **Up** (1 unit). | -| **p** | **Paste** from clipboard at cursor position (moves cursor to end of paste). | -| **>** or **.** | **Indent** the current line. | -| **<** or **,** | **Dedent** (un-indent) the current line. | -| **Space** | Move cursor Right. | -| **Backspace** (`0x7F`) | Move cursor Left. | -| **Enter** (`\n`, `\r`) | Move cursor Down. | -| **\| or \\** | Move cursor Up. | -| **n** | Enter **JUMPER** mode (Set Bookmark). | -| **m** | Enter **JUMPER** mode (Jump to Bookmark). | -| **N** | Clear specific Jumper hook (logic attempts to clear hook at current line). | - -### INSERT Mode - -Used for typing text. - -| Key | Function | -| --- | --- | -| **Esc** (`0x1B`) | Return to **NORMAL** mode. | -| **Tab** (`\t`) | Inserts 2 spaces. | -| **Enter** | Inserts newline + **Auto-indents** based on previous line/context. | -| **Backspace** | Deletes previous character or auto-collapses empty pairs (e.g., `{` -> `}`). | -| **Ctrl + w** | **Delete Previous Word**. | -| **Del** | Delete character under cursor. | -| **Ctrl + Del** | Delete **Next Word**. | -| **Typing** | Inserts characters. | -| **Ctrl + Shift + v or as configured in your terminal** | System pasting. | -| **{ ( [ " '** | Auto-inserts closing pair (e.g., typing `{` inserts `{}`). | -| **} ) ] " '** | If the next char matches the typed char, skip insertion (overwrite), otherwise insert. | - -#### Autocompletion (Inside Insert Mode) - -These function only if LSP and completion are active. - -| Key | Function | -| --- | --- | -| **Ctrl + p** | Select **Next** completion item. | -| **Ctrl + o** | Select **Previous** completion item. | -| **Ctrl + \\** | **Accept** selected completion OR trigger new completion request. | -| **Trigger Chars** | (e.g., `.`, `>`) Automatically triggers completion popup. | - -### SELECT Mode - -Used for highlighting text. - -| Key | Function | -| --- | --- | -| **Esc**, **s**, **v** | Cancel selection and return to **NORMAL** mode. | -| **y** | **Yank (Copy)** selection to clipboard → Return to Normal. | -| **x** | **Cut** selection to clipboard → Return to Normal. | -| **p** | **Paste** over selection (Replace text) → Return to Normal. | -| **f** | **Fold** the selected range (collapses code) → Return to Normal. | - -### JUMPER Mode - -This mode uses a bookmarking system mapped to keyboard characters. - -* **Entered via `n` (Set Mode):** -* Pressing any key `!` through `~` assigns the current line number to that key. - -* **Entered via `m` (Jump Mode):** -* Pressing any key `!` through `~` jumps the cursor to the line previously assigned to that key. - -### RUNNER Mode (Command Bar) - -Activated by `:` or `;`. - -| Key | Function | -| --- | --- | -| **Esc** | Cancel and return to **NORMAL** mode. | -| **Enter** | Execute the typed command. | -| **Left / Right** | Move cursor within the command bar. | -| **Up / Down** | Intended for command history. (Not implemented) | -| **Typing** | Insert characters into the command bar. (Not implemented) | +TODO: add keybind information on how to set in `config/main.rb` + and default / unchangeable keybinds ## Features Implemented @@ -250,7 +132,7 @@ Activated by `:` or `;`. - hooks jumping (bookmarking) - color hex code highlighting - current line highlighting - +- all instances of current word under cursor highlighting #### syntax highlighting and filetype detection (using extention or libmagic) for: - ruby @@ -286,11 +168,13 @@ Activated by `:` or `;`. #### LSP-powered features: - diagnostics - autocompletion -- hover docs (with markdown support) +- hover docs - formatting support - Full file formatting on save - Ontype formatting when inserting special characters defined by the language server - *(few lsp's actually support this - try to configure a few more which can but need configuration and for others need to add support for external formatters)* -- A list of all supported lsp's can be found [here](#lsps). +- A list of some lsp's can be found [here](#lsps). +- Any lsp can be added to the `config/main.rb` file. +- Though not all might work well. Open an issue if you find a lsp that doesn't work well. **A lot lot more to come** diff --git a/TODO.md b/TODO.md index 7cae467..17b8d2e 100644 --- a/TODO.md +++ b/TODO.md @@ -2,15 +2,12 @@ Copyright 2025 Syed Daanish # TODO -### Critical Fixes - -##### Check each lsp with each of the features implemented +##### BTW Check each lsp with each of the features implemented +* Static link pcre2-8 and libmagic and add precompiled libmagic magic database for better file detection * [ ] **LSP Bug:** Check why `fish-lsp` is behaving so off with completions filtering. -* [ ] **File IO:** Normalize/validate unicode on file open (enforce UTF-8, handle other types gracefully). * [ ] **Critical Crash:** Fix bug where closing immediately while LSP is still loading hangs and then segfaults (especially on slow ones like fish-lsp where quick edits and exit can hang). * [ ] **Line move:** fix the move line functions to work without the calculations from folds as folds are removed. -* [ ] **Modularize handle_events and renderer functions:** The function is over 700 lines with a lot of repeating blocks. Split into smaller functions. * [ ] **Editor Indentation Fix:** - Main : merger indentation with the parser for more accurate results. * [ ] Keep cache of language maps in engine to reduce lookup time. * [ ] In indents add function to support tab which indents if before any content and inserts a pure \t otherwise. @@ -20,22 +17,35 @@ Copyright 2025 Syed Daanish * [ ] These will dedent when the block immediately after them is dedented * [ ] Dont dedent if ending is valid starting is invalid but also empty * [ ] Just leave asis if starting is empty -* [ ] **Readme:** Update readme to show indetation mechanics. -* [ ] **LSP Bug:** Try to find out why emojis are breaking lsp edits. (check the ruby sample) +* [ ] **Readme:** Update readme to show ruby based config. * [ ] **UI Refinement:** * [ ] Allow completion list to be scrolled; show only `x` max items. * [ ] Finish autocomplete box style functions. * [ ] **Documentation UI:** Capture `Ctrl+h` / `Ctrl+l` for scrolling documentation windows. -* [ ] **Redo hooks and folding as proper engines**: With functions to checkstate/cursor like function and edits application. -* [ ] Do trextmate like regex grammar parsing with lsp symbols for semantic highlighting. - * Probably remove tre--sitter or just keep it for context tree. - * Making bracket matching andignoring strings/comments easier. -* remove tree-sitter mention from everywhere especially submodules -* make it faster for line inserts/deletes too (treeify the vector) +* [ ] Redo hooks as a struct. +* [ ] breakdown the render function into smaller functions. + * Try to make all functions better now that folds have been purged * Cleanup syntax and renderer files -probably remove solargraph support and use ruby-lsp (or sorbet?) instead +* Add a thing called view which is a rect with speacial type editor . but otherwise a ruby or c++ view +* can be used for stuff like file manager/git manager/theme picker. +* allow flushing functions in ruby to tell c++ to refresh keybinds/themes etc. +* allow keybinds to be set in ruby + +check:: + pull diagnostics for ruby-lsp + lsp selection range - use to highlight start / end of range maybe? + goto definiton + signature help + document symbol for the top bar maybe? (or workspace symbol) + also setup workspaces + Semantic highlighting + Quick fixes + Rename symbols + +probably remove solargraph support and use ruby-lsp (or sorbet?) instead because it is a pain for utf stuff +ruby-lsp also supports erb so thats a plus move lsp configs to json and also allow configs for windows-style vs unix-style line endings and utf-8 vs utf-16 @@ -55,7 +65,7 @@ move lsp configs to json and also allow configs for windows-style vs unix-style * [ ] **Tree-sitter Indent:** Attempt to allow Tree-sitter to handle indentation if possible. * [ ] **Scrolling:** Add logic where selecting at the end of the screen scrolls down (and vice versa). - * *Implementation:* Update `main.cc` to send drag events to the selected editor. + * *Implementation:* Update `main.cc` to send drag events to the selected editor even if coordinates are out of bounds. ### UX @@ -72,11 +82,6 @@ move lsp configs to json and also allow configs for windows-style vs unix-style * [ ] Handle snippets properly in autocomplete: use only the last word in signature when replacing and set cursor to the first one. * [ ] **Basic Autocomplete:** Keep a list of words in the current buffer for non-LSP fallback. -* [ ] **Language Support:** - * [ ] Add ECMA to JS and make TSX support. - * [ ] Add formatting for files where LSP doesn't provide it. - * [ ] Redo grammar files properly (especially cpp). - ### Major Features @@ -93,21 +98,12 @@ move lsp configs to json and also allow configs for windows-style vs unix-style * [ ] **Block Selection:** * [ ] Double-clicking a bracket selects the whole block (first time only) and sets mode to `WORD`. -* [ ] **Tree-sitter Context:** - * [ ] Get code context from Tree-sitter. - * [ ] Get node path of current cursor and add indicator bar (breadcrumbs). - * [ ] Highlight block edges when cursor is on/in a bracket. - ### Visuals, UI & Extensions? -*Focus: Aesthetics and external integrations.* - * [ ] **Status Bar:** Complete status bar and command runner. -* [ ] **Visual Aids:** - * [ ] Expand color regex to match CSS colors in CSS files. - * [ ] Add color picker/palette. +* [ ] Add color picker/palette. * [ ] **Git:** Add Git integration (status, diffs). * [ ] **AI/Snippets:** @@ -120,21 +116,8 @@ move lsp configs to json and also allow configs for windows-style vs unix-style ### Optimizations & Fluff -* [ ] **Event Loop:** - * [ ] Make the whole engine event-driven rather than clock-driven. - * [ ] Mybe keep background thread with dirty flag. - * [ ] But merge input and render into a single loop that only renders when input affects render or background thread needs refresh and try to couple multiple renders. - * [ ] LSP and inputs should be blocking (lsp on its fd) and inputs in io/input.cc - * [ ] **Performance:** * [ ] Switch JSON parser to `RapidJSON` (or similar high-performance lib). * [ ] Decrease usage of `std::string` in UI, LSP, and warnings. * [ ] Also for vectors into managed memory especially for completions/lsp-stuff. -* [ ] **Folding:** Redo folding system and its relation to `move_line_*` functions. - -* [ ] **Grammars:** - * [ ] Manually add redo SCM files (especially cpp/c/h). - * [ ] Create `lua-typed` and `man pages` Tree-sitter grammars. - -* [ ] **Repo Maintenance:** Once renderer is proven check commit `43f443e`. diff --git a/config/main.rb b/config/main.rb index ec6659f..256d2ec 100644 --- a/config/main.rb +++ b/config/main.rb @@ -1,6 +1,6 @@ -require_relative "libcrib" - # basic configuration +# This can also be used to do speacail configs for different projects. +# its ruby guys script whatever you want. C.startup do puts "Starting crib..." @@ -86,7 +86,7 @@ C.theme = { # lsp: "solargraph" # } -C.line_endings = :unix # or :windows +C.line_endings = :auto_unix # or :unix or :windows or :auto_windows # C.extra_highlights do |_line, _idx| # # the return can be an array of @@ -99,7 +99,7 @@ C.line_endings = :unix # or :windows # The highlighter will be aplied to the language as long as the langauge is defined in C.languages C.highlighters[:ruby_n] = { - parser: ->(line, state) { + parser: ->(line, state, line_idx) { # the return value is a hash # it contains the state and the highlights # state can be of any type but will be consistent between calls @@ -108,14 +108,14 @@ C.highlighters[:ruby_n] = { # the highlights can be an array of # [K_type, start, end] # K_type is a constant from the constants defined in libcrib.rb - # for ex: for strings it would be C::K_STRING or for numbers C::K_NUMBER etc. + # for ex: for strings it would be Tokens::K_STRING or for numbers Tokens::K_NUMBER etc. # and start and end are integers strictly inside the line return { state: "", tokens: [ # This will highlight the entire line as a string # Any wrong format will not be handled and lead to crashes - { type: C::K_STRING, start: 0, end: line.length } + { type: Tokens::K_STRING, start: 0, end: line.length } ] } }, diff --git a/include/editor/editor.h b/include/editor/editor.h index afa3b17..4b306c8 100644 --- a/include/editor/editor.h +++ b/include/editor/editor.h @@ -11,6 +11,7 @@ #include "ui/diagnostics.h" #include "ui/hover.h" #include "utils/utils.h" +#include #define CHAR 0 #define WORD 1 @@ -57,7 +58,7 @@ struct Editor { }; Editor *new_editor(const char *filename_arg, Coord position, Coord size, - bool unix_eol); + uint8_t eol); void save_file(Editor *editor); void free_editor(Editor *editor); void render_editor(Editor *editor); diff --git a/include/scripting/decl.h b/include/scripting/decl.h index faa3702..7a8a092 100644 --- a/include/scripting/decl.h +++ b/include/scripting/decl.h @@ -7,15 +7,16 @@ extern std::unordered_map> custom_highlighters; -void ruby_start(const char *main_file); +void ruby_start(); void ruby_shutdown(); void ruby_log(std::string msg); void load_theme(); void load_languages_info(); -bool read_line_endings(); +uint8_t read_line_endings(); void load_custom_highlighters(); VALUE parse_custom(std::vector *tokens, VALUE parser_block, - const char *line, uint32_t len, VALUE state); + const char *line, uint32_t len, VALUE state, + uint32_t line_num); bool custom_compare(VALUE match_block, VALUE state1, VALUE state2); #endif diff --git a/include/syntax/decl.h b/include/syntax/decl.h index 469efb0..0403ebb 100644 --- a/include/syntax/decl.h +++ b/include/syntax/decl.h @@ -26,6 +26,24 @@ const std::unordered_map kind_map = { #undef ADD }; +constexpr const char tokens_def[] = "module Tokens\n" +#define STRINGIFY_HELPER(x) #x +#define STRINGIFY(x) STRINGIFY_HELPER(x) +#define ADD(name) " " #name " = " STRINGIFY(__COUNTER__) "\n" +#include "syntax/tokens.def" +#undef ADD +#undef STRINGIFY +#undef STRINGIFY_HELPER + " freeze\n" + "end"; + +constexpr const char crib_module[] = { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc23-extensions" +#embed "libcrib.rb" +#pragma clang diagnostic pop + , '\0'}; + extern std::array highlights; struct Token { diff --git a/include/syntax/langs.h b/include/syntax/langs.h index 6e6b27a..384628a 100644 --- a/include/syntax/langs.h +++ b/include/syntax/langs.h @@ -4,9 +4,9 @@ #include "syntax/decl.h" #define DEF_LANG(name) \ - std::shared_ptr name##_parse(std::vector *tokens, \ - std::shared_ptr in_state, \ - const char *text, uint32_t len); \ + std::shared_ptr name##_parse( \ + std::vector *tokens, std::shared_ptr in_state, \ + const char *text, uint32_t len, uint32_t line_num); \ bool name##_state_match(std::shared_ptr state_1, \ std::shared_ptr state_2); @@ -34,9 +34,9 @@ DEF_LANG(bash); inline static const std::unordered_map< std::string, - std::tuple (*)(std::vector *tokens, - std::shared_ptr in_state, - const char *text, uint32_t len), + std::tuple (*)( + std::vector *tokens, std::shared_ptr in_state, + const char *text, uint32_t len, uint32_t line_num), bool (*)(std::shared_ptr state_1, std::shared_ptr state_2)>> parsers = { diff --git a/config/libcrib.rb b/include/syntax/libcrib.rb similarity index 84% rename from config/libcrib.rb rename to include/syntax/libcrib.rb index d0ecfd1..6d0076f 100644 --- a/config/libcrib.rb +++ b/include/syntax/libcrib.rb @@ -1,57 +1,4 @@ module C - K_DATA = 0 - K_SHEBANG = 1 - K_COMMENT = 2 - K_ERROR = 3 - K_STRING = 4 - K_ESCAPE = 5 - K_INTERPOLATION = 6 - K_REGEXP = 7 - K_NUMBER = 8 - K_TRUE = 9 - K_FALSE = 10 - K_CHAR = 11 - K_KEYWORD = 12 - K_KEYWORDOPERATOR = 13 - K_OPERATOR = 14 - K_FUNCTION = 15 - K_TYPE = 16 - K_CONSTANT = 17 - K_VARIABLEINSTANCE = 18 - K_VARIABLEGLOBAL = 19 - K_ANNOTATION = 20 - K_DIRECTIVE = 21 - K_LABEL = 22 - K_BRACE1 = 23 - K_BRACE2 = 24 - K_BRACE3 = 25 - K_BRACE4 = 26 - K_BRACE5 = 27 - K_HEADING1 = 28 - K_HEADING2 = 29 - K_HEADING3 = 30 - K_HEADING4 = 31 - K_HEADING5 = 32 - K_HEADING6 = 33 - K_BLOCKQUOTE = 34 - K_LIST = 35 - K_LISTITEM = 36 - K_CODE = 37 - K_LANGUAGENAME = 38 - K_LINKLABEL = 39 - K_IMAGELABEL = 40 - K_LINK = 41 - K_TABLE = 42 - K_TABLEHEADER = 43 - K_ITALIC = 44 - K_BOLD = 45 - K_UNDERLINE = 46 - K_STRIKETHROUGH = 47 - K_HORIXONTALRULE = 48 - K_TAG = 49 - K_ATTRIBUTE = 50 - K_CHECKDONE = 51 - K_CHECKNOTDONE = 52 @lsp_config = { "clangd" => [ "--background-index", @@ -195,7 +142,7 @@ module C extensions: ["erb"], filenames: [], mimetypes: ["text/x-erb"], - lsp: "emmet-language-server" + lsp: "ruby-lsp" }, lua: { color: 0x36a3d9, @@ -350,11 +297,45 @@ module C lsp: "bash-language-server" } } + @theme = { + :default => { fg: 0xEEEEEE }, + :shebang => { fg: 0x7DCFFF }, + :error => { fg: 0xEF5168 }, + :comment => { fg: 0xAAAAAA, italic: true }, + :string => { fg: 0xAAD94C }, + :escape => { fg: 0x7DCFFF }, + :interpolation => { fg: 0x7DCFFF }, + :regexp => { fg: 0xD2A6FF }, + :number => { fg: 0xE6C08A }, + # rubocop:disable Lint/BooleanSymbol + :true => { fg: 0x7AE93C }, + :false => { fg: 0xEF5168 }, + # rubocop:enable Lint/BooleanSymbol + :char => { fg: 0xFFAF70 }, + :keyword => { fg: 0xFF8F40 }, + :keywordoperator => { fg: 0xF07178 }, + :operator => { fg: 0xFFFFFF, italic: true }, + :function => { fg: 0xFFAF70 }, + :type => { fg: 0xF07178 }, + :constant => { fg: 0x7DCFFF }, + :variableinstance => { fg: 0x95E6CB }, + :variableglobal => { fg: 0xF07178 }, + :annotation => { fg: 0x7DCFFF }, + :directive => { fg: 0xFF8F40 }, + :label => { fg: 0xD2A6FF }, + :brace1 => { fg: 0xD2A6FF }, + :brace2 => { fg: 0xFFAFAF }, + :brace3 => { fg: 0xFFFF00 }, + :brace4 => { fg: 0x0FFF0F }, + :brace5 => { fg: 0xFF0F0F } + } + @line_endings = :auto_unix @key_handlers = {} @key_binds = {} @highlighters = {} @log_queue = [] - @line_endings = :unix + @b_startup = nil + @b_shutdown = nil class << self attr_accessor :theme, :lsp_config, :languages, diff --git a/include/syntax/parser.h b/include/syntax/parser.h index 84a42c3..9f965af 100644 --- a/include/syntax/parser.h +++ b/include/syntax/parser.h @@ -10,7 +10,8 @@ struct Parser { std::string lang; std::shared_ptr (*parse_func)(std::vector *tokens, std::shared_ptr in_state, - const char *text, uint32_t len); + const char *text, uint32_t len, + uint32_t line_num); bool (*state_match_func)(std::shared_ptr state_1, std::shared_ptr state_2); VALUE parser_block = Qnil; diff --git a/include/syntax/tokens.def b/include/syntax/tokens.def index 52cad74..451007a 100644 --- a/include/syntax/tokens.def +++ b/include/syntax/tokens.def @@ -1,4 +1,3 @@ -// When updating this file with new types do not forget to update libcrib.rb to parallel changes ADD(K_DATA) ADD(K_SHEBANG) ADD(K_COMMENT) diff --git a/include/utils/utils.h b/include/utils/utils.h index a9bea53..5958dff 100644 --- a/include/utils/utils.h +++ b/include/utils/utils.h @@ -154,7 +154,7 @@ std::string path_abs(const std::string &path_str); std::string path_to_file_uri(const std::string &path_str); std::string filename_from_path(const std::string &path); std::string get_exe_dir(); -char *load_file(const char *path, uint32_t *out_len); +char *load_file(const char *path, uint32_t *out_len, bool *out_eol); char *detect_file_type(const char *filename); Language language_for_file(const char *filename); diff --git a/samples/bash.sh b/samples/bash.sh index 190e20d..3e63339 100644 --- a/samples/bash.sh +++ b/samples/bash.sh @@ -47,7 +47,7 @@ while ((counter < 5)); do done # Subshelled loops and alternating quoting -for item in "${ITEMS[@]}"; do +for item in "${ITEMS[@]}}"; do ( msg="Processing $item" echo "$(colorize blue "$msg")" @@ -127,7 +127,7 @@ grep "world" <<<"$FOO" >/dev/null && log INFO "FOO contains world" diff <(echo foo) <(echo foo) >/dev/null && log INFO "diff matched" # Command substitution with pipeline -timestamp=$(date | sed 's/ /_/g') +timestamp=$(date | sed 's//_/g') log INFO "Timestamp: $timestamp" # Testing array slicing diff --git a/samples/ruby.rb b/samples/ruby.rb index 5aa100d..3603dc7 100644 --- a/samples/ruby.rb +++ b/samples/ruby.rb @@ -4,13 +4,12 @@ # Purpose: Test syntax highlighting + width calculation in your editor # --------------------------------------------------------------- - # Mixed-width CJKssssssssssssssss LoadErssssssssssssssssssssssss cjk_samples = [ - "漢字テスト", - "測試中文字串", - "한국어 테스트", - "ひらがなカタカナ混合", + '漢字テスト', + '測試中文字串', + '한국어 테스트', + 'ひらがなカタカナ混合' ] # a hex color: #FFFFFF shouldn't hl here: hsl(147rad, 50%, 47%) as it is not css-style file @@ -18,34 +17,33 @@ cjk_samples = [ 0x603010 # another hex color # Ruby regex with unicode -$unicode_regex_multiline = /[一-龯ぁ-ん#{0x3000}ァ +$unicode_regex_multiline = /[一-龯ぁ-ん12288ァ \-ヶー - s wow 々〆〤]/ -UNICORE = %r{ - [s] +UNICORE = / + s {#{ss}} \C-s\u{10} - } - -UNINITCORE = %{ - + / + +UNINITCORE = %( + {{#{}}} - + test = "A:\x41 B:\101 C:\u0043 D:\u{44 45} NUL:\0 DEL:\c? CTRL_A:\cA META_X:\M-x CTRL_META_X:\C-\M-x MIX:\C-\M-z N:\N{UNICODE NAME}" - - } + + ) # Unicode identifiers (valid in Ruby) 变量 = 0x5_4eddaee -π = 3.14_159e+2, ?\u0234, ?\,, ?\x0A, ?s, true, false, 0 +π = 0.314_159e+2, ?\u0234, "\,", ?\x0A, 's', true, false, 0 挨拶 = -> { "こんに \n ちは" } -arr = Array.new() -not_arr = NotABuiltin.new() +arr = [] +not_arr = NotABuiltin.new raise NameError or SystemExit or CustomError or Errno or ErrorNotAtAll @@ -56,7 +54,7 @@ end # Iterate through CJK samples cjk_samples.each_with_index do |str, idx:| - puts %Q! CJK[#{idx}] => #{str} (len=#{str.length})\! ! + puts %! CJK[#{idx}] => #{str} (len=#{str.length})\! ! symbol = :" a " @@ -67,49 +65,46 @@ end puts "Emoji count: #{emojis.length}" # Multi-line string with unicode -multi = < e puts "Caught an error: #{e}" ensure - puts "This runs no matter what" + puts 'This runs no matter what' end # Arrays of objects @@ -285,7 +277,7 @@ end end # Special objects -so = SpecialObject.new("Special", 10) +so = SpecialObject.new('Special', 10) puts "Double: #{so.double_value}, Triple: #{so.triple_value}" # String interpolation and formatting @@ -296,8 +288,8 @@ multi_line = <<~TEXT k kmW ; This is a multi-line string. It spans multiple lines. - Gossn m - dd + Gossn sssmss + ddsss od for testing highlighting. TEXT @@ -305,7 +297,7 @@ puts multi_line # Symbols and strings sym = :my_symbol == __dir__ -str = "my string" +str = 'my string' puts "Symbol: #{sym}, String: #{str}" # Random numbers @@ -324,25 +316,25 @@ end # Block with yield def wrapper - puts "Before block" + puts 'Before block' yield if block_given? - puts "After block" + puts 'After block' end # ss -wrapper { puts "Inside block" } +wrapper { puts 'Inside block' } # Sorting sorted = rand_nums.sort puts "Sorted: #{sorted.join(', ')}" # Regex -sample_text = "The quick brown fox jumps over the lazy dog" +sample_text = 'The quick brown fox jumps over the lazy dog' puts "Match 'fox'?" if sample_text =~ /fox/ # End of test script -puts "Ruby syntax highlighting test complete." +puts 'Ruby syntax highlighting test complete.' __END__ diff --git a/src/editor/editor.cc b/src/editor/editor.cc index 75b6225..509092a 100644 --- a/src/editor/editor.cc +++ b/src/editor/editor.cc @@ -5,18 +5,21 @@ #include "utils/utils.h" Editor *new_editor(const char *filename_arg, Coord position, Coord size, - bool unix_eol) { + uint8_t eol) { Editor *editor = new Editor(); if (!editor) return nullptr; uint32_t len = 0; std::string filename = path_abs(filename_arg); - char *str = load_file(filename.c_str(), &len); + editor->unix_eol = eol & 1; + char *str = load_file(filename.c_str(), &len, &editor->unix_eol); if (!str) { - free_editor(editor); - return nullptr; + str = (char *)malloc(1); + *str = '\n'; + len = 1; } - editor->unix_eol = unix_eol; + if ((eol >> 1) & 1) + editor->unix_eol = eol & 1; editor->filename = filename; editor->uri = path_to_file_uri(filename); editor->position = position; diff --git a/src/main.cc b/src/main.cc index afacb74..38a6cb6 100644 --- a/src/main.cc +++ b/src/main.cc @@ -76,7 +76,7 @@ int main(int argc, char *argv[]) { ruby_init(); - ruby_start((get_exe_dir() + "/../config/main.rb").c_str()); + ruby_start(); load_theme(); load_languages_info(); load_custom_highlighters(); @@ -84,10 +84,10 @@ int main(int argc, char *argv[]) { Coord screen = start_screen(); const char *filename = (argc > 1) ? argv[1] : ""; - bool unix_eol = read_line_endings(); + uint8_t eol = read_line_endings(); Editor *editor = - new_editor(filename, {0, 0}, {screen.row - 2, screen.col}, unix_eol); + new_editor(filename, {0, 0}, {screen.row - 2, screen.col}, eol); Bar bar(screen); auto end = std::chrono::high_resolution_clock::now(); diff --git a/src/scripting/process.cc b/src/scripting/process.cc index 836d1b4..6d749ce 100644 --- a/src/scripting/process.cc +++ b/src/scripting/process.cc @@ -1,5 +1,3 @@ -#include "ruby/internal/gc.h" -#include "ruby/internal/value.h" #include "scripting/decl.h" #include "utils/utils.h" @@ -28,16 +26,55 @@ struct R_Language { VALUE C_module = Qnil; std::mutex ruby_mutex; -void ruby_start(const char *main_file) { +namespace fs = std::filesystem; + +static void ruby_load(const char *main_file) { std::lock_guard lock(ruby_mutex); ruby_init_loadpath(); int state = 0; rb_load_protect(rb_str_new_cstr(main_file), 0, &state); if (state) { - rb_errinfo(); + VALUE err = rb_errinfo(); rb_set_errinfo(Qnil); - fprintf(stderr, "%d: Failed to load Ruby file\n", state); - return; + fprintf(stderr, "%d: Failed to load Ruby file %s\n", state, main_file); + } +} + +static void ruby_eval_string(const char *code) { + int state = 0; + rb_eval_string_protect(code, &state); + if (state) { + VALUE err = rb_errinfo(); + rb_set_errinfo(Qnil); + fprintf(stderr, "Ruby eval failed\n"); + } +} + +void ruby_start() { + fs::path exe_dir = get_exe_dir(); + std::vector candidates; + candidates.emplace_back("./crib.rb"); + const char *xdg = std::getenv("XDG_CONFIG_HOME"); + const char *home = std::getenv("HOME"); + if (xdg) { + candidates.emplace_back(fs::path(xdg) / "crib/crib.rb"); + candidates.emplace_back(fs::path(xdg) / "crib/main.rb"); + candidates.emplace_back(fs::path(xdg) / "crib.rb"); + } else if (home) { + fs::path base = fs::path(home) / ".config"; + candidates.emplace_back(base / "crib/crib.rb"); + candidates.emplace_back(base / "crib/main.rb"); + candidates.emplace_back(base / "crib.rb"); + } + candidates.emplace_back(exe_dir / "../config/main.rb"); + candidates.emplace_back(exe_dir / "../config/crib.rb"); + ruby_eval_string(crib_module); + ruby_eval_string(tokens_def); + for (const auto &p : candidates) { + if (fs::exists(p) && fs::is_regular_file(p)) { + ruby_load(p.string().c_str()); + break; + } } C_module = rb_const_get(rb_cObject, rb_intern("C")); if (C_module == Qnil) @@ -102,14 +139,16 @@ bool custom_compare(VALUE match_block, VALUE state1, VALUE state2) { } VALUE parse_custom(std::vector *tokens, VALUE parser_block, - const char *line, uint32_t len, VALUE state) { + const char *line, uint32_t len, VALUE state, + uint32_t c_line) { std::lock_guard lock(ruby_mutex); tokens->clear(); if (NIL_P(parser_block)) return {}; VALUE ruby_line = rb_str_new(line, len); - VALUE tokens_and_state_hash = - rb_funcall(parser_block, rb_intern("call"), 2, ruby_line, state); + VALUE line_idx = UINT2NUM(c_line); + VALUE tokens_and_state_hash = rb_funcall(parser_block, rb_intern("call"), 3, + ruby_line, state, line_idx); VALUE tokens_rb = rb_hash_aref(tokens_and_state_hash, ID2SYM(rb_intern("tokens"))); for (long i = 0; i < RARRAY_LEN(tokens_rb); ++i) { @@ -287,12 +326,22 @@ void load_languages_info() { lsps[lsp.command] = lsp; } -bool read_line_endings() { +uint8_t read_line_endings() { std::lock_guard lock(ruby_mutex); if (C_module == Qnil) - return true; + return 1; VALUE le = rb_funcall(C_module, rb_intern("line_endings"), 0); - if (SYMBOL_P(le)) - return std::string(rb_id2name(SYM2ID(le))) == "unix"; - return true; + if (!SYMBOL_P(le)) + return 1; + uint8_t flags = 1; + const char *name = rb_id2name(SYM2ID(le)); + if (std::strcmp(name, "unix") == 0) + flags = 0b01; + else if (std::strcmp(name, "windows") == 0) + flags = 0b00; + else if (std::strcmp(name, "auto_unix") == 0) + flags = 0b11; + else if (std::strcmp(name, "auto_windows") == 0) + flags = 0b10; + return flags; } diff --git a/src/syntax/bash.cc b/src/syntax/bash.cc index 5175b7a..baf8a75 100644 --- a/src/syntax/bash.cc +++ b/src/syntax/bash.cc @@ -1,11 +1,10 @@ #include "syntax/decl.h" #include "syntax/langs.h" -#include struct BashFullState { int brace_level = 0; - enum : uint8_t { NONE, STRING, HEREDOC }; + enum : uint8_t { NONE, STRING, HEREDOC, PARAMETER }; uint8_t in_state = BashFullState::NONE; bool line_cont = false; @@ -50,7 +49,8 @@ bool bash_state_match(std::shared_ptr state_1, std::shared_ptr bash_parse(std::vector *tokens, std::shared_ptr in_state, - const char *text, uint32_t len) { + const char *text, uint32_t len, + uint32_t line_num) { static bool keywords_trie_init = false; if (!keywords_trie_init) { keywords_trie_init = true; @@ -64,10 +64,44 @@ std::shared_ptr bash_parse(std::vector *tokens, if (len == 0) return state; while (i < len) { + if (state->full_state->in_state == BashFullState::PARAMETER) { + uint32_t start = i; + while (i < len) { + if (text[i] == '{') { + i++; + state->full_state->brace_level++; + continue; + } + if (text[i] == '}') { + if (--state->full_state->brace_level == 0 && + !state->interp_stack.empty()) { + tokens->push_back({i - 1, i, TokenKind::K_INTERPOLATION}); + state->full_state = state->interp_stack.top(); + state->interp_stack.pop(); + i++; + break; + } + } + i++; + } + continue; + } if (state->full_state->in_state == BashFullState::STRING) { uint32_t start = i; while (i < len) { + if (state->full_state->lit.allow_interp && text[i] == '$') { + if (++i < len && text[i] == '{') { + tokens->push_back({start, i - 1, TokenKind::K_STRING}); + tokens->push_back({i - 1, i, TokenKind::K_INTERPOLATION}); + state->interp_stack.push(state->full_state); + state->full_state = std::make_shared(); + state->full_state->in_state = BashFullState::PARAMETER; + state->full_state->brace_level = 1; + break; + } + } if (text[i] == state->full_state->lit.delim[0]) { + i++; tokens->push_back({start, i, TokenKind::K_STRING}); state->full_state->in_state = BashFullState::NONE; break; @@ -79,7 +113,7 @@ std::shared_ptr bash_parse(std::vector *tokens, continue; } if (text[i] == '#') { - if (i == 0 && len > 4 && text[i + 1] == '!') { + if (line_num == 0 && i == 0 && len > 4 && text[i + 1] == '!') { tokens->push_back({0, len, TokenKind::K_SHEBANG}); return state; } @@ -91,6 +125,12 @@ std::shared_ptr bash_parse(std::vector *tokens, state->full_state->lit.allow_interp = false; tokens->push_back({i, ++i, TokenKind::K_STRING}); continue; + } else if (text[i] == '"') { + state->full_state->in_state = BashFullState::STRING; + state->full_state->lit.delim = "\""; + state->full_state->lit.allow_interp = true; + tokens->push_back({i, ++i, TokenKind::K_STRING}); + continue; } i++; } diff --git a/src/syntax/parser.cc b/src/syntax/parser.cc index 3fb95b0..5a2017b 100644 --- a/src/syntax/parser.cc +++ b/src/syntax/parser.cc @@ -64,6 +64,10 @@ void Parser::work() { dirty_lines.push(c_line); continue; } + if (scroll_max > 50 && c_line < scroll_max - 50) { + dirty_lines.push(c_line); + continue; + } uint32_t line_count = line_tree.count(); lock_data.lock(); std::shared_ptr prev_state = @@ -118,13 +122,14 @@ void Parser::work() { std::static_pointer_cast(prev_state); state = state_ptr->state; } - VALUE out_state = - parse_custom(&line_data->tokens, parser_block, text, r_len, state); + VALUE out_state = parse_custom(&line_data->tokens, parser_block, text, + r_len, state, c_line); std::shared_ptr out_state_ptr = std::make_shared(out_state); new_state = out_state_ptr; } else { - new_state = parse_func(&line_data->tokens, prev_state, text, r_len); + new_state = + parse_func(&line_data->tokens, prev_state, text, r_len, c_line); } line_data->in_state = prev_state; line_data->out_state = new_state; @@ -134,7 +139,8 @@ void Parser::work() { } prev_state = new_state; c_line++; - if (c_line < line_count && c_line > scroll_max + 50) { + if (c_line < line_count && c_line > scroll_max + 50 && scroll_max < 50 && + c_line < scroll_max + 50) { lock_data.unlock(); if (lock.owns_lock()) lock.unlock(); diff --git a/src/syntax/ruby.cc b/src/syntax/ruby.cc index b7819b5..5b442f2 100644 --- a/src/syntax/ruby.cc +++ b/src/syntax/ruby.cc @@ -288,7 +288,8 @@ bool ruby_state_match(std::shared_ptr state_1, std::shared_ptr ruby_parse(std::vector *tokens, std::shared_ptr in_state, - const char *text, uint32_t len) { + const char *text, uint32_t len, + uint32_t line_num) { static bool keywords_trie_init = false; static Trie base_keywords_trie; static Trie expecting_keywords_trie; @@ -703,7 +704,7 @@ std::shared_ptr ruby_parse(std::vector *tokens, i++; continue; } else if (text[i] == '#') { - if (i == 0 && len > 4 && text[i + 1] == '!') { + if (line_num == 0 && i == 0 && len > 4 && text[i + 1] == '!') { state->full_state->expecting_expr = false; tokens->push_back({0, len, TokenKind::K_SHEBANG}); return state; diff --git a/src/utils/system.cc b/src/utils/system.cc index 7372d4f..0b0dcec 100644 --- a/src/utils/system.cc +++ b/src/utils/system.cc @@ -44,7 +44,7 @@ std::string get_exe_dir() { return path.substr(0, path.find_last_of('/')); } -char *load_file(const char *path, uint32_t *out_len) { +char *load_file(const char *path, uint32_t *out_len, bool *out_eol) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file.is_open()) return nullptr; @@ -64,18 +64,28 @@ char *load_file(const char *path, uint32_t *out_len) { if (!buf) return nullptr; file.read(buf, data_len); - if (memchr(buf, '\r', data_len) == nullptr) { + bool has_cr = memchr(buf, '\r', data_len) != nullptr; + bool has_lf = memchr(buf, '\n', data_len) != nullptr; + if (!has_cr && !has_lf) { uint32_t write = data_len; - if (write == 0 || buf[write - 1] != '\n') + buf[write++] = '\n'; + *out_len = write; + return buf; + } + if (!has_cr) { + *out_eol = true; + uint32_t write = data_len; + if (buf[write - 1] != '\n') buf[write++] = '\n'; *out_len = write; return buf; } + *out_eol = false; uint32_t write = 0; for (uint32_t i = 0; i < data_len; ++i) if (buf[i] != '\r') buf[write++] = buf[i]; - if (write == 0 || buf[write - 1] != '\n') + if (buf[write - 1] != '\n') buf[write++] = '\n'; *out_len = write; return buf;