First version

This commit is contained in:
SepehrYahyaee
2026-04-13 13:41:22 +03:30
commit 8b25fb90bf
11 changed files with 826 additions and 0 deletions

374
lua/lgwtdark/go_extra.lua Normal file
View File

@@ -0,0 +1,374 @@
-- Extra highlights Tree-sitter cannot express alone:
-- 1) fmt verbs (%q, %s, ...) inside interpreted strings -> warm (same as numbers/params)
-- 2) rune literals: single token — paint ' delimiters and inner body
-- 3) interpreted " and raw ` delimiters white (TS anonymous quotes are unreliable)
-- 4) comment // and /* */ edge markers white
--
-- Note: pointer `*` vertical alignment vs letters is controlled by the font/terminal,
-- not Neovim highlights. Try a font with a centered asterisk (e.g. Iosevka, JetBrains Mono).
local M = {}
local ns = vim.api.nvim_create_namespace('lgwtdark_go')
-- package fmt / fmt.Errorf verbs (trailing character; %% is handled separately)
local FMT_VERB = {
['%'] = true,
b = true,
c = true,
d = true,
e = true,
E = true,
f = true,
F = true,
g = true,
G = true,
h = true,
o = true,
O = true,
p = true,
q = true,
s = true,
t = true,
T = true,
U = true,
v = true,
V = true,
w = true,
x = true,
X = true,
}
--- Return inclusive [start, end] byte indices in `s` for each fmt verb span (package fmt style).
local function fmt_verb_spans(s)
local spans = {}
local i = 1
local n = #s
while i <= n do
if s:byte(i) ~= 37 then -- %
i = i + 1
elseif i < n and s:byte(i + 1) == 37 then
i = i + 2 -- %%
else
local j = i + 1
if j <= n and s:byte(j) == 91 then -- %[n]
local close = s:find('%]', j + 1, true)
if not close then
break
end
j = close + 1
end
while j <= n do
local c = s:byte(j)
if c == 35 or c == 43 or c == 45 or c == 32 or c == 48 then -- # + - space 0
j = j + 1
else
break
end
end
if j <= n and s:byte(j) == 42 then
j = j + 1
else
while j <= n do
local c = s:byte(j)
if c >= 48 and c <= 57 then
j = j + 1
else
break
end
end
end
if j <= n and s:byte(j) == 46 then
j = j + 1
if j <= n and s:byte(j) == 42 then
j = j + 1
else
while j <= n do
local c = s:byte(j)
if c >= 48 and c <= 57 then
j = j + 1
else
break
end
end
end
end
if j <= n then
local ch = s:sub(j, j)
if FMT_VERB[ch] then
spans[#spans + 1] = { i, j }
i = j + 1
else
i = i + 1
end
else
i = i + 1
end
end
end
return spans
end
local function clear_buf(bufnr)
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
end
local function extmark_verb(bufnr, row, col_base, text, a, b)
local start_col = col_base + #text:sub(1, a - 1)
local end_col = col_base + #text:sub(1, b)
vim.api.nvim_buf_set_extmark(bufnr, ns, row, start_col, {
end_col = end_col,
hl_group = 'LgwtdFmtVerb',
priority = 200,
})
end
local function hl_interpreted_double_quotes(bufnr, node)
local text = vim.treesitter.get_node_text(node, bufnr) or ''
if #text < 2 or text:sub(1, 1) ~= '"' or text:sub(-1) ~= '"' then
return
end
local sr, sc, er, ec = node:range()
vim.api.nvim_buf_set_extmark(bufnr, ns, sr, sc, {
end_col = sc + 1,
hl_group = 'LgwtdQuote',
priority = 210,
})
vim.api.nvim_buf_set_extmark(bufnr, ns, er, ec - 1, {
end_col = ec,
hl_group = 'LgwtdQuote',
priority = 210,
})
end
local function hl_raw_string_delims(bufnr, node)
local text = vim.treesitter.get_node_text(node, bufnr) or ''
if #text < 2 or text:sub(1, 1) ~= '`' or text:sub(-1) ~= '`' then
return
end
local sr, sc, er, ec = node:range()
vim.api.nvim_buf_set_extmark(bufnr, ns, sr, sc, {
end_col = sc + 1,
hl_group = 'LgwtdQuote',
priority = 210,
})
vim.api.nvim_buf_set_extmark(bufnr, ns, er, ec - 1, {
end_col = ec,
hl_group = 'LgwtdQuote',
priority = 210,
})
end
local function hl_comment_prefix(bufnr, node)
local sr, sc, er, ec = node:range()
local text = vim.treesitter.get_node_text(node, bufnr) or ''
if text:sub(1, 2) == '//' then
vim.api.nvim_buf_set_extmark(bufnr, ns, sr, sc, {
end_col = sc + 2,
hl_group = 'LgwtdQuote',
priority = 208,
})
elseif text:sub(1, 2) == '/*' then
vim.api.nvim_buf_set_extmark(bufnr, ns, sr, sc, {
end_col = sc + 2,
hl_group = 'LgwtdQuote',
priority = 208,
})
if text:sub(-2) == '*/' then
vim.api.nvim_buf_set_extmark(bufnr, ns, er, ec - 2, {
end_col = ec,
hl_group = 'LgwtdQuote',
priority = 208,
})
end
end
end
local function hl_verbs_in_interpreted_string(bufnr, str_node)
for idx = 0, str_node:child_count() - 1 do
local ch = str_node:child(idx)
if ch and ch:type() == 'interpreted_string_literal_content' then
local text = vim.treesitter.get_node_text(ch, bufnr) or ''
local sr, sc, er, _ = ch:range()
if sr == er then
for _, span in ipairs(fmt_verb_spans(text)) do
extmark_verb(bufnr, sr, sc, text, span[1], span[2])
end
end
end
end
end
-- `nil` is bundled with iota as @constant.builtin; LSP can also tag it as keyword.
-- Extmarks beat both so it matches true/false (warm).
local function hl_nil_literal(bufnr, node)
local sr, sc, er, ec = node:range()
vim.api.nvim_buf_set_extmark(bufnr, ns, sr, sc, {
end_row = er,
end_col = ec,
hl_group = 'LgwtdNil',
priority = 10000,
})
end
local function hl_rune_literal(bufnr, node)
local text = vim.treesitter.get_node_text(node, bufnr) or ''
if #text < 2 or text:sub(1, 1) ~= "'" or text:sub(-1) ~= "'" then
return
end
local sr, sc, er, ec = node:range()
vim.api.nvim_buf_set_extmark(bufnr, ns, sr, sc, {
end_col = sc + 1,
hl_group = 'LgwtdQuote',
priority = 210,
})
local q0 = ec - 1
vim.api.nvim_buf_set_extmark(bufnr, ns, er, q0, {
end_col = ec,
hl_group = 'LgwtdQuote',
priority = 210,
})
if ec - sc <= 2 then
return
end
local inner = text:sub(2, -2)
local inner_hl = inner:match('^\\') and 'LgwtdEscape' or 'LgwtdStrInner'
vim.api.nvim_buf_set_extmark(bufnr, ns, sr, sc + 1, {
end_col = ec - 1,
hl_group = inner_hl,
priority = 190,
})
end
function M.refresh(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) or vim.bo[bufnr].filetype ~= 'go' then
return
end
clear_buf(bufnr)
local ok, parser = pcall(vim.treesitter.get_parser, bufnr, 'go')
if not ok or not parser then
return
end
parser:parse()
local trees = parser:trees()
local tree = trees and trees[1]
if not tree then
return
end
local root = tree:root()
local q = vim.treesitter.query.parse('go', '(interpreted_string_literal) @s')
local nlines = vim.api.nvim_buf_line_count(bufnr)
for _, node in q:iter_captures(root, bufnr, 0, nlines) do
if node:type() == 'interpreted_string_literal' then
hl_verbs_in_interpreted_string(bufnr, node)
hl_interpreted_double_quotes(bufnr, node)
end
end
local qraw = vim.treesitter.query.parse('go', '(raw_string_literal) @raw')
for _, node in qraw:iter_captures(root, bufnr, 0, nlines) do
if node:type() == 'raw_string_literal' then
hl_raw_string_delims(bufnr, node)
end
end
local qr = vim.treesitter.query.parse('go', '(rune_literal) @r')
for _, node in qr:iter_captures(root, bufnr, 0, nlines) do
if node:type() == 'rune_literal' then
hl_rune_literal(bufnr, node)
end
end
local qc = vim.treesitter.query.parse('go', '(comment) @c')
for _, node in qc:iter_captures(root, bufnr, 0, nlines) do
if node:type() == 'comment' then
hl_comment_prefix(bufnr, node)
end
end
local qnil = vim.treesitter.query.parse('go', '(nil) @n')
for _, node in qnil:iter_captures(root, bufnr, 0, nlines) do
if node:type() == 'nil' then
hl_nil_literal(bufnr, node)
end
end
end
local scheduled = {}
function M.schedule_refresh(bufnr)
if scheduled[bufnr] then
return
end
scheduled[bufnr] = true
vim.schedule(function()
scheduled[bufnr] = nil
M.refresh(bufnr)
end)
end
local function attach_parser_cbs(bufnr)
if vim.b[bufnr].lgwtd_go_ts_cbs then
return
end
local ok, parser = pcall(vim.treesitter.get_parser, bufnr, 'go')
if not ok or not parser or not parser.register_cbs then
return
end
vim.b[bufnr].lgwtd_go_ts_cbs = true
parser:register_cbs({
on_changed = function()
M.schedule_refresh(bufnr)
end,
})
end
local setup_done = false
function M.setup()
if setup_done then
return
end
setup_done = true
local aug = vim.api.nvim_create_augroup('LgwtdarkGoExtra', { clear = true })
vim.api.nvim_create_autocmd('FileType', {
group = aug,
pattern = 'go',
callback = function(ev)
M.attach(ev.buf)
end,
})
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
group = aug,
callback = function(ev)
if vim.bo[ev.buf].filetype == 'go' then
M.schedule_refresh(ev.buf)
end
end,
})
vim.api.nvim_create_autocmd('ColorScheme', {
group = aug,
pattern = '*',
callback = function()
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[buf].filetype == 'go' then
M.schedule_refresh(buf)
end
end
end,
})
end
function M.attach(bufnr)
if not pcall(vim.treesitter.start, bufnr, 'go') then
if vim.g.lgwtd_go_parser_warned == nil then
vim.g.lgwtd_go_parser_warned = true
vim.notify(
'Go Tree-sitter parser is missing. Run :Lazy sync then :TSInstall go',
vim.log.levels.WARN,
{ title = 'lgwtdark' }
)
end
return
end
M.refresh(bufnr)
attach_parser_cbs(bufnr)
end
return M

