From fb8296c1a91df0ae56fb9ccf05eed76dc1ea7afe Mon Sep 17 00:00:00 2001 From: Meow Honk <21010072+catgoose@users.noreply.github.com> Date: Sun, 12 Jan 2025 21:11:23 -0600 Subject: [PATCH] Perf: Trie implementation is space efficient (#131) * ref(utils): renames byte_is_valid_colorchar to byte_is_valid_color_char * test: adds test cases for nonalphanumeric custom_names including spaces * fix(util): do not add spaces as an additional color character for checking valid color chars * chore: adds error message if trie returns length but not rgb_hex * doc: updates readme and ldoc * ref(trie): dynamically build char_lookup based on inserted words, removes need for adding additional characters manually * ref(tailwind_names): tailwind names uses same trie as names * fix: removes reference to tailwind_names module * feat: Trie dynamically allocates memory for each node and resizes capacity when needed. Starts with an initial capacity of 8 * chore: removes old ldoc html file * doc: let ldoc decide which markdown parser to use * test: creates start_minimal-trie.sh script to run trie tests * test: creates makefile for executing testing targets * chore: creates trie benchmarks for different initial capacity settings --- Makefile | 35 ++ README.md | 135 ++++++-- doc/colorizer.txt | 87 ++--- doc/index.html | 11 +- doc/modules/colorizer.buffer.html | 3 +- doc/modules/colorizer.color.html | 3 +- doc/modules/colorizer.config.html | 11 +- doc/modules/colorizer.constants.html | 3 +- doc/modules/colorizer.html | 3 +- doc/modules/colorizer.matcher.html | 3 +- doc/modules/colorizer.parser.hsl.html | 3 +- doc/modules/colorizer.parser.names.html | 30 +- doc/modules/colorizer.parser.rgb.html | 3 +- doc/modules/colorizer.parser.rgb_hex.html | 3 +- doc/modules/colorizer.parser.rgba_hex.html | 3 +- .../colorizer.parser.tailwind_names.html | 2 +- doc/modules/colorizer.sass.html | 3 +- doc/modules/colorizer.tailwind.html | 17 +- doc/modules/colorizer.trie.html | 11 +- doc/modules/colorizer.usercmds.html | 3 +- doc/modules/colorizer.utils.html | 19 +- doc/modules/trie.html | 34 +- doc/modules/utils.html | 326 ------------------ lua/colorizer.lua | 1 - lua/colorizer/buffer.lua | 26 +- lua/colorizer/config.lua | 6 +- lua/colorizer/matcher.lua | 24 +- lua/colorizer/parser/names.lua | 95 +++-- lua/colorizer/parser/tailwind_names.lua | 91 ----- lua/colorizer/tailwind.lua | 6 +- lua/colorizer/trie.lua | 288 +++++++++------- lua/colorizer/utils.lua | 36 +- scripts/gen_docs.sh | 2 +- scripts/minimal-colorizer.sh | 4 + scripts/start_minimal.sh | 4 - scripts/trie-benchmark.sh | 10 + scripts/trie-test.sh | 11 + test/.gitignore | 1 + test/expect.lua | 43 ++- test/{minimal.lua => minimal-colorizer.lua} | 5 +- test/print-trie.lua | 59 ---- test/trie/benchmark.lua | 146 ++++++++ test/trie/minimal.lua | 65 ++++ test/trie/test.lua | 61 ++++ test/trie/trie-benchmark.txt | 43 +++ test/trie/trie-test.txt | 34 ++ 46 files changed, 945 insertions(+), 867 deletions(-) create mode 100644 Makefile delete mode 100644 doc/modules/utils.html delete mode 100644 lua/colorizer/parser/tailwind_names.lua create mode 100755 scripts/minimal-colorizer.sh delete mode 100755 scripts/start_minimal.sh create mode 100755 scripts/trie-benchmark.sh create mode 100755 scripts/trie-test.sh rename test/{minimal.lua => minimal-colorizer.lua} (95%) delete mode 100644 test/print-trie.lua create mode 100644 test/trie/benchmark.lua create mode 100644 test/trie/minimal.lua create mode 100644 test/trie/test.lua create mode 100644 test/trie/trie-benchmark.txt create mode 100644 test/trie/trie-test.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0da0129 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +# Define variables for script paths +SCRIPTS_DIR=scripts +TRIE_TEST_SCRIPT=$(SCRIPTS_DIR)/trie-test.sh +TRIE_BENCHMARK_SCRIPT=$(SCRIPTS_DIR)/trie-benchmark.sh +MINIMAL_SCRIPT=$(SCRIPTS_DIR)/minimal-colorizer.sh + +help: + @echo "Available targets:" + @echo " make trie - Run trie test and benchmark" + @echo " make trie-test - Run trie test" + @echo " make trie-benchmark - Run trie benchmark" + @echo " make minimal - Run the minimal script" + @echo " make clean - Remove test/colorizer_*" + +trie: trie-test trie-benchmark + +trie-test: + @echo "Running trie test..." + @bash $(TRIE_TEST_SCRIPT) + +trie-benchmark: + @echo "Running trie benchmark..." + @bash $(TRIE_BENCHMARK_SCRIPT) + +minimal: + @echo "Running minimal config..." + @bash $(MINIMAL_SCRIPT) + +clean: + @echo "Removing test/colorizer_repro" + @rm -rf test/colorizer_repro + @echo "Removing test/trie/colorizer_trie" + @rm -rf test/trie/colorizer_trie + +.PHONY: help trie trie-test trie-benchmark minimal clean diff --git a/README.md b/README.md index d856d91..e77e1bb 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ - [Lazyload Colorizer with Lazy.nvim](#lazyload-colorizer-with-lazynvim) - [Tailwind](#tailwind) - [Testing](#testing) + - [Minimal Colorizer](#minimal-colorizer) + - [Trie](#trie) + - [Test](#test) + - [Benchmark](#benchmark) + - [Results](#results) - [Extras](#extras) - [TODO](#todo) @@ -114,10 +119,8 @@ library to do custom highlighting themselves. ```lua require("colorizer").setup({ - -- Filetype options. Accepts table like `user_default_options` - filetypes = { "*" }, - -- Buftype options. Accepts table like `user_default_options` - buftypes = {}, + filetypes = { "*" }, -- Filetype options. Accepts table like `user_default_options` + buftypes = {}, -- Buftype options. Accepts table like `user_default_options` -- Boolean | List of usercommands to enable. See User commands section. user_commands = true, -- Enable all or some usercommands lazy_load = false, -- Lazily schedule buffer highlighting setup function @@ -144,19 +147,18 @@ library to do custom highlighting themselves. css = false, -- Enable all CSS *features*: -- names, RGB, RGBA, RRGGBB, RRGGBBAA, AARRGGBB, rgb_fn, hsl_fn css_fn = false, -- Enable all CSS *functions*: rgb_fn, hsl_fn - -- Highlighting mode. 'background'|'foreground'|'virtualtext' - mode = "background", -- Set the display mode - -- Tailwind colors. boolean|'normal'|'lsp'|'both'. True is same as normal + -- Tailwind colors. boolean|'normal'|'lsp'|'both'. True sets to 'normal' tailwind = false, -- Enable tailwind colors tailwind_opts = { -- Options for highlighting tailwind names - update_names = false, -- When using tailwind = 'both', update tailwind - -- names from LSP results. See tailwind section + update_names = false, -- When using tailwind = 'both', update tailwind names from LSP results. See tailwind section }, -- parsers can contain values used in `user_default_options` sass = { enable = false, parsers = { "css" } }, -- Enable sass colors + -- Highlighting mode. 'background'|'foreground'|'virtualtext' + mode = "background", -- Set the display mode -- Virtualtext character to use virtualtext = "■", - -- Display virtualtext inline with color + -- Display virtualtext inline with color. boolean|'before'|'after'. True sets to 'after' virtualtext_inline = false, -- Virtualtext highlight mode: 'background'|'foreground' virtualtext_mode = "foreground", @@ -167,14 +169,6 @@ library to do custom highlighting themselves. }) ``` -Highlighting modes: - -- `background`: sets the background text color. -- `foreground`: sets the foreground text color. -- `virtualtext`: indicate the color behind the virtualtext. - -Virtualtext symbol can be displayed at end of line, or - Setup examples: ```lua @@ -379,13 +373,21 @@ are cached and returned on `WinScrolled` event. ## Testing -For troubleshooting use `test/minimal.lua`. -Startup neovim with `nvim --clean -u minimal.lua` in the `test` directory. +### Minimal Colorizer -Alternatively, use the following script from root directory: +For troubleshooting use `test/minimal-colorizer.lua`. +Startup neovim with `nvim --clean -u minimal-colorizer.lua` in the `test` directory. + +Alternatively, ```bash -scripts/start_minimal.sh +make minimal +``` + +or + +```bash +scripts/minimal-colorizer.sh ``` To test colorization with your config, edit `test/expect.lua` to see expected @@ -393,6 +395,94 @@ highlights. The returned table of `user_default_options` from `text/expect.lua` will be used to conveniently reattach Colorizer to `test/expect.lua` on save. +### Trie + +Colorizer uses a space efficient LuaJIT Trie implementation, which starts with +an initial node capacity of 8 bytes and expands capacity per node when needed. + +The trie can be tested and benchmarked using `test/trie/test.lua` and +`test/trie/benchmark.lua` respectively. + +#### Test + +```bash +# runs both trie-test and trie-benchmark targets +make trie +``` + +```bash +# runs trie test which inserts words and checks longest prefix +make trie-test +``` + +#### Benchmark + +```bash +scripts/trie-test.sh +``` + +```bash +# runs benchmark for different node initial capacity allocation +make trie-benchmark +``` + +```bash +scripts/trie-benchmark.sh +``` + +##### Results + +Inserting 7245 words: using uppercase, lowercase, camelcase from `vim.api.nvim_get_color_map()` and Tailwind colors + +| Initial Capacity | Resize Count | Insert Time (ms) | Lookup Time (ms) | +| ---------------- | ------------ | ---------------- | ---------------- | +| 1 | 3652 | 25 | 16 | +| 2 | 2056 | 11 | 8 | +| 4 | 1174 | 6 | 5 | +| 8 | 576 | 7 | 5 | +| 16 | 23 | 7 | 5 | +| 32 | 1 | 8 | 6 | +| 64 | 0 | 10 | 7 | + +Inserting 1000 randomized words + +| Initial Capacity | Resize Count | Insert Time (ms) | Lookup Time (ms) | +| ---------------- | ------------ | ---------------- | ---------------- | +| 1 | 434 | 1 | 0 | +| 2 | 234 | 1 | 1 | +| 4 | 129 | 1 | 0 | +| 8 | 51 | 1 | 0 | +| 16 | 17 | 1 | 1 | +| 32 | 3 | 1 | 2 | +| 64 | 1 | 2 | 1 | +| 128 | 0 | 4 | 1 | + +Inserting 10,000 randomized words + +| Initial Capacity | Resize Count | Insert Time (ms) | Lookup Time (ms) | +| ---------------- | ------------ | ---------------- | ---------------- | +| 1 | 4614 | 9 | 7 | +| 2 | 2106 | 8 | 8 | +| 4 | 842 | 9 | 7 | +| 8 | 362 | 9 | 8 | +| 16 | 208 | 11 | 9 | +| 32 | 113 | 14 | 11 | +| 64 | 24 | 21 | 14 | +| 128 | 0 | 34 | 25 | + +Inserting 100,000 randomized words + +| Initial Capacity | Resize Count | Insert Time (ms) | Lookup Time (ms) | +| ---------------- | ------------ | ---------------- | ---------------- | +| 1 | 40656 | 160 | 117 | +| 2 | 21367 | 116 | 111 | +| 4 | 11604 | 122 | 109 | +| 8 | 5549 | 133 | 113 | +| 16 | 1954 | 141 | 138 | +| 32 | 499 | 173 | 158 | +| 64 | 100 | 233 | 173 | +| 128 | 0 | 343 | 198 | + ## Extras Documentation is generated using ldoc. See @@ -402,6 +492,5 @@ Documentation is generated using ldoc. See - [ ] Add more color types ( var, advanced css functions ) - [ ] Add more display modes. E.g - sign column -- [ ] Use a more space efficient trie implementation. - [ ] Support custom parsers - [ ] Options support providing function to enable/disable instead of just boolean diff --git a/doc/colorizer.txt b/doc/colorizer.txt index ff8c283..6c414f0 100644 --- a/doc/colorizer.txt +++ b/doc/colorizer.txt @@ -553,11 +553,11 @@ user_default_options *colorizer.config.user_default_options* {css} - boolean: Enables all CSS features (`rgb_fn`, `hsl_fn`, `names`, `RGB`, `RRGGBB`). {css_fn} - boolean: Enables all CSS functions (`rgb_fn`, `hsl_fn`). - {mode} - 'background'|'foreground'|'virtualtext': Display mode {tailwind} - boolean|string: Enables Tailwind CSS colors (e.g., `"normal"`, `"lsp"`, `"both"`). {tailwind_opts} - table: Tailwind options for updating names cache, etc {sass} - table: Sass color configuration (`enable` flag and `parsers`). + {mode} - 'background'|'foreground'|'virtualtext': Display mode {virtualtext} - string: Character used for virtual text display. {virtualtext_inline} - boolean|'before'|'after': Shows virtual text inline with color. @@ -611,8 +611,6 @@ opts *colorizer.config.opts* `RRGGBB`, `hsl_fn`, `rgb_fn`). - `css_fn` (boolean): Enables all CSS function-related features (e.g., `rgb_fn`, `hsl_fn`). - - `mode` (string): Determines the display mode for highlights. Options are - `"background"`, `"foreground"`, and `"virtualtext"`. - `tailwind` (boolean|string): Enables Tailwind CSS colors. Accepts `true`, `"normal"`, `"lsp"`, or `"both"`. - `tailwind_opts` (table): Tailwind options for updating names cache, etc @@ -623,6 +621,8 @@ opts *colorizer.config.opts* - `sass` (table): Configures Sass color support. - `enable` (boolean): Enables Sass color parsing. - `parsers` (table): A list of parsers to use, typically includes `"css"`. + - `mode` (string): Determines the display mode for highlights. Options are + `"background"`, `"foreground"`, and `"virtualtext"`. - `virtualtext` (string): Character used for virtual text display of colors (default is `"■"`). - `virtualtext_inline` (boolean|'before'|'after'): Shows the virtual text @@ -825,6 +825,8 @@ LUA API *colorizer.parser.names-lua-api* Functions: ~ |reset_cache| - Reset the color names cache. + |update_color| - Updates the color value for a given color name. + |parser| - Parses a line to identify color names. @@ -836,6 +838,15 @@ reset_cache() *colorizer.parser.names.reset_cache* +update_color({name}, {hex}) *colorizer.parser.names.update_color* + Updates the color value for a given color name. + + Parameters: ~ + {name} - string: The color name. + {hex} - string: The color value in hex format. + + + parser({line}, {i}, {opts}) *colorizer.parser.names.parser* Parses a line to identify color names. @@ -981,44 +992,6 @@ parser({line}, {i}, {opts}) *colorizer.parser.rgb.parser* -============================================================================== -TAILWIND_NAMES *colorizer.parser.tailwind_names-introduction* - -This module provides a parser that identifies named colors from a given line of -text. - - The module uses a Trie structure for efficient matching of color names to - #rrggbb values - -============================================================================== -LUA API *colorizer.parser.tailwind_names-lua-api* - -Functions: ~ - |reset_cache| - Reset the color names cache. - - |parser| - Parses a line to identify color names. - - -reset_cache() *colorizer.parser.tailwind_names.reset_cache* - Reset the color names cache. - - Called from colorizer.setup - - - - -parser({line}, {i}) *colorizer.parser.tailwind_names.parser* - Parses a line to identify color names. - - Parameters: ~ - {line} - string: The text line to parse. - {i} - number: The index to start parsing from. - - returns:~ - number or nil, string or nil: Length of match and hex value if found. - - - ============================================================================== SASS *colorizer.sass-introduction* @@ -1118,34 +1091,23 @@ cleanup({bufnr}) *colorizer.tailwind.cleanup* *colorizer.tailwind.lsp_highlight* lsp_highlight({bufnr}, {ud_opts}, {buf_local_opts}, {add_highlight}, -{on_detach}) +{on_detach}, {line_start}, {line_end}) Highlight buffer using values returned by tailwindcss Parameters: ~ {bufnr} - number: Buffer number (0 for current) {ud_opts} - table: `user_default_options` {buf_local_opts} - table: Buffer local options - {add_highlight} - function - {on_detach} - function + {add_highlight} - function: Function to add highlights + {on_detach} - function: Function to call when LSP is detached + {line_start} - number: Start line + {line_end} - number: End line returns:~ boolean or nil -============================================================================== -TRIE *colorizer.trie-introduction* - -Trie implementation in luajit. - - This module provides a Trie data structure implemented in LuaJIT with efficient - memory handling. - It supports operations such as inserting, searching, finding the longest - prefix, and converting the Trie into a table format. - The implementation uses LuaJIT's Foreign Function Interface (FFI) for optimized - memory allocation. - - ============================================================================== USERCMDS *colorizer.usercmds-introduction* @@ -1206,7 +1168,7 @@ Functions: ~ |add_additional_color_chars| - Adds additional characters to the list of valid color characters. - |byte_is_valid_colorchar| - Checks if a byte is valid as a color character + |byte_is_valid_color_char| - Checks if a byte is valid as a color character (alphanumeric, dynamically added chars, or hardcoded characters). |count| - Count the number of character in a string @@ -1265,13 +1227,10 @@ get_non_alphanum_keys({tbl}) *colorizer.utils.get_non_alphanum_keys* - - *colorizer.utils.add_additional_color_chars* -add_additional_color_chars({key}, {chars}) +add_additional_color_chars({chars}) *colorizer.utils.add_additional_color_chars* Adds additional characters to the list of valid color characters. Parameters: ~ - {key} - string: The key to associate with the additional characters. {chars} - string: The additional characters to add. returns:~ @@ -1279,14 +1238,12 @@ add_additional_color_chars({key}, {chars}) -byte_is_valid_colorchar({byte}, {key}) *colorizer.utils.byte_is_valid_colorchar* +byte_is_valid_color_char({byte}) *colorizer.utils.byte_is_valid_color_char* Checks if a byte is valid as a color character (alphanumeric, dynamically added chars, or hardcoded characters). Parameters: ~ {byte} - number: The byte to check. - {key} - string|nil: The key for additional characters to validate - against. returns:~ boolean: `true` if the byte is valid, otherwise `false`. diff --git a/doc/index.html b/doc/index.html index 7692df0..4b5e2a8 100644 --- a/doc/index.html +++ b/doc/index.html @@ -43,10 +43,9 @@

Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • @@ -103,10 +102,6 @@

    Modules

    colorizer.parser.rgba_hex This module provides a parser for identifying and converting `#RRGGBBAA` hexadecimal color values to RGB hexadecimal format. - - colorizer.parser.tailwind_names - This module provides a parser that identifies named colors from a given line of text. - colorizer.sass Manages Sass variable parsing and color detection for buffers. @@ -116,8 +111,8 @@

    Modules

    Handles Tailwind CSS color highlighting within buffers. - colorizer.trie - Trie implementation in luajit. + trie + Trie implementation in LuaJIT. colorizer.usercmds diff --git a/doc/modules/colorizer.buffer.html b/doc/modules/colorizer.buffer.html index 0ffb1e7..3939a1f 100644 --- a/doc/modules/colorizer.buffer.html +++ b/doc/modules/colorizer.buffer.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.color.html b/doc/modules/colorizer.color.html index 097894c..8d29323 100644 --- a/doc/modules/colorizer.color.html +++ b/doc/modules/colorizer.color.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.config.html b/doc/modules/colorizer.config.html index 45bb535..5a60eb5 100644 --- a/doc/modules/colorizer.config.html +++ b/doc/modules/colorizer.config.html @@ -51,10 +51,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • @@ -325,9 +324,6 @@

    Fields:

  • css_fn boolean: Enables all CSS functions (`rgb_fn`, `hsl_fn`).
  • -
  • mode - 'background'|'foreground'|'virtualtext': Display mode -
  • tailwind boolean|string: Enables Tailwind CSS colors (e.g., `"normal"`, `"lsp"`, `"both"`).
  • @@ -337,6 +333,9 @@

    Fields:

  • sass table: Sass color configuration (`enable` flag and `parsers`).
  • +
  • mode + 'background'|'foreground'|'virtualtext': Display mode +
  • virtualtext string: Character used for virtual text display.
  • @@ -425,13 +424,13 @@

    Fields:

    - `hsl_fn` (boolean): Enables CSS `hsl()` and `hsla()` functions. - `css` (boolean): Enables all CSS-related features (e.g., `names`, `RGB`, `RRGGBB`, `hsl_fn`, `rgb_fn`). - `css_fn` (boolean): Enables all CSS function-related features (e.g., `rgb_fn`, `hsl_fn`). - - `mode` (string): Determines the display mode for highlights. Options are `"background"`, `"foreground"`, and `"virtualtext"`. - `tailwind` (boolean|string): Enables Tailwind CSS colors. Accepts `true`, `"normal"`, `"lsp"`, or `"both"`. - `tailwind_opts` (table): Tailwind options for updating names cache, etc - `update_names` (boolean): Updates Tailwind "normal" names cache from LSP results. This provides a smoother highlighting experience when tailwind = "both" is used. Highlighting on non-tailwind lsp buffers (like cmp) becomes more consistent. - `sass` (table): Configures Sass color support. - `enable` (boolean): Enables Sass color parsing. - `parsers` (table): A list of parsers to use, typically includes `"css"`. + - `mode` (string): Determines the display mode for highlights. Options are `"background"`, `"foreground"`, and `"virtualtext"`. - `virtualtext` (string): Character used for virtual text display of colors (default is `"■"`). - `virtualtext_inline` (boolean|'before'|'after'): Shows the virtual text inline with the color. True defaults to 'before'. False or nil disables. - `virtualtext_mode` ('background'|'foreground'): Determines the display mode for virtual text. diff --git a/doc/modules/colorizer.constants.html b/doc/modules/colorizer.constants.html index 994f54a..445c711 100644 --- a/doc/modules/colorizer.constants.html +++ b/doc/modules/colorizer.constants.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.html b/doc/modules/colorizer.html index fd74b8f..2ed9bc5 100644 --- a/doc/modules/colorizer.html +++ b/doc/modules/colorizer.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.matcher.html b/doc/modules/colorizer.matcher.html index 7482d8d..56c34ef 100644 --- a/doc/modules/colorizer.matcher.html +++ b/doc/modules/colorizer.matcher.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.parser.hsl.html b/doc/modules/colorizer.parser.hsl.html index 2eefac9..472e0cd 100644 --- a/doc/modules/colorizer.parser.hsl.html +++ b/doc/modules/colorizer.parser.hsl.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.parser.names.html b/doc/modules/colorizer.parser.names.html index f005c18..f02bf9b 100644 --- a/doc/modules/colorizer.parser.names.html +++ b/doc/modules/colorizer.parser.names.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • @@ -75,6 +74,10 @@

    Functions

    Reset the color names cache. + update_color (name, hex) + Updates the color value for a given color name. + + parser (line, i, opts) Parses a line to identify color names. @@ -101,6 +104,29 @@

    Functions

    + +
    + + update_color (name, hex) +
    +
    + Updates the color value for a given color name. + + +

    Parameters:

    + + + + + +
    diff --git a/doc/modules/colorizer.parser.rgb.html b/doc/modules/colorizer.parser.rgb.html index 3ab2e5d..caf4f6f 100644 --- a/doc/modules/colorizer.parser.rgb.html +++ b/doc/modules/colorizer.parser.rgb.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.parser.rgb_hex.html b/doc/modules/colorizer.parser.rgb_hex.html index 5b964ef..f363d21 100644 --- a/doc/modules/colorizer.parser.rgb_hex.html +++ b/doc/modules/colorizer.parser.rgb_hex.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.parser.rgba_hex.html b/doc/modules/colorizer.parser.rgba_hex.html index b802d39..7340910 100644 --- a/doc/modules/colorizer.parser.rgba_hex.html +++ b/doc/modules/colorizer.parser.rgba_hex.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.parser.tailwind_names.html b/doc/modules/colorizer.parser.tailwind_names.html index 39a6e49..ff49476 100644 --- a/doc/modules/colorizer.parser.tailwind_names.html +++ b/doc/modules/colorizer.parser.tailwind_names.html @@ -53,7 +53,7 @@

    Modules

  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.sass.html b/doc/modules/colorizer.sass.html index cd32f0f..b900abc 100644 --- a/doc/modules/colorizer.sass.html +++ b/doc/modules/colorizer.sass.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.tailwind.html b/doc/modules/colorizer.tailwind.html index f8a25ea..c2962ff 100644 --- a/doc/modules/colorizer.tailwind.html +++ b/doc/modules/colorizer.tailwind.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • @@ -77,7 +76,7 @@

    Functions

    Cleanup tailwind variables and autocmd - lsp_highlight (bufnr, ud_opts, buf_local_opts, add_highlight, on_detach) + lsp_highlight (bufnr, ud_opts, buf_local_opts, add_highlight, on_detach, line_start, line_end) Highlight buffer using values returned by tailwindcss @@ -111,7 +110,7 @@

    Parameters:

    - lsp_highlight (bufnr, ud_opts, buf_local_opts, add_highlight, on_detach) + lsp_highlight (bufnr, ud_opts, buf_local_opts, add_highlight, on_detach, line_start, line_end)
    Highlight buffer using values returned by tailwindcss @@ -129,10 +128,16 @@

    Parameters:

    table: Buffer local options
  • add_highlight - function + function: Function to add highlights
  • on_detach - function + function: Function to call when LSP is detached +
  • +
  • line_start + number: Start line +
  • +
  • line_end + number: End line
  • diff --git a/doc/modules/colorizer.trie.html b/doc/modules/colorizer.trie.html index 7fd896f..1302763 100644 --- a/doc/modules/colorizer.trie.html +++ b/doc/modules/colorizer.trie.html @@ -63,7 +63,16 @@

    Module colorizer.trie

    This module provides a Trie data structure implemented in LuaJIT with efficient memory handling. It supports operations such as inserting, searching, finding the longest prefix, and converting the Trie into a table format. - The implementation uses LuaJIT's Foreign Function Interface (FFI) for optimized memory allocation.

    + The implementation uses LuaJIT's Foreign Function Interface (FFI) for optimized memory allocation. + Dynamic Allocation: + - The `character` array in each Trie node is dynamically allocated using a double pointer (`struct Trie**`). + - Each Trie node contains: + - A `bool is_leaf` field to indicate whether the node represents the end of a string. + - A `struct Trie** character` pointer that references the dynamically allocated array. + - Memory for the `character` array is allocated only when the node is created. + - The `character` array can support up to 256 child nodes, corresponding to ASCII values. + - Each slot in the array is initialized to `NULL` and represents a potential child node. + - Memory for each node and its `character` array is allocated using `ffi.C.malloc` and freed recursively using `ffi.C.free`.

    diff --git a/doc/modules/colorizer.usercmds.html b/doc/modules/colorizer.usercmds.html index 5953cc8..72de190 100644 --- a/doc/modules/colorizer.usercmds.html +++ b/doc/modules/colorizer.usercmds.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • diff --git a/doc/modules/colorizer.utils.html b/doc/modules/colorizer.utils.html index 5c56a18..b45d8b8 100644 --- a/doc/modules/colorizer.utils.html +++ b/doc/modules/colorizer.utils.html @@ -50,10 +50,9 @@

    Modules

  • parser.names
  • parser.rgb
  • parser.rgba_hex
  • -
  • parser.tailwind_names
  • sass
  • tailwind
  • -
  • trie
  • +
  • trie
  • usercmds
  • utils
  • @@ -88,11 +87,11 @@

    Functions

    Extract non-alphanumeric characters to add as a valid index in the Trie - add_additional_color_chars (key, chars) + add_additional_color_chars (chars) Adds additional characters to the list of valid color characters. - byte_is_valid_colorchar (byte, key) + byte_is_valid_color_char (byte) Checks if a byte is valid as a color character (alphanumeric, dynamically added chars, or hardcoded characters). @@ -231,7 +230,7 @@

    Returns:

    - add_additional_color_chars (key, chars) + add_additional_color_chars (chars)
    Adds additional characters to the list of valid color characters. @@ -239,9 +238,6 @@

    Returns:

    Parameters:

    - - byte_is_valid_colorchar (byte, key) + + byte_is_valid_color_char (byte)
    Checks if a byte is valid as a color character (alphanumeric, dynamically added chars, or hardcoded characters). @@ -270,9 +266,6 @@

    Parameters:

  • byte number: The byte to check.
  • -
  • key - string|nil: The key for additional characters to validate against. -
  • Returns:

    diff --git a/doc/modules/trie.html b/doc/modules/trie.html index f092c4d..152360e 100644 --- a/doc/modules/trie.html +++ b/doc/modules/trie.html @@ -39,8 +39,9 @@

    Modules

  • buffer
  • color
  • config
  • +
  • constants
  • matcher
  • -
  • parser.argb_hex
  • +
  • parser.rgb_hex
  • parser.hsl
  • parser.names
  • parser.rgb
  • @@ -57,8 +58,33 @@

    Modules

    Module trie

    -

    -

    +

    Trie implementation in LuaJIT.

    +

    + This module provides an optimized Trie data structure using LuaJIT's Foreign Function Interface (FFI). + It supports operations like insertion, search, finding the longest prefix, and converting the Trie into a table format. +

    Dynamic Allocation: + The implementation uses dynamic memory allocation for efficient storage and manipulation of nodes: + - Each Trie node dynamically allocates memory for its `children` and `keys` arrays using `ffi.C.malloc` and `ffi.C.realloc`. + - Arrays are initially allocated with a small capacity and are resized as needed to accommodate more child nodes. +

    Node Structure: + Each Trie node contains the following fields: + - **`is_leaf`** (boolean): Indicates whether the node represents the end of a string. + - **`capacity`** (number): The current maximum number of children the node can hold. + - Starts at a small initial value (e.g., 8) and doubles as needed. + - **`size`** (number): The current number of children the node has. + - Always ≤ `capacity`. + - **`children`** (array): A dynamically allocated array of pointers to child nodes. + - **`keys`** (array): A dynamically allocated array of ASCII values corresponding to the `children` nodes. +

    Dynamic Resizing: + - If a node's `size` exceeds its `capacity` during insertion, the `capacity` is doubled. + - The `children` and `keys` arrays are reallocated to match the new capacity using `ffi.C.realloc`. + - Resizing ensures efficient use of memory while avoiding frequent allocations. +

    Memory Management: + - Memory is manually managed: + - **Allocation**: Done using `ffi.C.malloc` for new nodes and `ffi.C.realloc` for resizing arrays. + - **Deallocation**: Performed recursively for all child nodes using `ffi.C.free`. + - The implementation includes safeguards to handle allocation failures and ensure proper cleanup. +

    @@ -72,7 +98,7 @@

    Module trie

    generated by LDoc 1.5.0 -Last updated - November +Last updated - January
    diff --git a/doc/modules/utils.html b/doc/modules/utils.html deleted file mode 100644 index af8d2ff..0000000 --- a/doc/modules/utils.html +++ /dev/null @@ -1,326 +0,0 @@ - - - - - colorizer Docs - - - - -
    - -
    - -
    -
    -
    - - -
    - - - - - - -
    - -

    Module utils

    -

    Helper utils

    -

    - -

    - - -

    Functions

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    byte_is_alphanumeric (byte)Obvious.
    byte_is_hex (byte)Obvious.
    byte_is_valid_colorchar (byte)Obvious.
    get_last_modified (path)Get last modified time of a file
    merge (...)Merge two tables.
    parse_hex (byte)Obvious.
    percent_or_hex (v)Obvious.
    watch_file (path, callback, ...)Watch a file for changes and execute callback
    - -
    -
    - - -

    Functions

    - -
    -
    - - byte_is_alphanumeric (byte) -
    -
    - Obvious. - - -

    Parameters:

    -
      -
    • byte - number -
    • -
    - -

    Returns:

    -
      - - boolean -
    - - - - -
    -
    - - byte_is_hex (byte) -
    -
    - Obvious. - - -

    Parameters:

    -
      -
    • byte - number -
    • -
    - -

    Returns:

    -
      - - boolean -
    - - - - -
    -
    - - byte_is_valid_colorchar (byte) -
    -
    - Obvious. - - -

    Parameters:

    -
      -
    • byte - number -
    • -
    - -

    Returns:

    -
      - - boolean -
    - - - - -
    -
    - - get_last_modified (path) -
    -
    - Get last modified time of a file - - -

    Parameters:

    -
      -
    • path - string: file path -
    • -
    - -

    Returns:

    -
      - - number|nil: modified time -
    - - - - -
    -
    - - merge (...) -
    -
    - Merge two tables.

    - -

    todo: Remove this and use vim.tbl_deep_extend - - -

    Parameters:

    -
      -
    • ... - - - -
    • -
    - -

    Returns:

    -
      - - table -
    - - - - -
    -
    - - parse_hex (byte) -
    -
    - Obvious. - - -

    Parameters:

    -
      -
    • byte - number -
    • -
    - -

    Returns:

    -
      - - number -
    - - - - -
    -
    - - percent_or_hex (v) -
    -
    - Obvious. - - -

    Parameters:

    -
      -
    • v - string -
    • -
    - -

    Returns:

    -
      - - number|nil -
    - - - - -
    -
    - - watch_file (path, callback, ...) -
    -
    - Watch a file for changes and execute callback - - -

    Parameters:

    -
      -
    • path - string: File path -
    • -
    • callback - function: Callback to execute -
    • -
    • ... - array: params for callback -
    • -
    - -

    Returns:

    -
      - - function|nil -
    - - - - -
    -
    - - -
    -
    -
    -generated by LDoc 1.4.6 -Last updated - September -
    -
    - - diff --git a/lua/colorizer.lua b/lua/colorizer.lua index f77abd6..a5056af 100644 --- a/lua/colorizer.lua +++ b/lua/colorizer.lua @@ -467,7 +467,6 @@ function M.setup(opts) } require("colorizer.matcher").reset_cache() require("colorizer.parser.names").reset_cache() - require("colorizer.parser.tailwind_names").reset_cache() require("colorizer.buffer").reset_cache() require("colorizer.config").reset_cache() diff --git a/lua/colorizer/buffer.lua b/lua/colorizer/buffer.lua index 566340a..f52add2 100644 --- a/lua/colorizer/buffer.lua +++ b/lua/colorizer/buffer.lua @@ -6,9 +6,9 @@ local M = {} local color = require("colorizer.color") local const = require("colorizer.constants") local matcher = require("colorizer.matcher") +local names = require("colorizer.parser.names") local sass = require("colorizer.sass") local tailwind = require("colorizer.tailwind") -local tailwind_names = require("colorizer.parser.tailwind_names") local utils = require("colorizer.utils") local hl_state @@ -59,6 +59,7 @@ local function create_highlight(rgb_hex, mode) if mode == "foreground" then vim.api.nvim_set_hl(0, highlight_name, { fg = "#" .. rgb_hex }) else + -- TODO: 2025-01-11 - Should this check for background or virtualtext local rr, gg, bb = rgb_hex:sub(1, 2), rgb_hex:sub(3, 4), rgb_hex:sub(5, 6) local r, g, b = tonumber(rr, 16), tonumber(gg, 16), tonumber(bb, 16) local fg_color = color.is_bright(r, g, b) and "Black" or "White" @@ -105,7 +106,7 @@ function M.add_highlight(bufnr, ns_id, line_start, line_end, data, ud_opts, hl_o local txt = slice_line(bufnr, linenr, hl.range[1], hl.range[2]) if txt and not hl_state.updated_colors[txt] then hl_state.updated_colors[txt] = true - tailwind_names.update_color(txt, hl.rgb_hex) + names.update_color(txt, hl.rgb_hex) end end end @@ -127,7 +128,7 @@ function M.add_highlight(bufnr, ns_id, line_start, line_end, data, ud_opts, hl_o local txt = slice_line(bufnr, linenr, hl.range[1], hl.range[2]) if txt and not hl_state.updated_colors[txt] then hl_state.updated_colors[txt] = true - tailwind_names.update_color(txt, hl.rgb_hex) + names.update_color(txt, hl.rgb_hex) end end end @@ -234,15 +235,7 @@ end ---@return table|nil function M.parse_lines(bufnr, lines, line_start, ud_opts, parse_opts) parse_opts = parse_opts or {} - local loop_parse_fn - local use_tailwind = parse_opts.tailwind == true and ud_opts.tailwind ~= "lsp" - if use_tailwind then - loop_parse_fn = function(line, i, _bufnr) - return tailwind_names.parser(line, i) - end - else - loop_parse_fn = matcher.make(ud_opts) - end + local loop_parse_fn = matcher.make(ud_opts) if not loop_parse_fn then return end @@ -253,6 +246,15 @@ function M.parse_lines(bufnr, lines, line_start, ud_opts, parse_opts) local i = 1 while i < #line do local length, rgb_hex = loop_parse_fn(line, i, bufnr) + if length and not rgb_hex then + vim.api.nvim_err_writeln( + string.format( + "Colorizer: Error parsing line %d, index %d. Please report this issue.", + line_nr, + i + ) + ) + end if length and rgb_hex then data[line_nr] = data[line_nr] or {} table.insert(data[line_nr] or {}, { rgb_hex = rgb_hex, range = { i - 1, i + length - 1 } }) diff --git a/lua/colorizer/config.lua b/lua/colorizer/config.lua index be9151b..1c97934 100644 --- a/lua/colorizer/config.lua +++ b/lua/colorizer/config.lua @@ -21,12 +21,12 @@ local plugin_user_default_options = { hsl_fn = false, css = false, css_fn = false, - mode = "background", tailwind = false, tailwind_opts = { update_names = false, }, sass = { enable = false, parsers = { css = true } }, + mode = "background", virtualtext = "■", virtualtext_inline = false, virtualtext_mode = "foreground", @@ -60,10 +60,10 @@ local plugin_user_default_options = { -- @field hsl_fn boolean: Enables CSS `hsl()` and `hsla()` functions. -- @field css boolean: Enables all CSS features (`rgb_fn`, `hsl_fn`, `names`, `RGB`, `RRGGBB`). -- @field css_fn boolean: Enables all CSS functions (`rgb_fn`, `hsl_fn`). --- @field mode 'background'|'foreground'|'virtualtext': Display mode -- @field tailwind boolean|string: Enables Tailwind CSS colors (e.g., `"normal"`, `"lsp"`, `"both"`). -- @field tailwind_opts table: Tailwind options for updating names cache, etc -- @field sass table: Sass color configuration (`enable` flag and `parsers`). +-- @field mode 'background'|'foreground'|'virtualtext': Display mode -- @field virtualtext string: Character used for virtual text display. -- @field virtualtext_inline boolean|'before'|'after': Shows virtual text inline with color. -- @field virtualtext_mode 'background'|'foreground': Mode for virtual text display. @@ -193,13 +193,13 @@ end -- - `hsl_fn` (boolean): Enables CSS `hsl()` and `hsla()` functions. -- - `css` (boolean): Enables all CSS-related features (e.g., `names`, `RGB`, `RRGGBB`, `hsl_fn`, `rgb_fn`). -- - `css_fn` (boolean): Enables all CSS function-related features (e.g., `rgb_fn`, `hsl_fn`). --- - `mode` (string): Determines the display mode for highlights. Options are `"background"`, `"foreground"`, and `"virtualtext"`. -- - `tailwind` (boolean|string): Enables Tailwind CSS colors. Accepts `true`, `"normal"`, `"lsp"`, or `"both"`. -- - `tailwind_opts` (table): Tailwind options for updating names cache, etc -- - `update_names` (boolean): Updates Tailwind "normal" names cache from LSP results. This provides a smoother highlighting experience when tailwind = "both" is used. Highlighting on non-tailwind lsp buffers (like cmp) becomes more consistent. -- - `sass` (table): Configures Sass color support. -- - `enable` (boolean): Enables Sass color parsing. -- - `parsers` (table): A list of parsers to use, typically includes `"css"`. +-- - `mode` (string): Determines the display mode for highlights. Options are `"background"`, `"foreground"`, and `"virtualtext"`. -- - `virtualtext` (string): Character used for virtual text display of colors (default is `"■"`). -- - `virtualtext_inline` (boolean|'before'|'after'): Shows the virtual text inline with the color. True defaults to 'before'. False or nil disables. -- - `virtualtext_mode` ('background'|'foreground'): Determines the display mode for virtual text. diff --git a/lua/colorizer/matcher.lua b/lua/colorizer/matcher.lua index f854514..f48f4d4 100644 --- a/lua/colorizer/matcher.lua +++ b/lua/colorizer/matcher.lua @@ -11,7 +11,6 @@ local min, max = math.min, math.max local parsers = { color_name = require("colorizer.parser.names").parser, - tailwind_name = require("colorizer.parser.tailwind_names").parser, argb_hex = require("colorizer.parser.argb_hex").parser, hsl_function = require("colorizer.parser.hsl").parser, rgb_function = require("colorizer.parser.rgb").parser, @@ -61,21 +60,6 @@ local function compile(matchers, matchers_trie) end end - -- Color names - -- if matchers.color_name_parser and not matchers.tailwind_names_parser then - -- return parsers.color_name(line, i, matchers.color_name_parser) - -- end - -- if not matchers.color_name_parser and matchers.tailwind_names_parser then - -- return parsers.tailwind_name(line, i) - -- end - -- if matchers.color_name_parser and matchers.tailwind_names_parser then - -- local length, rgb_hex - -- length, rgb_hex = parsers.color_name(line, i, matchers.color_name_parser) - -- if length and rgb_hex then - -- return length, rgb_hex - -- end - -- return parsers.tailwind_name(line, i) - -- end if matchers.color_name_parser then return parsers.color_name(line, i, matchers.color_name_parser) end @@ -136,7 +120,7 @@ function M.make(opts) + (enable_AARRGGBB and 1024 or 0) + (enable_rgb and 2048 or 0) + (enable_hsl and 4096 or 0) - + ((enable_tailwind == true or enable_tailwind == "normal") and 8192 or 0) + + (enable_tailwind == "normal" and 8192 or 0) + (enable_tailwind == "lsp" and 16384 or 0) + (enable_tailwind == "both" and 32768 or 0) + (enable_sass and 65536 or 0) @@ -153,7 +137,8 @@ function M.make(opts) local matchers = {} local matchers_prefix = {} - if enable_names or enable_names_custom then + local enable_tailwind_names = enable_tailwind == "normal" or enable_tailwind == "both" + if enable_names or enable_names_custom or enable_tailwind_names then matchers.color_name_parser = matchers.color_name_parser or {} if enable_names then matchers.color_name_parser.color_names = enable_names @@ -167,6 +152,9 @@ function M.make(opts) if enable_names_custom then matchers.color_name_parser.names_custom = enable_names_custom end + if enable_tailwind_names then + matchers.color_name_parser.tailwind_names = enable_tailwind_names + end end matchers.sass_name_parser = enable_sass or nil diff --git a/lua/colorizer/parser/names.lua b/lua/colorizer/parser/names.lua index 540730a..bb68796 100644 --- a/lua/colorizer/parser/names.lua +++ b/lua/colorizer/parser/names.lua @@ -14,27 +14,53 @@ local names_cache function M.reset_cache() names_cache = { color_map = {}, - color_trie = nil, - color_name_minlen = nil, - color_name_maxlen = nil, + trie = nil, + name_minlen = nil, + name_maxlen = nil, } end do M.reset_cache() end +--- Updates the color value for a given color name. +---@param name string: The color name. +---@param hex string: The color value in hex format. +function M.update_color(name, hex) + if not name or not hex then + return + end + if names_cache.color_map[name] then + names_cache.color_map[name] = hex + end +end + --- Internal function to add a color to the Trie and map. ---@param name string: The color name. ---@param val string: The color value in hex format. local function add_color(name, val) - names_cache.color_name_minlen = names_cache.color_name_minlen - and min(#name, names_cache.color_name_minlen) - or #name - names_cache.color_name_maxlen = names_cache.color_name_maxlen - and max(#name, names_cache.color_name_maxlen) - or #name + names_cache.name_minlen = names_cache.name_minlen and min(#name, names_cache.name_minlen) or #name + names_cache.name_maxlen = names_cache.name_maxlen and max(#name, names_cache.name_maxlen) or #name names_cache.color_map[name] = val - names_cache.color_trie:insert(name) + names_cache.trie:insert(name) +end + +--- Handles Vim's color map and adds colors to the Trie and map. +local function handle_names(opts) + for name, value in pairs(vim.api.nvim_get_color_map()) do + if not (opts.strip_digits and name:match("%d+$")) then + local rgb_hex = tohex(value, 6) + if opts.lowercase then + add_color(name:lower(), rgb_hex) + end + if opts.camelcase then + add_color(name, rgb_hex) + end + if opts.uppercase then + add_color(name:upper(), rgb_hex) + end + end + end end --- Handles additional color names provided as a table or function. @@ -43,7 +69,6 @@ local function handle_names_custom(names_custom) if not names_custom then return end - local names = {} if type(names_custom) == "table" then names = names_custom @@ -58,12 +83,9 @@ local function handle_names_custom(names_custom) return end end - -- Add additional characters found in names_custom keys local chars = utils.get_non_alphanum_keys(names) - names_cache.color_trie:additional_chars(chars) - utils.add_additional_color_chars("names", chars) - + utils.add_additional_color_chars(chars) for name, hex in pairs(names) do if type(hex) == "string" then local normalized_hex = hex:gsub("^#", ""):gsub("%s", "") @@ -80,20 +102,14 @@ local function handle_names_custom(names_custom) end end ---- Handles Vim's color map and adds colors to the Trie and map. -local function handle_names(opts) - for name, value in pairs(vim.api.nvim_get_color_map()) do - if not (opts.strip_digits and name:match("%d+$")) then - local rgb_hex = tohex(value, 6) - if opts.lowercase then - add_color(name:lower(), rgb_hex) - end - if opts.camelcase then - add_color(name, rgb_hex) - end - if opts.uppercase then - add_color(name:upper(), rgb_hex) - end +--- Handles Tailwind classnames and adds colors to the Trie and map. +local function handle_tailwind_names() + local tw_delimeter = "-" + utils.add_additional_color_chars(tw_delimeter) + local data = require("colorizer.data.tailwind_colors") + for name, hex in pairs(data.colors) do + for _, prefix in ipairs(data.prefixes) do + add_color(string.format("%s%s%s", prefix, tw_delimeter, name), hex) end end end @@ -102,18 +118,21 @@ end ---@param opts table Configuration options for color names. local function populate_colors(opts) names_cache.color_map = {} - names_cache.color_trie = Trie() - names_cache.color_name_minlen, names_cache.color_name_maxlen = nil, nil + names_cache.trie = Trie() + names_cache.name_minlen, names_cache.name_maxlen = nil, nil -- Add Vim's color map if opts.color_names then handle_names(opts.color_names_opts) end - - -- Add extra names + -- Add custom names if opts.names_custom then handle_names_custom(opts.names_custom) end + -- Add tailwind names + if opts.tailwind_names then + handle_tailwind_names() + end end --- Parses a line to identify color names. @@ -122,21 +141,21 @@ end ---@param opts table: Parsing options. ---@return number|nil, string|nil: Length of match and hex value if found. function M.parser(line, i, opts) - if not names_cache.color_trie then + if not names_cache.trie then populate_colors(opts) end if - #line < i + (names_cache.color_name_minlen or 0) - 1 - or (i > 1 and utils.byte_is_valid_colorchar(line:byte(i - 1))) + #line < i + (names_cache.name_minlen or 0) - 1 + or (i > 1 and utils.byte_is_valid_color_char(line:byte(i - 1))) then return end - local prefix = names_cache.color_trie:longest_prefix(line, i) + local prefix = names_cache.trie:longest_prefix(line, i) if prefix then local next_byte_index = i + #prefix - if #line >= next_byte_index and utils.byte_is_valid_colorchar(line:byte(next_byte_index)) then + if #line >= next_byte_index and utils.byte_is_valid_color_char(line:byte(next_byte_index)) then return end return #prefix, names_cache.color_map[prefix] diff --git a/lua/colorizer/parser/tailwind_names.lua b/lua/colorizer/parser/tailwind_names.lua deleted file mode 100644 index b3432e2..0000000 --- a/lua/colorizer/parser/tailwind_names.lua +++ /dev/null @@ -1,91 +0,0 @@ ---- This module provides a parser that identifies named colors from a given line of text. --- The module uses a Trie structure for efficient matching of color names to #rrggbb values --- @module colorizer.parser.tailwind_names -local M = {} - -local Trie = require("colorizer.trie") -local utils = require("colorizer.utils") -local min, max = math.min, math.max - -local names_cache ----Reset the color names cache. --- Called from colorizer.setup -function M.reset_cache() - names_cache = { - color_map = {}, - color_trie = nil, - color_name_minlen = nil, - color_name_maxlen = nil, - } -end -do - M.reset_cache() -end - -function M.update_color(name, val) - if not name or not val then - return - end - if names_cache.color_map[name] then - names_cache.color_map[name] = val - end -end - ---- Internal function to add a color to the Trie and map. ----@param name string: The color name. ----@param val string: The color value in hex format. -local function add_color(name, val) - names_cache.color_name_minlen = names_cache.color_name_minlen - and min(#name, names_cache.color_name_minlen) - or #name - names_cache.color_name_maxlen = names_cache.color_name_maxlen - and max(#name, names_cache.color_name_maxlen) - or #name - names_cache.color_map[name] = val - names_cache.color_trie:insert(name) -end - ---- Populates the Trie and map with Tailwind classnames -local function populate_colors() - names_cache.color_map = {} - names_cache.color_trie = Trie() - names_cache.color_name_minlen, names_cache.color_name_maxlen = nil, nil - - local tw_delimeter = "-" - names_cache.color_trie:additional_chars(tw_delimeter) - utils.add_additional_color_chars("tailwind_names", tw_delimeter) - local data = require("colorizer.data.tailwind_colors") - for name, hex in pairs(data.colors) do - for _, prefix in ipairs(data.prefixes) do - add_color(prefix .. tw_delimeter .. name, hex) - end - end -end - ---- Parses a line to identify color names. ----@param line string: The text line to parse. ----@param i number: The index to start parsing from. ----@return number|nil, string|nil: Length of match and hex value if found. -function M.parser(line, i) - if not names_cache.color_trie then - populate_colors() - end - - if - #line < i + (names_cache.color_name_minlen or 0) - 1 - or (i > 1 and utils.byte_is_valid_colorchar(line:byte(i - 1))) - then - return - end - - local prefix = names_cache.color_trie:longest_prefix(line, i) - if prefix then - local next_byte_index = i + #prefix - if #line >= next_byte_index and utils.byte_is_valid_colorchar(line:byte(next_byte_index)) then - return - end - return #prefix, names_cache.color_map[prefix] - end -end - -return M diff --git a/lua/colorizer/tailwind.lua b/lua/colorizer/tailwind.lua index fd2c467..8aa7c79 100644 --- a/lua/colorizer/tailwind.lua +++ b/lua/colorizer/tailwind.lua @@ -82,8 +82,10 @@ end ---@param bufnr number: Buffer number (0 for current) ---@param ud_opts table: `user_default_options` ---@param buf_local_opts table: Buffer local options ----@param add_highlight function ----@param on_detach function +---@param add_highlight function: Function to add highlights +---@param on_detach function: Function to call when LSP is detached +---@param line_start number: Start line +---@param line_end number: End line ---@return boolean|nil function M.lsp_highlight( bufnr, diff --git a/lua/colorizer/trie.lua b/lua/colorizer/trie.lua index 6816145..304e36f 100644 --- a/lua/colorizer/trie.lua +++ b/lua/colorizer/trie.lua @@ -1,8 +1,3 @@ ----Trie implementation in luajit. --- This module provides a Trie data structure implemented in LuaJIT with efficient memory handling. --- It supports operations such as inserting, searching, finding the longest prefix, and converting the Trie into a table format. --- The implementation uses LuaJIT's Foreign Function Interface (FFI) for optimized memory allocation. - -- Copyright © 2019 Ashkan Kiani -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by @@ -16,16 +11,52 @@ -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see . ---@module trie + +--- Trie implementation in LuaJIT. +-- This module provides an optimized Trie data structure using LuaJIT's Foreign Function Interface (FFI). +-- It supports operations like insertion, search, finding the longest prefix, and converting the Trie into a table format. +-- +-- Dynamic Allocation: +-- The implementation uses dynamic memory allocation for efficient storage and manipulation of nodes: +-- - Each Trie node dynamically allocates memory for its `children` and `keys` arrays using `ffi.C.malloc` and `ffi.C.realloc`. +-- - Arrays are initially allocated with a small capacity and are resized as needed to accommodate more child nodes. +-- +-- Node Structure: +-- Each Trie node contains the following fields: +-- - **`is_leaf`** (boolean): Indicates whether the node represents the end of a string. +-- - **`capacity`** (number): The current maximum number of children the node can hold. +-- - Starts at a small initial value (e.g., 8) and doubles as needed. +-- - **`size`** (number): The current number of children the node has. +-- - Always ≤ `capacity`. +-- - **`children`** (array): A dynamically allocated array of pointers to child nodes. +-- - **`keys`** (array): A dynamically allocated array of ASCII values corresponding to the `children` nodes. +-- +-- Dynamic Resizing: +-- - If a node's `size` exceeds its `capacity` during insertion, the `capacity` is doubled. +-- - The `children` and `keys` arrays are reallocated to match the new capacity using `ffi.C.realloc`. +-- - Resizing ensures efficient use of memory while avoiding frequent allocations. +-- +-- Memory Management: +-- - Memory is manually managed: +-- - **Allocation**: Done using `ffi.C.malloc` for new nodes and `ffi.C.realloc` for resizing arrays. +-- - **Deallocation**: Performed recursively for all child nodes using `ffi.C.free`. +-- - The implementation includes safeguards to handle allocation failures and ensure proper cleanup. +-- +-- @module trie local ffi = require("ffi") +--- Trie Node Structure. ffi.cdef([[ struct Trie { - bool is_leaf; - struct Trie* character[62]; + bool is_leaf; + size_t capacity; // Current capacity of the character array + size_t size; // Number of children currently in use + struct Trie** children; // Dynamically allocated array of children + uint8_t* keys; // Array of corresponding ASCII keys }; void *malloc(size_t size); +void *realloc(void *ptr, size_t size); void free(void *ptr); ]]) @@ -33,125 +64,153 @@ local Trie_t = ffi.typeof("struct Trie") local Trie_ptr_t = ffi.typeof("$ *", Trie_t) local Trie_size = ffi.sizeof(Trie_t) -local function trie_create() +local initial_capacity = 8 + +local function trie_create(capacity) + capacity = capacity or initial_capacity + local node_ptr = ffi.C.malloc(Trie_size) + if not node_ptr then + error("Failed to allocate memory for Trie node") + end if not Trie_size then - return + error("Failed to get size of Trie node") end - local ptr = ffi.C.malloc(Trie_size) - if not ptr then - return + ffi.fill(node_ptr, Trie_size) + local node = ffi.cast(Trie_ptr_t, node_ptr) + node.is_leaf = false + node.capacity = capacity + node.size = 0 + node.children = ffi.cast("struct Trie**", ffi.C.malloc(capacity * ffi.sizeof("struct Trie*"))) + if not node.children then + ffi.C.free(node_ptr) + error("Failed to allocate memory for children") end - ffi.fill(ptr, Trie_size) - return ffi.cast(Trie_ptr_t, ptr) + ffi.fill(node.children, capacity * ffi.sizeof("struct Trie*")) + node.keys = ffi.cast("uint8_t*", ffi.C.malloc(capacity * ffi.sizeof("uint8_t"))) + if not node.keys then + ffi.C.free(node.children) + ffi.C.free(node_ptr) + error("Failed to allocate memory for keys") + end + ffi.fill(node.keys, capacity * ffi.sizeof("uint8_t")) + return node end -local function trie_destroy(trie) - if trie == nil then - return +local resize_count = 0 +local function trie_resize(node) + local current_capacity = tonumber(node.capacity) -- convert to lua number + local new_capacity = current_capacity * 2 + local new_children = ffi.C.realloc(node.children, new_capacity * ffi.sizeof("struct Trie*")) + if not new_children then + error("Failed to reallocate memory for children") end - for i = 0, 61 do - local child = trie.character[i] - if child ~= nil then - trie_destroy(child) - end + node.children = ffi.cast("struct Trie**", new_children) + local new_keys = ffi.C.realloc(node.keys, new_capacity * ffi.sizeof("uint8_t")) + if not new_keys then + error("Failed to reallocate memory for keys") + end + node.keys = ffi.cast("uint8_t*", new_keys) + for i = current_capacity, new_capacity - 1 do + node.children[i] = nil + node.keys[i] = 0 end - ffi.C.free(trie) + node.capacity = new_capacity + resize_count = resize_count + 1 end -local total_char = 255 -local index_lookup = ffi.new("uint8_t[?]", total_char) -local char_lookup = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" -do - local b = string.byte - local byte = { - ["0"] = b("0"), - ["9"] = b("9"), - ["a"] = b("a"), - ["A"] = b("A"), - ["z"] = b("z"), - ["Z"] = b("Z"), - } - for i = 0, total_char do - if i >= byte["0"] and i <= byte["9"] then - index_lookup[i] = i - byte["0"] - elseif i >= byte["A"] and i <= byte["Z"] then - index_lookup[i] = i - byte["A"] + 10 - elseif i >= byte["a"] and i <= byte["z"] then - index_lookup[i] = i - byte["a"] + 10 + 26 - else - index_lookup[i] = total_char - end +local function trie_destroy(node) + if not node then + return + end + for i = 0, node.size - 1 do + trie_destroy(node.children[i]) end + ffi.C.free(node.children) + ffi.C.free(node.keys) + ffi.C.free(node) + node = nil end -local function trie_insert(trie, value) - if trie == nil then +local function trie_insert(node, value, capacity) + if not node or type(value) ~= "string" then + print("Invalid node or value for insertion") return false end - local node = trie + local current = node for i = 1, #value do - local index = index_lookup[value:byte(i)] - if index == total_char then - return false + local char_byte = value:byte(i) + local found = false + for j = 0, tonumber(current.size) - 1 do + if current.keys[j] == char_byte then + current = current.children[j] + found = true + break + end end - if node.character[index] == nil then - node.character[index] = trie_create() + if not found then + if current.size >= current.capacity then + trie_resize(current) + end + current.keys[current.size] = char_byte + current.children[current.size] = trie_create(capacity or initial_capacity) + current.size = current.size + 1 + current = current.children[current.size - 1] end - node = node.character[index] end - node.is_leaf = true - return node, trie + current.is_leaf = true + return true end -local function trie_search(trie, value, start) - if trie == nil then +local function trie_search(node, value) + if not node or type(value) ~= "string" then return false end - local node = trie - for i = (start or 1), #value do - local index = index_lookup[value:byte(i)] - if index == total_char then - return + local current = node + for i = 1, #value do + local char_byte = value:byte(i) + local found = false + for j = 0, tonumber(current.size) - 1 do + if current.keys[j] == char_byte then + current = current.children[j] + found = true + break + end end - local child = node.character[index] - if child == nil then + if not found then return false end - node = child end - return node.is_leaf + return current.is_leaf end local function trie_longest_prefix(trie, value, start, exact) if trie == nil then - return false + return nil end start = start or 1 local node = trie local last_i = nil for i = start, #value do - local index = index_lookup[value:byte(i)] - if index == total_char then - break + local char_byte = value:byte(i) + local found = false + for j = 0, tonumber(node.size) - 1 do + if node.keys[j] == char_byte then + node = node.children[j] + found = true + if node.is_leaf then + last_i = i + end + break + end end - local child = node.character[index] - if child == nil then + if not found then break end - if child.is_leaf then - last_i = i - end - node = child end - if last_i then - -- Avoid a copy if the whole string is a match. - if start == 1 and last_i == #value then - return value - end - - if not exact then - return value:sub(start, last_i) - end + if exact then + return last_i == #value and value or nil + else + return last_i and value:sub(start, last_i) or nil end end @@ -162,44 +221,20 @@ local function trie_extend(trie, t) end end -local function trie_additional_chars(trie, chars) - if trie == nil or type(chars) ~= "string" then - return - end - for i = 1, #chars do - local char = chars:sub(i, i) - local char_byte = string.byte(char) - if index_lookup[char_byte] == total_char then - char_lookup = char_lookup .. char - index_lookup[char_byte] = total_char + 1 - end - end -end - ---- Printing utilities - -local function index_to_char(index) - if index < 0 or index > 61 then - return - end - return char_lookup:sub(index + 1, index + 1) -end - -local function trie_as_table(trie) - if trie == nil then - return +local function trie_as_table(node) + if node == nil then + return nil end local children = {} - for i = 0, 61 do - local child = trie.character[i] - if child ~= nil then - local child_table = trie_as_table(child) - child_table.c = index_to_char(i) + for i = 0, tonumber(node.size) - 1 do + local child_table = trie_as_table(node.children[i]) + if child_table then + child_table.c = string.char(node.keys[i]) table.insert(children, child_table) end end return { - is_leaf = trie.is_leaf, + is_leaf = node.is_leaf, children = children, } end @@ -263,9 +298,16 @@ local function trie_to_string(trie) return table.concat(print_trie_table(as_table), "\n") end +local function trie_resize_count() + return resize_count +end + local Trie_mt = { - __new = function(_, init) - local trie = trie_create() + __new = function(_, init, opts) + opts = opts or {} + local capacity = opts.initial_capacity or initial_capacity + local trie = trie_create(capacity) + resize_count = 0 if type(init) == "table" then trie_extend(trie, init) end @@ -277,7 +319,7 @@ local Trie_mt = { longest_prefix = trie_longest_prefix, extend = trie_extend, destroy = trie_destroy, - additional_chars = trie_additional_chars, + resize_count = trie_resize_count, }, __tostring = trie_to_string, __gc = trie_destroy, diff --git a/lua/colorizer/utils.lua b/lua/colorizer/utils.lua index a1ac26d..633f672 100644 --- a/lua/colorizer/utils.lua +++ b/lua/colorizer/utils.lua @@ -23,14 +23,12 @@ local byte_category = ffi.new("uint8_t[256]") local category_hex = lshift(1, 2) local category_alphanum = bor(lshift(1, 1) --[[alpha]], lshift(1, 0) --[[digit]]) -local additional_color_chars = {} +local additional_color_chars = "" do - -- do not run the loop multiple times local b = string.byte local byte_values = { ["0"] = b("0"), ["9"] = b("9"), ["a"] = b("a"), ["f"] = b("f"), ["z"] = b("z") } - for i = 0, 255 do local v = 0 local lowercase = bor(i, 0x20) @@ -64,8 +62,7 @@ end ---@param byte number The byte to check. ---@return boolean: `true` if the byte is alphanumeric, otherwise `false`. function M.byte_is_alphanumeric(byte) - local category = byte_category[byte] - return band(category, category_alphanum) ~= 0 + return band(byte_category[byte], category_alphanum) ~= 0 end --- Checks if a byte represents a hexadecimal character. @@ -93,24 +90,15 @@ function M.get_non_alphanum_keys(tbl) end --- Adds additional characters to the list of valid color characters. ----@param key string: The key to associate with the additional characters. ---@param chars string: The additional characters to add. ---@return boolean: `true` if the characters were added, otherwise `false`. -function M.add_additional_color_chars(key, chars) - if type(chars) ~= "string" then - vim.api.nvim_err_writeln( - string.format("colorizer.utils.add_additional_chars: invalid chars: %s", chars) - ) - return false - end - if not additional_color_chars[key] then - additional_color_chars[key] = "" - end +function M.add_additional_color_chars(chars) for i = 1, #chars do local char = chars:sub(i, i) local char_byte = string.byte(char) - if byte_category[char_byte] == 0 then - additional_color_chars[key] = additional_color_chars[key] .. char + -- It's possible to define `custom_names` with spaces. Ignore space: it's by empty space that separate things may exist 🧘 + if char_byte ~= 32 and byte_category[char_byte] == 0 then + additional_color_chars = additional_color_chars .. char byte_category[char_byte] = 1 end end @@ -119,19 +107,15 @@ end --- Checks if a byte is valid as a color character (alphanumeric, dynamically added chars, or hardcoded characters). ---@param byte number: The byte to check. ----@param key string|nil: The key for additional characters to validate against. ---@return boolean: `true` if the byte is valid, otherwise `false`. -function M.byte_is_valid_colorchar(byte, key) - -- Check alphanumeric characters +function M.byte_is_valid_color_char(byte) if M.byte_is_alphanumeric(byte) then return true end -- Check additional characters for the provided key - if additional_color_chars[key] then - for i = 1, #additional_color_chars[key] do - if byte == additional_color_chars[key]:byte(i) then - return true - end + for i = 1, #additional_color_chars do + if byte == additional_color_chars:byte(i) then + return true end end return false diff --git a/scripts/gen_docs.sh b/scripts/gen_docs.sh index 65c8122..38985d5 100755 --- a/scripts/gen_docs.sh +++ b/scripts/gen_docs.sh @@ -56,7 +56,7 @@ main() { project_name="colorizer" if command -v ldoc 1>/dev/null; then # html docs - ldoc -f discount -p "$project_name" -t "${project_name} Docs" -u lua "${@}" -s doc --date "- $(date +'%B')" || cleanup + ldoc -p "$project_name" -t "${project_name} Docs" -u lua "${@}" -s doc --date "- $(date +'%B')" || cleanup # vim docs create_vim_doc "$project_name" lua doc/ldoc_vim.ltp || cleanup diff --git a/scripts/minimal-colorizer.sh b/scripts/minimal-colorizer.sh new file mode 100755 index 0000000..5efe8bf --- /dev/null +++ b/scripts/minimal-colorizer.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd test || exit +nvim --clean -u minimal-colorizer.lua diff --git a/scripts/start_minimal.sh b/scripts/start_minimal.sh deleted file mode 100755 index 60fccb3..0000000 --- a/scripts/start_minimal.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -cd test || exit -nvim --clean -u minimal.lua diff --git a/scripts/trie-benchmark.sh b/scripts/trie-benchmark.sh new file mode 100755 index 0000000..c259d33 --- /dev/null +++ b/scripts/trie-benchmark.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +cd test/trie || exit +nvim --clean --headless -u benchmark.lua -c quit +file=trie-benchmark.txt +if [[ -f $file ]]; then + # Format and display the output + echo -e "\nTrie benchmarks:\n" + column -L -t -s $'\t' <"$file" +fi diff --git a/scripts/trie-test.sh b/scripts/trie-test.sh new file mode 100755 index 0000000..4c202c7 --- /dev/null +++ b/scripts/trie-test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +cd test/trie || exit +nvim --clean --headless -u test.lua -c quit +file=trie-test.txt + +if [[ -f $file ]]; then + # Display small list file + echo -e "\nTrie tests:\n" + cat "$file" +fi diff --git a/test/.gitignore b/test/.gitignore index 328bf17..96f1a43 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1 +1,2 @@ colorizer_repro/ +colorizer_trie/ diff --git a/test/expect.lua b/test/expect.lua index b354c21..d7113d2 100644 --- a/test/expect.lua +++ b/test/expect.lua @@ -6,9 +6,17 @@ local opts = { lua = { names = true, names_opts = { - -- strip_digits = false, + lowercase = true, + camelcase = true, + uppercase = true, + strip_digits = false, }, + tailwind = true, names_custom = { + [" NOTE:"] = "#5CA204", + ["TODO: "] = "#3457D5", + [" WARN: "] = "#EAFE01", + [" FIX: "] = "#FF0000", one_two = "#017dac", ["three=four"] = "#3700c2", ["five@six"] = "#e9e240", @@ -20,11 +28,11 @@ local opts = { buftypes = { "*", "!prompt", "!popup" }, user_commands = true, user_default_options = { - names = false, + names = true, names_opts = { lowercase = true, camelcase = true, - uppercase = false, + uppercase = true, strip_digits = false, }, names_custom = function() @@ -46,6 +54,7 @@ local opts = { virtualtext = "■", virtualtext_inline = false, virtualtext_mode = "foreground", + lazy_load = true, always_update = false, }, } @@ -63,7 +72,9 @@ cyan magenta gold chartreuse lightgreen pink violet orange lightcoral lightcyan lemonchiffon papayawhip peachpuff blue gray lightblue gray100 white gold blue Blue LightBlue Gray100 White -White +Gray Gray Gray +gray100 gray20 gray30 +White white blue blue blue pink pink pink Names options: casing, strip digits deepskyblue deepskyblue1 @@ -71,11 +82,25 @@ DeepSkyBlue DeepSkyBlue2 DEEPSKYBLUE DEEPSKYBLUE3 Extra names: -oniViolet oniViolet2 crystalBlue springViolet1 springViolet2 springBlue -lightBlue waveAqua2 - -Custom names with non-alphanumeric characters -one_two three=four five@six seven!eight nine!!ten + oniViolet oniViolet2 crystalBlue springViolet1 springViolet2 springBlue + lightBlue waveAqua2 + +Custom names with non-alphanumeric characters: + one_two three=four five@six seven!eight nine!!ten + NOTE: TODO: WARN: FIX: . + NOTE: + NOTE: NOTE: + NOTE: NOTE: note + TODO: todo + TODO: TODO: . + TODO: TODO: todo + WARN: warn + WARN: WARN: warn + FIX: . + FIX: fix + +Tailwind names: + accent-blue-100 bg-gray-200 border-black border-x-zinc-300 border-y-yellow-400 border-t-teal-500 border-r-neutral-600 border-b-blue-700 border-l-lime-800 caret-indigo-900 decoration-sky-950 divide-white fill-violet-950 from-indigo-900 shadow-blue-800 stroke-sky-700 text-cyan-500 to-red-400 via-green-300 ring-emerald-200 ring-offset-violet-100 Hexadecimal: #RGB: diff --git a/test/minimal.lua b/test/minimal-colorizer.lua similarity index 95% rename from test/minimal.lua rename to test/minimal-colorizer.lua index 1ecac3e..1945832 100644 --- a/test/minimal.lua +++ b/test/minimal-colorizer.lua @@ -1,7 +1,7 @@ --- Run this file as `nvim --clean -u minimal.lua` +-- Run this file as `nvim --clean -u minimal-colorizer.lua` local settings = { - use_remote = true, -- Use colorizer master or local git directory + use_remote = false, -- Use colorizer master or local git directory base_dir = "colorizer_repro", -- Directory to clone lazy.nvim local_plugin_dir = os.getenv("HOME") .. "/git/nvim-colorizer.lua", -- Local git directory for colorizer. Used if use_remote is false expect = "expect.lua", @@ -100,5 +100,4 @@ lazy.setup(settings.plugins) require("colorizer").reload_on_save(settings.expect) vim.cmd.edit(settings.expect) - -- ADD INIT.LUA SETTINGS _NECESSARY_ FOR REPRODUCING THE ISSUE diff --git a/test/print-trie.lua b/test/print-trie.lua deleted file mode 100644 index af4461c..0000000 --- a/test/print-trie.lua +++ /dev/null @@ -1,59 +0,0 @@ ----@diagnostic disable: undefined-field -local Trie = require("colorizer.trie") - ----@diagnostic disable-next-line: unused-function, unused-local -local function print_color_trie() - local tohex = bit.tohex - local min, max = math.min, math.max - - local COLOR_NAME_SETTINGS = { - lowercase = false, - strip_digits = true, - } - ---@diagnostic disable-next-line: unused-local - local COLOR_MAP = {} - local COLOR_TRIE = Trie() - for k, v in pairs(vim.api.nvim_get_color_map()) do - if not (COLOR_NAME_SETTINGS.strip_digits and k:match("%d+$")) then - COLOR_NAME_MINLEN = COLOR_NAME_MINLEN and min(#k, COLOR_NAME_MINLEN) or #k - COLOR_NAME_MAXLEN = COLOR_NAME_MAXLEN and max(#k, COLOR_NAME_MAXLEN) or #k - ---@diagnostic disable-next-line: unused-local - COLOR_MAP[k] = tohex(v, 6) - ---@diagnostic disable-next-line: undefined-field - COLOR_TRIE:insert(k) - if COLOR_NAME_SETTINGS.lowercase then - local lowercase = k:lower() - ---@diagnostic disable-next-line: unused-local - COLOR_MAP[lowercase] = tohex(v, 6) - ---@diagnostic disable-next-line: undefined-field - COLOR_TRIE:insert(lowercase) - end - end - end - print(COLOR_TRIE) -end - -local trie = Trie({ - "cat", - "car", - "celtic", - "carb", - "carb0", - "CART0", - "CaRT0", - "Cart0", - "931", - "191", - "121", - "cardio", - "call", - "calcium", - "calciur", - "carry", - "dog", - "catdog", -}) - -print(trie) -print("catdo", trie:longest_prefix("catdo")) -print("catastrophic", trie:longest_prefix("catastrophic")) diff --git a/test/trie/benchmark.lua b/test/trie/benchmark.lua new file mode 100644 index 0000000..623657c --- /dev/null +++ b/test/trie/benchmark.lua @@ -0,0 +1,146 @@ +-- Run this file as `nvim --clean -u benchmark.lua` + +local opts = { + use_remote = true, +} +require("minimal").setup(opts) + +-- @diagnostic disable: undefined-field +local Trie = require("colorizer.trie") +local bit = require("bit") +local ffi = require("ffi") + +--- Generate random strings with specified count and length range. +---@param count number: number of strings to generate +---@param range table: min and max length of strings +---@return table: list of random strings +local function rand_strings(count, range) + local strings = {} + local char_pools = { + { 97, 122 }, -- 'a' to 'z' (lowercase) + { 65, 90 }, -- 'A' to 'Z' (uppercase) + { 48, 57 }, -- '0' to '9' (numbers) + { 33, 47 }, -- Special characters: '!' to '/' + { 58, 64 }, -- Special characters: ':' to '@' + { 91, 96 }, -- Special characters: '[' to '`' + { 123, 126 }, -- Special characters: '{' to '~' + } + + for _ = 1, count do + local length = math.random(range[1], range[2]) + local str = {} + for _ = 1, length do + local pool = char_pools[math.random(1, #char_pools)] + table.insert(str, string.char(math.random(pool[1], pool[2]))) + end + table.insert(strings, table.concat(str)) + end + + return strings +end + +ffi.cdef([[ + typedef struct timeval { + long tv_sec; + long tv_usec; + } timeval; + int gettimeofday(struct timeval* tv, void* tz); +]]) + +--- Get the current time in milliseconds. +-- @return number time in milliseconds +local function get_time_in_ms() + local tv = ffi.new("struct timeval") + ffi.C.gettimeofday(tv, nil) + ---@diagnostic disable-next-line: undefined-field + return tv.tv_sec * 1000 + tv.tv_usec / 1000 +end + +--- Benchmark Trie insertions and lookups. +---@param file file*: file handle for writing results +---@param data table: list of strings to insert and search +---@param description string: description of the dataset +local function benchmark_trie(file, data, description) + file:write(string.format("*** %s ***\n", description)) + file:write("Initial Capacity\tResize Count\tInsert Time (ms)\tLookup Time (ms)\n") + + local resizing = true + local shift_bit = 1 + + while resizing do + local initial_capacity = bit.lshift(1, shift_bit - 1) + local trie = Trie({}, { initial_capacity = initial_capacity }) + + -- Measure insertion time + local insert_start = get_time_in_ms() + for _, name in ipairs(data) do + ---@diagnostic disable-next-line: undefined-field + trie:insert(name, initial_capacity) + end + local insert_stop = get_time_in_ms() + + -- Measure lookup time + local lookup_start = get_time_in_ms() + for _, name in ipairs(data) do + ---@diagnostic disable-next-line: undefined-field + local _ = trie:search(name) -- Perform lookups + end + local lookup_stop = get_time_in_ms() + + ---@diagnostic disable-next-line: undefined-field + local resize_count = trie:resize_count() + file:write( + string.format( + "%d\t%d\t%d\t%d\n", + initial_capacity, + resize_count, + insert_stop - insert_start, + lookup_stop - lookup_start + ) + ) + + resizing = resize_count > 0 + shift_bit = shift_bit + 1 + end + + file:write("\n") +end + +local file = io.open("trie-benchmark.txt", "w") +if not file then + error("Failed to open file for writing") +end + +-- Benchmark with Vim color map and Tailwind data +local words = {} +for word in pairs(vim.api.nvim_get_color_map()) do + table.insert(words, word:lower()) + table.insert(words, word) + table.insert(words, word:upper()) +end +local tw_delimeter = "-" +local data = require("colorizer.data.tailwind_colors") +for name in pairs(data.colors) do + for _, prefix in ipairs(data.prefixes) do + table.insert(words, string.format("%s%s%s", prefix, tw_delimeter, name)) + end +end +benchmark_trie( + file, + words, + string.format( + "Inserting %d words: uppercase, lowercase, camelcase from vim.api.nvim_get_color_map() and Tailwind colors", + #words + ) +) + +-- Benchmark with random strings +local rs = 1000 +local rs_scale = 10 +while rs <= 100000 do + local strings = rand_strings(rs, { 3, 15 }) + benchmark_trie(file, strings, string.format("Inserting %d randomized words", #strings)) + rs = rs * rs_scale +end + +file:close() diff --git a/test/trie/minimal.lua b/test/trie/minimal.lua new file mode 100644 index 0000000..4b42863 --- /dev/null +++ b/test/trie/minimal.lua @@ -0,0 +1,65 @@ +local M = {} + +function M.setup(opts) + opts = opts or {} + opts = vim.tbl_extend("keep", opts, { + use_remote = true, + base_dir = "colorizer_trie", + local_plugin_dir = os.getenv("HOME") .. "/git/nvim-colorizer.lua", + plugins = {}, + }) + + if not vim.loop.fs_stat(opts.base_dir) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", + opts.base_dir, + }) + end + vim.opt.rtp:prepend(opts.base_dir) + + local function add_colorizer() + local base_config = { + event = "BufReadPre", + config = false, + } + if opts.use_remote then + table.insert( + opts.plugins, + vim.tbl_extend("force", base_config, { + "catgoose/nvim-colorizer.lua", + url = "https://github.com/catgoose/nvim-colorizer.lua", + }) + ) + else + local local_dir = opts.local_plugin_dir + if vim.fn.isdirectory(local_dir) == 1 then + vim.opt.rtp:append(local_dir) + table.insert( + opts.plugins, + vim.tbl_extend("force", base_config, { + dir = local_dir, + lazy = false, + }) + ) + else + vim.notify("Local plugin directory not found: " .. local_dir, vim.log.levels.ERROR) + end + end + end + + -- Initialize and setup lazy.nvim + local ok, lazy = pcall(require, "lazy") + if not ok then + vim.notify("Failed to require lazy.nvim", vim.log.levels.ERROR) + return + end + + add_colorizer() + lazy.setup(opts.plugins) +end + +return M diff --git a/test/trie/test.lua b/test/trie/test.lua new file mode 100644 index 0000000..9e0401d --- /dev/null +++ b/test/trie/test.lua @@ -0,0 +1,61 @@ +-- Run this file as `nvim --clean -u test.lua` + +local opts = { + use_remote = true, +} +require("minimal").setup(opts) + +local Trie = require("colorizer.trie") + +local file = io.open("trie-test.txt", "w") +if not file then + error("Failed to open file for appending") +end +local list = { + "cat", + "car", + "celtic", + "carb", + "carb0", + "CART0", + "CaRT0", + "Cart0", + "931", + "191", + "121", + "cardio", + "call", + "calcium", + "calciur", + "carry", + "dog", + "catdog", + " spaces ", + " catspace", + " dog", + "dogspace ", +} +local trie = Trie(list) +file:write("*** Testing trie with small list ***\n") +file:write(string.format("list: \n%s\n", vim.inspect(list))) +file:write(string.format("trie: \n%s\n", trie)) +file:write("checking longest prefix:\n") + +local function long_prefix(txt) + ---@diagnostic disable-next-line: undefined-field + file:write(string.format("'%s': '%s'\n", txt, trie:longest_prefix(txt) or nil)) +end +long_prefix("ffffff") +long_prefix("") +long_prefix("cat") +long_prefix("catastrophic") +long_prefix(" spaces ") +long_prefix(" spaces ") +long_prefix(" catspace") +long_prefix("catspace ") +long_prefix("dogspace ") +long_prefix(" dogspace") + +file:write("\n") + +file:close() diff --git a/test/trie/trie-benchmark.txt b/test/trie/trie-benchmark.txt new file mode 100644 index 0000000..0fc4324 --- /dev/null +++ b/test/trie/trie-benchmark.txt @@ -0,0 +1,43 @@ +*** Inserting 7245 words: uppercase, lowercase, camelcase from vim.api.nvim_get_color_map() and Tailwind colors *** +Initial Capacity Resize Count Insert Time (ms) Lookup Time (ms) +1 3652 13 3 +2 2056 7 2 +4 1174 7 4 +8 576 7 2 +16 23 7 3 +32 1 9 4 +64 0 24 10 + +*** Inserting 1000 randomized words *** +Initial Capacity Resize Count Insert Time (ms) Lookup Time (ms) +1 434 6 1 +2 234 2 1 +4 129 2 0 +8 51 1 0 +16 17 2 1 +32 3 2 1 +64 1 7 1 +128 0 8 1 + +*** Inserting 10000 randomized words *** +Initial Capacity Resize Count Insert Time (ms) Lookup Time (ms) +1 4614 16 4 +2 2106 11 4 +4 842 14 3 +8 362 19 14 +16 208 24 4 +32 113 21 6 +64 24 23 10 +128 0 37 9 + +*** Inserting 100000 randomized words *** +Initial Capacity Resize Count Insert Time (ms) Lookup Time (ms) +1 40656 133 71 +2 21367 130 70 +4 11604 152 68 +8 5549 141 73 +16 1954 155 88 +32 499 201 104 +64 100 251 117 +128 0 360 129 + diff --git a/test/trie/trie-test.txt b/test/trie/trie-test.txt new file mode 100644 index 0000000..2c28b5a --- /dev/null +++ b/test/trie/trie-test.txt @@ -0,0 +1,34 @@ +*** Testing trie with small list *** +list: +{ "cat", "car", "celtic", "carb", "carb0", "CART0", "CaRT0", "Cart0", "931", "191", "121", "cardio", "call", "calcium", "calciur", "carry", "dog", "catdog", " spaces ", " catspace", " dog", "dogspace " } +trie: +├─c─a─t*d─o─g* +│ │ ├─r*b*0* +│ │ │ ├─d─i─o* +│ │ │ └─r─y* +│ │ └─l─l* +│ │ └─c─i─u─m* +│ │ └─r* +│ └─e─l─t─i─c* +├─C─A─R─T─0* +│ └─a─R─T─0* +│ └─r─t─0* +├─9─3─1* +├─1─9─1* +│ └─2─1* +├─d─o─g*s─p─a─c─e─ * +│ ─s─p─a─c─e─s─ * +│ ├─c─a─t─s─p─a─c─e* +│ └─d─o─g* +checking longest prefix: +'ffffff': 'nil' +'': 'nil' +'cat': 'cat' +'catastrophic': 'cat' +' spaces ': ' spaces ' +' spaces ': ' spaces ' +' catspace': ' catspace' +'catspace ': 'cat' +'dogspace ': 'dogspace ' +' dogspace': ' dog' +