|
|
@@ -0,0 +1,321 @@
|
|
|
+-- C/C++ Major Mode for Lumacs
|
|
|
+
|
|
|
+-- Define helper function for escaping Lua patterns
|
|
|
+local function escape_pattern(text)
|
|
|
+ return text:gsub("([%%^%%$%ስ%%(%%)%%%.%%[%]%*%+%-%?])", "%%%%%1")
|
|
|
+end
|
|
|
+
|
|
|
+-- C/C++ Keywords
|
|
|
+local cpp_keywords = {
|
|
|
+ "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit",
|
|
|
+ "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case",
|
|
|
+ "catch", "char", "char8_t", "char16_t", "char32_t", "class", "compl", "concept",
|
|
|
+ "const", "consteval", "constexpr", "constinit", "const_cast", "continue",
|
|
|
+ "co_await", "co_return", "co_yield", "decltype", "default", "delete", "do",
|
|
|
+ "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern",
|
|
|
+ "false", "float", "for", "friend", "goto", "if", "inline", "int", "long",
|
|
|
+ "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator",
|
|
|
+ "or", "or_eq", "private", "protected", "public", "reflexpr", "register",
|
|
|
+ "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static",
|
|
|
+ "static_assert", "static_cast", "struct", "switch", "synchronized", "template",
|
|
|
+ "this", "thread_local", "throw", "true", "try", "typedef", "typeid", "typename",
|
|
|
+ "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while",
|
|
|
+ "xor", "xor_eq"
|
|
|
+}
|
|
|
+
|
|
|
+-- C/C++ Types (some overlap with keywords, but good to have separate for flexibility)
|
|
|
+local cpp_types = {
|
|
|
+ "void", "char", "short", "int", "long", "float", "double", "bool",
|
|
|
+ "signed", "unsigned", "class", "struct", "union", "enum", "typename",
|
|
|
+ -- Standard library types commonly used
|
|
|
+ "size_t", "ptrdiff_t", "int8_t", "int16_t", "int32_t", "int64_t",
|
|
|
+ "uint8_t", "uint16_t", "uint32_t", "uint64_t", "string", "vector",
|
|
|
+ "map", "set", "unique_ptr", "shared_ptr", "cout", "cin", "cerr", "endl" -- Common std:: things
|
|
|
+}
|
|
|
+
|
|
|
+-- C/C++ Preprocessor directives
|
|
|
+local preprocessor_directives = {
|
|
|
+ "include", "define", "undef", "if", "ifdef", "ifndef", "else", "elif",
|
|
|
+ "endif", "error", "pragma", "line"
|
|
|
+}
|
|
|
+
|
|
|
+-- C/C++ Operators (for potential future highlighting, regex is hard here)
|
|
|
+-- This list is mostly for reference, direct highlighting of all operators is complex
|
|
|
+local cpp_operators = {
|
|
|
+ "+", "-", "*", "/", "%", "==", "!=", "<", ">", "<= ", ">=", "&&", "||", "!",
|
|
|
+ "&", "|", "^", "~", "<<", ">>", "=", "+=", "-=", "*=", "/=", "%=", "&=",
|
|
|
+ "|=", "^=", "<<=", ">>=", "?", ":", ".", "->", "::", "++", "--"
|
|
|
+}
|
|
|
+
|
|
|
+-- Build keyword patterns
|
|
|
+local keyword_patterns = {}
|
|
|
+for _, kw in ipairs(cpp_keywords) do
|
|
|
+ table.insert(keyword_patterns, "%f[%w]" .. escape_pattern(kw) .. "%f[%W]")
|
|
|
+end
|
|
|
+local full_keyword_pattern = table.concat(keyword_patterns, "|")
|
|
|
+
|
|
|
+-- Build type patterns
|
|
|
+local type_patterns = {}
|
|
|
+for _, ty in ipairs(cpp_types) do
|
|
|
+ table.insert(type_patterns, "%f[%w]" .. escape_pattern(ty) .. "%f[%W]")
|
|
|
+end
|
|
|
+local full_type_pattern = table.concat(type_patterns, "|")
|
|
|
+
|
|
|
+-- Function to perform syntax highlighting
|
|
|
+local function highlight_cpp_buffer()
|
|
|
+ local buf = editor.buffer
|
|
|
+ buf:clear_styles()
|
|
|
+
|
|
|
+ local in_multiline_comment = false
|
|
|
+
|
|
|
+ for line_num = 0, buf:line_count() - 1 do
|
|
|
+ local line_text = buf:line(line_num)
|
|
|
+ local line_len = #line_text
|
|
|
+
|
|
|
+ -- Handle multiline comments
|
|
|
+ if in_multiline_comment then
|
|
|
+ local comment_end_pos = string.find(line_text, "*/", 1, true)
|
|
|
+ if comment_end_pos then
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, 0),
|
|
|
+ lumacs.Position(line_num, comment_end_pos + 1)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Comment, 0))
|
|
|
+ in_multiline_comment = false
|
|
|
+ else
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, 0),
|
|
|
+ lumacs.Position(line_num, line_len)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Comment, 0))
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Single-line comments (//)
|
|
|
+ local single_comment_pos = string.find(line_text, "//", 1, true)
|
|
|
+ if single_comment_pos and not in_multiline_comment then -- only if not already in multi-line
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, single_comment_pos - 1),
|
|
|
+ lumacs.Position(line_num, line_len)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Comment, 0))
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Multiline comment start (/*)
|
|
|
+ local multi_comment_start_pos = string.find(line_text, "/*", 1, true)
|
|
|
+ if multi_comment_start_pos and not in_multiline_comment then -- only if not already in multi-line
|
|
|
+ local multi_comment_end_pos = string.find(line_text, "*/", multi_comment_start_pos + 2, true)
|
|
|
+ if multi_comment_end_pos then
|
|
|
+ -- Single line multiline comment
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, multi_comment_start_pos - 1),
|
|
|
+ lumacs.Position(line_num, multi_comment_end_pos + 1)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Comment, 0))
|
|
|
+ else
|
|
|
+ -- Multiline comment continues to next line
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, multi_comment_start_pos - 1),
|
|
|
+ lumacs.Position(line_num, line_len)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Comment, 0))
|
|
|
+ in_multiline_comment = true
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Strings (double and single quotes)
|
|
|
+ local search_from = 1
|
|
|
+ while search_from <= line_len do
|
|
|
+ -- Pattern for double-quoted strings (handles escaped quotes)
|
|
|
+ local dq_start_idx, dq_end_idx = string.find(line_text, '"[^"\\]*(?:\\.[^"\\]*)*"', search_from)
|
|
|
+ -- Pattern for single-quoted characters/strings (handles escaped quotes)
|
|
|
+ local sq_start_idx, sq_end_idx = string.find(line_text, "'[^'\\]*(?:\\.[^'\\]*)*'", search_from)
|
|
|
+
|
|
|
+ local current_match_start, current_match_end
|
|
|
+ if dq_start_idx and (not sq_start_idx or dq_start_idx < sq_start_idx) then
|
|
|
+ current_match_start, current_match_end = dq_start_idx, dq_end_idx
|
|
|
+ elseif sq_start_idx then
|
|
|
+ current_match_start, current_match_end = sq_start_idx, sq_end_idx
|
|
|
+ else
|
|
|
+ break -- No more strings found on this line
|
|
|
+ end
|
|
|
+
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, current_match_start - 1),
|
|
|
+ lumacs.Position(line_num, current_match_end - 1)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.String, 0))
|
|
|
+ search_from = current_match_end + 1
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Numbers
|
|
|
+ -- This regex matches integers, decimals, and scientific notation, ensuring it's not part of a word.
|
|
|
+ for num_start_idx, num_end_idx in string.gmatch(line_text, "()%f[%D]%d+%.?%d*[eE]?[%+%-]?%d*%f[%D]()") do
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, num_start_idx - 1),
|
|
|
+ lumacs.Position(line_num, num_end_idx - 1)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Number, 0))
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Preprocessor directives
|
|
|
+ local preprocessor_match_start, preprocessor_match_end = string.find(line_text, "^%s*#(%a+)", 1) -- find and capture directive name
|
|
|
+ if preprocessor_match_start then
|
|
|
+ local directive_name = string.match(line_text, "^%s*#(%a+)")
|
|
|
+ for _, pp_dir in ipairs(preprocessor_directives) do
|
|
|
+ if directive_name == pp_dir then
|
|
|
+ local hash_start_idx = string.find(line_text, "#", 1, true)
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, hash_start_idx - 1),
|
|
|
+ lumacs.Position(line_num, line_len)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Variable, 0)) -- Using Variable for directives
|
|
|
+ break -- Found and styled, move to next
|
|
|
+ end
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Keywords
|
|
|
+ for kw_start_idx, kw_end_idx in string.gmatch(line_text, "()" .. full_keyword_pattern .. "()") do
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, kw_start_idx - 1),
|
|
|
+ lumacs.Position(line_num, kw_end_idx - 1)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Keyword, 0))
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Types
|
|
|
+ for type_start_idx, type_end_idx in string.gmatch(line_text, "()" .. full_type_pattern .. "()") do
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, type_start_idx - 1),
|
|
|
+ lumacs.Position(line_num, type_end_idx - 1)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Type, 0))
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Operators (simple approach: highlight some common multi-char operators)
|
|
|
+ local ops_to_highlight = {"==", "!=", "<= ", ">=", "&&", "||", "++", "--", "->", "::", "<<", ">>", ".*", "->*"}
|
|
|
+ for _, op in ipairs(ops_to_highlight) do
|
|
|
+ local op_start_pos = 1
|
|
|
+ while op_start_pos <= line_len do
|
|
|
+ local pos = string.find(line_text, escape_pattern(op), op_start_pos, true)
|
|
|
+ if not pos then break end
|
|
|
+ local range = lumacs.Range(
|
|
|
+ lumacs.Position(line_num, pos - 1),
|
|
|
+ lumacs.Position(line_num, pos + #op - 1)
|
|
|
+ )
|
|
|
+ buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Operator, 0))
|
|
|
+ op_start_pos = pos + #op
|
|
|
+ end
|
|
|
+ end
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+-- Function to format the current buffer using clang-format
|
|
|
+local function format_buffer()
|
|
|
+ local buf = editor.buffer
|
|
|
+ local filepath = buf:filepath()
|
|
|
+
|
|
|
+ if not filepath then
|
|
|
+ editor:message("Cannot format unsaved buffer. Save the file first.")
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ editor:message("Formatting buffer with clang-format...")
|
|
|
+
|
|
|
+ -- Create a temporary file
|
|
|
+ local temp_file = os.tmpname() .. ".cpp" -- Use .cpp extension for clang-format detection
|
|
|
+
|
|
|
+ -- Write buffer content to temp file
|
|
|
+ local file = io.open(temp_file, "w")
|
|
|
+ if file then
|
|
|
+ file:write(buf:get_all_text())
|
|
|
+ file:close()
|
|
|
+ else
|
|
|
+ editor:message("Error: Could not create temporary file for formatting.")
|
|
|
+ os.remove(temp_file) -- Try to clean up if file creation failed mid-way
|
|
|
+ return
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Execute clang-format
|
|
|
+ local clang_format_cmd = string.format("clang-format -style=file -i %s", temp_file)
|
|
|
+ local success, status_string, code = os.execute(clang_format_cmd) -- os.execute returns true/false, exit status string, and exit code
|
|
|
+
|
|
|
+ if success then
|
|
|
+ -- Read formatted content back
|
|
|
+ local formatted_file = io.open(temp_file, "r")
|
|
|
+ if formatted_file then
|
|
|
+ local formatted_text = formatted_file:read("*all")
|
|
|
+ formatted_file:close()
|
|
|
+
|
|
|
+ -- Preserve cursor position
|
|
|
+ local original_cursor = editor.cursor
|
|
|
+
|
|
|
+ -- Replace buffer content
|
|
|
+ local full_buffer_range = lumacs.Range(lumacs.Position(0,0), lumacs.Position(buf:line_count() - 1, #buf:line(buf:line_count() - 1)))
|
|
|
+ buf:replace(full_buffer_range, formatted_text)
|
|
|
+
|
|
|
+ -- Restore cursor position (clamp if necessary)
|
|
|
+ local new_line_count = buf:line_count()
|
|
|
+ local new_line = original_cursor.line
|
|
|
+ local new_col = original_cursor.column
|
|
|
+
|
|
|
+ if new_line >= new_line_count then
|
|
|
+ new_line = new_line_count - 1
|
|
|
+ if new_line < 0 then new_line = 0 end -- Handle empty buffer after format
|
|
|
+ end
|
|
|
+ if new_line >= 0 then
|
|
|
+ local line_len = #buf:line(new_line)
|
|
|
+ if new_col > line_len then
|
|
|
+ new_col = line_len
|
|
|
+ end
|
|
|
+ else
|
|
|
+ new_col = 0 -- Should be (0,0) for empty buffer
|
|
|
+ end
|
|
|
+ editor.cursor = lumacs.Position(new_line, new_col)
|
|
|
+
|
|
|
+ editor:message("Buffer formatted successfully.")
|
|
|
+ else
|
|
|
+ editor:message("Error: Could not read formatted content from temporary file.")
|
|
|
+ end
|
|
|
+ else
|
|
|
+ editor:message(string.format("Error: clang-format failed with status '%s', code '%s'", tostring(status_string), tostring(code)))
|
|
|
+ editor:message("Please ensure clang-format is installed and available in your PATH.")
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Clean up temporary file
|
|
|
+ os.remove(temp_file)
|
|
|
+end
|
|
|
+
|
|
|
+-- Register the C/C++ Major Mode
|
|
|
+define_major_mode("c-cpp-mode", {
|
|
|
+ file_patterns = {
|
|
|
+ "%.c$", "%.cpp$", "%.h$", "%.hpp$", "%.cc$", "%.cxx$", "%.hxx$",
|
|
|
+ "%.C$", "%.CPP$", "%.H$", "%.HPP$", "%.CC$", "%.CXX$", "%.HXX$" -- Case-insensitive extensions
|
|
|
+ },
|
|
|
+ comment_syntax = "//", -- Default for single-line comments
|
|
|
+
|
|
|
+ setup = function()
|
|
|
+ editor:message("[c-cpp-mode] C/C++ mode activated")
|
|
|
+ end,
|
|
|
+
|
|
|
+ cleanup = function()
|
|
|
+ editor:message("[c-cpp-mode] C/C++ mode deactivated")
|
|
|
+ end,
|
|
|
+
|
|
|
+ highlight = highlight_cpp_buffer,
|
|
|
+
|
|
|
+ keybindings = {
|
|
|
+ [" "] = function() editor:execute_command("self-insert-command", {" "}) end, -- Explicitly bind space
|
|
|
+ ["M-q"] = function() format_buffer() end, -- Bind M-q to format the buffer
|
|
|
+ ["C-c C-f"] = function() format_buffer() end, -- Alternative binding
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+-- Register format-buffer command so it can be called via M-x
|
|
|
+editor:register_command(
|
|
|
+ "format-buffer",
|
|
|
+ "Formats the current C/C++ buffer using clang-format.",
|
|
|
+ format_buffer,
|
|
|
+ true -- Interactive, can be called via M-x
|
|
|
+)
|