Skip to content

crafts69guy/styled-components.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

17 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

styled-components.nvim

A best-in-class Neovim plugin providing native CSS LSP experience for styled-components using TreeSitter language injection. Match or exceed VS Code features with Neovim's native capabilities!

✨ Features

  • πŸš€ TreeSitter Injection: Native CSS syntax highlighting and LSP support in template literals
  • πŸ’‘ Full CSS LSP: Completions, hover documentation, and diagnostics from cssls
  • ⚑ Zero Overhead: Uses Neovim's built-in TreeSitter injection (no virtual buffers, no hacks)
  • 🎯 Auto-Setup: Automatically configures injection queries and cssls
  • πŸ“– Native Experience: Works exactly like editing a .css file
  • πŸ”§ Extensible: Supports styled, css, createGlobalStyle, and keyframes

πŸ—οΈ How It Works

This plugin uses TreeSitter language injection - the same approach VS Code uses, but better! When you type in a styled-component template:

const Button = styled.div`
  display: flex;
  ^^^^^^^^^^^^^^  ← TreeSitter marks this as CSS!
  align-items: center;
  ^^^^^^^^^^^^^^^^^^^^  ← cssls provides completions/hover/diagnostics!
`;

Architecture:

  1. Plugin installs TreeSitter injection queries
  2. Neovim TreeSitter automatically detects styled-component templates
  3. Injected CSS regions get native LSP support from cssls
  4. You get the same experience as editing a .css file!

No virtual buffers, no position mapping, no race conditions - just native Neovim features! πŸŽ‰

πŸ“¦ Requirements

Note: Neovim 0.11+ users can use the native vim.lsp.config API without nvim-lspconfig. The plugin automatically detects and uses the appropriate API.

Installing CSS Language Server

npm install -g vscode-langservers-extracted

This provides vscode-css-language-server with:

  • Full CSS property/value completions
  • Hover documentation
  • CSS validation and diagnostics
  • Syntax checking

πŸš€ Installation

Lazy.nvim (Recommended)

-- In your lazy.nvim plugin spec (e.g., ~/.config/nvim/lua/plugins/styled-components.lua)
return {
  -- styled-components.nvim: CSS in JS with TreeSitter injection
  {
    "crafts69guy/styled-components.nvim",
    dependencies = {
      "nvim-treesitter/nvim-treesitter",
      "neovim/nvim-lspconfig",  -- Optional for Neovim 0.11+
    },
    ft = { "typescript", "typescriptreact", "javascript", "javascriptreact" },
    opts = {
      enabled = true,
      debug = false,
      auto_setup = true,
    },
  },

  -- blink.cmp: Configure with styled-components integration
  {
    "saghen/blink.cmp",
    dependencies = { "crafts69guy/styled-components.nvim" },
    opts = function(_, opts)
      local styled = require("styled-components.blink")

      -- Ensure sources table exists
      opts.sources = opts.sources or {}
      opts.sources.default = opts.sources.default or { "lsp", "path", "snippets", "buffer" }
      opts.sources.providers = opts.sources.providers or {}

      -- Add styled-components to default sources
      table.insert(opts.sources.default, "styled-components")

      -- Configure LSP source to filter cssls completions
      opts.sources.providers.lsp = vim.tbl_deep_extend("force",
        opts.sources.providers.lsp or {},
        {
          override = {
            transform_items = styled.get_lsp_transform_items(),
          },
        }
      )

      -- Register styled-components completion source
      opts.sources.providers["styled-components"] = {
        name = "styled-components",
        module = "styled-components.completion",
        enabled = styled.enabled,
      }

      return opts
    end,
  },
}

Why this config?

  • ft: Lazy loads styled-components on TypeScript/JavaScript filetypes
  • Uses blink.cmp's official override API (stable, future-proof)
  • Filters cssls completions to ONLY appear in styled-component templates
  • Zero timing issues, no internal patching
  • Result: Reliable, maintainable, works perfectly with LazyVim!

Manual Setup (if not using lazy.nvim)

-- Setup styled-components
require("styled-components").setup({
  enabled = true,
  debug = false,
  auto_setup = true,

  -- Optional: Completion performance tuning
  completion = {
    cache_ttl_ms = 100,  -- Context detection cache TTL (ms)
  },

  -- Optional: custom cssls configuration
  cssls_config = {
    settings = {
      css = {
        validate = true,
        lint = {
          unknownAtRules = "ignore",
        },
      },
    },
  },
})

