-- SPDX-FileCopyrightText: © 2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. -- C syntax highlighter for LEM. local style = require("lem.highlight.style") local STATE_NORMAL = nil local STATE_BLOCK_COMMENT = "block_comment" local STATE_PREPROCESSOR = "preprocessor" local PUNCTUATION = style.COMMON_PUNCTUATION local scan_quoted = style.scan_quoted local scan_ident_end = style.scan_ident_end local scan_number_end = style.scan_number_end local OPERATORS = { ["+"] = true, ["-"] = true, ["*"] = true, ["/"] = true, ["%"] = true, ["="] = true, ["<"] = true, [">"] = true, ["!"] = true, ["&"] = true, ["|"] = true, ["^"] = true, ["~"] = true, ["?"] = true, [":"] = true, } local LITERALS = { ["true"] = true, ["false"] = true, ["NULL"] = true, } local highlight_line = function(self, text, state) local spans = {} local i = 1 local len = #text -- Continuation of a preprocessor directive from previous line. -- A blank line terminates the directive. if state == STATE_PREPROCESSOR then if len == 0 then return spans, STATE_NORMAL end spans[#spans + 1] = { 1, len, "directive" } if text:match("\\%s*$") then return spans, STATE_PREPROCESSOR end return spans, STATE_NORMAL end -- Inside a block comment from previous line if state == STATE_BLOCK_COMMENT then local close = text:find("%*/", i) if close then spans[#spans + 1] = { i, close + 1, "disabled" } i = close + 2 state = STATE_NORMAL else spans[#spans + 1] = { i, len, "disabled" } return spans, STATE_BLOCK_COMMENT end end -- Preprocessor directive; trailing backslash extends it to the next line. local directive_start = text:match("^(%s*#)") if directive_start and i == 1 then spans[#spans + 1] = { 1, len, "directive" } if text:match("\\%s*$") then return spans, STATE_PREPROCESSOR end return spans, STATE_NORMAL end while i <= len do local ch = text:sub(i, i) -- Line comment: // if ch == "/" and text:sub(i + 1, i + 1) == "/" then spans[#spans + 1] = { i, len, "comment" } return spans, STATE_NORMAL -- Block comment: /* elseif ch == "/" and text:sub(i + 1, i + 1) == "*" then local close = text:find("%*/", i + 2) if close then spans[#spans + 1] = { i, close + 1, "disabled" } i = close + 2 else spans[#spans + 1] = { i, len, "disabled" } return spans, STATE_BLOCK_COMMENT end -- String or char literal (double or single quote) elseif ch == '"' or ch == "'" then local j = scan_quoted(text, i, len, ch) spans[#spans + 1] = { i, j, "literal" } i = j + 1 -- Number elseif ch:match("%d") or (ch == "." and i + 1 <= len and text:sub(i + 1, i + 1):match("%d")) then local j = scan_number_end(text, i, len, "[%d%.eEfFlLuU]") spans[#spans + 1] = { i, j, "literal" } i = j + 1 -- Punctuation elseif PUNCTUATION[ch] then spans[#spans + 1] = { i, i, "punctuation" } i = i + 1 -- Operators elseif OPERATORS[ch] then local j = i local next_ch = text:sub(i + 1, i + 1) if (ch == "=" or ch == "!" or ch == "<" or ch == ">") and next_ch == "=" then j = i + 1 elseif (ch == "&" and next_ch == "&") or (ch == "|" and next_ch == "|") then j = i + 1 elseif (ch == "+" and next_ch == "+") or (ch == "-" and next_ch == "-") then j = i + 1 elseif ch == "-" and next_ch == ">" then j = i + 1 elseif (ch == "<" and next_ch == "<") or (ch == ">" and next_ch == ">") then j = i + 1 end spans[#spans + 1] = { i, j, "operator" } i = j + 1 -- Identifiers elseif ch:match("[%a_]") then local word_end = scan_ident_end(text, i, len) local word = text:sub(i, word_end) if LITERALS[word] then spans[#spans + 1] = { i, word_end, "literal" } else -- Definition: name followed by ( at start of line (function definition) local before = text:sub(1, i - 1) local after = text:sub(word_end + 1) if before:match("^[%w_%s%*]+$") and after:match("^%s*%(") and not before:match("^%s*$") then -- Likely a function definition: "type name(" -- But only if this isn't an if/while/for/return if not word:match("^if$") and not word:match("^while$") and not word:match("^for$") and not word:match("^return$") and not word:match("^switch$") and not word:match("^sizeof$") then spans[#spans + 1] = { i, word_end, "definition" } end end end i = word_end + 1 else i = i + 1 end end return spans, STATE_NORMAL end local new = function() return { highlight_line = highlight_line, } end return { new = new }