From 97176508d3f48c246e9a1f5b3b0602e762b98caf Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Sat, 21 Feb 2026 01:24:04 +0100 Subject: [PATCH] feat: auto-uppercase keywords and auto-semicolon for END_* keywords - Auto-uppercase SCL keywords (IF, CASE, FOR, NOT, AND, etc.) when typing space - Auto-add semicolon after END_* keywords (END_IF, END_FOR, etc.) when: - Leaving insert mode on a line ending with END_* - Pressing Enter after typing END_* - Added complete keyword list including TYPE, STRUCT, DATA_BLOCK, etc. - Works for .scl, .udt, and .db files --- lua/scl/auto_prefix.lua | 137 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/lua/scl/auto_prefix.lua b/lua/scl/auto_prefix.lua index 9df9b82..511ed33 100644 --- a/lua/scl/auto_prefix.lua +++ b/lua/scl/auto_prefix.lua @@ -33,9 +33,24 @@ local keywords = { "ORGANIZATION_BLOCK", "END_ORGANIZATION_BLOCK", "FUNCTION_BLOCK", "END_FUNCTION_BLOCK", "FUNCTION", "END_FUNCTION", - "RETURN", + "RETURN", "TYPE", "END_TYPE", "STRUCT", "END_STRUCT", + "DATA_BLOCK", "END_DATA_BLOCK", + "CONSTANT", "RETAIN", "NON_RETAIN", } +-- Keywords that need semicolon after them (when at end of statement) +local end_keywords = { + "END_IF", "END_FOR", "END_WHILE", "END_REPEAT", "END_CASE", + "END_VAR", "END_FUNCTION", "END_FUNCTION_BLOCK", "END_ORGANIZATION_BLOCK", + "END_TYPE", "END_STRUCT", "END_DATA_BLOCK", "END_REGION", +} + +-- Map lowercase to uppercase for auto-conversion +local keyword_map = {} +for _, kw in ipairs(keywords) do + keyword_map[kw:lower()] = kw +end + -- Check if word is a keyword local function is_keyword(word) for _, kw in ipairs(keywords) do @@ -393,6 +408,74 @@ local function on_dot_typed() end end +-- Auto-uppercase keywords when typing space +local function auto_uppercase_keyword() + -- Only in SCL files + local ft = vim.bo.filetype + if ft ~= "scl" and ft ~= "udt" and ft ~= "db" then + return + end + + local bufnr = vim.api.nvim_get_current_buf() + local row = vim.api.nvim_win_get_cursor(0)[1] + local col = vim.api.nvim_win_get_cursor(0)[2] + local line = vim.api.nvim_buf_get_lines(bufnr, row - 1, row, false)[1] + + -- Check if we just typed a space + if col < 1 or line:sub(col, col) ~= " " then + return + end + + -- Find word before the space + local word_end = col - 1 + local word_start = word_end + while word_start > 0 and line:sub(word_start, word_start):match("[%w_]") do + word_start = word_start - 1 + end + word_start = word_start + 1 + + if word_start > word_end then + return + end + + local word = line:sub(word_start, word_end) + local lower_word = word:lower() + + -- Check if it's a keyword that needs uppercase + if keyword_map[lower_word] and word ~= keyword_map[lower_word] then + local upper_word = keyword_map[lower_word] + vim.api.nvim_buf_set_text(bufnr, row - 1, word_start - 1, row - 1, word_end, { upper_word }) + end +end + +-- Auto-add semicolon after END_* keywords +local function auto_add_semicolon() + -- Only in SCL files + local ft = vim.bo.filetype + if ft ~= "scl" and ft ~= "udt" and ft ~= "db" then + return + end + + local bufnr = vim.api.nvim_get_current_buf() + local row = vim.api.nvim_win_get_cursor(0)[1] + local line = vim.api.nvim_buf_get_lines(bufnr, row - 1, row, false)[1] + local trimmed = line:match("^%s*(.-)%s*$") + + -- Check if line ends with an END_* keyword (without semicolon) + for _, end_kw in ipairs(end_keywords) do + if trimmed:upper() == end_kw then + -- Check if already has semicolon + if not line:match(";%s*$") then + local new_line = line .. ";" + vim.api.nvim_buf_set_lines(bufnr, row - 1, row, false, { new_line }) + -- Move cursor to end of line + vim.api.nvim_win_set_cursor(0, { row, #new_line }) + end + break + end + end +end + -- Setup auto-prefixing on specific triggers function M.setup() local augroup = vim.api.nvim_create_augroup("SCLAutoPrefix", { clear = true }) @@ -414,8 +497,14 @@ function M.setup() vim.api.nvim_create_autocmd("InsertLeave", { group = augroup, callback = function() - if vim.bo.filetype == "scl" then - auto_prefix_current_line() + local ft = vim.bo.filetype + if ft == "scl" or ft == "udt" or ft == "db" then + -- Auto-add semicolon after END_* keywords + auto_add_semicolon() + -- Variable prefixing (only for scl) + if ft == "scl" then + auto_prefix_current_line() + end end end, }) @@ -424,12 +513,18 @@ function M.setup() vim.api.nvim_create_autocmd("TextChangedI", { group = augroup, callback = function() - if vim.bo.filetype == "scl" then + local ft = vim.bo.filetype + if ft == "scl" or ft == "udt" or ft == "db" then local line = vim.api.nvim_get_current_line() local cursor_col = vim.api.nvim_win_get_cursor(0)[2] if cursor_col > 0 and line:sub(cursor_col, cursor_col) == " " then - auto_prefix_current_line() + -- Auto-uppercase keywords + auto_uppercase_keyword() + -- Variable prefixing (only for scl) + if ft == "scl" then + auto_prefix_current_line() + end end end end, @@ -484,6 +579,38 @@ function M.setup() end end, }) + + -- Auto-add semicolon when pressing Enter after END_* keyword + vim.api.nvim_create_autocmd("TextChangedI", { + group = augroup, + callback = function() + local ft = vim.bo.filetype + if ft ~= "scl" and ft ~= "udt" and ft ~= "db" then + return + end + + local bufnr = vim.api.nvim_get_current_buf() + local row = vim.api.nvim_win_get_cursor(0)[1] + local line = vim.api.nvim_buf_get_lines(bufnr, row - 1, row, false)[1] + + -- Check if we're on a new/empty line (Enter was pressed) + if line and line:match("^%s*$") and row > 1 then + local prev_line = vim.api.nvim_buf_get_lines(bufnr, row - 2, row - 1, false)[1] + if prev_line then + local trimmed = prev_line:match("^%s*(.-)%s*$") + -- Check if previous line is an END_* keyword without semicolon + for _, end_kw in ipairs(end_keywords) do + if trimmed:upper() == end_kw and not prev_line:match(";%s*$") then + -- Add semicolon to previous line + local new_prev = prev_line .. ";" + vim.api.nvim_buf_set_lines(bufnr, row - 2, row - 1, false, { new_prev }) + break + end + end + end + end + end, + }) end -- Manual trigger function