aboutsummaryrefslogtreecommitdiff
path: root/lua/colorizer.lua
blob: 8da9ce02e8849a114530e4613165a52395a0753e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
--- Requires Neovim >= 0.7.0 and `set termguicolors`
--
--Highlights terminal CSI ANSI color codes.
-- @module colorizer
-- @author Ashkan Kiani <from-nvim-colorizer.lua@kiani.io>
-- @usage Establish the autocmd to highlight all filetypes.
--
--       `lua require 'colorizer'.setup()`
--
-- Highlight using all css highlight modes in every filetype
--
--       `lua require 'colorizer'.setup(user_default_options = { css = true; })`
--
--==============================================================================
--USE WITH COMMANDS                                          *colorizer-commands*
--
--   *:ColorizerAttachToBuffer*
--
--       Attach to the current buffer and start highlighting with the settings as
--       specified in setup (or the defaults).
--
--       If the buffer was already attached(i.e. being highlighted), the
--       settings will be reloaded with the ones from setup.
--       This is useful for reloading settings for just one buffer.
--
--   *:ColorizerDetachFromBuffer*
--
--       Stop highlighting the current buffer (detach).
--
--   *:ColorizerReloadAllBuffers*
--
--       Reload all buffers that are being highlighted currently.
--       Shortcut for ColorizerAttachToBuffer on every buffer.
--
--   *:ColorizerToggle*
--       Toggle highlighting of the current buffer.
--
--USE WITH LUA
--
--       All options that can be passed to user_default_options in `setup`
--       can be passed here. Can be empty too.
--       `0` is the buffer number here
--
--       Attach to current buffer <pre>
--           require("colorizer").attach_to_buffer(0, {
--             mode = "background",
--             css = false,
--           })
--</pre>
--       Detach from buffer <pre>
--           require("colorizer").detach_from_buffer(0, {
--             mode = "background",
--             css = false,
--           })
--</pre>
-- @see colorizer.setup
-- @see colorizer.attach_to_buffer
-- @see colorizer.detach_from_buffer

local buffer_utils = require "colorizer.buffer"
local clear_hl_cache = buffer_utils.clear_hl_cache
local rehighlight_buffer = buffer_utils.rehighlight

local utils = require "colorizer.utils"
local merge = utils.merge

local api = vim.api
local augroup = api.nvim_create_augroup
local autocmd = api.nvim_create_autocmd
local buf_get_option = api.nvim_buf_get_option
local clear_namespace = api.nvim_buf_clear_namespace
local current_buf = api.nvim_get_current_buf

local colorizer = {}

---Default namespace used in `colorizer.buffer.highlight` and `attach_to_buffer`.
-- @see colorizer.buffer.highlight
-- @see attach_to_buffer
colorizer.DEFAULT_NAMESPACE = buffer_utils.default_namespace

---Highlight the buffer region
---@function highlight_buffer
-- @see colorizer.buffer.highlight
colorizer.highlight_buffer = buffer_utils.highlight

-- USER FACING FUNCTIONALITY --
local AUGROUP_ID
local AUGROUP_NAME = "ColorizerSetup"
-- buffer specific options given in setup
local BUFFER_OPTIONS = {}
-- buffer local options created after setup
local BUFFER_LOCAL = {}