28
lua/plugins/init.lua Normal file
View File

@@ -0,0 +1,28 @@
-- nvim-treesitter: accurate syntax (your colorscheme maps @highlight groups).
-- We pin `master`: the default `main` branch is a rewrite (different API, Nvim 0.12+).
return {
{
'nvim-treesitter/nvim-treesitter',
branch = 'master',
lazy = false,
build = ':TSUpdate',
opts = {
ensure_installed = {
'go',
'lua',
'vim',
'vimdoc',
'query',
},
auto_install = true,
highlight = {
enable = true,
additional_vim_regex_highlighting = false,
},
indent = { enable = true },
},
config = function(_, opts)
require('nvim-treesitter.configs').setup(opts)
end,
},
}

23
lua/user/cursor.lua Normal file
View File

@@ -0,0 +1,23 @@
-- Terminal cursor shapes (Vim t_SI / t_EI / t_SR). Many Neovim builds no longer define these; pcall skips safely.
local esc = '\27'
local function set_termcap(name, value)
pcall(function()
vim.o[name] = value
end)
end
if vim.env.TMUX then
set_termcap('t_SI', esc .. 'Ptmux;' .. esc .. esc .. '[6 q' .. esc .. '\\')
set_termcap('t_EI', esc .. 'Ptmux;' .. esc .. esc .. '[2 q' .. esc .. '\\')
else
set_termcap('t_SI', esc .. '[6 q') -- insert: bar
set_termcap('t_EI', esc .. '[2 q') -- normal: block
end
set_termcap('t_SR', esc .. '[4 q') -- replace: underline
-- Modern Neovim uses guicursor for mode shapes (also in many terminals/UIs).
-- Keep operator-pending block so d/c don't look like replace mode.
vim.opt.guicursor = 'n-v-c-sm:block,i-ci-ve:ver25,r-cr:hor20,o:block'

