commit 8b25fb90bf2474d41c9c028d98c78eb589d0a407 Author: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon Apr 13 13:41:22 2026 +0330 First version diff --git a/after/ftplugin/go.lua b/after/ftplugin/go.lua new file mode 100644 index 0000000..96cb999 --- /dev/null +++ b/after/ftplugin/go.lua @@ -0,0 +1,3 @@ +local go = require('lgwtdark.go_extra') +go.setup() +go.attach(vim.api.nvim_get_current_buf()) diff --git a/after/queries/go/highlights.scm b/after/queries/go/highlights.scm new file mode 100644 index 0000000..a5d255b --- /dev/null +++ b/after/queries/go/highlights.scm @@ -0,0 +1,31 @@ +;; lgwtdark: split string bodies vs delimiters (delim white via extmarks in go_extra.lua). +;; Lower parent literal ranges so inner captures win. + +((interpreted_string_literal) @string + (#set! priority 95)) + +((interpreted_string_literal_content) @string + (#set! priority 115)) + +((escape_sequence) @string.escape + (#set! priority 115)) + +((raw_string_literal) @string + (#set! priority 95)) + +((raw_string_literal_content) @string + (#set! priority 115)) + +;; rune 'x' is one token — quotes/inner in go_extra.lua +((rune_literal) @string + (#set! priority 88)) + +;; Site: nil / booleans read as values (warm), not like keywords. iota stays default @constant.builtin. +((nil) @number + (#set! priority 128)) + +((true) @number + (#set! priority 128)) + +((false) @number + (#set! priority 128)) diff --git a/colors/lgwtdark.lua b/colors/lgwtdark.lua new file mode 100644 index 0000000..45396a4 --- /dev/null +++ b/colors/lgwtdark.lua @@ -0,0 +1,234 @@ +-- Learn Go With Tests (dark) - palette from site CSS, dark-mode token colors. +-- Requires: termguicolors. For Go, nvim-treesitter is strongly recommended. + +vim.cmd.hi('clear') +vim.g.colors_name = 'lgwtdark' + +if vim.fn.exists('syntax_on') == 1 then + vim.cmd.syntax('reset') +end + +vim.o.termguicolors = true +vim.o.background = 'dark' + +-- Dark-mode values from your spec (second branch of light-dark / --shiki-dark). +local C = { + -- primary-2: code panels on the site read slightly cooler than primary-1; warm text pops closer to the book. + bg = '#202325', + keyword = '#f8a8a1', -- danger-11 + string_ = '#9bd0a1', -- success-11 + entity = '#a0c6e0', -- primary-11 (packages, types, func/method names & calls) + -- warning-11 nudged warmer (more R, less B) so it matches the orange-gold on the page next to this bg + warm = '#e5b899', + fg = '#feffff', -- tint-12 (variables, brackets, delimiters) + comment = '#787878', -- neutral-9 + operator = '#bdc1c3', -- tint-11 + -- subtle UI derived from primary scale + cursorline = '#262d32', -- primary-3 + visual = '#34495a', -- between primary-4 / 5 + linenr = '#525252', -- neutral-7 + float_bg = '#222222', +} + +local function hi(name, opts) + vim.api.nvim_set_hl(0, name, opts) +end + +-- Editor chrome +hi('Normal', { fg = C.fg, bg = C.bg }) +hi('NormalNC', { fg = C.fg, bg = C.bg }) +hi('NormalFloat', { fg = C.fg, bg = C.float_bg }) +hi('Cursor', { fg = C.bg, bg = C.entity }) +hi('lCursor', { link = 'Cursor' }) +hi('CursorLine', { bg = C.cursorline }) +hi('CursorColumn', { bg = C.cursorline }) +hi('LineNr', { fg = C.linenr, bg = C.bg }) +hi('CursorLineNr', { fg = C.warm, bg = C.cursorline }) +hi('SignColumn', { bg = C.bg }) +hi('Folded', { fg = C.comment, bg = '#242424' }) +hi('FoldColumn', { fg = C.comment, bg = C.bg }) +hi('Whitespace', { fg = '#333333' }) +hi('WinSeparator', { fg = '#2a2a2a', bg = C.bg }) +hi('EndOfBuffer', { fg = C.bg, bg = C.bg }) + +hi('Search', { fg = C.bg, bg = C.warm }) +hi('IncSearch', { fg = C.bg, bg = C.keyword }) +hi('Visual', { bg = C.visual }) +hi('VisualNOS', { bg = C.visual }) + +hi('StatusLine', { fg = C.fg, bg = '#333333' }) +hi('StatusLineNC', { fg = C.comment, bg = '#252525' }) +hi('TabLine', { fg = C.comment, bg = '#252525' }) +hi('TabLineFill', { bg = C.bg }) +hi('TabLineSel', { fg = C.entity, bg = '#333333' }) + +hi('Error', { fg = '#fb2c36', bold = true }) +hi('WarningMsg', { fg = '#fe9a00' }) +hi('MatchParen', { fg = C.keyword, bg = '#3a3a3a', bold = true }) + +hi('Pmenu', { fg = C.fg, bg = '#2d2d2d' }) +hi('PmenuSel', { fg = C.bg, bg = C.entity }) +hi('PmenuSbar', { bg = '#444444' }) +hi('PmenuThumb', { bg = '#666666' }) + +hi('Title', { fg = C.entity, bold = true }) +hi('Directory', { fg = C.entity }) + +-- Builtin highlight groups (legacy syntax / fallbacks) +hi('Comment', { fg = C.comment }) +hi('String', { fg = C.string_ }) +hi('Character', { fg = C.string_ }) +hi('Number', { fg = C.warm }) +hi('Float', { fg = C.warm }) +hi('Boolean', { fg = C.warm }) +hi('Constant', { fg = C.warm }) + +hi('Identifier', { fg = C.fg }) +hi('Function', { fg = C.entity }) +hi('Statement', { fg = C.keyword }) +hi('Conditional', { fg = C.keyword }) +hi('Repeat', { fg = C.keyword }) +hi('Label', { fg = C.keyword }) +hi('Operator', { fg = C.operator }) +hi('Keyword', { fg = C.keyword }) +hi('Exception', { fg = C.keyword }) +hi('PreProc', { fg = C.keyword }) +hi('Include', { fg = C.keyword }) +hi('Define', { fg = C.keyword }) +hi('Macro', { fg = C.entity }) +hi('Type', { fg = C.entity }) +hi('StorageClass', { fg = C.keyword }) +hi('Structure', { fg = C.keyword }) +hi('Special', { fg = C.warm }) +hi('SpecialChar', { fg = C.warm }) +hi('Delimiter', { fg = C.fg }) +hi('Underlined', { fg = C.entity, underline = true }) +hi('Ignore', { fg = C.bg }) +hi('Todo', { fg = C.warm, bold = true }) + +-- Treesitter (Go and general) +local ts = { + ['@comment'] = { fg = C.comment }, + ['@comment.documentation'] = { fg = C.comment }, + + ['@keyword'] = { fg = C.keyword }, + ['@keyword.function'] = { fg = C.keyword }, + ['@keyword.operator'] = { fg = C.keyword }, + ['@keyword.return'] = { fg = C.keyword }, + ['@keyword.import'] = { fg = C.keyword }, + ['@keyword.exception'] = { fg = C.keyword }, + ['@keyword.coroutine'] = { fg = C.keyword }, + ['@keyword.directive'] = { fg = C.keyword }, + ['@keyword.directive.define'] = { fg = C.keyword }, + + ['@type.builtin'] = { fg = C.keyword }, + + ['@string'] = { fg = C.string_ }, + ['@string.documentation'] = { fg = C.string_ }, + ['@string.regex'] = { fg = C.string_ }, + ['@string.escape'] = { fg = C.warm }, + ['@string.special'] = { fg = C.warm }, + ['@string.special.url'] = { fg = C.entity, underline = true }, + ['@character'] = { fg = C.string_ }, + ['@character.special'] = { fg = C.warm }, + + ['@number'] = { fg = C.warm }, + ['@float'] = { fg = C.warm }, + ['@boolean'] = { fg = C.warm }, + + ['@function'] = { fg = C.entity }, + ['@function.call'] = { fg = C.entity }, + ['@function.builtin'] = { fg = C.entity }, + ['@function.macro'] = { fg = C.entity }, + + ['@method'] = { fg = C.entity }, + ['@method.call'] = { fg = C.entity }, + + ['@constructor'] = { fg = C.entity }, + + ['@type'] = { fg = C.entity }, + ['@type.definition'] = { fg = C.entity }, + + ['@namespace'] = { fg = C.entity }, + ['@module'] = { fg = C.entity }, + + ['@variable'] = { fg = C.fg }, + ['@variable.builtin'] = { fg = C.keyword }, + ['@variable.parameter'] = { fg = C.warm }, + ['@variable.member'] = { fg = C.fg }, + + ['@parameter'] = { fg = C.warm }, + + ['@constant'] = { fg = C.warm }, + ['@constant.builtin'] = { fg = C.keyword }, + ['@constant.macro'] = { fg = C.entity }, + + ['@property'] = { fg = C.fg }, + ['@field'] = { fg = C.fg }, + + ['@operator'] = { fg = C.operator }, + + ['@punctuation.delimiter'] = { fg = C.fg }, + ['@punctuation.bracket'] = { fg = C.fg }, + ['@punctuation.special'] = { fg = C.fg }, + + ['@label'] = { fg = C.warm }, + ['@exception'] = { fg = C.keyword }, + ['@include'] = { fg = C.keyword }, + ['@repeat'] = { fg = C.keyword }, + ['@conditional'] = { fg = C.keyword }, + + ['@tag'] = { fg = C.entity }, + ['@tag.attribute'] = { fg = C.warm }, + ['@tag.delimiter'] = { fg = C.fg }, + + ['@markup.link'] = { fg = C.entity, underline = true }, + ['@markup.link.url'] = { fg = C.entity, underline = true }, + ['@markup.strong'] = { bold = true }, + ['@markup.italic'] = { italic = true }, +} + +for g, o in pairs(ts) do + hi(g, o) +end + +-- Diagnostics (readable on this bg) +hi('DiagnosticError', { fg = '#fb2c36' }) +hi('DiagnosticWarn', { fg = '#fe9a00' }) +hi('DiagnosticInfo', { fg = C.entity }) +hi('DiagnosticHint', { fg = C.comment }) +hi('DiagnosticUnderlineError', { sp = '#fb2c36', undercurl = true }) +hi('DiagnosticUnderlineWarn', { sp = '#fe9a00', undercurl = true }) +hi('DiagnosticUnderlineInfo', { sp = C.entity, undercurl = true }) +hi('DiagnosticUnderlineHint', { sp = C.comment, undercurl = true }) + +-- LSP semantic tokens → same story as Treesitter where possible +hi('@lsp.type.namespace', { link = '@module' }) +hi('@lsp.type.type', { link = '@type' }) +hi('@lsp.type.class', { link = '@type' }) +hi('@lsp.type.enum', { link = '@type' }) +hi('@lsp.type.interface', { link = '@type' }) +hi('@lsp.type.struct', { link = '@type' }) +hi('@lsp.type.typeParameter', { link = '@type' }) +hi('@lsp.type.function', { link = '@function' }) +hi('@lsp.type.method', { link = '@method' }) +hi('@lsp.type.property', { link = '@property' }) +hi('@lsp.type.variable', { link = '@variable' }) +hi('@lsp.type.parameter', { link = '@parameter' }) +hi('@lsp.type.enumMember', { link = '@constant' }) +hi('@lsp.type.decorator', { link = '@function.macro' }) +hi('@lsp.type.macro', { link = '@function.macro' }) +hi('@lsp.type.keyword', { link = '@keyword' }) +hi('@lsp.type.operator', { link = '@operator' }) +hi('@lsp.type.string', { link = '@string' }) +hi('@lsp.type.number', { link = '@number' }) +hi('@lsp.type.regexp', { link = '@string.regex' }) +hi('@lsp.mod.readonly', { link = '@constant' }) +hi('@lsp.mod.defaultLibrary', { link = '@type.builtin' }) + +-- Extmarks from lua/lgwtdark/go_extra.lua (fmt verbs, rune delimiters) +hi('LgwtdFmtVerb', { fg = C.warm }) +hi('LgwtdQuote', { fg = C.fg }) +hi('LgwtdStrInner', { fg = C.string_ }) +hi('LgwtdEscape', { fg = C.warm }) +hi('LgwtdNil', { link = '@number' }) diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..b1590a5 --- /dev/null +++ b/init.lua @@ -0,0 +1,29 @@ +vim.g.mapleader = ' ' +vim.g.maplocalleader = ' ' + +require('user.cursor') +require('user.keymaps') +require('user.options') + +vim.opt.termguicolors = true + +-- Plugin manager: https://github.com/folke/lazy.nvim +local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' +if not (vim.uv or vim.loop).fs_stat(lazypath) then + vim.fn.system({ + 'git', + 'clone', + '--filter=blob:none', + 'https://github.com/folke/lazy.nvim.git', + '--branch=stable', + lazypath, + }) +end +vim.opt.rtp:prepend(lazypath) + +require('lazy').setup('plugins', { + install = { colorscheme = { 'lgwtdark', 'habamax' } }, + checker = { enabled = false }, +}) + +vim.cmd.colorscheme('lgwtdark') diff --git a/lazy-lock.json b/lazy-lock.json new file mode 100644 index 0000000..d17c595 --- /dev/null +++ b/lazy-lock.json @@ -0,0 +1,4 @@ +{ + "lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" }, + "nvim-treesitter": { "branch": "master", "commit": "cf12346a3414fa1b06af75c79faebe7f76df080a" } +} diff --git a/lua/lgwtdark/go_extra.lua b/lua/lgwtdark/go_extra.lua new file mode 100644 index 0000000..3261e3d --- /dev/null +++ b/lua/lgwtdark/go_extra.lua @@ -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 diff --git a/lua/plugins/init.lua b/lua/plugins/init.lua new file mode 100644 index 0000000..075b4c9 --- /dev/null +++ b/lua/plugins/init.lua @@ -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, + }, +} diff --git a/lua/user/cursor.lua b/lua/user/cursor.lua new file mode 100644 index 0000000..a6327a2 --- /dev/null +++ b/lua/user/cursor.lua @@ -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' diff --git a/lua/user/keymaps.lua b/lua/user/keymaps.lua new file mode 100644 index 0000000..311d777 --- /dev/null +++ b/lua/user/keymaps.lua @@ -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', '', { 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', '') + +-- 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', '') +n('k', '') +n('O', '') +x('O', '') +o('O', '') diff --git a/lua/user/options.lua b/lua/user/options.lua new file mode 100644 index 0000000..85799ff --- /dev/null +++ b/lua/user/options.lua @@ -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') diff --git a/scratch_go.go b/scratch_go.go new file mode 100644 index 0000000..a872c23 --- /dev/null +++ b/scratch_go.go @@ -0,0 +1,4 @@ +package main +func main() { + t.Errorf("got %q want %q", got, want) +}