diff --git a/.idea/workspace.xml b/.idea/workspace.xml
deleted file mode 100644
index 5ddee4a..0000000
--- a/.idea/workspace.xml
+++ /dev/null
@@ -1,390 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- "useNewFormat": true
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- "associatedIndex": 3
-}
-
-
-
-
-
-
-
-
-
-
- {
- "keyToString": {
- "CMake Application.kge.executor": "Run",
- "CMake Application.test_example.executor": "Run",
- "CMake Application.test_undo.executor": "Run",
- "ModuleVcsDetector.initialDetectionPerformed": "true",
- "NIXITCH_NIXPKGS_CONFIG": "",
- "NIXITCH_NIX_CONF_DIR": "",
- "NIXITCH_NIX_OTHER_STORES": "",
- "NIXITCH_NIX_PATH": "",
- "NIXITCH_NIX_PROFILES": "",
- "NIXITCH_NIX_REMOTE": "",
- "NIXITCH_NIX_USER_PROFILE_DIR": "",
- "RunOnceActivity.RadMigrateCodeStyle": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.cidr.known.project.marker": "true",
- "RunOnceActivity.git.unshallow": "true",
- "RunOnceActivity.readMode.enableVisualFormatting": "true",
- "RunOnceActivity.west.config.association.type.startup.service": "true",
- "cf.first.check.clang-format": "false",
- "cidr.known.project.marker": "true",
- "code.cleanup.on.save": "true",
- "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
- "git-widget-placeholder": "master",
- "junie.onboarding.icon.badge.shown": "true",
- "node.js.detected.package.eslint": "true",
- "node.js.detected.package.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_package_manager_path": "npm",
- "onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
- "rearrange.code.on.save": "true",
- "settings.editor.selected.configurable": "CMakeSettings",
- "to.speed.mode.migration.done": "true",
- "vue.rearranger.settings.migration": "true"
- }
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1764457173148
-
-
- 1764457173148
-
-
-
-
-
-
-
-
-
-
-
-
- 1764485311566
-
-
-
- 1764485311566
-
-
-
- 1764486011231
-
-
-
- 1764486011231
-
-
-
- 1764486876984
-
-
-
- 1764486876984
-
-
-
- 1764489870957
-
-
-
- 1764489870957
-
-
-
- 1764496151303
-
-
-
- 1764496151303
-
-
-
- 1764500200942
-
-
-
- 1764500200942
-
-
-
- 1764501532446
-
-
-
- 1764501532446
-
-
-
- 1764502480274
-
-
-
- 1764502480274
-
-
-
- 1764505723411
-
-
-
- 1764505723411
-
-
-
- 1764550164829
-
-
-
- 1764550164829
-
-
-
- 1764551986561
-
-
-
- 1764551986561
-
-
-
- 1764556512864
-
-
-
- 1764556512864
-
-
-
- 1764556854788
-
-
-
- 1764556854788
-
-
-
- 1764557759844
-
-
-
- 1764557759844
-
-
-
- 1764568264996
-
-
-
- 1764568264996
-
-
-
- 1764574397967
-
-
-
- 1764574397967
-
-
-
- 1764586480092
-
-
-
- 1764586480092
-
-
-
- 1764619193516
-
-
-
- 1764619193517
-
-
-
- 1764619210817
-
-
-
- 1764619210817
-
-
-
- 1764635874362
-
-
-
- 1764635874362
-
-
-
- 1764635891710
-
-
-
- 1764635891710
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Buffer.h b/Buffer.h
index 59e8525..926f67c 100644
--- a/Buffer.h
+++ b/Buffer.h
@@ -12,6 +12,10 @@
#include "AppendBuffer.h"
#include "UndoSystem.h"
+#include
+#include
+#include "HighlighterEngine.h"
+#include "Highlight.h"
class Buffer {
@@ -326,6 +330,12 @@ public:
void SetDirty(bool d)
{
dirty_ = d;
+ if (d) {
+ ++version_;
+ if (highlighter_) {
+ highlighter_->InvalidateFrom(0);
+ }
+ }
}
@@ -364,6 +374,23 @@ public:
[[nodiscard]] std::string AsString() const;
+ // Syntax highlighting integration (per-buffer)
+ [[nodiscard]] std::uint64_t Version() const { return version_; }
+
+ void SetSyntaxEnabled(bool on) { syntax_enabled_ = on; }
+ [[nodiscard]] bool SyntaxEnabled() const { return syntax_enabled_; }
+
+ void SetFiletype(const std::string &ft) { filetype_ = ft; }
+ [[nodiscard]] const std::string &Filetype() const { return filetype_; }
+
+ kte::HighlighterEngine *Highlighter() { return highlighter_.get(); }
+ const kte::HighlighterEngine *Highlighter() const { return highlighter_.get(); }
+
+ void EnsureHighlighter()
+ {
+ if (!highlighter_) highlighter_ = std::make_unique();
+ }
+
// Raw, low-level editing APIs used by UndoSystem apply().
// These must NOT trigger undo recording. They also do not move the cursor.
void insert_text(int row, int col, std::string_view text);
@@ -400,6 +427,12 @@ private:
// Per-buffer undo state
std::unique_ptr undo_tree_;
std::unique_ptr undo_sys_;
+
+ // Syntax/highlighting state
+ std::uint64_t version_ = 0; // increment on edits
+ bool syntax_enabled_ = true;
+ std::string filetype_;
+ std::unique_ptr highlighter_;
};
#endif // KTE_BUFFER_H
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2aba041..6b95e77 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -67,6 +67,17 @@ set(COMMON_SOURCES
UndoNode.cc
UndoTree.cc
UndoSystem.cc
+ HighlighterEngine.cc
+ CppHighlighter.cc
+ HighlighterRegistry.cc
+ NullHighlighter.cc
+ JsonHighlighter.cc
+ MarkdownHighlighter.cc
+ ShellHighlighter.cc
+ GoHighlighter.cc
+ PythonHighlighter.cc
+ RustHighlighter.cc
+ LispHighlighter.cc
)
set(COMMON_HEADERS
@@ -90,6 +101,19 @@ set(COMMON_HEADERS
UndoNode.h
UndoTree.h
UndoSystem.h
+ Highlight.h
+ LanguageHighlighter.h
+ HighlighterEngine.h
+ CppHighlighter.h
+ HighlighterRegistry.h
+ NullHighlighter.h
+ JsonHighlighter.h
+ MarkdownHighlighter.h
+ ShellHighlighter.h
+ GoHighlighter.h
+ PythonHighlighter.h
+ RustHighlighter.h
+ LispHighlighter.h
)
# kte (terminal-first) executable
diff --git a/Command.cc b/Command.cc
index 44a0ccb..3b5a12f 100644
--- a/Command.cc
+++ b/Command.cc
@@ -7,10 +7,15 @@
#include
#include "Command.h"
+#include "HighlighterRegistry.h"
+#include "NullHighlighter.h"
#include "Editor.h"
#include "Buffer.h"
#include "UndoSystem.h"
#include "HelpText.h"
+#include "LanguageHighlighter.h"
+#include "HighlighterEngine.h"
+#include "CppHighlighter.h"
#ifdef KTE_BUILD_GUI
#include "GUITheme.h"
#endif
@@ -757,6 +762,124 @@ cmd_unknown_kcommand(CommandContext &ctx)
return true;
}
+// --- Syntax highlighting commands ---
+static void apply_filetype(Buffer &buf, const std::string &ft)
+{
+ buf.EnsureHighlighter();
+ auto *eng = buf.Highlighter();
+ if (!eng) return;
+ std::string val = ft;
+ // trim + lower
+ auto trim = [](const std::string &s){
+ std::string r = s;
+ auto notsp = [](int ch){ return !std::isspace(ch); };
+ r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
+ r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
+ return r;
+ };
+ val = trim(val);
+ for (auto &ch: val) ch = static_cast(std::tolower(static_cast(ch)));
+ if (val == "off") {
+ eng->SetHighlighter(nullptr);
+ buf.SetFiletype("");
+ buf.SetSyntaxEnabled(false);
+ return;
+ }
+ if (val.empty()) {
+ // Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
+ buf.SetFiletype("");
+ buf.SetSyntaxEnabled(true);
+ eng->SetHighlighter(std::make_unique());
+ eng->InvalidateFrom(0);
+ return;
+ }
+ // Normalize and create via registry
+ std::string norm = kte::HighlighterRegistry::Normalize(val);
+ auto hl = kte::HighlighterRegistry::CreateFor(norm);
+ if (hl) {
+ eng->SetHighlighter(std::move(hl));
+ buf.SetFiletype(norm);
+ buf.SetSyntaxEnabled(true);
+ eng->InvalidateFrom(0);
+ } else {
+ // Unknown -> install NullHighlighter and keep syntax enabled
+ eng->SetHighlighter(std::make_unique());
+ buf.SetFiletype(val); // record what user asked even if unsupported
+ buf.SetSyntaxEnabled(true);
+ eng->InvalidateFrom(0);
+ }
+}
+
+static bool cmd_syntax(CommandContext &ctx)
+{
+ Buffer *b = ctx.editor.CurrentBuffer();
+ if (!b) {
+ ctx.editor.SetStatus("No buffer");
+ return true;
+ }
+ std::string arg = ctx.arg;
+ // trim
+ auto trim = [](std::string &s){
+ auto notsp = [](int ch){ return !std::isspace(ch); };
+ s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
+ s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
+ };
+ trim(arg);
+ if (arg == "on") {
+ b->SetSyntaxEnabled(true);
+ // If no highlighter but filetype is cpp by extension, set it
+ if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
+ apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
+ }
+ ctx.editor.SetStatus("syntax: on");
+ } else if (arg == "off") {
+ b->SetSyntaxEnabled(false);
+ ctx.editor.SetStatus("syntax: off");
+ } else if (arg == "reload") {
+ if (auto *eng = b->Highlighter()) eng->InvalidateFrom(0);
+ ctx.editor.SetStatus("syntax: reloaded");
+ } else {
+ ctx.editor.SetStatus("usage: :syntax on|off|reload");
+ }
+ return true;
+}
+
+static bool cmd_set_option(CommandContext &ctx)
+{
+ Buffer *b = ctx.editor.CurrentBuffer();
+ if (!b) {
+ ctx.editor.SetStatus("No buffer");
+ return true;
+ }
+ // Expect key=value
+ auto eq = ctx.arg.find('=');
+ if (eq == std::string::npos) {
+ ctx.editor.SetStatus("usage: :set key=value");
+ return true;
+ }
+ std::string key = ctx.arg.substr(0, eq);
+ std::string val = ctx.arg.substr(eq + 1);
+ // trim
+ auto trim = [](std::string &s){
+ auto notsp = [](int ch){ return !std::isspace(ch); };
+ s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
+ s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
+ };
+ trim(key); trim(val);
+ // lower-case value for filetype
+ for (auto &ch: val) ch = static_cast(std::tolower(static_cast(ch)));
+ if (key == "filetype") {
+ apply_filetype(*b, val);
+ if (b->SyntaxEnabled())
+ ctx.editor.SetStatus(std::string("filetype: ") + (b->Filetype().empty()?"off":b->Filetype()));
+ else
+ ctx.editor.SetStatus("filetype: off");
+ return true;
+ }
+ ctx.editor.SetStatus("unknown option: " + key);
+ return true;
+}
+
// GUI theme cycling commands (available in GUI build; show message otherwise)
#ifdef KTE_BUILD_GUI
@@ -3611,9 +3734,12 @@ InstallDefaultCommands()
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
cmd_change_working_directory_start
});
- // UI helpers
- CommandRegistry::Register(
- {CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
+ // UI helpers
+ CommandRegistry::Register(
+ {CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
+ // Syntax highlighting (public commands)
+ CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
+ CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
}
diff --git a/Command.h b/Command.h
index e08f85e..7ee15eb 100644
--- a/Command.h
+++ b/Command.h
@@ -94,7 +94,10 @@ enum class CommandId {
// Theme by name
ThemeSetByName,
// Background mode (GUI)
- BackgroundSet,
+ BackgroundSet,
+ // Syntax highlighting
+ Syntax, // ":syntax on|off|reload"
+ SetOption, // generic ":set key=value" (v1: filetype=)
};
diff --git a/CppHighlighter.cc b/CppHighlighter.cc
new file mode 100644
index 0000000..c1e0f9c
--- /dev/null
+++ b/CppHighlighter.cc
@@ -0,0 +1,170 @@
+#include "CppHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static bool is_digit(char c) { return c >= '0' && c <= '9'; }
+
+CppHighlighter::CppHighlighter()
+{
+ const char *kw[] = {
+ "if","else","for","while","do","switch","case","default","break","continue",
+ "return","goto","struct","class","namespace","using","template","typename",
+ "public","private","protected","virtual","override","const","constexpr","auto",
+ "static","inline","operator","new","delete","try","catch","throw","friend",
+ "enum","union","extern","volatile","mutable","noexcept","sizeof","this"
+ };
+ for (auto s: kw) keywords_.insert(s);
+ const char *types[] = {
+ "int","long","short","char","signed","unsigned","float","double","void",
+ "bool","wchar_t","size_t","ptrdiff_t","uint8_t","uint16_t","uint32_t","uint64_t",
+ "int8_t","int16_t","int32_t","int64_t"
+ };
+ for (auto s: types) types_.insert(s);
+}
+
+bool CppHighlighter::is_ident_start(char c) { return std::isalpha(static_cast(c)) || c == '_'; }
+bool CppHighlighter::is_ident_char(char c) { return std::isalnum(static_cast(c)) || c == '_'; }
+
+void CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ // Stateless entry simply delegates to stateful with a clean previous state
+ StatefulHighlighter::LineState prev;
+ (void)HighlightLineStateful(buf, row, prev, out);
+}
+
+StatefulHighlighter::LineState CppHighlighter::HighlightLineStateful(const Buffer &buf,
+ int row,
+ const LineState &prev,
+ std::vector &out) const
+{
+ const auto &rows = buf.Rows();
+ StatefulHighlighter::LineState state = prev;
+ if (row < 0 || static_cast(row) >= rows.size()) return state;
+ std::string s = static_cast(rows[static_cast(row)]);
+ if (s.empty()) return state;
+
+ auto push = [&](int a, int b, TokenKind k){ if (b> a) out.push_back({a,b,k}); };
+ int n = static_cast(s.size());
+ int bol = 0; while (bol < n && (s[bol] == ' ' || s[bol] == '\t')) ++bol;
+ int i = 0;
+
+ // Continue multi-line raw string from previous line
+ if (state.in_raw_string) {
+ std::string needle = ")" + state.raw_delim + "\"";
+ auto pos = s.find(needle);
+ if (pos == std::string::npos) {
+ push(0, n, TokenKind::String);
+ state.in_raw_string = true;
+ return state;
+ } else {
+ int end = static_cast(pos + needle.size());
+ push(0, end, TokenKind::String);
+ i = end;
+ state.in_raw_string = false;
+ state.raw_delim.clear();
+ }
+ }
+
+ // Continue multi-line block comment from previous line
+ if (state.in_block_comment) {
+ int j = i;
+ while (i + 1 < n) {
+ if (s[i] == '*' && s[i+1] == '/') { i += 2; push(j, i, TokenKind::Comment); state.in_block_comment = false; break; }
+ ++i;
+ }
+ if (state.in_block_comment) { push(j, n, TokenKind::Comment); return state; }
+ }
+
+ while (i < n) {
+ char c = s[i];
+ // Preprocessor at beginning of line (after leading whitespace)
+ if (i == bol && c == '#') { push(0, n, TokenKind::Preproc); break; }
+
+ // Whitespace
+ if (c == ' ' || c == '\t') {
+ int j = i+1; while (j < n && (s[j] == ' ' || s[j] == '\t')) ++j; push(i,j,TokenKind::Whitespace); i=j; continue;
+ }
+
+ // Line comment
+ if (c == '/' && i+1 < n && s[i+1] == '/') { push(i, n, TokenKind::Comment); break; }
+
+ // Block comment
+ if (c == '/' && i+1 < n && s[i+1] == '*') {
+ int j = i+2;
+ bool closed = false;
+ while (j + 1 <= n) {
+ if (j + 1 < n && s[j] == '*' && s[j+1] == '/') { j += 2; closed = true; break; }
+ ++j;
+ }
+ if (closed) { push(i, j, TokenKind::Comment); i = j; continue; }
+ // Spill to next lines
+ push(i, n, TokenKind::Comment);
+ state.in_block_comment = true;
+ return state;
+ }
+
+ // Raw string start: very simple detection: R"delim(
+ if (c == 'R' && i+1 < n && s[i+1] == '"') {
+ int k = i + 2;
+ std::string delim;
+ while (k < n && s[k] != '(') { delim.push_back(s[k]); ++k; }
+ if (k < n && s[k] == '(') {
+ int body_start = k + 1;
+ std::string needle = ")" + delim + "\"";
+ auto pos = s.find(needle, static_cast(body_start));
+ if (pos == std::string::npos) {
+ push(i, n, TokenKind::String);
+ state.in_raw_string = true;
+ state.raw_delim = delim;
+ return state;
+ } else {
+ int end = static_cast(pos + needle.size());
+ push(i, end, TokenKind::String);
+ i = end;
+ continue;
+ }
+ }
+ // If malformed, just treat 'R' as identifier fallback
+ }
+
+ // Regular string literal
+ if (c == '"') {
+ int j = i+1; bool esc=false; while (j < n) { char d = s[j++]; if (esc) { esc=false; continue; } if (d == '\\') { esc=true; continue; } if (d == '"') break; }
+ push(i, j, TokenKind::String); i = j; continue;
+ }
+
+ // Char literal
+ if (c == '\'') {
+ int j = i+1; bool esc=false; while (j < n) { char d = s[j++]; if (esc) { esc=false; continue; } if (d == '\\') { esc=true; continue; } if (d == '\'') break; }
+ push(i, j, TokenKind::Char); i = j; continue;
+ }
+
+ // Number literal (simple)
+ if (is_digit(c) || (c == '.' && i+1 < n && is_digit(s[i+1]))) {
+ int j = i+1; while (j < n && (std::isalnum(static_cast(s[j])) || s[j]=='.' || s[j]=='x' || s[j]=='X' || s[j]=='b' || s[j]=='B' || s[j]=='_')) ++j;
+ push(i, j, TokenKind::Number); i = j; continue;
+ }
+
+ // Identifier / keyword / type
+ if (is_ident_start(c)) {
+ int j = i+1; while (j < n && is_ident_char(s[j])) ++j; std::string id = s.substr(i, j-i);
+ TokenKind k = TokenKind::Identifier; if (keywords_.count(id)) k = TokenKind::Keyword; else if (types_.count(id)) k = TokenKind::Type; push(i, j, k); i = j; continue;
+ }
+
+ // Operators and punctuation (single char for now)
+ TokenKind kind = TokenKind::Operator;
+ if (std::ispunct(static_cast(c)) && c != '_' && c != '#') {
+ if (c==';' || c==',' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']') kind = TokenKind::Punctuation;
+ push(i, i+1, kind); ++i; continue;
+ }
+
+ // Fallback
+ push(i, i+1, TokenKind::Default); ++i;
+ }
+
+ return state;
+}
+
+} // namespace kte
diff --git a/CppHighlighter.h b/CppHighlighter.h
new file mode 100644
index 0000000..9b63bff
--- /dev/null
+++ b/CppHighlighter.h
@@ -0,0 +1,34 @@
+// CppHighlighter.h - minimal stateless C/C++ line highlighter
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "LanguageHighlighter.h"
+
+class Buffer;
+
+namespace kte {
+
+class CppHighlighter final : public StatefulHighlighter {
+public:
+ CppHighlighter();
+ ~CppHighlighter() override = default;
+
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+ LineState HighlightLineStateful(const Buffer &buf,
+ int row,
+ const LineState &prev,
+ std::vector &out) const override;
+
+private:
+ std::unordered_set keywords_;
+ std::unordered_set types_;
+
+ static bool is_ident_start(char c);
+ static bool is_ident_char(char c);
+};
+
+} // namespace kte
diff --git a/Editor.cc b/Editor.cc
index cc0f01e..5695a29 100644
--- a/Editor.cc
+++ b/Editor.cc
@@ -3,6 +3,9 @@
#include
#include "Editor.h"
+#include "HighlighterRegistry.h"
+#include "CppHighlighter.h"
+#include "NullHighlighter.h"
Editor::Editor() = default;
@@ -143,26 +146,72 @@ Editor::OpenFile(const std::string &path, std::string &err)
{
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
// of creating a new one.
- if (buffers_.size() == 1) {
- Buffer &cur = buffers_[curbuf_];
- const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
- const bool clean = !cur.Dirty();
- const auto &rows = cur.Rows();
- const bool rows_empty = rows.empty();
- const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
- if (unnamed && clean && (rows_empty || single_empty_line)) {
- return cur.OpenFromFile(path, err);
- }
- }
+ if (buffers_.size() == 1) {
+ Buffer &cur = buffers_[curbuf_];
+ const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
+ const bool clean = !cur.Dirty();
+ const auto &rows = cur.Rows();
+ const bool rows_empty = rows.empty();
+ const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
+ if (unnamed && clean && (rows_empty || single_empty_line)) {
+ bool ok = cur.OpenFromFile(path, err);
+ if (!ok) return false;
+ // Setup highlighting using registry (extension + shebang)
+ cur.EnsureHighlighter();
+ std::string first = "";
+ const auto &rows = cur.Rows();
+ if (!rows.empty()) first = static_cast(rows[0]);
+ std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
+ if (!ft.empty()) {
+ cur.SetFiletype(ft);
+ cur.SetSyntaxEnabled(true);
+ if (auto *eng = cur.Highlighter()) {
+ eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
+ eng->InvalidateFrom(0);
+ }
+ } else {
+ cur.SetFiletype("");
+ cur.SetSyntaxEnabled(true);
+ if (auto *eng = cur.Highlighter()) {
+ eng->SetHighlighter(std::make_unique());
+ eng->InvalidateFrom(0);
+ }
+ }
+ return true;
+ }
+ }
- Buffer b;
- if (!b.OpenFromFile(path, err)) {
- return false;
- }
- // Add as a new buffer and switch to it
- std::size_t idx = AddBuffer(std::move(b));
- SwitchTo(idx);
- return true;
+ Buffer b;
+ if (!b.OpenFromFile(path, err)) {
+ return false;
+ }
+ // Initialize syntax highlighting by extension + shebang via registry (v2)
+ b.EnsureHighlighter();
+ std::string first = "";
+ {
+ const auto &rows = b.Rows();
+ if (!rows.empty()) first = static_cast(rows[0]);
+ }
+ std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
+ if (!ft.empty()) {
+ b.SetFiletype(ft);
+ b.SetSyntaxEnabled(true);
+ if (auto *eng = b.Highlighter()) {
+ eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
+ eng->InvalidateFrom(0);
+ }
+ } else {
+ b.SetFiletype("");
+ b.SetSyntaxEnabled(true);
+ if (auto *eng = b.Highlighter()) {
+ eng->SetHighlighter(std::make_unique());
+ eng->InvalidateFrom(0);
+ }
+ }
+ // Add as a new buffer and switch to it
+ std::size_t idx = AddBuffer(std::move(b));
+ SwitchTo(idx);
+ return true;
}
diff --git a/GUIConfig.cc b/GUIConfig.cc
index 4dd5651..9820ef0 100644
--- a/GUIConfig.cc
+++ b/GUIConfig.cc
@@ -102,17 +102,27 @@ GUIConfig::LoadFromFile(const std::string &path)
if (v > 0.0f) {
font_size = v;
}
- } else if (key == "theme") {
- theme = val;
- } else if (key == "background" || key == "bg") {
- std::string v = val;
- std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
- return (char) std::tolower(c);
- });
- if (v == "light" || v == "dark")
- background = v;
- }
- }
+ } else if (key == "theme") {
+ theme = val;
+ } else if (key == "background" || key == "bg") {
+ std::string v = val;
+ std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
+ return (char) std::tolower(c);
+ });
+ if (v == "light" || v == "dark")
+ background = v;
+ } else if (key == "syntax") {
+ std::string v = val;
+ std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
+ return (char) std::tolower(c);
+ });
+ if (v == "1" || v == "on" || v == "true" || v == "yes") {
+ syntax = true;
+ } else if (v == "0" || v == "off" || v == "false" || v == "no") {
+ syntax = false;
+ }
+ }
+ }
- return true;
+ return true;
}
diff --git a/GUIConfig.h b/GUIConfig.h
index f43e97f..63799e1 100644
--- a/GUIConfig.h
+++ b/GUIConfig.h
@@ -12,14 +12,18 @@
class GUIConfig {
public:
- bool fullscreen = false;
- int columns = 80;
- int rows = 42;
- float font_size = (float) KTE_FONT_SIZE;
- std::string theme = "nord";
- // Background mode for themes that support light/dark variants
- // Values: "dark" (default), "light"
- std::string background = "dark";
+ bool fullscreen = false;
+ int columns = 80;
+ int rows = 42;
+ float font_size = (float) KTE_FONT_SIZE;
+ std::string theme = "nord";
+ // Background mode for themes that support light/dark variants
+ // Values: "dark" (default), "light"
+ std::string background = "dark";
+
+ // Default syntax highlighting state for GUI (kge): on/off
+ // Accepts: on/off/true/false/yes/no/1/0 in the ini file.
+ bool syntax = true; // default: enabled
// Load from default path: $HOME/.config/kte/kge.ini
static GUIConfig Load();
diff --git a/GUIFrontend.cc b/GUIFrontend.cc
index 560521f..afd0dcc 100644
--- a/GUIFrontend.cc
+++ b/GUIFrontend.cc
@@ -16,6 +16,8 @@
#include "Font.h" // embedded default font (DefaultFontRegular)
#include "GUIConfig.h"
#include "GUITheme.h"
+#include "HighlighterRegistry.h"
+#include "NullHighlighter.h"
#ifndef KTE_FONT_SIZE
@@ -106,12 +108,42 @@ GUIFrontend::Init(Editor &ed)
(void) io;
ImGui::StyleColorsDark();
- // Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
- if (cfg.background == "light")
- kte::SetBackgroundMode(kte::BackgroundMode::Light);
- else
- kte::SetBackgroundMode(kte::BackgroundMode::Dark);
- kte::ApplyThemeByName(cfg.theme);
+ // Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
+ if (cfg.background == "light")
+ kte::SetBackgroundMode(kte::BackgroundMode::Light);
+ else
+ kte::SetBackgroundMode(kte::BackgroundMode::Dark);
+ kte::ApplyThemeByName(cfg.theme);
+
+ // Apply default syntax highlighting preference from GUI config to the current buffer
+ if (Buffer *b = ed.CurrentBuffer()) {
+ if (cfg.syntax) {
+ b->SetSyntaxEnabled(true);
+ // Ensure a highlighter is available if possible
+ b->EnsureHighlighter();
+ if (auto *eng = b->Highlighter()) {
+ if (!eng->HasHighlighter()) {
+ // Try detect from filename and first line; fall back to cpp or existing filetype
+ std::string first_line;
+ const auto &rows = b->Rows();
+ if (!rows.empty()) first_line = static_cast(rows[0]);
+ std::string ft = kte::HighlighterRegistry::DetectForPath(b->Filename(), first_line);
+ if (!ft.empty()) {
+ eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
+ b->SetFiletype(ft);
+ eng->InvalidateFrom(0);
+ } else {
+ // Unknown/unsupported -> install a null highlighter to keep syntax enabled
+ eng->SetHighlighter(std::make_unique());
+ b->SetFiletype("");
+ eng->InvalidateFrom(0);
+ }
+ }
+ }
+ } else {
+ b->SetSyntaxEnabled(false);
+ }
+ }
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
return false;
diff --git a/GUIRenderer.cc b/GUIRenderer.cc
index d8d6d3e..e3b8f74 100644
--- a/GUIRenderer.cc
+++ b/GUIRenderer.cc
@@ -10,6 +10,8 @@
#include
#include "GUIRenderer.h"
+#include "Highlight.h"
+#include "GUITheme.h"
#include "Buffer.h"
#include "Command.h"
#include "Editor.h"
@@ -321,21 +323,50 @@ GUIRenderer::Draw(Editor &ed)
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
}
- // Emit entire line (ImGui child scrolling will handle clipping)
- for (std::size_t src = 0; src < line.size(); ++src) {
- char c = line[src];
- if (c == '\t') {
- std::size_t adv = (tabw - (rx_abs_draw % tabw));
- // Emit spaces for the tab
- expanded.append(adv, ' ');
- rx_abs_draw += adv;
- } else {
- expanded.push_back(c);
- rx_abs_draw += 1;
- }
- }
+ // Emit entire line to an expanded buffer (tabs -> spaces)
+ for (std::size_t src = 0; src < line.size(); ++src) {
+ char c = line[src];
+ if (c == '\t') {
+ std::size_t adv = (tabw - (rx_abs_draw % tabw));
+ expanded.append(adv, ' ');
+ rx_abs_draw += adv;
+ } else {
+ expanded.push_back(c);
+ rx_abs_draw += 1;
+ }
+ }
- ImGui::TextUnformatted(expanded.c_str());
+ // Draw syntax-colored runs (text above background highlights)
+ if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
+ const kte::LineHighlight &lh = buf->Highlighter()->GetLine(*buf, static_cast(i), buf->Version());
+ // Helper to convert a src column to expanded rx position
+ auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
+ std::size_t rx = 0;
+ for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
+ rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
+ }
+ return rx;
+ };
+ for (const auto &sp: lh.spans) {
+ std::size_t rx_s = src_to_rx_full(static_cast(std::max(0, sp.col_start)));
+ std::size_t rx_e = src_to_rx_full(static_cast(std::max(sp.col_start, sp.col_end)));
+ if (rx_e <= coloffs_now)
+ continue;
+ std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
+ std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
+ if (vx0 >= expanded.size()) continue;
+ vx1 = std::min(vx1, expanded.size());
+ if (vx1 <= vx0) continue;
+ ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
+ ImVec2 p = ImVec2(line_pos.x + static_cast(vx0) * space_w, line_pos.y);
+ ImGui::GetWindowDrawList()->AddText(p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
+ }
+ // We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
+ ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
+ } else {
+ // No syntax: draw as one run
+ ImGui::TextUnformatted(expanded.c_str());
+ }
// Draw a visible cursor indicator on the current line
if (i == cy) {
diff --git a/GUITheme.h b/GUITheme.h
index afc0d6c..1606c4a 100644
--- a/GUITheme.h
+++ b/GUITheme.h
@@ -1,57 +1,48 @@
-// GUITheme.h - ImGui theme configuration for kte GUI
-// Provides theme palettes and style settings (Nord + Gruvbox variants).
-
+// GUITheme.h — ImGui theming helpers and background mode
#pragma once
#include
-#include
-#include
#include
+#include
#include
+#include
+#include
+#include
-namespace kte {
-// Theme identifiers (legacy API kept for compatibility)
-enum class ThemeId {
- Nord,
- GruvboxDarkMedium,
- GruvboxLightMedium,
- EInk, // monochrome e-ink style
- Solarized, // solarized (light/dark via background)
- Plan9, // plan9-inspired minimal theme (single acme-like palette)
-};
-
-// Background mode for themes that support light/dark variants
-enum class BackgroundMode {
- Dark,
- Light,
-};
-
-// Forward declaration of registry helpers
-class Theme;
-
-static inline const std::vector > &ThemeRegistry();
-
-static inline size_t ThemeIndexFromId(ThemeId id);
-
-static inline ThemeId ThemeIdFromIndex(size_t idx);
-
-// Keep track of current theme (program-wide)
-static inline ThemeId gCurrentTheme = ThemeId::Nord;
-// Background preference (defaults to Dark)
-static inline BackgroundMode gBackgroundMode = BackgroundMode::Dark;
-// Mirror index of current theme in the registry; keep it consistent with gCurrentTheme
-// Current alphabetical order: 0=eink, 1=gruvbox, 2=nord, 3=solarized
-static inline size_t gCurrentThemeIndex = ThemeIndexFromId(gCurrentTheme);
-// Convert RGB hex (0xRRGGBB) to ImVec4 with optional alpha
-static inline ImVec4
-RGBA(unsigned int rgb, float a = 1.0f)
+// Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4
+static inline ImVec4 RGBA(unsigned int rgb, float a = 1.0f)
{
- float r = ((rgb >> 16) & 0xFF) / 255.0f;
- float g = ((rgb >> 8) & 0xFF) / 255.0f;
- float b = ((rgb) & 0xFF) / 255.0f;
- return ImVec4(r, g, b, a);
+ const float r = static_cast((rgb >> 16) & 0xFF) / 255.0f;
+ const float g = static_cast((rgb >> 8) & 0xFF) / 255.0f;
+ const float b = static_cast(rgb & 0xFF) / 255.0f;
+ return ImVec4(r, g, b, a);
}
+namespace kte {
+
+// Background mode selection for light/dark palettes
+enum class BackgroundMode { Light, Dark };
+
+// Global background mode; default to Dark to match prior defaults
+static inline BackgroundMode gBackgroundMode = BackgroundMode::Dark;
+
+// Basic theme identifier (kept minimal; some ids are aliases)
+enum class ThemeId {
+ EInk = 0,
+ GruvboxDarkMedium = 1,
+ GruvboxLightMedium = 1, // alias to unified gruvbox index
+ Nord = 2,
+ Plan9 = 3,
+ Solarized = 4,
+};
+
+// Current theme tracking
+static inline ThemeId gCurrentTheme = ThemeId::Nord;
+static inline std::size_t gCurrentThemeIndex = 0;
+
+// Forward declarations for helpers used below
+static inline size_t ThemeIndexFromId(ThemeId id);
+static inline ThemeId ThemeIdFromIndex(size_t idx);
// Helpers to set/query background mode
static inline void
@@ -1107,20 +1098,18 @@ CurrentThemeName()
static inline size_t
ThemeIndexFromId(ThemeId id)
{
- switch (id) {
- case ThemeId::EInk:
- return 0;
- case ThemeId::GruvboxDarkMedium:
- return 1;
- case ThemeId::GruvboxLightMedium: // legacy alias maps to unified gruvbox index
- return 1;
- case ThemeId::Nord:
- return 2;
- case ThemeId::Plan9:
- return 3;
- case ThemeId::Solarized:
- return 4;
- }
+ switch (id) {
+ case ThemeId::EInk:
+ return 0;
+ case ThemeId::GruvboxDarkMedium:
+ return 1;
+ case ThemeId::Nord:
+ return 2;
+ case ThemeId::Plan9:
+ return 3;
+ case ThemeId::Solarized:
+ return 4;
+ }
return 0;
}
@@ -1142,4 +1131,30 @@ ThemeIdFromIndex(size_t idx)
return ThemeId::Solarized;
}
}
+
+// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
+static inline ImVec4 SyntaxInk(TokenKind k)
+{
+ // Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
+ const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
+ // Base text
+ ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
+ switch (k) {
+ case TokenKind::Keyword: return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
+ case TokenKind::Type: return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
+ case TokenKind::String: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
+ case TokenKind::Char: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
+ case TokenKind::Comment: return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
+ case TokenKind::Number: return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
+ case TokenKind::Preproc: return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
+ case TokenKind::Constant: return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
+ case TokenKind::Function: return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
+ case TokenKind::Operator: return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
+ case TokenKind::Punctuation: return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
+ case TokenKind::Identifier: return def;
+ case TokenKind::Whitespace: return def;
+ case TokenKind::Error: return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
+ case TokenKind::Default: default: return def;
+ }
+}
} // namespace kte
diff --git a/GoHighlighter.cc b/GoHighlighter.cc
new file mode 100644
index 0000000..d63e240
--- /dev/null
+++ b/GoHighlighter.cc
@@ -0,0 +1,48 @@
+#include "GoHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static void push(std::vector &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
+static bool is_ident_start(char c){ return std::isalpha(static_cast(c)) || c=='_'; }
+static bool is_ident_char(char c){ return std::isalnum(static_cast(c)) || c=='_'; }
+
+GoHighlighter::GoHighlighter()
+{
+ const char* kw[] = {"break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"};
+ for (auto s: kw) kws_.insert(s);
+ const char* tp[] = {"bool","byte","complex64","complex128","error","float32","float64","int","int8","int16","int32","int64","rune","string","uint","uint8","uint16","uint32","uint64","uintptr"};
+ for (auto s: tp) types_.insert(s);
+}
+
+void GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+ int i = 0;
+ int bol=0; while (bol(c))) { int j=i+1; while (j(s[j]))||s[j]=='.'||s[j]=='x'||s[j]=='X'||s[j]=='_')) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
+ if (is_ident_start(c)) { int j=i+1; while (j(c))) { TokenKind k=TokenKind::Operator; if (c==';'||c==','||c=='('||c==')'||c=='{'||c=='}'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
+ push(out,i,i+1,TokenKind::Default); ++i;
+ }
+}
+
+} // namespace kte
diff --git a/GoHighlighter.h b/GoHighlighter.h
new file mode 100644
index 0000000..89ec530
--- /dev/null
+++ b/GoHighlighter.h
@@ -0,0 +1,18 @@
+// GoHighlighter.h - simple Go highlighter
+#pragma once
+
+#include "LanguageHighlighter.h"
+#include
+
+namespace kte {
+
+class GoHighlighter final : public LanguageHighlighter {
+public:
+ GoHighlighter();
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+private:
+ std::unordered_set kws_;
+ std::unordered_set types_;
+};
+
+} // namespace kte
diff --git a/Highlight.h b/Highlight.h
new file mode 100644
index 0000000..f10961b
--- /dev/null
+++ b/Highlight.h
@@ -0,0 +1,39 @@
+// Highlight.h - core syntax highlighting types for kte
+#pragma once
+
+#include
+#include
+
+namespace kte {
+
+// Token kinds shared between renderers and highlighters
+enum class TokenKind {
+ Default,
+ Keyword,
+ Type,
+ String,
+ Char,
+ Comment,
+ Number,
+ Preproc,
+ Constant,
+ Function,
+ Operator,
+ Punctuation,
+ Identifier,
+ Whitespace,
+ Error
+};
+
+struct HighlightSpan {
+ int col_start{0}; // inclusive, 0-based columns in buffer indices
+ int col_end{0}; // exclusive
+ TokenKind kind{TokenKind::Default};
+};
+
+struct LineHighlight {
+ std::vector spans;
+ std::uint64_t version{0}; // buffer version used for this line
+};
+
+} // namespace kte
diff --git a/HighlighterEngine.cc b/HighlighterEngine.cc
new file mode 100644
index 0000000..6db0b56
--- /dev/null
+++ b/HighlighterEngine.cc
@@ -0,0 +1,94 @@
+#include "HighlighterEngine.h"
+#include "Buffer.h"
+#include "LanguageHighlighter.h"
+
+namespace kte {
+
+HighlighterEngine::HighlighterEngine() = default;
+HighlighterEngine::~HighlighterEngine() = default;
+
+void
+HighlighterEngine::SetHighlighter(std::unique_ptr hl)
+{
+ hl_ = std::move(hl);
+ cache_.clear();
+ state_cache_.clear();
+}
+
+const LineHighlight &
+HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
+{
+ auto it = cache_.find(row);
+ if (it != cache_.end()) {
+ if (it->second.version == buf_version) {
+ return it->second;
+ }
+ }
+ LineHighlight updated;
+ updated.version = buf_version;
+ updated.spans.clear();
+ if (!hl_) {
+ auto &slot = cache_[row];
+ slot = std::move(updated);
+ return cache_[row];
+ }
+
+ if (auto *stateful = dynamic_cast(hl_.get())) {
+ // Find nearest cached state at or before row-1 with matching version
+ StatefulHighlighter::LineState prev_state;
+ int start_row = -1;
+ if (!state_cache_.empty()) {
+ // linear search over map (unordered), track best candidate
+ int best = -1;
+ for (const auto &kv : state_cache_) {
+ int r = kv.first;
+ if (r <= row - 1 && kv.second.version == buf_version) {
+ if (r > best) {
+ best = r;
+ }
+ }
+ }
+ if (best >= 0) {
+ start_row = best;
+ prev_state = state_cache_.at(best).state;
+ }
+ }
+
+ // Walk from start_row+1 up to row computing states; only collect spans at the target row
+ for (int r = start_row + 1; r <= row; ++r) {
+ std::vector tmp;
+ std::vector &out = (r == row) ? updated.spans : tmp;
+ auto next_state = stateful->HighlightLineStateful(buf, r, prev_state, out);
+ // store state for this row (state after finishing r)
+ StateEntry se;
+ se.version = buf_version;
+ se.state = next_state;
+ state_cache_[r] = se;
+ prev_state = next_state;
+ }
+ } else {
+ // Stateless path
+ hl_->HighlightLine(buf, row, updated.spans);
+ }
+
+ auto &slot = cache_[row];
+ slot = std::move(updated);
+ return cache_[row];
+}
+
+void
+HighlighterEngine::InvalidateFrom(int row)
+{
+ if (cache_.empty()) return;
+ // Simple implementation: erase all rows >= row
+ for (auto it = cache_.begin(); it != cache_.end(); ) {
+ if (it->first >= row) it = cache_.erase(it); else ++it;
+ }
+ if (!state_cache_.empty()) {
+ for (auto it = state_cache_.begin(); it != state_cache_.end(); ) {
+ if (it->first >= row) it = state_cache_.erase(it); else ++it;
+ }
+ }
+}
+
+} // namespace kte
diff --git a/HighlighterEngine.h b/HighlighterEngine.h
new file mode 100644
index 0000000..544b6cf
--- /dev/null
+++ b/HighlighterEngine.h
@@ -0,0 +1,45 @@
+// HighlighterEngine.h - caching layer for per-line highlights
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "Highlight.h"
+#include "LanguageHighlighter.h"
+
+class Buffer;
+
+namespace kte {
+
+class HighlighterEngine {
+public:
+ HighlighterEngine();
+ ~HighlighterEngine();
+
+ void SetHighlighter(std::unique_ptr hl);
+
+ // Retrieve highlights for a given line and buffer version.
+ // If cache is stale, recompute using the current highlighter.
+ const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
+
+ // Invalidate cached lines from row (inclusive)
+ void InvalidateFrom(int row);
+
+ bool HasHighlighter() const { return static_cast(hl_); }
+
+private:
+ std::unique_ptr hl_;
+ // Simple cache by row index (mutable to allow caching in const GetLine)
+ mutable std::unordered_map cache_;
+ // For stateful highlighters, remember per-line state (state after finishing that row)
+ struct StateEntry {
+ std::uint64_t version{0};
+ // Using the interface type; forward-declare via header
+ StatefulHighlighter::LineState state;
+ };
+ mutable std::unordered_map state_cache_;
+};
+
+} // namespace kte
diff --git a/HighlighterRegistry.cc b/HighlighterRegistry.cc
new file mode 100644
index 0000000..66141fd
--- /dev/null
+++ b/HighlighterRegistry.cc
@@ -0,0 +1,93 @@
+#include "HighlighterRegistry.h"
+#include "CppHighlighter.h"
+
+#include
+#include
+
+// Forward declare simple highlighters implemented in this project
+namespace kte {
+class JSONHighlighter; class MarkdownHighlighter; class ShellHighlighter;
+class GoHighlighter; class PythonHighlighter; class RustHighlighter; class LispHighlighter;
+}
+
+// Headers for the above
+#include "JsonHighlighter.h"
+#include "MarkdownHighlighter.h"
+#include "ShellHighlighter.h"
+#include "GoHighlighter.h"
+#include "PythonHighlighter.h"
+#include "RustHighlighter.h"
+#include "LispHighlighter.h"
+
+namespace kte {
+
+static std::string to_lower(std::string_view s) {
+ std::string r(s);
+ std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); });
+ return r;
+}
+
+std::string HighlighterRegistry::Normalize(std::string_view ft)
+{
+ std::string f = to_lower(ft);
+ if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx") return "cpp";
+ if (f == "cpp") return "cpp";
+ if (f == "json") return "json";
+ if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown") return "markdown";
+ if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish") return "shell";
+ if (f == "go" || f == "golang") return "go";
+ if (f == "py" || f == "python") return "python";
+ if (f == "rs" || f == "rust") return "rust";
+ if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f == "cl") return "lisp";
+ return f;
+}
+
+std::unique_ptr HighlighterRegistry::CreateFor(std::string_view filetype)
+{
+ std::string ft = Normalize(filetype);
+ if (ft == "cpp") return std::make_unique();
+ if (ft == "json") return std::make_unique();
+ if (ft == "markdown") return std::make_unique();
+ if (ft == "shell") return std::make_unique();
+ if (ft == "go") return std::make_unique();
+ if (ft == "python") return std::make_unique();
+ if (ft == "rust") return std::make_unique();
+ if (ft == "lisp") return std::make_unique();
+ return nullptr;
+}
+
+static std::string shebang_to_ft(std::string_view first_line) {
+ if (first_line.size() < 2 || first_line.substr(0,2) != "#!") return "";
+ std::string low = to_lower(first_line);
+ if (low.find("python") != std::string::npos) return "python";
+ if (low.find("bash") != std::string::npos) return "shell";
+ if (low.find("sh") != std::string::npos) return "shell";
+ if (low.find("zsh") != std::string::npos) return "shell";
+ if (low.find("fish") != std::string::npos) return "shell";
+ if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") != std::string::npos) return "lisp";
+ return "";
+}
+
+std::string HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
+{
+ // Extension
+ std::string p(path);
+ std::error_code ec;
+ std::string ext = std::filesystem::path(p).extension().string();
+ for (auto &ch: ext) ch = static_cast(std::tolower(static_cast(ch)));
+ if (!ext.empty()) {
+ if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext == ".hh") return "cpp";
+ if (ext == ".json") return "json";
+ if (ext == ".md" || ext == ".markdown" || ext == ".mkd") return "markdown";
+ if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish") return "shell";
+ if (ext == ".go") return "go";
+ if (ext == ".py") return "python";
+ if (ext == ".rs") return "rust";
+ if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc" || ext == ".cl") return "lisp";
+ }
+ // Shebang
+ std::string ft = shebang_to_ft(first_line);
+ return ft;
+}
+
+} // namespace kte
diff --git a/HighlighterRegistry.h b/HighlighterRegistry.h
new file mode 100644
index 0000000..f4985bd
--- /dev/null
+++ b/HighlighterRegistry.h
@@ -0,0 +1,26 @@
+// HighlighterRegistry.h - create/detect language highlighters
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "LanguageHighlighter.h"
+
+namespace kte {
+
+class HighlighterRegistry {
+public:
+ // Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
+ static std::unique_ptr CreateFor(std::string_view filetype);
+
+ // Detect filetype by path extension and shebang (first line).
+ // Returns normalized id or empty string if unknown.
+ static std::string DetectForPath(std::string_view path, std::string_view first_line);
+
+ // Normalize various aliases/extensions to canonical ids.
+ static std::string Normalize(std::string_view ft);
+};
+
+} // namespace kte
diff --git a/JsonHighlighter.cc b/JsonHighlighter.cc
new file mode 100644
index 0000000..693d3ff
--- /dev/null
+++ b/JsonHighlighter.cc
@@ -0,0 +1,42 @@
+#include "JsonHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static bool is_digit(char c) { return c >= '0' && c <= '9'; }
+
+void JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+ auto push = [&](int a, int b, TokenKind k){ if (b> a) out.push_back({a,b,k}); };
+
+ int i = 0;
+ while (i < n) {
+ char c = s[i];
+ if (c == ' ' || c == '\t') { int j=i+1; while (j(s[j]))||s[j]=='.'||s[j]=='e'||s[j]=='E'||s[j]=='+'||s[j]=='-'||s[j]=='_')) ++j; push(i,j,TokenKind::Number); i=j; continue;
+ }
+ // booleans/null
+ if (std::isalpha(static_cast(c))) {
+ int j=i+1; while (j(s[j]))) ++j;
+ std::string id = s.substr(i, j-i);
+ if (id == "true" || id == "false" || id == "null") push(i,j,TokenKind::Constant); else push(i,j,TokenKind::Identifier);
+ i=j; continue;
+ }
+ // punctuation
+ if (c=='{'||c=='}'||c=='['||c==']'||c==','||c==':' ) { push(i,i+1,TokenKind::Punctuation); ++i; continue; }
+ // fallback
+ push(i,i+1,TokenKind::Default); ++i;
+ }
+}
+
+} // namespace kte
diff --git a/JsonHighlighter.h b/JsonHighlighter.h
new file mode 100644
index 0000000..314619f
--- /dev/null
+++ b/JsonHighlighter.h
@@ -0,0 +1,14 @@
+// JsonHighlighter.h - simple JSON line highlighter
+#pragma once
+
+#include "LanguageHighlighter.h"
+#include
+
+namespace kte {
+
+class JSONHighlighter final : public LanguageHighlighter {
+public:
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+};
+
+} // namespace kte
diff --git a/LanguageHighlighter.h b/LanguageHighlighter.h
new file mode 100644
index 0000000..cdd0d45
--- /dev/null
+++ b/LanguageHighlighter.h
@@ -0,0 +1,43 @@
+// LanguageHighlighter.h - interface for line-based highlighters
+#pragma once
+
+#include
+#include
+#include
+
+#include "Highlight.h"
+
+class Buffer;
+
+namespace kte {
+
+class LanguageHighlighter {
+public:
+ virtual ~LanguageHighlighter() = default;
+ // Produce highlight spans for a given buffer row. Implementations should append to out.
+ virtual void HighlightLine(const Buffer &buf, int row, std::vector &out) const = 0;
+ virtual bool Stateful() const { return false; }
+};
+
+// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
+// Engines may detect and use this via dynamic_cast without breaking stateless impls.
+class StatefulHighlighter : public LanguageHighlighter {
+public:
+ struct LineState {
+ bool in_block_comment{false};
+ bool in_raw_string{false};
+ // For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
+ std::string raw_delim;
+ };
+
+ // Highlight one line given the previous line state; return the resulting state after this line.
+ // Implementations should append spans for this line to out and compute the next state.
+ virtual LineState HighlightLineStateful(const Buffer &buf,
+ int row,
+ const LineState &prev,
+ std::vector &out) const = 0;
+
+ bool Stateful() const override { return true; }
+};
+
+} // namespace kte
diff --git a/LispHighlighter.cc b/LispHighlighter.cc
new file mode 100644
index 0000000..e8b0763
--- /dev/null
+++ b/LispHighlighter.cc
@@ -0,0 +1,41 @@
+#include "LispHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static void push(std::vector &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
+
+LispHighlighter::LispHighlighter()
+{
+ const char* kw[] = {"defun","lambda","let","let*","define","set!","if","cond","begin","quote","quasiquote","unquote","unquote-splicing","loop","do","and","or","not"};
+ for (auto s: kw) kws_.insert(s);
+}
+
+void LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+ int i = 0;
+ int bol = 0; while (bol0) push(out,0,bol,TokenKind::Whitespace); return; }
+ while (i < n) {
+ char c = s[i];
+ if (c==' '||c=='\t') { int j=i+1; while (j(c)) || c=='*' || c=='-' || c=='+' || c=='/' || c=='_' ) {
+ int j=i+1; while (j(s[j])) || s[j]=='*' || s[j]=='-' || s[j]=='+' || s[j]=='/' || s[j]=='_' || s[j]=='!')) ++j;
+ std::string id=s.substr(i,j-i);
+ TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
+ push(out,i,j,k); i=j; continue;
+ }
+ if (std::isdigit(static_cast(c))) { int j=i+1; while (j(s[j]))||s[j]=='.')) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
+ if (std::ispunct(static_cast(c))) { TokenKind k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
+ push(out,i,i+1,TokenKind::Default); ++i;
+ }
+}
+
+} // namespace kte
diff --git a/LispHighlighter.h b/LispHighlighter.h
new file mode 100644
index 0000000..35f6b52
--- /dev/null
+++ b/LispHighlighter.h
@@ -0,0 +1,17 @@
+// LispHighlighter.h - simple Lisp/Scheme family highlighter
+#pragma once
+
+#include "LanguageHighlighter.h"
+#include
+
+namespace kte {
+
+class LispHighlighter final : public LanguageHighlighter {
+public:
+ LispHighlighter();
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+private:
+ std::unordered_set kws_;
+};
+
+} // namespace kte
diff --git a/MarkdownHighlighter.cc b/MarkdownHighlighter.cc
new file mode 100644
index 0000000..6314dfb
--- /dev/null
+++ b/MarkdownHighlighter.cc
@@ -0,0 +1,88 @@
+#include "MarkdownHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static void push_span(std::vector &out, int a, int b, TokenKind k) {
+ if (b > a) out.push_back({a,b,k});
+}
+
+void MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ LineState st; // not used in stateless entry
+ (void)HighlightLineStateful(buf, row, st, out);
+}
+
+StatefulHighlighter::LineState MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector &out) const
+{
+ StatefulHighlighter::LineState state = prev;
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return state;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+
+ // Reuse in_block_comment flag as "in fenced code" state.
+ if (state.in_block_comment) {
+ // If line contains closing fence ``` then close after it
+ auto pos = s.find("```");
+ if (pos == std::string::npos) {
+ push_span(out, 0, n, TokenKind::String);
+ state.in_block_comment = true;
+ return state;
+ } else {
+ int end = static_cast(pos + 3);
+ push_span(out, 0, end, TokenKind::String);
+ // rest of line processed normally after fence
+ int i = end;
+ // whitespace
+ if (i < n) push_span(out, i, n, TokenKind::Default);
+ state.in_block_comment = false;
+ return state;
+ }
+ }
+
+ // Detect fenced code block start at beginning (allow leading spaces)
+ int bol = 0; while (bol < n && (s[bol]==' '||s[bol]=='\t')) ++bol;
+ if (bol + 3 <= n && s.compare(bol, 3, "```") == 0) {
+ push_span(out, bol, n, TokenKind::String);
+ state.in_block_comment = true; // enter fenced mode
+ return state;
+ }
+
+ // Headings: lines starting with 1-6 '#'
+ if (bol < n && s[bol] == '#') {
+ int j = bol; while (j < n && s[j] == '#') ++j; // hashes
+ // include following space and text as Keyword to stand out
+ push_span(out, bol, n, TokenKind::Keyword);
+ return state;
+ }
+
+ // Process inline: emphasis and code spans
+ int i = 0;
+ while (i < n) {
+ char c = s[i];
+ if (c == '`') {
+ int j = i + 1; while (j < n && s[j] != '`') ++j; if (j < n) ++j;
+ push_span(out, i, j, TokenKind::String); i = j; continue;
+ }
+ if (c == '*' || c == '_') {
+ // bold/italic markers: treat the marker and until next same marker as Type to highlight
+ char m = c; int j = i + 1; while (j < n && s[j] != m) ++j; if (j < n) ++j;
+ push_span(out, i, j, TokenKind::Type); i = j; continue;
+ }
+ // links []() minimal: treat [text](url) as Function
+ if (c == '[') {
+ int j = i + 1; while (j < n && s[j] != ']') ++j; if (j < n) ++j; // include ]
+ if (j < n && s[j] == '(') { while (j < n && s[j] != ')') ++j; if (j < n) ++j; }
+ push_span(out, i, j, TokenKind::Function); i = j; continue;
+ }
+ // whitespace
+ if (c == ' ' || c == '\t') { int j=i+1; while (j &out) const override;
+ LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector &out) const override;
+};
+
+} // namespace kte
diff --git a/NullHighlighter.cc b/NullHighlighter.cc
new file mode 100644
index 0000000..9a59036
--- /dev/null
+++ b/NullHighlighter.cc
@@ -0,0 +1,16 @@
+#include "NullHighlighter.h"
+#include "Buffer.h"
+
+namespace kte {
+
+void NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+ if (n <= 0) return;
+ out.push_back({0, n, TokenKind::Default});
+}
+
+} // namespace kte
diff --git a/NullHighlighter.h b/NullHighlighter.h
new file mode 100644
index 0000000..64e82d0
--- /dev/null
+++ b/NullHighlighter.h
@@ -0,0 +1,13 @@
+// NullHighlighter.h - default highlighter that emits a single Default span per line
+#pragma once
+
+#include "LanguageHighlighter.h"
+
+namespace kte {
+
+class NullHighlighter final : public LanguageHighlighter {
+public:
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+};
+
+} // namespace kte
diff --git a/PythonHighlighter.cc b/PythonHighlighter.cc
new file mode 100644
index 0000000..ffc9235
--- /dev/null
+++ b/PythonHighlighter.cc
@@ -0,0 +1,85 @@
+#include "PythonHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static void push(std::vector &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
+static bool is_ident_start(char c){ return std::isalpha(static_cast(c)) || c=='_'; }
+static bool is_ident_char(char c){ return std::isalnum(static_cast(c)) || c=='_'; }
+
+PythonHighlighter::PythonHighlighter()
+{
+ const char* kw[] = {"and","as","assert","break","class","continue","def","del","elif","else","except","False","finally","for","from","global","if","import","in","is","lambda","None","nonlocal","not","or","pass","raise","return","True","try","while","with","yield"};
+ for (auto s: kw) kws_.insert(s);
+}
+
+void PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ LineState st; (void)HighlightLineStateful(buf, row, st, out);
+}
+
+StatefulHighlighter::LineState PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector &out) const
+{
+ StatefulHighlighter::LineState state = prev;
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return state;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+
+ // Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
+ if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
+ auto pos = s.find(state.raw_delim);
+ if (pos == std::string::npos) {
+ push(out, 0, n, TokenKind::String);
+ return state; // still inside
+ } else {
+ int end = static_cast(pos + static_cast(state.raw_delim.size()));
+ push(out, 0, end, TokenKind::String);
+ // remainder processed normally
+ s = s.substr(end);
+ n = static_cast(s.size());
+ state.in_raw_string = false; state.raw_delim.clear();
+ // Continue parsing remainder as a separate small loop
+ int base = end; // original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
+ // For simplicity, mark rest as Default
+ if (n>0) push(out, base, base + n, TokenKind::Default);
+ return state;
+ }
+ }
+
+ int i = 0;
+ // Detect comment start '#', ignoring inside strings
+ while (i < n) {
+ char c = s[i];
+ if (c==' '||c=='\t') { int j=i+1; while (j(j));
+ if (pos == std::string::npos) {
+ push(out,i,n,TokenKind::String);
+ state.in_raw_string = true; state.raw_delim = delim; return state;
+ } else {
+ int end = static_cast(pos + 3);
+ push(out,i,end,TokenKind::String); i=end; continue;
+ }
+ } else {
+ int j=i+1; bool esc=false; while (j(c))) { int j=i+1; while (j(s[j]))||s[j]=='.'||s[j]=='_' )) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
+ if (is_ident_start(c)) { int j=i+1; while (j(c))) { TokenKind k=TokenKind::Operator; if (c==':'||c==','||c=='('||c==')'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
+ push(out,i,i+1,TokenKind::Default); ++i;
+ }
+ return state;
+}
+
+} // namespace kte
diff --git a/PythonHighlighter.h b/PythonHighlighter.h
new file mode 100644
index 0000000..d7d2d3c
--- /dev/null
+++ b/PythonHighlighter.h
@@ -0,0 +1,18 @@
+// PythonHighlighter.h - simple Python highlighter with triple-quote state
+#pragma once
+
+#include "LanguageHighlighter.h"
+#include
+
+namespace kte {
+
+class PythonHighlighter final : public StatefulHighlighter {
+public:
+ PythonHighlighter();
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+ LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector &out) const override;
+private:
+ std::unordered_set kws_;
+};
+
+} // namespace kte
diff --git a/RustHighlighter.cc b/RustHighlighter.cc
new file mode 100644
index 0000000..8e51007
--- /dev/null
+++ b/RustHighlighter.cc
@@ -0,0 +1,39 @@
+#include "RustHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static void push(std::vector &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
+static bool is_ident_start(char c){ return std::isalpha(static_cast(c)) || c=='_'; }
+static bool is_ident_char(char c){ return std::isalnum(static_cast(c)) || c=='_'; }
+
+RustHighlighter::RustHighlighter()
+{
+ const char* kw[] = {"as","break","const","continue","crate","else","enum","extern","false","fn","for","if","impl","in","let","loop","match","mod","move","mut","pub","ref","return","self","Self","static","struct","super","trait","true","type","unsafe","use","where","while","dyn","async","await","try"};
+ for (auto s: kw) kws_.insert(s);
+ const char* tp[] = {"u8","u16","u32","u64","u128","usize","i8","i16","i32","i64","i128","isize","f32","f64","bool","char","str"};
+ for (auto s: tp) types_.insert(s);
+}
+
+void RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+ int i = 0;
+ while (i < n) {
+ char c = s[i];
+ if (c==' '||c=='\t') { int j=i+1; while (j(c))) { int j=i+1; while (j(s[j]))||s[j]=='.'||s[j]=='_' )) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
+ if (is_ident_start(c)) { int j=i+1; while (j(c))) { TokenKind k=TokenKind::Operator; if (c==';'||c==','||c=='('||c==')'||c=='{'||c=='}'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
+ push(out,i,i+1,TokenKind::Default); ++i;
+ }
+}
+
+} // namespace kte
diff --git a/RustHighlighter.h b/RustHighlighter.h
new file mode 100644
index 0000000..6b75c77
--- /dev/null
+++ b/RustHighlighter.h
@@ -0,0 +1,18 @@
+// RustHighlighter.h - simple Rust highlighter
+#pragma once
+
+#include "LanguageHighlighter.h"
+#include
+
+namespace kte {
+
+class RustHighlighter final : public LanguageHighlighter {
+public:
+ RustHighlighter();
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+private:
+ std::unordered_set kws_;
+ std::unordered_set types_;
+};
+
+} // namespace kte
diff --git a/ShellHighlighter.cc b/ShellHighlighter.cc
new file mode 100644
index 0000000..2061601
--- /dev/null
+++ b/ShellHighlighter.cc
@@ -0,0 +1,43 @@
+#include "ShellHighlighter.h"
+#include "Buffer.h"
+#include
+
+namespace kte {
+
+static void push(std::vector &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
+
+void ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector &out) const
+{
+ const auto &rows = buf.Rows();
+ if (row < 0 || static_cast(row) >= rows.size()) return;
+ std::string s = static_cast(rows[static_cast(row)]);
+ int n = static_cast(s.size());
+ int i = 0;
+ // if first non-space is '#', whole line is comment
+ int bol = 0; while (bol < n && (s[bol]==' '||s[bol]=='\t')) ++bol;
+ if (bol < n && s[bol] == '#') { push(out, bol, n, TokenKind::Comment); if (bol>0) push(out,0,bol,TokenKind::Whitespace); return; }
+ while (i < n) {
+ char c = s[i];
+ if (c == ' ' || c == '\t') { int j=i+1; while (j(c))) {
+ int j=i+1; while (j(s[j]))||s[j]=='_')) ++j; std::string id=s.substr(i,j-i);
+ static const char* kws[] = {"if","then","fi","for","in","do","done","case","esac","while","function","elif","else"};
+ bool kw=false; for (auto k: kws) if (id==k) { kw=true; break; }
+ push(out,i,j, kw?TokenKind::Keyword:TokenKind::Identifier); i=j; continue;
+ }
+ if (std::ispunct(static_cast(c))) {
+ TokenKind k = TokenKind::Operator;
+ if (c=='('||c==')'||c=='{'||c=='}'||c==','||c==';') k=TokenKind::Punctuation;
+ push(out,i,i+1,k); ++i; continue;
+ }
+ push(out,i,i+1,TokenKind::Default); ++i;
+ }
+}
+
+} // namespace kte
diff --git a/ShellHighlighter.h b/ShellHighlighter.h
new file mode 100644
index 0000000..c819bb7
--- /dev/null
+++ b/ShellHighlighter.h
@@ -0,0 +1,13 @@
+// ShellHighlighter.h - simple POSIX shell highlighter
+#pragma once
+
+#include "LanguageHighlighter.h"
+
+namespace kte {
+
+class ShellHighlighter final : public LanguageHighlighter {
+public:
+ void HighlightLine(const Buffer &buf, int row, std::vector &out) const override;
+};
+
+} // namespace kte
diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc
index 860fade..a8f4423 100644
--- a/TerminalRenderer.cc
+++ b/TerminalRenderer.cc
@@ -9,6 +9,7 @@
#include "TerminalRenderer.h"
#include "Buffer.h"
#include "Editor.h"
+#include "Highlight.h"
// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
@@ -97,13 +98,49 @@ TerminalRenderer::Draw(Editor &ed)
bool hl_on = false;
bool cur_on = false;
int written = 0;
- if (li < lines.size()) {
- std::string line = static_cast(lines[li]);
- src_i = 0;
- render_col = 0;
- while (written < cols) {
- char ch = ' ';
- bool from_src = false;
+ if (li < lines.size()) {
+ std::string line = static_cast(lines[li]);
+ src_i = 0;
+ render_col = 0;
+ // Syntax highlighting: fetch per-line spans
+ const kte::LineHighlight *lh_ptr = nullptr;
+ if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
+ lh_ptr = &buf->Highlighter()->GetLine(*buf, static_cast(li), buf->Version());
+ }
+ auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
+ if (!lh_ptr) return kte::TokenKind::Default;
+ for (const auto &sp: lh_ptr->spans) {
+ if (static_cast(src_index) >= sp.col_start && static_cast(src_index) < sp.col_end)
+ return sp.kind;
+ }
+ return kte::TokenKind::Default;
+ };
+ auto apply_token_attr = [&](kte::TokenKind k) {
+ // Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
+ attrset(A_NORMAL);
+ switch (k) {
+ case kte::TokenKind::Keyword:
+ case kte::TokenKind::Type:
+ case kte::TokenKind::Constant:
+ case kte::TokenKind::Function:
+ attron(A_BOLD);
+ break;
+ case kte::TokenKind::Comment:
+ attron(A_DIM);
+ break;
+ case kte::TokenKind::String:
+ case kte::TokenKind::Char:
+ case kte::TokenKind::Number:
+ // standout a bit using A_UNDERLINE if available
+ attron(A_UNDERLINE);
+ break;
+ default:
+ break;
+ }
+ };
+ while (written < cols) {
+ char ch = ' ';
+ bool from_src = false;
if (src_i < line.size()) {
unsigned char c = static_cast(line[src_i]);
if (c == '\t') {
@@ -122,41 +159,45 @@ TerminalRenderer::Draw(Editor &ed)
next_tab -= to_skip;
}
// Now render visible spaces
- while (next_tab > 0 && written < cols) {
- bool in_hl = search_mode && is_src_in_hl(src_i);
- bool in_cur =
- has_current && li == cur_my && src_i >= cur_mx
- && src_i < cur_mend;
- // Toggle highlight attributes
- int attr = 0;
- if (in_hl)
- attr |= A_STANDOUT;
- if (in_cur)
- attr |= A_BOLD;
- if ((attr & A_STANDOUT) && !hl_on) {
- attron(A_STANDOUT);
- hl_on = true;
- }
- if (!(attr & A_STANDOUT) && hl_on) {
- attroff(A_STANDOUT);
- hl_on = false;
- }
- if ((attr & A_BOLD) && !cur_on) {
- attron(A_BOLD);
- cur_on = true;
- }
- if (!(attr & A_BOLD) && cur_on) {
- attroff(A_BOLD);
- cur_on = false;
- }
- addch(' ');
- ++written;
- ++render_col;
- --next_tab;
- }
- ++src_i;
- continue;
- } else {
+ while (next_tab > 0 && written < cols) {
+ bool in_hl = search_mode && is_src_in_hl(src_i);
+ bool in_cur =
+ has_current && li == cur_my && src_i >= cur_mx
+ && src_i < cur_mend;
+ // Toggle highlight attributes
+ int attr = 0;
+ if (in_hl)
+ attr |= A_STANDOUT;
+ if (in_cur)
+ attr |= A_BOLD;
+ if ((attr & A_STANDOUT) && !hl_on) {
+ attron(A_STANDOUT);
+ hl_on = true;
+ }
+ if (!(attr & A_STANDOUT) && hl_on) {
+ attroff(A_STANDOUT);
+ hl_on = false;
+ }
+ if ((attr & A_BOLD) && !cur_on) {
+ attron(A_BOLD);
+ cur_on = true;
+ }
+ if (!(attr & A_BOLD) && cur_on) {
+ attroff(A_BOLD);
+ cur_on = false;
+ }
+ // Apply syntax attribute only if not in search highlight
+ if (!in_hl) {
+ apply_token_attr(token_at(src_i));
+ }
+ addch(' ');
+ ++written;
+ ++render_col;
+ --next_tab;
+ }
+ ++src_i;
+ continue;
+ } else {
// normal char
if (render_col < coloffs) {
++render_col;
@@ -171,45 +212,49 @@ TerminalRenderer::Draw(Editor &ed)
ch = ' ';
from_src = false;
}
- bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
- bool in_cur =
- has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
- cur_mend;
- if (in_hl && !hl_on) {
- attron(A_STANDOUT);
- hl_on = true;
- }
- if (!in_hl && hl_on) {
- attroff(A_STANDOUT);
- hl_on = false;
- }
- if (in_cur && !cur_on) {
- attron(A_BOLD);
- cur_on = true;
- }
- if (!in_cur && cur_on) {
- attroff(A_BOLD);
- cur_on = false;
- }
- addch(static_cast(ch));
- ++written;
- ++render_col;
- if (from_src)
- ++src_i;
+ bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
+ bool in_cur =
+ has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
+ cur_mend;
+ if (in_hl && !hl_on) {
+ attron(A_STANDOUT);
+ hl_on = true;
+ }
+ if (!in_hl && hl_on) {
+ attroff(A_STANDOUT);
+ hl_on = false;
+ }
+ if (in_cur && !cur_on) {
+ attron(A_BOLD);
+ cur_on = true;
+ }
+ if (!in_cur && cur_on) {
+ attroff(A_BOLD);
+ cur_on = false;
+ }
+ if (!in_hl && from_src) {
+ apply_token_attr(token_at(src_i));
+ }
+ addch(static_cast(ch));
+ ++written;
+ ++render_col;
+ if (from_src)
+ ++src_i;
if (src_i >= line.size() && written >= cols)
break;
}
}
- if (hl_on) {
- attroff(A_STANDOUT);
- hl_on = false;
- }
- if (cur_on) {
- attroff(A_BOLD);
- cur_on = false;
- }
- clrtoeol();
- }
+ if (hl_on) {
+ attroff(A_STANDOUT);
+ hl_on = false;
+ }
+ if (cur_on) {
+ attroff(A_BOLD);
+ cur_on = false;
+ }
+ attrset(A_NORMAL);
+ clrtoeol();
+ }
// Place terminal cursor at logical position accounting for tabs and coloffs
std::size_t cy = buf->Cury();
diff --git a/docs/syntax.md b/docs/syntax.md
new file mode 100644
index 0000000..6d55ddc
--- /dev/null
+++ b/docs/syntax.md
@@ -0,0 +1,52 @@
+Syntax highlighting in kte
+==========================
+
+Overview
+--------
+
+kte provides lightweight syntax highlighting with a pluggable highlighter interface. The initial implementation targets C/C++ and focuses on speed and responsiveness.
+
+Core types
+----------
+
+- `TokenKind` — token categories (keywords, types, strings, comments, numbers, preprocessor, operators, punctuation, identifiers, whitespace, etc.).
+- `HighlightSpan` — a half-open column range `[col_start, col_end)` with a `TokenKind`.
+- `LineHighlight` — a vector of `HighlightSpan` and the buffer `version` used to compute it.
+
+Engine and caching
+------------------
+
+- `HighlighterEngine` maintains a per-line cache of `LineHighlight` keyed by row and buffer version.
+- Cache invalidation occurs when the buffer version changes or when the buffer calls `InvalidateFrom(row)`, which clears cached lines and line states from `row` downward.
+- The engine supports both stateless and stateful highlighters. For stateful highlighters, it memoizes a simple per-line state and computes lines sequentially when necessary.
+
+Stateful highlighters
+---------------------
+
+- `LanguageHighlighter` is the base interface for stateless per-line tokenization.
+- `StatefulHighlighter` extends it with a `LineState` and the method `HighlightLineStateful(buf, row, prev_state, out)`.
+- The engine detects `StatefulHighlighter` via dynamic_cast and feeds each line the previous line’s state, caching the resulting state per line.
+
+C/C++ highlighter
+-----------------
+
+- `CppHighlighter` implements `StatefulHighlighter`.
+- Stateless constructs: line comments `//`, strings `"..."`, chars `'...'`, numbers, identifiers (keywords/types), preprocessor at beginning of line after leading whitespace, operators/punctuation, and whitespace.
+- Stateful constructs (v2):
+ - Multi-line block comments `/* ... */` — the state records whether the next line continues a comment.
+ - Raw strings `R"delim(... )delim"` — the state tracks whether we are inside a raw string and its delimiter `delim` until the closing sequence appears.
+
+Limitations and TODOs
+---------------------
+
+- Raw string detection is intentionally simple and does not handle all corner cases of the C++ standard.
+- Preprocessor handling is line-based; continuation lines with `\\` are not yet tracked.
+- No semantic analysis; identifiers are classified via small keyword/type sets.
+- Additional languages (JSON, Markdown, Shell, Python, Go, Rust, Lisp, …) are planned.
+- Terminal color mapping is conservative to support 8/16-color terminals. Rich color-pair themes can be added later.
+
+Renderer integration
+--------------------
+
+- Terminal and GUI renderers request line spans via `Highlighter()->GetLine(buf, row, buf.Version())`.
+- Search highlight and cursor overlays take precedence over syntax colors.
diff --git a/kte-cloc b/kte-cloc
index 08bc3b1..865d039 100755
--- a/kte-cloc
+++ b/kte-cloc
@@ -1,3 +1,9 @@
#!/usr/bin/env bash
-ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc -fmt 3
+fmt_arg=""
+if [ "${V}" = "1" ]
+then
+ fmt_args="-fmt 3"
+fi
+
+ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc ${fmt_args}