---defaults options.
--In `user_default_options`, there are 2 types of options
--
--1. Individual options - `names`, `RGB`, `RRGGBB`, `RRGGBBAA`, `hsl_fn`, `rgb_fn` , `RRGGBBAA`, `AARRGGBB`, `tailwind`, `sass`
--
--1. Alias options - `css`, `css_fn`
--
--If `css_fn` is true, then `hsl_fn`, `rgb_fn` becomes `true`
--
--If `css` is true, then `names`, `RGB`, `RRGGBB`, `RRGGBBAA`, `hsl_fn`, `rgb_fn` becomes `true`
--
--These options have a priority, Individual options have the highest priority, then alias options
--
--For alias, `css_fn` has more priority over `css`
--
--e.g: Here `RGB`, `RRGGBB`, `RRGGBBAA`, `hsl_fn`, `rgb_fn` is enabled but not `names`
--
--<pre>
--  require 'colorizer'.setup { user_default_options = { names = false, css = true } }
--</pre>
--
--e.g: Here `names`, `RGB`, `RRGGBB`, `RRGGBBAA` is enabled but not `rgb_fn` and `hsl_fn`
--
--<pre>
--  require 'colorizer'.setup { user_default_options = { css_fn = false, css = true } }
--</pre>
--
--<pre>
--  user_default_options = {
--      RGB = true, -- #RGB hex codes
--      RRGGBB = true, -- #RRGGBB hex codes
--      names = true, -- "Name" codes like Blue or blue
--      RRGGBBAA = false, -- #RRGGBBAA hex codes
--      AARRGGBB = false, -- 0xAARRGGBB hex codes
--      rgb_fn = false, -- CSS rgb() and rgba() functions
--      hsl_fn = false, -- CSS hsl() and hsla() functions
--      css = false, -- Enable all CSS features: rgb_fn, hsl_fn, names, RGB, RRGGBB
--      css_fn = false, -- Enable all CSS *functions*: rgb_fn, hsl_fn
--      -- Available modes for `mode`: foreground, background,  virtualtext
--      mode = "background", -- Set the display mode.
--      -- Available methods are false / true / "normal" / "lsp" / "both"
--      -- True is same as normal
--      tailwind = false, -- Enable tailwind colors
--      -- parsers can contain values used in |user_default_options|
--      sass = { enable = false, parsers = { css }, }, -- Enable sass colors
--      virtualtext = "■",
--  }
--</pre>
---@table user_default_options
--@field RGB boolean
--@field RRGGBB boolean
--@field names boolean
--@field RRGGBBAA boolean
--@field AARRGGBB boolean
--@field rgb_fn boolean
--@field hsl_fn boolean
--@field css boolean
--@field css_fn boolean
--@field mode string
--@field tailwind boolean|string
--@field sass table
--@field virtualtext string
local USER_DEFAULT_OPTIONS = {
  RGB = true,
  RRGGBB = true,
  names = true,
  RRGGBBAA = false,
  AARRGGBB = false,
  rgb_fn = false,
  hsl_fn = false,
  css = false,
  css_fn = false,
  mode = "background",
  tailwind = false,
  sass = { enable = false, parsers = { css = true } },
  virtualtext = "■",
}

local OPTIONS = { buf = {}, file = {} }
local SETUP_SETTINGS = {
  exclusions = { buf = {}, file = {} },
  all = { file = false, buf = false },
  default_options = USER_DEFAULT_OPTIONS,
}

--- Make new buffer Configuration
---@param buf number: buffer number
---@param typ string|nil: "buf" or "file" - The type of buffer option
---@return table
local function new_buffer_options(buf, typ)
  local value
  if typ == "buf" then
    value = buf_get_option(buf, "buftype")
  else
    value = buf_get_option(buf, "filetype")
  end
  return OPTIONS.file[value] or SETUP_SETTINGS.default_options
end