-- Configure blink.cmp integration
local styled = require("styled-components.blink")
require("blink.cmp").setup({
  sources = {
    default = { "lsp", "path", "snippets", "buffer", "styled-components" },
    providers = {
      lsp = {
        override = {
          transform_items = styled.get_lsp_transform_items(),
        },
      },
      ["styled-components"] = {
        name = "styled-components",
        module = "styled-components.completion",
        enabled = styled.enabled,
      },
    },
  },
})

πŸ“– Usage

Automatic (Recommended)

With auto_setup = true (default), the plugin automatically:

  1. βœ… Installs TreeSitter injection queries
  2. βœ… Configures cssls to work with TypeScript/JavaScript files
  3. βœ… Enables CSS completions, hover, and diagnostics in styled-components

Just start typing!

What Gets Injected

The plugin recognizes these styled-components patterns:

// βœ… styled.element
const Box = styled.div`
  display: flex;
`;

// βœ… styled(Component)
const StyledButton = styled(Button)`
  color: red;
`;

// βœ… css helper
import { css } from 'styled-components';
const styles = css`
  margin: 10px;
`;

// βœ… createGlobalStyle
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
  body { margin: 0; }
`;

// βœ… keyframes
import { keyframes } from 'styled-components';
const fadeIn = keyframes`
  from { opacity: 0; }
  to { opacity: 1; }
