aboutsummaryrefslogtreecommitdiff
path: root/lua/colorizer.lua
blob: b1786d807c61129fad3150443b5acb77b1787478 (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
--- Requires Neovim >= 0.6.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_utils"

---Default namespace used in `colorizer.buffer_utils.highlight_buffer` and `attach_to_buffer`.
-- @see colorizer.buffer_utils.highlight_buffer
-- @see attach_to_buffer
local DEFAULT_NAMESPACE = buffer_utils.DEFAULT_NAMESPACE

local HIGHLIGHT_MODE_NAMES = buffer_utils.HIGHLIGHT_MODE_NAMES
local rehighlight_buffer = buffer_utils.rehighlight_buffer

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

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

-- 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.
--<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
--      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 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,
  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

--- 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 highlight_buffer
local function is_buffer_attached(buf)
  if buf == 0 or buf == nil then
    buf = current_buf()
  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
local function detach_from_buffer(buf, ns)
  buf = is_buffer_attached(buf)
  if not buf then
    return
  end

  clear_namespace(buf, ns or DEFAULT_NAMESPACE, 0, -1)
  if BUFFER_LOCAL[buf] then
    if BUFFER_LOCAL[buf].__tailwind_ns then
      clear_namespace(buf, BUFFER_LOCAL[buf].__tailwind_ns, 0, -1)
      if type(BUFFER_LOCAL[buf].__tailwind_detach) == "function" then
        BUFFER_LOCAL[buf].__tailwind_detach()
      end
    end

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

    BUFFER_LOCAL[buf].__autocmds = 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: Configuration options as described in `setup`
---@param typ string|nil: "buf" or "file" - The type of buffer option
local function attach_to_buffer(buf, options, typ)
  if buf == 0 or buf == nil then
    buf = current_buf()
  end

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

  if not 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 tailwind_ns, tailwind_detach = rehighlight_buffer(buf, options)
  BUFFER_LOCAL[buf].__tailwind_ns = tailwind_ns
  BUFFER_LOCAL[buf].__tailwind_detach = tailwind_detach

  BUFFER_LOCAL[buf].__init = true

  if BUFFER_LOCAL[buf].__autocmds then
    return
  end

  local autocmds = {}
  local au_group_id = AUGROUP_ID

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

  autocmds[#autocmds + 1] = autocmd({ "WinScrolled" }, {
    group = au_group_id,
    buffer = buf,
    callback = function()
      -- only reload if it was not disabled using detach_from_buffer
      if BUFFER_OPTIONS[buf] then
        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
        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()`
local function setup(config)
  if not vim.opt.termguicolors then
    vim.schedule(function()
      vim.notify("Colorizer: Error: &termguicolors must be set", "Error")
    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 = merge(USER_DEFAULT_OPTIONS, user_default_options),
  }

  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
        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
      attach_to_buffer(buf, options, typ)
    end
  end

  local au_group_id = augroup(AUGROUP_NAME, {})
  AUGROUP_ID = au_group_id

  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, "ErrorMsg")
          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 = au_group_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)), "ErrorMsg")
    end
  end

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

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

--- Return the currently active buffer options.
---@param buf number|nil: Buffer number
local function get_buffer_options(buf)
  if buf == 0 or buf == nil then
    buf = current_buf()
  end
  return merge({}, BUFFER_OPTIONS[buf])
end

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

--- Clear the highlight cache and reload all buffers.
local function clear_highlight_cache()
  HIGHLIGHT_CACHE = {}
  vim.schedule(reload_all_buffers)
end

--- @export
return {
  DEFAULT_NAMESPACE = DEFAULT_NAMESPACE,
  setup = setup,
  is_buffer_attached = is_buffer_attached,
  attach_to_buffer = attach_to_buffer,
  detach_from_buffer = detach_from_buffer,
  highlight_buffer = highlight_buffer,
  reload_all_buffers = reload_all_buffers,
  get_buffer_options = get_buffer_options,
  clear_highlight_cache = clear_highlight_cache,
}