--- Parse buffer Configuration and convert aliases to normal values
---@param options table: options table
---@return table
local function parse_buffer_options(options)
  local includes = {
    ["css"] = { "names", "RGB", "RRGGBB", "RRGGBBAA", "hsl_fn", "rgb_fn" },
    ["css_fn"] = { "hsl_fn", "rgb_fn" },
  }
  local css_includes = { "names", "RGB", "RRGGBB", "RRGGBBAA", "hsl_fn", "rgb_fn" }
  local css_fn_includes = { "hsl_fn", "rgb_fn" }
  local default_opts = USER_DEFAULT_OPTIONS

  local function handle_alias(name, opts, d_opts)
    if not includes[name] then
      return
    end
    if opts == true or opts[name] == true then
      for _, child in ipairs(includes[name]) do
        d_opts[child] = true
      end
    elseif opts[name] == false then
      for _, child in ipairs(includes[name]) do
        d_opts[child] = false
      end
    end
  end
  -- https://github.com/NvChad/nvim-colorizer.lua/issues/48
  handle_alias("css", options, default_opts)
  handle_alias("css_fn", options, default_opts)

  if options.sass then
    if type(options.sass.parsers) == "table" then
      for child, _ in pairs(options.sass.parsers) do
        handle_alias(child, options.sass.parsers, default_opts.sass.parsers)
      end
    else
      options.sass.parsers = {}
      for child, _ in pairs(default_opts.sass.parsers) do
        handle_alias(child, true, options.sass.parsers)
      end
    end
  end

  options = merge(default_opts, options)
  return options
end

--- Check if attached to a buffer.
---@param buf number|nil: A value of 0 implies the current buffer.
---@return number|nil: if attached to the buffer, false otherwise.
---@see colorizer.buffer.highlight
function colorizer.is_buffer_attached(buf)
  if buf == 0 or buf == nil then
    buf = current_buf()
  else
    if not api.nvim_buf_is_valid(buf) then
      BUFFER_LOCAL[buf], BUFFER_OPTIONS[buf] = nil, nil
      return
    end
  end

  local au = api.nvim_get_autocmds {
    group = AUGROUP_ID,
    event = { "WinScrolled", "TextChanged", "TextChangedI", "TextChangedP" },
    buffer = buf,
  }
  if not BUFFER_OPTIONS[buf] or vim.tbl_isempty(au) then
    return
  end

  return buf
end

--- Stop highlighting the current buffer.
---@param buf number|nil: buf A value of 0 or nil implies the current buffer.
---@param ns number|nil: ns the namespace id, if not given DEFAULT_NAMESPACE is used
function colorizer.detach_from_buffer(buf, ns)
  buf = colorizer.is_buffer_attached(buf)
  if not buf then
    return
  end

  clear_namespace(buf, ns or colorizer.DEFAULT_NAMESPACE, 0, -1)
  if BUFFER_LOCAL[buf] then
    for _, namespace in pairs(BUFFER_LOCAL[buf].__detach.ns) do
      clear_namespace(buf, namespace, 0, -1)
    end

    for _, f in pairs(BUFFER_LOCAL[buf].__detach.functions) do
      if type(f) == "function" then
        f(buf)
      end
    end

    for _, id in ipairs(BUFFER_LOCAL[buf].__autocmds or {}) do
      pcall(api.nvim_del_autocmd, id)
    end

    BUFFER_LOCAL[buf].__autocmds = nil
    BUFFER_LOCAL[buf].__detach = nil
  end
  -- because now the buffer is not visible, so delete its information
  BUFFER_OPTIONS[buf] = nil
end