`;

LSP Features

In any styled-component template, you get:

Completions:

  • Type dis β†’ see display, display-inside, etc.
  • Type display: f β†’ see flex, flow-root, etc.
  • Full CSS property and value completions!

Hover Documentation:

  • Move cursor to any CSS property
  • Press K β†’ see MDN documentation!

Diagnostics:

  • Typo: colr: red; β†’ Error: Unknown property
  • Invalid: display: flexxx; β†’ Error: Invalid value

All powered by native cssls!

βš™οΈ Configuration

Default Configuration

{
  enabled = true,         -- Enable/disable the plugin
  debug = false,          -- Show debug messages
  auto_setup = true,      -- Auto-setup injection and cssls
  filetypes = {           -- Supported filetypes
    "typescript",
    "typescriptreact",
    "javascript",
    "javascriptreact",
  },
  cssls_config = {},      -- Custom cssls configuration (merged with defaults)

  -- Completion source performance options
  completion = {
    cache_ttl_ms = 100,   -- Context detection cache TTL (ms)
                          -- Higher = less overhead, but slightly stale detection
                          -- Lower = more responsive, but more TreeSitter queries
  },
}

-- Note: blink.cmp integration is now configured separately
-- See the "blink.cmp Integration" section above

blink.cmp Integration

This plugin provides a custom completion source for blink.cmp using the official Provider Override API.

Configuration

Add both plugins to your lazy.nvim config:

return {
  {
    "crafts69guy/styled-components.nvim",
    ft = { "typescript", "typescriptreact", "javascript", "javascriptreact" },
    opts = {},
  },
  {
    "saghen/blink.cmp",
    dependencies = { "crafts69guy/styled-components.nvim" },
    opts = function(_, opts)
      local styled = require("styled-components.blink")

      opts.sources = opts.sources or {}
      opts.sources.default = opts.sources.default or { "lsp", "path", "snippets", "buffer" }
      opts.sources.providers = opts.sources.providers or {}

      -- Add styled-components source
      table.insert(opts.sources.default, "styled-components")

      -- Filter cssls completions using override API
      opts.sources.providers.lsp = vim.tbl_deep_extend("force",
        opts.sources.providers.lsp or {},
        {
          override = {
            transform_items = styled.get_lsp_transform_items(),
          },
        }
      )

      -- Register styled-components provider
      opts.sources.providers["styled-components"] = {
        name = "styled-components",
        module = "styled-components.completion",
        enabled = styled.enabled,
      }

      return opts
    end,
  },
}

How It Works

The Problem:

  • styled-components.nvim configures cssls to attach to TypeScript/JavaScript files (required for TreeSitter injection)
  • blink.cmp's LSP source shows ALL completions from ALL attached LSP clients
  • Without filtering, users see CSS completions everywhere (React components, hooks, normal TypeScript code)

The Solution:

  • Uses blink.cmp's official override API to filter cssls completions
  • transform_items checks if cursor is inside TreeSitter-injected CSS region
  • CSS completions ONLY appear in styled-component templates
  • Outside templates, cssls completions are filtered out

Benefits:

  • βœ… Uses official, stable blink.cmp API (future-proof)
  • βœ… No internal patching or hacks
  • βœ… Zero timing issues
  • βœ… Transparent and easy to debug
  • βœ… Works perfectly with LazyVim

Performance Notes:

  • Context detection is cached (100ms TTL) to minimize overhead
  • Smart pattern verification prevents false positives
  • Only triggers inside styled-component templates
  • Typical overhead: ~0.1-5ms per completion request

Custom cssls Configuration

require("styled-components").setup({
  cssls_config = {
    settings = {
      css = {
        validate = true,
        lint = {
          unknownAtRules = "ignore",
          vendorPrefix = "warning",
        },
      },
    },
  },
})

Manual Setup (Advanced)

If you prefer manual control:

require("styled-components").setup({
  auto_setup = false,  -- Disable auto-setup
})

-- For Neovim 0.11+ (Native API):
vim.lsp.config.cssls = {
  cmd = { 'vscode-css-language-server', '--stdio' },
  root_markers = { 'package.json', '.git' },
  filetypes = { 'css', 'scss', 'less', 'typescript', 'typescriptreact', 'javascript', 'javascriptreact' },
}
vim.lsp.enable('cssls')

-- For Neovim 0.10.x (nvim-lspconfig):
require('lspconfig').cssls.setup({
  filetypes = { 'css', 'scss', 'less', 'typescript', 'typescriptreact', 'javascript', 'javascriptreact' },
})

πŸ› Debugging

Check Status

:lua require("styled-components").print_status()

This shows:

  • Is injection available?
  • Is injection active in current buffer?
  • Does buffer have styled-components import?
  • Current injected language at cursor
  • Full configuration

Common Issues

Plugin not loading / No completions:

  1. Check cssls is installed:

    :!which vscode-css-language-server
  2. Check LSP is attached:

    :LspInfo

    Should show cssls attached to .tsx files.

  3. Check injection is working:

    :lua print(require("styled-components").is_injection_working())
  4. Check you're in a styled-component: Place cursor in template literal and run:

    :lua require("styled-components").print_status()

TreeSitter errors:

Install parsers:

:TSInstall typescript tsx javascript
:TSUpdate

cssls not attaching:

Ensure you have nvim-lspconfig installed and loaded before this plugin.

LazyVim users: Error on startup about Snacks:

This is fixed in the latest version! The plugin now automatically loads queries on VimEnter to avoid timing issues.

If you're using an older version and have an init function in your config, you can remove it:

-- ❌ OLD (not needed anymore)
init = function()
  require("styled-components").load_queries_early()
end,

-- βœ… NEW (automatic)
-- Just use opts or config, no init needed!
opts = { debug = false }

The plugin handles timing automatically to work with UI plugins like Snacks, lualine, etc.

🎯 Performance

Metric Value
Completion latency (in CSS) ~5-15ms (LSP request)
Context detection (cached) ~0.1ms (cache hit)
Context detection (uncached) ~1-3ms (TreeSitter query)
Memory overhead ~1KB (small cache)
CPU overhead ~0% (efficient caching)
Startup time ~5ms (query installation)

Optimization Strategy:

  • βœ… Context Detection Caching: 100ms TTL cache prevents repeated TreeSitter queries
  • βœ… Smart Trigger Characters: Only CSS symbols (:, ;, -), not a-z
  • βœ… Fast Early Return: Exits immediately if not in styled-component template
  • βœ… Cache Cleanup: Automatic cleanup prevents memory leaks

Performance Impact:

Before optimization: 11 triggers Γ— 5ms = ~55ms overhead per line
After optimization:  2 triggers Γ— 0.1ms = ~0.2ms overhead per line
Improvement: 275x faster! πŸš€

Comparison with other approaches:

  • Virtual Buffers: ~50ms + 500ms init + bugs
  • Static Data: ~1ms but limited features
  • TreeSitter Injection + Smart Caching: ~0.1-5ms with full LSP features βœ…

πŸ“š How It Compares

VS Code styled-components Extension

Feature VS Code styled-components.nvim
Syntax Highlighting βœ… TextMate βœ… TreeSitter (better!)
CSS Completions βœ… typescript-plugin βœ… Native LSP
Hover Docs βœ… Yes βœ… Yes
Diagnostics βœ… Yes βœ… Yes
Performance ~1-5ms ~1-5ms
Architecture TypeScript plugin TreeSitter injection

Result: Feature parity or better! πŸŽ‰

🀝 Contributing

Contributions welcome! This plugin uses:

  • TreeSitter injection queries (in queries/ directory)
  • Neovim's native LSP client
  • No external dependencies (besides cssls)

πŸ“„ License

MIT

πŸ™ Credits

About

🎨 Neovim plugin providing CSS autocompletion for styled-components with blink.cmp

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •