Blink Pairs

repository·main·Indexed 43 days ago

https://github.com/saghen/blink.pairs

An intelligent auto-pairing plugin for Neovim featuring rainbow highlighting, indent-aware delimiter matching, and a high-performance internal parser for large files. Includes a custom Matcher trait and procedural macros for defining token matchers.

Tokens
7.2K
Snippets
21
Records
30
Agent score
55%

What's inside Blink Pairs

  1. Install Blink Pairs

    main

    Blink Pairs requires saghen/blink.lib as a dependency. You must either download prebuilt binaries or build from source.

    Note: If downloading prebuilt binaries, you must be using a versioned release (not a commit hash).

    {
      'saghen/blink.pairs',
      dependencies = 'saghen/blink.lib',
    
      version = '*',
      -- download prebuilt binaries from github releases, must be on a versioned release
      build = function() require('blink.pairs').download():pwait(60000) end,
      -- OR build from source
      -- build = function() require('blink.pairs').build():pwait(60000) end,
    
      --- @module 'blink.pairs'
      --- @type blink.pairs.Config
      opts = {
        -- ... configuration ...
      }
    }
  2. Setup Blink Pairs with vim.pack

    main

    If you are using vim.pack to manage your plugins, you can add Blink Pairs and its dependency, then trigger the build/download process manually.

    vim.pack.add({
      'https://github.com/saghen/blink.lib',
      { src = 'https://github.com/saghen/blink.pairs', version = vim.version.range('*') },
    })
    
    -- download prebuilt binaries from github releases
    require('blink.pairs').download():pwait(60000)
    -- OR build from source
    -- require('blink.pairs').build():pwait(60000)
    
    require('blink.pairs').setup()
  3. Configure Blink Pairs highlights

    main

    Blink Pairs provides rainbow highlighting and matching pair detection.

    Requirement: For highlights to work, you must have enabled the new Neovim UI2 core via require('vim._core.ui2').enable({}).

    Highlight Options:

    • enabled: Boolean to enable rainbow highlighting.
    • cmdline: Boolean to enable highlighting in the command line.
    • groups: A list of highlight groups for the rainbow effect (e.g., {'BlinkPairsOrange', 'BlinkPairsPurple', 'BlinkPairsBlue'}).
    • separate: A list of delimiters whose nesting is counted separately (e.g., {'<'}). Set to true to count every pair separately.
    • unmatched_group: The highlight group used for mismatched pairs.
    • matchparen: Configuration for highlighting matching pairs under the cursor.
      • enabled: Boolean.
      • cmdline: Boolean.
      • include_surrounding: If true, includes pairs surrounding the cursor, not just the one on top.
      • group: The highlight group for matchparen (e.g., 'BlinkPairsMatchParen').
      • priority: Integer priority for the highlight.
    opts = {
      highlights = {
        enabled = true,
        cmdline = true,
        groups = { 'BlinkPairsOrange', 'BlinkPairsPurple', 'BlinkPairsBlue' },
        separate = { '<' },
        unmatched_group = 'BlinkPairsUnmatched',
        matchparen = {
          enabled = true,
          cmdline = false,
          include_surrounding = false,
          group = 'BlinkPairsMatchParen',
          priority = 250,
        },
      },
    }
  4. Configure Blink Pairs mappings

    main

    The mappings table in the opts configuration allows you to control auto-pairing behavior and motion-based wrapping.

    Key Options:

    • enabled: Boolean to enable/disable mappings.
    • cmdline: Boolean to enable/disable mappings in the command line.
    • disabled_filetypes: A list of filetypes where mappings should be disabled.
    • wrap: Configures how to move pairs using motions or Treesitter.
      • ['<C-b>'] = 'motion': Move the closing pair via a motion.
      • ['<C-S-b>'] = 'motion_reverse': Move the opening pair via a motion.
      • Use 'treesitter' or 'treesitter_reverse' to use Treesitter instead of motions.
    • pairs: Custom pair definitions (refer to defaults for available keys).
    opts = {
      mappings = {
        enabled = true,
        cmdline = true,
        disabled_filetypes = {},
        wrap = {
          ['<C-b>'] = 'motion',
          ['<C-S-b>'] = 'motion_reverse',
        },
        pairs = {},
      },
    }
  5. Define a Matcher using MatcherDef macro syntax

    main

    The MatcherDef struct defines the syntax for creating custom matchers. When using the macro, you provide a name followed by a braced block containing various configuration sections. Each section is identified by a name and a colon, followed by its content wrapped in either brackets [] or braces {}.

    Supported Sections

    Section NameContent WrapperDescription
    delimiters[]Pairs of single-character strings (e.g., "(" -> ")")
    angle_brackets[]Pairs of single-character strings (e.g., "<" -> ">")
    line_comment[]List of single-character strings representing line comment starts
    block_comment[]Pairs of strings for block comment start/end (e.g., "/*" -> "*/")
    string[]List of strings that represent string delimiters
    char[]List of single-character strings representing character delimiters
    block_string[]Pairs of strings for block string start/end
    inline_span{}Named spans with start/end pairs (e.g., name: { "(" -> ")" })
    block_span{}Named spans with start/end pairs

    Syntax Rules

    • Delimiters/Brackets: Must use single characters. Format: "open" -> "close".
    • Spans: Use braces and a colon to map a name to a pair. Format: name: { "open" -> "close" }.
    • Lists: Sections like line_comment or string use comma-separated values inside brackets.
    // Example of the conceptual macro structure defined by MatcherDef
    my_matcher! {
        delimiters: ["(" -> ")", "[" -> "]"],
        line_comment: ["//", "#"],
        block_comment: ["/*" -> "*/"],
        inline_span: {
            comment: { "//" -> "\n" }
        }
    }
  6. Understand the Match and MatchWithLine types

    main

    The Match and MatchWithLine structs represent a detected pair or delimiter in the text. They are used to communicate the location and nature of a match to the consumer (e.g., a Lua script or a UI component).

    Match Fields

    • kind: The Kind of the match (e.g., Opening, Closing, or NonPair).
    • token: The static Token definition associated with this match.
    • col: The column position of the match.
    • depth: The nesting depth of the match (if applicable).
    • pair_depth: The depth specific to this particular pair, ignoring depth contributed by other types of pairs.

    MatchWithLine Fields

    MatchWithLine is an extension of Match that includes a line field, providing the specific line number where the match occurred.

    Lua Interop

    Both types implement IntoLua, meaning they can be converted into Lua tables. When converted to Lua, they follow this structure:

    • [1]: The opening token string.
    • [2]: The closing token string (if it exists).
    • span: (Optional) The span if the token is an InlineSpan or BlockSpan.
    • col: The column index.
    • line: (Only in MatchWithLine) The line index.
    • depth: The nesting depth.
    • pair_depth: The pair-specific depth (omitted if depth is None).
  7. Enable or disable mappings at runtime

    main

    You can programmatically control the availability of mappings using the following methods:

    1. Via the mappings module:

      • require("blink.pairs.mappings").enable()
      • require("blink.pairs.mappings").disable()
    2. Via Neovim variables (Global/Buffer):

      • To disable globally: vim.g.pairs = false or vim.g.blink_pairs = false.
      • To disable per-buffer: vim.b.pairs = false or vim.b.blink_pairs = false.
  8. Check if a filetype is supported

    main

    Use the supports_filetype function to determine if the parser has built-in support for a specific filetype string. This is useful for deciding whether to attempt tokenization or to fall back to a default behavior.

    supports_filetype("rust") // returns true
  9. Tokenize lines with `tokenize`

    main

    The tokenize function processes an iterator of byte slices (lines) and produces an iterator of TokenizedLine tuples. It tracks the parser State across multiple lines, allowing for multi-line constructs like block comments or strings.

    Each yielded TokenizedLine contains:

    1. Vec<Match>: A list of matches (delimiters, comments, etc.) found on that specific line.
    2. (u8, u8): The indentation of the line, represented as (tabs, spaces).
    3. State: The parser state at the end of the line, which determines how the next line is processed.

    To use this, you must provide an initial State and an implementation of the Matcher trait.

    // Example conceptual usage
    let lines = vec![b"fn foo<T>" as &[u8], b"{
    " as &[u8], b"}" as &[u8]];
    let matcher = MyMatcher::new();
    let tokenized_lines = tokenize(lines.into_iter(), State::Normal, matcher);
    
    for (matches, indent, next_state) in tokenized_lines {
        // Process matches and indentation
    }
  10. Manage buffer state with `remove_buffer` and `supports_filetype`

    main

    remove_buffer

    Clears the parsed data for a specific buffer from the internal cache. Call this when a buffer is deleted to prevent memory leaks.

    • Arguments: bufnr (number).

    supports_filetype

    Checks if the parser has logic to handle the given filetype.

    • Arguments: filetype (string).
    • Returns: boolean.
  11. Find unmatched or unterminated delimiters

    main

    Use these functions to identify syntax errors or incomplete pairs in the buffer.

    Unmatched Delimiters

    Used to find a delimiter that does not have a corresponding partner.

    • get_unmatched_opening_before(bufnr, opening, closing, row, col): Finds an unmatched opening delimiter before the cursor.
    • get_unmatched_closing_after(bufnr, opening, closing, row, col): Finds an unmatched closing delimiter after the cursor.

    Unterminated Delimiters

    Used to find an opening delimiter that hasn't been closed yet.

    • get_unterminated_opening_before(bufnr, opening, row, col): Finds an unterminated opening delimiter before the cursor.
    • get_unterminated_opening_after(bufnr, opening, row, col): Finds an unterminated opening delimiter after the cursor.
  12. Build match arms with MatchArm builder

    main

    The MatchArm struct provides a builder pattern to construct match arms for pattern matching against strings. It handles the generation of the match condition (including lookahead and adjacency checks) and the resulting code body.

    Builder Methods

    • builder(pattern: String, lookahead: usize): Initializes the builder. adjacent is automatically set to true if the pattern length is greater than 1.
    • input_state(input_state: TokenStream2): Sets the required State for this match arm.
    • non_adjacent(): Forces the adjacent flag to false, meaning the pattern characters do not need to be strictly next to each other.
    • ignore_escaped(): Sets the arm to ignore the escaped status of the token.
    • if_condition(if_condition: TokenStream2): Adds an additional boolean condition to the match arm.
    • body(body: TokenStream2): Defines the code block to execute when the pattern matches.
    • build(): Consumes the builder and returns the generated TokenStream2 code block.
    // Conceptual usage of MatchArm builder
    let arm = MatchArm::builder("//".to_string(), 3)
        .input_state(quote! { State::Normal })
        .body(quote! { /* code to execute */ })
        .build();