---Attach to a buffer and continuously highlight changes.
---@param buf integer: A value of 0 implies the current buffer.
---@param options table|nil: Configuration options as described in `setup`
---@param typ string|nil: "buf" or "file" - The type of buffer option
function colorizer.attach_to_buffer(buf, options, typ)
  if buf == 0 or buf == nil then
    buf = current_buf()
  else
    if not api.nvim_buf_is_valid(buf) then
      BUFFER_LOCAL[buf], BUFFER_OPTIONS[buf] = nil, nil
      return
    end
  end

  -- if the buffer is already attached then grab those options
  if not options then
    options = colorizer.get_buffer_options(buf)
  end

  -- if not make new options
  if not options then
    options = new_buffer_options(buf, typ)
  end

  options = parse_buffer_options(options)

  if not buffer_utils.highlight_mode_names[options.mode] then
    if options.mode ~= nil then
      local mode = options.mode
      vim.defer_fn(function()
        -- just notify the user once
        vim.notify_once(string.format("Warning: Invalid mode given to colorizer setup [ %s ]", mode))
      end, 0)
    end
    options.mode = "background"
  end

  BUFFER_OPTIONS[buf] = options

  BUFFER_LOCAL[buf] = BUFFER_LOCAL[buf] or {}
  local highlighted, returns = rehighlight_buffer(buf, options)

  if not highlighted then
    return
  end

  BUFFER_LOCAL[buf].__detach = BUFFER_LOCAL[buf].__detach or returns.detach

  BUFFER_LOCAL[buf].__init = true

  if BUFFER_LOCAL[buf].__autocmds then
    return
  end

  local autocmds = {}
  local au_group_id = AUGROUP_ID

  local text_changed_au = { "TextChanged", "TextChangedI", "TextChangedP" }
  -- only enable InsertLeave in sass, rest don't require it
  if options.sass and options.sass.enable then
    table.insert(text_changed_au, "InsertLeave")
  end

  autocmds[#autocmds + 1] = autocmd(text_changed_au, {
    group = au_group_id,
    buffer = buf,
    callback = function(args)
      -- only reload if it was not disabled using detach_from_buffer
      if BUFFER_OPTIONS[buf] then
        BUFFER_LOCAL[buf].__event = args.event
        if args.event == "TextChanged" or args.event == "InsertLeave" then
          rehighlight_buffer(buf, options, BUFFER_LOCAL[buf])
        else
          local pos = vim.fn.getpos "."
          BUFFER_LOCAL[buf].__startline = pos[2] - 1
          BUFFER_LOCAL[buf].__endline = pos[2]
          rehighlight_buffer(buf, options, BUFFER_LOCAL[buf], true)
        end
      end
    end,
  })

  autocmds[#autocmds + 1] = autocmd({ "WinScrolled" }, {
    group = au_group_id,
    buffer = buf,
    callback = function(args)
      -- only reload if it was not disabled using detach_from_buffer
      if BUFFER_OPTIONS[buf] then
        BUFFER_LOCAL[buf].__event = args.event
        rehighlight_buffer(buf, options, BUFFER_LOCAL[buf])
      end
    end,
  })

  autocmd({ "BufUnload", "BufDelete" }, {
    group = au_group_id,
    buffer = buf,
    callback = function()
      if BUFFER_OPTIONS[buf] then
        colorizer.detach_from_buffer(buf)
      end
      BUFFER_LOCAL[buf].__init = nil
    end,
  })

  BUFFER_LOCAL[buf].__autocmds = autocmds
  BUFFER_LOCAL[buf].__augroup_id = au_group_id
end