81
lua/user/keymaps.lua Normal file
View File

@@ -0,0 +1,81 @@
-- WASD-style layout (ported from Vim). Uses same keys as your vimrc; 'x' = visual (vnoremap).
local function n(lhs, rhs, opts)
vim.keymap.set('n', lhs, rhs, opts or {})
end
local function x(lhs, rhs)
vim.keymap.set('x', lhs, rhs)
end
local function o(lhs, rhs, opts)
vim.keymap.set('o', lhs, rhs, opts or {})
end
-- Movement (normal, visual, operator-pending)
for _, lhs in ipairs({ 'w', 's', 'a', 'd' }) do
local rhs = ({ w = 'k', s = 'j', a = 'h', d = 'l' })[lhs]
n(lhs, rhs)
x(lhs, rhs)
o(lhs, rhs)
end
-- Word motions (normal + operator; visual keeps native w/W/e/E on i,I,j,J now remapped)
for _, mode in ipairs({ 'n', 'o' }) do
local opts = mode == 'o' and { nowait = true } or {}
vim.keymap.set(mode, 'i', 'w', opts)
vim.keymap.set(mode, 'I', 'W', opts)
vim.keymap.set(mode, 'j', 'e', opts)
vim.keymap.set(mode, 'J', 'E', opts)
end
-- Operator-pending fixes so combos behave as expected:
-- cw -> ci, ce -> cj, and rk does nothing.
o('w', 'i', { nowait = true })
o('e', 'j', { nowait = true })
o('k', '<Nop>', { nowait = true })
-- Insert / append
n('e', 'i')
n('E', 'a')
n('D', 'A')
n('A', 'I')
-- Operators
n('r', 'd', { nowait = true })
n('R', 'D')
o('r', 'd', { nowait = true })
n('c', 'c')
o('c', 'c')
n('f', 's')
n('F', 'S')
n('t', 'r')
n('T', 'R')
-- Open lines
n('W', 'O')
n('S', 'o')
-- Visual mode entry from normal
n('H', 'V')
n('h', '<Nop>')
-- Find / motion
n('l', 'f')
n('L', 'F')
n('p', 't')
n('P', 'T')
o('l', 'f', { nowait = true })
o('L', 'F', { nowait = true })
o('p', 't', { nowait = true })
o('P', 'T', { nowait = true })
-- Paste
n('v', 'p')
n('V', 'P')
n('o', 'p')
n('K', '<Nop>')
n('k', '<Nop>')
n('O', '<Nop>')
x('O', '<Nop>')
o('O', '<Nop>')

15
lua/user/options.lua Normal file
View File

@@ -0,0 +1,15 @@
-- Line numbers
vim.opt.number = true
vim.opt.relativenumber = true
-- Indent width4 (spaces); new lines inherit current indent
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.softtabstop = -1 -- follow shiftwidth (Neovim)
vim.opt.autoindent = true
vim.opt.copyindent = true
vim.opt.smartindent = true -- pressing Enter after `{` indents the next line
vim.cmd('filetype plugin indent on')