---Easy to use function if you want the full setup without fine grained control.
--Setup an autocmd which enables colorizing for the filetypes and options specified.
--
--By default highlights all FileTypes.
--
--Example config:~
--<pre>
--  { filetypes = { "css", "html" }, user_default_options = { names = true } }
--</pre>
--Setup with all the default options:~
--<pre>
--    require("colorizer").setup {
--      filetypes = { "*" },
--      user_default_options,
--      -- all the sub-options of filetypes apply to buftypes
--      buftypes = {},
--    }
--</pre>
--For all user_default_options, see |user_default_options|
---@param config table: Config containing above parameters.
---@usage `require'colorizer'.setup()`
function colorizer.setup(config)
  if not vim.opt.termguicolors then
    vim.schedule(function()
      vim.notify("Colorizer: Error: &termguicolors must be set", 4)
    end)
    return
  end

  local conf = vim.deepcopy(config) or {}

  -- if nothing given the enable for all filetypes
  local filetypes = conf.filetypes or conf[1] or { "*" }
  local user_default_options = conf.user_default_options or conf[2] or {}
  local buftypes = conf.buftypes or conf[3] or nil

  OPTIONS = { buf = {}, file = {} }
  SETUP_SETTINGS = {
    exclusions = { buf = {}, file = {} },
    all = { file = false, buf = false },
    default_options = user_default_options,
  }
  BUFFER_OPTIONS, BUFFER_LOCAL = {}, {}

  local function COLORIZER_SETUP_HOOK(typ)
    local filetype = vim.bo.filetype
    local buftype = vim.bo.buftype
    local buf = current_buf()
    BUFFER_LOCAL[buf] = BUFFER_LOCAL[buf] or {}

    if SETUP_SETTINGS.exclusions.file[filetype] or SETUP_SETTINGS.exclusions.buf[buftype] then
      -- when a filetype is disabled but buftype is enabled, it can Attach in
      -- some cases, so manually detach
      if BUFFER_OPTIONS[buf] then
        colorizer.detach_from_buffer(buf)
      end
      BUFFER_LOCAL[buf].__init = nil
      return
    end

    local fopts, bopts, options = OPTIONS[typ][filetype], OPTIONS[typ][buftype], nil
    if typ == "file" then
      options = fopts
      -- if buffer and filetype options both are given, then prefer fileoptions
    elseif fopts and bopts then
      options = fopts
    else
      options = bopts
    end

    if not options and not SETUP_SETTINGS.all[typ] then
      return
    end

    options = options or SETUP_SETTINGS.default_options

    -- this should ideally be triggered one time per buffer
    -- but BufWinEnter also triggers for split formation
    -- but we don't want that so add a check using local buffer variable
    if not BUFFER_LOCAL[buf].__init then
      colorizer.attach_to_buffer(buf, options, typ)
    end
  end

  AUGROUP_ID = augroup(AUGROUP_NAME, {})

  local aucmd = { buf = "BufWinEnter", file = "FileType" }
  local function parse_opts(typ, tbl)
    if type(tbl) == "table" then
      local list = {}

      for k, v in pairs(tbl) do
        local value
        local options = SETUP_SETTINGS.default_options
        if type(k) == "string" then
          value = k
          if type(v) ~= "table" then
            vim.notify("colorizer: Invalid option type for " .. typ .. "type" .. value, 4)
          else
            options = merge(SETUP_SETTINGS.default_options, v)
          end
        else
          value = v
        end
        -- Exclude
        if value:sub(1, 1) == "!" then
          SETUP_SETTINGS.exclusions[typ][value:sub(2)] = true
        else
          OPTIONS[typ][value] = options
          if value == "*" then
            SETUP_SETTINGS.all[typ] = true
          else
            table.insert(list, value)
          end
        end
      end
      autocmd({ aucmd[typ] }, {
        group = AUGROUP_ID,
        pattern = typ == "file" and (SETUP_SETTINGS.all[typ] and "*" or list) or nil,
        callback = function()
          COLORIZER_SETUP_HOOK(typ)
        end,
      })
    elseif tbl then
      vim.notify_once(string.format("colorizer: Invalid type for %stypes %s", typ, vim.inspect(tbl)), 4)
    end
  end

  parse_opts("file", filetypes)
  parse_opts("buf", buftypes)

  autocmd("ColorScheme", {
    group = AUGROUP_ID,
    callback = function()
      require("colorizer").clear_highlight_cache()
    end,
  })
end

--- Return the currently active buffer options.
---@param buf number|nil: Buffer number
---@return table|nil
function colorizer.get_buffer_options(buf)
  local buffer = colorizer.is_buffer_attached(buf)
  if buffer then
    return BUFFER_OPTIONS[buffer]
  end
end

--- Reload all of the currently active highlighted buffers.
function colorizer.reload_all_buffers()
  for buf, _ in pairs(BUFFER_OPTIONS) do
    colorizer.attach_to_buffer(buf, colorizer.get_buffer_options(buf))
  end
end

--- Clear the highlight cache and reload all buffers.
function colorizer.clear_highlight_cache()
  clear_hl_cache()
  vim.schedule(colorizer.reload_all_buffers)
end

return colorizer