This is page 2 of 8. Use http://codebase.md/moisnx/arc?page={x} to view the full context. # Directory Structure ``` ├── .clang-format ├── .config │ └── arceditor │ ├── config.yaml │ ├── keybinds.conf │ └── themes │ ├── catppuccin-mocha.theme │ ├── cyberpunk-neon.theme │ ├── default.theme │ ├── dracula.theme │ ├── github_dark.theme │ ├── gruvbox_dark.theme │ ├── gruvbox_light.theme │ ├── high_constrast_dark.theme │ ├── monokai.theme │ ├── onedark.theme │ ├── solarized_dark.theme │ ├── solarized_light.theme │ ├── tokyo_night.theme │ └── vscode_light.theme ├── .github │ └── assets │ └── screenshot.gif ├── .gitignore ├── .gitmessage ├── .gitmodules ├── build.md ├── CMakeLists.txt ├── deps │ └── tree-sitter-markdown │ ├── .editorconfig │ ├── .gitattributes │ ├── .github │ │ ├── screenshot.png │ │ └── workflows │ │ ├── ci.yml │ │ ├── publish.yml │ │ └── release.yml │ ├── .gitignore │ ├── binding.gyp │ ├── bindings │ │ ├── go │ │ │ ├── binding_test.go │ │ │ ├── markdown_inline.go │ │ │ └── markdown.go │ │ ├── node │ │ │ ├── binding_test.js │ │ │ ├── binding.cc │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── inline.js │ │ ├── python │ │ │ ├── tests │ │ │ │ └── test_binding.py │ │ │ └── tree_sitter_markdown │ │ │ ├── __init__.py │ │ │ ├── __init__.pyi │ │ │ ├── binding.c │ │ │ └── py.typed │ │ ├── rust │ │ │ ├── benchmark.rs │ │ │ ├── build.rs │ │ │ ├── lib.rs │ │ │ └── parser.rs │ │ └── swift │ │ ├── .gitignore │ │ └── TreeSitterMarkdownTests │ │ └── TreeSitterMarkdownTests.swift │ ├── Cargo.toml │ ├── CMakeLists.txt │ ├── common │ │ ├── common.js │ │ ├── common.mak │ │ └── html_entities.json │ ├── CONTRIBUTING.md │ ├── go.mod │ ├── LICENSE │ ├── Makefile │ ├── package-lock.json │ ├── package.json │ ├── Package.resolved │ ├── Package.swift │ ├── pyproject.toml │ ├── README.md │ ├── scripts │ │ ├── build.js │ │ └── test.js │ ├── setup.py │ ├── tree-sitter-markdown │ │ ├── bindings │ │ │ ├── c │ │ │ │ ├── tree-sitter-markdown.h │ │ │ │ └── tree-sitter-markdown.pc.in │ │ │ └── swift │ │ │ └── TreeSitterMarkdown │ │ │ └── markdown.h │ │ ├── CMakeLists.txt │ │ ├── grammar.js │ │ ├── Makefile │ │ ├── package.json │ │ ├── queries │ │ │ ├── highlights.scm │ │ │ └── injections.scm │ │ ├── src │ │ │ ├── grammar.json │ │ │ ├── node-types.json │ │ │ ├── parser.c │ │ │ ├── scanner.c │ │ │ └── tree_sitter │ │ │ ├── alloc.h │ │ │ ├── array.h │ │ │ └── parser.h │ │ └── test │ │ └── corpus │ │ ├── extension_minus_metadata.txt │ │ ├── extension_pipe_table.txt │ │ ├── extension_plus_metadata.txt │ │ ├── extension_task_list.txt │ │ ├── failing.txt │ │ ├── issues.txt │ │ └── spec.txt │ ├── tree-sitter-markdown-inline │ │ ├── bindings │ │ │ ├── c │ │ │ │ ├── tree-sitter-markdown-inline.h │ │ │ │ └── tree-sitter-markdown-inline.pc.in │ │ │ └── swift │ │ │ └── TreeSitterMarkdownInline │ │ │ └── markdown_inline.h │ │ ├── CMakeLists.txt │ │ ├── grammar.js │ │ ├── Makefile │ │ ├── package.json │ │ ├── queries │ │ │ ├── highlights.scm │ │ │ └── injections.scm │ │ ├── src │ │ │ ├── grammar.json │ │ │ ├── node-types.json │ │ │ ├── parser.c │ │ │ ├── scanner.c │ │ │ └── tree_sitter │ │ │ ├── alloc.h │ │ │ ├── array.h │ │ │ └── parser.h │ │ └── test │ │ └── corpus │ │ ├── extension_latex.txt │ │ ├── extension_strikethrough.txt │ │ ├── extension_wikilink.txt │ │ ├── failing.txt │ │ ├── issues.txt │ │ ├── spec.txt │ │ └── tags.txt │ └── tree-sitter.json ├── LICENSE ├── Makefile ├── quickstart.md ├── README.md ├── src │ ├── core │ │ ├── buffer.cpp │ │ ├── buffer.h │ │ ├── config_manager.cpp │ │ ├── config_manager.h │ │ ├── editor_delta.h │ │ ├── editor_validation.h │ │ ├── editor.cpp │ │ └── editor.h │ ├── features │ │ ├── markdown_state.h │ │ ├── syntax_config_loader.cpp │ │ ├── syntax_config_loader.h │ │ ├── syntax_highlighter.cpp │ │ └── syntax_highlighter.h │ ├── main.cpp │ └── ui │ ├── input_handler.cpp │ ├── input_handler.h │ ├── renderer.cpp │ ├── renderer.h │ ├── style_manager.cpp │ └── style_manager.h └── treesitter ├── languages.yaml └── queries ├── _javascript │ ├── highlights.scm │ ├── locals.scm │ └── tags.scm ├── _jsx │ ├── highlights.scm │ ├── indents.scm │ └── textobjects.scm ├── _typescript │ ├── highlights.scm │ ├── indents.scm │ ├── locals.scm │ ├── tags.scm │ └── textobjects.scm ├── bash │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── c │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── cpp │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── css │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ └── rainbows.scm ├── ecma │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── README.md │ └── textobjects.scm ├── go │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── javascript │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── markdown │ ├── highlights.scm │ ├── injections.scm │ └── tags.scm ├── markdown.inline │ ├── highlights.scm │ └── injections.scm ├── python │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── rust │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── toml │ ├── highlights.scm │ ├── injections.scm │ ├── rainbows.scm │ └── textobjects.scm ├── tsx │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── typescript │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ ├── rainbows.scm │ ├── tags.scm │ └── textobjects.scm ├── yaml │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── rainbows.scm │ └── textobjects.scm └── zig ├── highlights.scm ├── indents.scm ├── injections.scm └── textobjects.scm ``` # Files -------------------------------------------------------------------------------- /src/ui/renderer.cpp: -------------------------------------------------------------------------------- ```cpp #include "renderer.h" #include "src/ui/style_manager.h" #include <algorithm> #include <cstring> #include <iostream> Renderer::Renderer() : tab_size_(4) {} Renderer::~Renderer() { restoreDefaultCursor(); } void Renderer::renderEditor(const EditorState &state, const GapBuffer &buffer, const SyntaxHighlighter *highlighter) { // Set background color for entire screen setDefaultColors(); clear(); // Render main content area renderContent(state, buffer, highlighter); // Render status bar renderStatusBar(state, buffer); // Position cursor positionCursor(state.cursor, state.viewport); // Update cursor style based on mode updateCursorStyle(state.mode); refresh(); } void Renderer::renderContent(const EditorState &state, const GapBuffer &buffer, const SyntaxHighlighter *highlighter) { const ViewportInfo &viewport = state.viewport; int line_num_width = calculateLineNumberWidth(buffer.getLineCount()); int end_line = std::min(viewport.top + viewport.height, buffer.getLineCount()); // Pre-calculate syntax highlighting for visible lines std::vector<std::vector<ColorSpan>> line_spans(end_line - viewport.top); if (highlighter) { for (int i = viewport.top; i < end_line; i++) { try { std::string line = buffer.getLine(i); std::string expanded_line = expandTabs(line, tab_size_); line_spans[i - viewport.top] = highlighter->getHighlightSpans(expanded_line, i, buffer); } catch (const std::exception &e) { std::cerr << "Syntax highlighting error on line " << i << ": " << e.what() << std::endl; line_spans[i - viewport.top].clear(); } } } // Render each visible line for (int i = viewport.top; i < end_line; i++) { int screen_row = i - viewport.top; move(screen_row, 0); // Set background for entire line setDefaultColors(); // Render line number bool is_current_line = (state.cursor.line == i); renderLineNumbers(i, i + 1, state.cursor.line, line_num_width, viewport.top); // Render line content std::string line = buffer.getLine(i); std::string expanded_line = expandTabs(line, tab_size_); const std::vector<ColorSpan> &spans = highlighter ? line_spans[i - viewport.top] : std::vector<ColorSpan>(); renderLine(expanded_line, i, spans, viewport, state); // Clear to end of line with proper background setDefaultColors(); clrtoeol(); } // Fill remaining screen with background color setDefaultColors(); for (int i = end_line - viewport.top; i < viewport.height; i++) { move(i, 0); clrtoeol(); } } void Renderer::renderLine(const std::string &line, int line_number, const std::vector<ColorSpan> &spans, const ViewportInfo &viewport, const EditorState &state) { int content_width = viewport.width - viewport.content_start_col; for (int screen_col = 0; screen_col < content_width; screen_col++) { int file_col = viewport.left + screen_col; bool char_exists = (file_col >= 0 && file_col < static_cast<int>(line.length())); char ch = char_exists ? line[file_col] : ' '; // Filter non-printable characters if (char_exists && (ch < 32 || ch > 126)) { ch = ' '; } // Check for selection bool is_selected = isPositionSelected(line_number, file_col, state); if (is_selected) { attron(COLOR_PAIR(SELECTION) | A_REVERSE); addch(ch); attroff(COLOR_PAIR(SELECTION) | A_REVERSE); } else { // Apply syntax highlighting bool color_applied = false; if (char_exists && !spans.empty()) { for (const auto &span : spans) { if (file_col >= span.start && file_col < span.end) { if (span.colorPair >= 0 && span.colorPair < COLOR_PAIRS) { applyColorSpan(span, ch); color_applied = true; break; } } } } if (!color_applied) { setDefaultColors(); addch(ch); } } } } void Renderer::renderStatusBar(const EditorState &state, const GapBuffer &buffer) { int rows, cols; getmaxyx(stdscr, rows, cols); int status_row = rows - 1; move(status_row, 0); // Set status bar background attrset(COLOR_PAIR(STATUS_BAR)); clrtoeol(); move(status_row, 0); // Render mode renderStatusMode(state.mode); // Render file info renderStatusFile(state.filename, state.is_modified); // Render position info (right-aligned) renderStatusPosition(state.cursor, buffer.getLineCount(), state.has_selection, 0); // TODO: calculate selection size } void Renderer::renderStatusMode(EditorMode mode) { std::string mode_str; int mode_color; switch (mode) { case EditorMode::NORMAL: mode_str = " NORMAL "; mode_color = STATUS_BAR; break; case EditorMode::INSERT: mode_str = " INSERT "; mode_color = STATUS_BAR_ACTIVE; break; case EditorMode::VISUAL: mode_str = " VISUAL "; mode_color = STATUS_BAR_ACTIVE; break; } attron(COLOR_PAIR(mode_color) | A_BOLD); printw("%s", mode_str.c_str()); attroff(COLOR_PAIR(mode_color) | A_BOLD); attron(COLOR_PAIR(STATUS_BAR)); printw(" "); } void Renderer::renderStatusFile(const std::string &filename, bool is_modified) { attron(COLOR_PAIR(STATUS_BAR_CYAN) | A_BOLD); if (filename.empty()) { printw("[No Name]"); } else { size_t last_slash = filename.find_last_of("/\\"); std::string display_name = (last_slash != std::string::npos) ? filename.substr(last_slash + 1) : filename; printw("%s", display_name.c_str()); } attroff(COLOR_PAIR(STATUS_BAR_CYAN) | A_BOLD); if (is_modified) { attron(COLOR_PAIR(STATUS_BAR_ACTIVE) | A_BOLD); printw(" [+]"); attroff(COLOR_PAIR(STATUS_BAR_ACTIVE) | A_BOLD); } } void Renderer::renderStatusPosition(const CursorInfo &cursor, int total_lines, bool has_selection, int selection_size) { int rows, cols; getmaxyx(stdscr, rows, cols); int status_row = rows - 1; char position_info[256]; int percentage = total_lines == 0 ? 0 : ((cursor.line + 1) * 100 / total_lines); if (has_selection) { snprintf(position_info, sizeof(position_info), "[selection] %d:%d %d/%d %d%% ", cursor.line + 1, cursor.col + 1, cursor.line + 1, total_lines, percentage); } else { snprintf(position_info, sizeof(position_info), "%d:%d %d/%d %d%% ", cursor.line + 1, cursor.col + 1, cursor.line + 1, total_lines, percentage); } int info_len = strlen(position_info); int current_pos = getcurx(stdscr); int right_start = cols - info_len; if (right_start <= current_pos) { right_start = current_pos + 2; } // Fill middle space attron(COLOR_PAIR(STATUS_BAR)); for (int i = current_pos; i < right_start && i < cols; i++) { move(status_row, i); addch(' '); } // Render position info if (right_start < cols) { move(status_row, right_start); attron(COLOR_PAIR(STATUS_BAR_YELLOW) | A_BOLD); printw("%s", position_info); attroff(COLOR_PAIR(STATUS_BAR_YELLOW) | A_BOLD); } } void Renderer::renderLineNumbers(int start_line, int end_line, int current_line, int line_num_width, int viewport_top) { int line_index = start_line - viewport_top; bool is_current = (start_line == current_line); int color_pair = is_current ? LINE_NUMBERS_ACTIVE : LINE_NUMBERS; attron(COLOR_PAIR(color_pair)); printw("%*d ", line_num_width, start_line + 1); attroff(COLOR_PAIR(color_pair)); // Separator attron(COLOR_PAIR(LINE_NUMBERS_DIM)); addch(' '); attroff(COLOR_PAIR(LINE_NUMBERS_DIM)); addch(' '); } void Renderer::positionCursor(const CursorInfo &cursor, const ViewportInfo &viewport) { int screen_row = cursor.line - viewport.top; if (screen_row >= 0 && screen_row < viewport.height) { int screen_col = viewport.content_start_col + cursor.col - viewport.left; int rows, cols; getmaxyx(stdscr, rows, cols); if (screen_col >= viewport.content_start_col && screen_col < cols) { move(screen_row, screen_col); } else { move(screen_row, viewport.content_start_col); } } } void Renderer::updateCursorStyle(EditorMode mode) { switch (mode) { case EditorMode::NORMAL: printf("\033[2 q"); // Block cursor break; case EditorMode::INSERT: printf("\033[6 q"); // Vertical bar cursor break; case EditorMode::VISUAL: printf("\033[4 q"); // Underline cursor break; } fflush(stdout); } void Renderer::restoreDefaultCursor() { printf("\033[0 q"); fflush(stdout); } void Renderer::clear() { ::clear(); } void Renderer::refresh() { ::refresh(); } void Renderer::handleResize() { clear(); refresh(); } Renderer::ViewportInfo Renderer::calculateViewport() const { ViewportInfo viewport; int rows, cols; getmaxyx(stdscr, rows, cols); viewport.height = rows - 1; // Reserve last row for status bar viewport.width = cols; // Calculate content start column (after line numbers) int max_lines = 1000; // Reasonable default, should be passed as parameter int line_num_width = calculateLineNumberWidth(max_lines); viewport.content_start_col = line_num_width + 3; // +3 for space and separator return viewport; } int Renderer::calculateLineNumberWidth(int max_line) const { if (max_line <= 0) return 1; int width = 0; int num = max_line; while (num > 0) { width++; num /= 10; } return std::max(width, 3); // Minimum width of 3 } bool Renderer::isPositionSelected(int line, int col, const EditorState &state) const { if (!state.has_selection) { return false; } int start_line = state.selection_start_line; int start_col = state.selection_start_col; int end_line = state.selection_end_line; int end_col = state.selection_end_col; // Normalize selection if (start_line > end_line || (start_line == end_line && start_col > end_col)) { std::swap(start_line, end_line); std::swap(start_col, end_col); } if (line < start_line || line > end_line) { return false; } if (start_line == end_line) { return col >= start_col && col < end_col; } else if (line == start_line) { return col >= start_col; } else if (line == end_line) { return col < end_col; } else { return true; } } std::string Renderer::expandTabs(const std::string &line, int tab_size) const { std::string result; for (char c : line) { if (c == '\t') { int spaces_to_add = tab_size - (result.length() % tab_size); result.append(spaces_to_add, ' '); } else if (c >= 32 && c <= 126) { result += c; } else { result += ' '; } } return result; } void Renderer::applyColorSpan(const ColorSpan &span, char ch) { int attrs = COLOR_PAIR(span.colorPair); if (span.attribute != 0) { attrs |= span.attribute; } attron(attrs); addch(ch); attroff(attrs); } void Renderer::setDefaultColors() { attrset(COLOR_PAIR(0)); // Use default background } ``` -------------------------------------------------------------------------------- /src/core/buffer.cpp: -------------------------------------------------------------------------------- ```cpp #include "buffer.h" #include <algorithm> #include <cstring> #include <fstream> #include <iostream> #include <stdexcept> #include <vector> const size_t GapBuffer::DEFAULT_GAP_SIZE; const size_t GapBuffer::MIN_GAP_SIZE; GapBuffer::GapBuffer() : gapStart(0), gapSize(DEFAULT_GAP_SIZE), lineIndexDirty(true) { buffer.resize(DEFAULT_GAP_SIZE); } GapBuffer::GapBuffer(const std::string &initialText) : GapBuffer() { if (!initialText.empty()) { insertText(0, initialText); } } bool GapBuffer::loadFromFile(const std::string &filename) { // --- Step 1: Fast I/O (Read entire file into memory) --- std::ifstream file(filename, std::ios::binary | std::ios::ate); if (!file.is_open()) { return false; } std::streamsize fileSize = file.tellg(); file.seekg(0, std::ios::beg); // Handle empty file case if (fileSize <= 0) { clear(); insertChar(0, '\n'); return true; } std::vector<char> content(fileSize); if (!file.read(content.data(), fileSize)) { return false; } file.close(); // --- Step 2: Check if normalization is needed --- const char *src = content.data(); bool has_carriage_returns = false; // OPTIMIZATION: Quick scan for \r (memchr is highly optimized) if (std::memchr(src, '\r', fileSize) != nullptr) { has_carriage_returns = true; } // --- Step 3: Clear and Initial Buffer Sizing --- clear(); if (!has_carriage_returns) { // FAST PATH: No normalization needed, direct copy buffer.resize(fileSize + DEFAULT_GAP_SIZE); char *dest = buffer.data() + DEFAULT_GAP_SIZE; std::memcpy(dest, src, fileSize); buffer.resize(fileSize + DEFAULT_GAP_SIZE); gapStart = 0; gapSize = DEFAULT_GAP_SIZE; invalidateLineIndex(); return true; } // SLOW PATH: Normalization required buffer.resize(fileSize + DEFAULT_GAP_SIZE); char *dest = buffer.data() + DEFAULT_GAP_SIZE; size_t actualTextSize = 0; for (std::streamsize i = 0; i < fileSize; ++i) { char c = src[i]; if (c == '\r') { // Windows \r\n sequence: skip \r, write \n if (i + 1 < fileSize && src[i + 1] == '\n') { c = '\n'; i++; // Skip next \n } else { // Old Mac/Lone \r: Replace with \n c = '\n'; } } *dest++ = c; actualTextSize++; } // --- Step 4: Finalize Gap Buffer State --- buffer.resize(actualTextSize + DEFAULT_GAP_SIZE); gapStart = 0; gapSize = DEFAULT_GAP_SIZE; invalidateLineIndex(); return true; } bool GapBuffer::saveToFile(const std::string &filename) const { std::ofstream file(filename); if (!file.is_open()) { return false; } file << getText(); return file.good(); } void GapBuffer::loadFromString(const std::string &content) { clear(); if (content.empty()) { insertChar(0, '\n'); return; } // Resize buffer to hold content + gap size_t content_size = content.length(); buffer.resize(content_size + DEFAULT_GAP_SIZE); // Copy content directly after gap std::memcpy(buffer.data() + DEFAULT_GAP_SIZE, content.data(), content_size); gapStart = 0; gapSize = DEFAULT_GAP_SIZE; // Mark index as dirty - will rebuild on first access invalidateLineIndex(); } void GapBuffer::clear() { buffer.clear(); buffer.resize(DEFAULT_GAP_SIZE); gapStart = 0; gapSize = DEFAULT_GAP_SIZE; invalidateLineIndex(); } int GapBuffer::getLineCount() const { if (lineIndexDirty) { rebuildLineIndex(); } return static_cast<int>(lineIndex.size()); } std::string GapBuffer::getLine(int lineNum) const { if (lineIndexDirty) { rebuildLineIndex(); } if (lineNum < 0 || lineNum >= static_cast<int>(lineIndex.size())) { return ""; } size_t lineStart = lineIndex[lineNum]; size_t lineEnd; if (lineNum + 1 < static_cast<int>(lineIndex.size())) { lineEnd = lineIndex[lineNum + 1] - 1; // -1 to exclude newline } else { lineEnd = textSize(); } if (lineEnd <= lineStart) { return ""; } std::string result; result.reserve(lineEnd - lineStart); for (size_t pos = lineStart; pos < lineEnd; ++pos) { result += charAt(pos); } return result; } size_t GapBuffer::getLineLength(int lineNum) const { if (lineIndexDirty) { rebuildLineIndex(); } if (lineNum < 0 || lineNum >= static_cast<int>(lineIndex.size())) { return 0; } size_t lineStart = lineIndex[lineNum]; size_t lineEnd; if (lineNum + 1 < static_cast<int>(lineIndex.size())) { lineEnd = lineIndex[lineNum + 1] - 1; // -1 to exclude the newline } else { lineEnd = textSize(); } return (lineEnd > lineStart) ? lineEnd - lineStart : 0; } bool GapBuffer::isEmpty() const { return textSize() == 0; } size_t GapBuffer::lineColToPos(int line, int col) const { if (lineIndexDirty) { rebuildLineIndex(); } if (line < 0 || line >= static_cast<int>(lineIndex.size())) { return textSize(); } size_t lineStart = lineIndex[line]; size_t maxCol = getLineLength(line); size_t actualCol = std::min(static_cast<size_t>(std::max(0, col)), maxCol); return lineStart + actualCol; } std::pair<int, int> GapBuffer::posToLineCol(size_t pos) const { if (lineIndexDirty) { rebuildLineIndex(); } pos = std::min(pos, textSize()); // Binary search to find the line int line = 0; for (int i = static_cast<int>(lineIndex.size()) - 1; i >= 0; --i) { if (pos >= lineIndex[i]) { line = i; break; } } int col = static_cast<int>(pos - lineIndex[line]); return std::make_pair(line, col); } void GapBuffer::insertChar(size_t pos, char c) { pos = std::min(pos, textSize()); moveGapTo(pos); if (gapSize == 0) { expandGap(); } buffer[gapStart] = c; gapStart++; gapSize--; if (c == '\n') { invalidateLineIndex(); } } void GapBuffer::insertText(size_t pos, const std::string &text) { if (text.empty()) return; pos = std::min(pos, textSize()); moveGapTo(pos); if (gapSize < text.size()) { expandGap(text.size()); } bool hasNewlines = false; for (char c : text) { buffer[gapStart] = c; gapStart++; gapSize--; if (c == '\n') { hasNewlines = true; } } if (hasNewlines) { invalidateLineIndex(); } } void GapBuffer::deleteChar(size_t pos) { deleteRange(pos, 1); } void GapBuffer::deleteRange(size_t start, size_t length) { if (length == 0) return; start = std::min(start, textSize()); length = std::min(length, textSize() - start); if (length == 0) return; // Check if we're deleting newlines bool hasNewlines = false; for (size_t i = start; i < start + length; ++i) { if (charAt(i) == '\n') { hasNewlines = true; break; } } moveGapTo(start); gapSize += length; if (hasNewlines) { invalidateLineIndex(); } } void GapBuffer::insertLine(int lineNum, const std::string &line) { size_t pos = lineColToPos(lineNum, 0); size_t lineLength = line.length() + 1; // +1 for newline // Insert the text insertText(pos, line + "\n"); // OPTIMIZATION: Update line index incrementally instead of rebuilding if (!lineIndexDirty && lineNum < static_cast<int>(lineIndex.size())) { // Insert new line offset at position lineNum + 1 lineIndex.insert(lineIndex.begin() + lineNum + 1, pos + lineLength); // Shift all subsequent offsets by the length of inserted content for (size_t i = lineNum + 2; i < lineIndex.size(); ++i) { lineIndex[i] += lineLength; } } else { // Fallback: mark index as dirty for rebuild invalidateLineIndex(); } } void GapBuffer::deleteLine(int lineNum) { if (lineNum < 0 || lineNum >= getLineCount()) return; size_t lineStart = lineColToPos(lineNum, 0); size_t lineLength = getLineLength(lineNum); // Include the newline character if it exists bool has_newline = (lineNum < getLineCount() - 1); if (has_newline) { lineLength++; } // Delete the range deleteRange(lineStart, lineLength); // OPTIMIZATION: Update line index incrementally if (!lineIndexDirty && lineNum + 1 < static_cast<int>(lineIndex.size())) { // Remove line offset at position lineNum + 1 lineIndex.erase(lineIndex.begin() + lineNum + 1); // Shift all subsequent offsets back by the deleted length for (size_t i = lineNum + 1; i < lineIndex.size(); ++i) { lineIndex[i] -= lineLength; } } else { // Fallback: mark index as dirty for rebuild invalidateLineIndex(); } } void GapBuffer::replaceLine(int lineNum, const std::string &newLine) { if (lineNum < 0 || lineNum >= getLineCount()) return; size_t lineStart = lineColToPos(lineNum, 0); size_t oldLineLength = getLineLength(lineNum); // Check if the new line contains newlines bool newLineHasNewlines = (newLine.find('\n') != std::string::npos); if (newLineHasNewlines) { // Complex case: line split - must rebuild index moveGapTo(lineStart); if (gapSize < newLine.length()) { expandGap(newLine.length()); } gapSize += oldLineLength; for (char c : newLine) { buffer[gapStart] = c; gapStart++; gapSize--; } invalidateLineIndex(); return; } // Simple case: Same line, just different text moveGapTo(lineStart); if (gapSize < newLine.length()) { expandGap(newLine.length()); } gapSize += oldLineLength; for (char c : newLine) { buffer[gapStart] = c; gapStart++; gapSize--; } // CRITICAL OPTIMIZATION: Update line index incrementally if (!lineIndexDirty && lineNum + 1 < static_cast<int>(lineIndex.size())) { // Adjust all subsequent line offsets by the length difference int lengthDiff = static_cast<int>(newLine.length()) - static_cast<int>(oldLineLength); if (lengthDiff != 0) { for (size_t i = lineNum + 1; i < lineIndex.size(); ++i) { lineIndex[i] += lengthDiff; } } // Line index is now up-to-date, no need to invalidate! } else if (lineIndexDirty) { // Already dirty, no change needed } } // std::string GapBuffer::getText() const // { // std::string result; // result.reserve(textSize()); // // Add text before gap // for (size_t i = 0; i < gapStart; ++i) // { // result += buffer[i]; // } // // Add text after gap // for (size_t i = gapEnd(); i < buffer.size(); ++i) // { // result += buffer[i]; // } // return result; // } // In buffer.cpp, inside GapBuffer::getText() std::string GapBuffer::getText() const { // Build line index ONCE if needed (for future getLine() calls) if (lineIndexDirty) { rebuildLineIndex(); } // The size of the final string *must* match textSize() size_t text_size = textSize(); std::string result; result.reserve(text_size); // Append text before gap result.append(buffer.data(), gapStart); // Append text after gap size_t afterGapLength = text_size - gapStart; if (afterGapLength > 0 && gapEnd() < buffer.size()) { result.append(buffer.data() + gapEnd(), afterGapLength); } return result; } std::string GapBuffer::getTextRange(size_t start, size_t length) const { start = std::min(start, textSize()); length = std::min(length, textSize() - start); std::string result; result.reserve(length); for (size_t i = 0; i < length; ++i) { result += charAt(start + i); } return result; } size_t GapBuffer::size() const { return textSize(); } size_t GapBuffer::getBufferSize() const { return buffer.size(); } // In buffer.cpp void GapBuffer::moveGapTo(size_t pos) { if (pos == gapStart) return; // Get the base pointer once char *base_ptr = buffer.data(); if (pos < gapStart) { // Move gap left - move text from before new gap position to after gap size_t moveSize = gapStart - pos; size_t gapEndPos = gapEnd(); // Move text from [pos] to [gapEndPos - moveSize] // This moves the text segment that was at pos and pushes it to the end of // the text segment after the gap std::memmove(base_ptr + gapEndPos - moveSize, base_ptr + pos, moveSize); gapStart = pos; } else { // Move gap right - move text from after gap to before new gap position size_t moveSize = pos - gapStart; size_t gapEndPos = gapEnd(); // Move text from [gapEndPos] to [gapStart] // This pulls the text segment that was after the gap and fills the newly // created gap space std::memmove(base_ptr + gapStart, base_ptr + gapEndPos, moveSize); gapStart = pos; } // IMPORTANT: The gap size must remain the same, but its position is now // correct } void GapBuffer::expandGap(size_t minSize) { size_t newGapSize = std::max(minSize, std::max(gapSize * 2, MIN_GAP_SIZE)); size_t oldBufferSize = buffer.size(); size_t newBufferSize = oldBufferSize + newGapSize - gapSize; // Create new buffer std::vector<char> newBuffer(newBufferSize); // Copy text before gap std::copy(buffer.begin(), buffer.begin() + gapStart, newBuffer.begin()); // Copy text after gap size_t afterGapSize = oldBufferSize - gapEnd(); std::copy(buffer.begin() + gapEnd(), buffer.end(), newBuffer.begin() + gapStart + newGapSize); buffer = std::move(newBuffer); gapSize = newGapSize; } void GapBuffer::rebuildLineIndex() const { lineIndex.clear(); lineIndex.push_back(0); // First line always starts at position 0 for (size_t pos = 0; pos < textSize(); ++pos) { if (charAt(pos) == '\n') { lineIndex.push_back(pos + 1); } } // If the buffer doesn't end with a newline, we still have the last line // The line index is already correct as we added positions after each newline lineIndexDirty = false; } void GapBuffer::invalidateLineIndex() { lineIndexDirty = true; } char GapBuffer::charAt(size_t pos) const { if (pos >= textSize()) { return '\0'; } if (pos < gapStart) { return buffer[pos]; } else { // Position is after gap, so add gap size to skip over it return buffer[pos + gapSize]; } } ``` -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- ```cpp // src/main.cpp #include "src/core/config_manager.h" #include "src/core/editor.h" #include "src/features/syntax_highlighter.h" #include "src/ui/input_handler.h" #include "src/ui/style_manager.h" #include <algorithm> #include <cstdlib> #include <filesystem> #include <iostream> #include <vector> #ifdef _WIN32 #include <curses.h> #else #include <csignal> #include <ncurses.h> #include <termios.h> #include <unistd.h> #endif // void disableXonXoff(); // void disableSignalHandling(); // void restoreSignalHandling(); bool initializeNcurses(); bool initializeThemes(); void setupMouse(); void cleanupMouse(); enum class BenchmarkMode { NONE, STARTUP_ONLY, // Just ncurses + theme init (your current --quit) STARTUP_INTERACTIVE, // Full init to first input ready (RECOMMENDED) FILE_LOAD, // Measure file loading separately FULL_CYCLE // Complete startup + single operation + exit }; struct BenchmarkResult { std::chrono::milliseconds init_time; std::chrono::milliseconds theme_time; std::chrono::milliseconds editor_creation_time; std::chrono::milliseconds file_load_time; std::chrono::milliseconds first_render_time; std::chrono::milliseconds syntax_highlight_time; std::chrono::milliseconds total_time; void print(std::ostream &os) const { os << "=== Benchmark Results ===" << std::endl; os << "Init (ncurses): " << init_time.count() << "ms" << std::endl; os << "Theme load: " << theme_time.count() << "ms" << std::endl; os << "Editor creation: " << editor_creation_time.count() << "ms" << std::endl; os << "File load: " << file_load_time.count() << "ms" << std::endl; os << "Syntax highlighting: " << syntax_highlight_time.count() << "ms" << std::endl; os << "First render: " << first_render_time.count() << "ms" << std::endl; os << "------------------------" << std::endl; os << "TOTAL (user-perceived): " << total_time.count() << "ms" << std::endl; } }; BenchmarkResult runStartupInteractiveBenchmark(const std::string &filename, bool enable_syntax_highlighting) { BenchmarkResult result{}; auto start = std::chrono::high_resolution_clock::now(); // Phase 1: Initialize ncurses // disableXonXoff(); // #ifndef _win32 // disableSignalHandling(); // #endif if (!initializeNcurses()) { throw std::runtime_error("ncurses init failed"); } if (!initializeThemes()) { endwin(); throw std::runtime_error("theme init failed"); } auto after_init = std::chrono::high_resolution_clock::now(); result.init_time = std::chrono::duration_cast<std::chrono::milliseconds>(after_init - start); // Phase 2: Load theme std::string active_theme = ConfigManager::getActiveTheme(); std::string theme_file = ConfigManager::getThemeFile(active_theme); if (!theme_file.empty()) { g_style_manager.load_theme_from_file(theme_file); } auto after_theme = std::chrono::high_resolution_clock::now(); result.theme_time = std::chrono::duration_cast<std::chrono::milliseconds>( after_theme - after_init); // Phase 3: Create syntax highlighter (if enabled) SyntaxHighlighter syntaxHighlighter; SyntaxHighlighter *highlighterPtr = nullptr; if (enable_syntax_highlighting) { std::string syntax_dir = ConfigManager::getSyntaxRulesDir(); if (syntaxHighlighter.initialize(syntax_dir)) { highlighterPtr = &syntaxHighlighter; } } auto after_highlighter_init = std::chrono::high_resolution_clock::now(); // Phase 4: Create editor and load file Editor editor(highlighterPtr); auto after_editor_creation = std::chrono::high_resolution_clock::now(); result.editor_creation_time = std::chrono::duration_cast<std::chrono::milliseconds>( after_editor_creation - after_theme); if (!editor.loadFile(filename)) { endwin(); throw std::runtime_error("Failed to load file"); } auto after_file_load = std::chrono::high_resolution_clock::now(); result.file_load_time = std::chrono::duration_cast<std::chrono::milliseconds>( after_file_load - after_editor_creation); // Phase 5: Initialize syntax highlighting for viewport if (highlighterPtr) { editor.initializeViewportHighlighting(); } auto after_syntax = std::chrono::high_resolution_clock::now(); result.syntax_highlight_time = std::chrono::duration_cast<std::chrono::milliseconds>(after_syntax - after_file_load); // Phase 6: Render first display editor.setCursorMode(); editor.display(); wnoutrefresh(stdscr); auto after_render = std::chrono::high_resolution_clock::now(); result.first_render_time = std::chrono::duration_cast<std::chrono::milliseconds>(after_render - after_syntax); // This is the "interactive ready" point - user can now type result.total_time = std::chrono::duration_cast<std::chrono::milliseconds>( after_render - start); // Cleanup endwin(); return result; } void flushInputQueue(); int main(int argc, char *argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " <filename> [options]" << std::endl; std::cerr << "\nBenchmark options:" << std::endl; std::cerr << " --bench-startup Benchmark startup to interactive" << std::endl; std::cerr << " --bench-startup-nosyntax Same but without syntax highlighting" << std::endl; std::cerr << " --bench-file-only Benchmark only file loading" << std::endl; return 1; } // Parse command line arguments std::vector<std::string> args(argv, argv + argc); bool bench_quit = std::any_of(args.begin(), args.end(), [](const auto &arg) { return arg == "--bench-quit"; }); bool force_no_highlighting = std::any_of(args.begin(), args.end(), [](const auto &arg) { return arg == "--none"; }); bool quit_immediately = std::any_of(args.begin(), args.end(), [](const auto &arg) { return arg == "--quit"; }); // Bechmark bool bench_startup = std::any_of(args.begin(), args.end(), [](const auto &arg) { return arg == "--bench-startup"; }); bool bench_startup_nosyntax = std::any_of(args.begin(), args.end(), [](const auto &arg) { return arg == "--bench-startup-nosyntax"; }); std::string filename = std::filesystem::absolute(argv[1]).string(); // Initialize config if (!ConfigManager::ensureConfigStructure()) { std::cerr << "Warning: Failed to ensure config structure" << std::endl; } ConfigManager::copyProjectFilesToConfig(); ConfigManager::loadConfig(); if (bench_startup || bench_startup_nosyntax) { try { BenchmarkResult result = runStartupInteractiveBenchmark(filename, !bench_startup_nosyntax); result.print(std::cerr); return 0; } catch (const std::exception &e) { std::cerr << "Benchmark failed: " << e.what() << std::endl; return 1; } } // BENCHMARK PATH: Load file and quit immediately if (quit_immediately) { auto start = std::chrono::high_resolution_clock::now(); // Initialize ncurses (users see this cost) // disableXonXoff(); // #ifndef _win32 // disableSignalHandling(); // #endif if (!initializeNcurses()) return 1; if (!initializeThemes()) return 1; auto after_init = std::chrono::high_resolution_clock::now(); // Load theme (users see this) std::string active_theme = ConfigManager::getActiveTheme(); std::string theme_file = ConfigManager::getThemeFile(active_theme); if (!theme_file.empty()) { g_style_manager.load_theme_from_file(theme_file); } auto after_theme = std::chrono::high_resolution_clock::now(); // Render once (users see this) // editor.display(); wnoutrefresh(stdscr); doupdate(); auto after_render = std::chrono::high_resolution_clock::now(); // Cleanup endwin(); // Print timings to stderr (won't interfere with hyperfine) auto ms = [](auto s, auto e) { return std::chrono::duration_cast<std::chrono::milliseconds>(e - s) .count(); }; std::cerr << "Init: " << ms(start, after_init) << "ms, " << "Theme: " << ms(after_init, after_theme) << "ms, " << "Render: " << ms(after_theme, after_render) << "ms, " << "Total: " << ms(start, after_render) << "ms" << std::endl; return 0; } // Determine syntax mode (CLI args override config) SyntaxMode syntax_mode = force_no_highlighting ? SyntaxMode::NONE : ConfigManager::getSyntaxMode(); // Create syntax highlighter SyntaxHighlighter syntaxHighlighter; SyntaxHighlighter *highlighterPtr = nullptr; if (syntax_mode != SyntaxMode::NONE) { std::string syntax_dir = ConfigManager::getSyntaxRulesDir(); if (syntaxHighlighter.initialize(syntax_dir)) { highlighterPtr = &syntaxHighlighter; } else { std::cerr << "Warning: Syntax highlighter init failed" << std::endl; } } // Create editor Editor editor(highlighterPtr); editor.setDeltaUndoEnabled(true); // Initialize delta group (even if disabled) editor.beginDeltaGroup(); if (!editor.loadFile(filename)) { std::cerr << "Warning: Could not open file " << filename << std::endl; } // Initialize ncurses // disableXonXoff(); // #ifndef _win32 // disableSignalHandling(); // #endif if (!initializeNcurses()) { return 1; } if (!initializeThemes()) { endwin(); return 1; } // Load theme std::string active_theme = ConfigManager::getActiveTheme(); std::string theme_file = ConfigManager::getThemeFile(active_theme); if (!theme_file.empty()) { if (!g_style_manager.load_theme_from_file(theme_file)) { std::cerr << "FATAL: Theme load failed" << std::endl; } } setupMouse(); if (highlighterPtr) { editor.initializeViewportHighlighting(); } // Start editor InputHandler inputHandler(editor); editor.setCursorMode(); editor.display(); wnoutrefresh(stdscr); doupdate(); curs_set(1); // Ensure cursor is visible // Handle --quit for testing if (quit_immediately) { cleanupMouse(); attrset(A_NORMAL); curs_set(1); endwin(); std::cerr << "First line: " << editor.getFirstLine() << std::endl; return 0; } if (highlighterPtr) { highlighterPtr->scheduleBackgroundParse(editor.getBuffer()); } // Start config watching if (!ConfigManager::startWatchingConfig()) { std::cerr << "Warning: Config watching failed" << std::endl; } // Main loop int key; bool running = true; while (running) { if (ConfigManager::isReloadPending()) { curs_set(0); // Hide cursor editor.display(); wnoutrefresh(stdscr); doupdate(); editor.positionCursor(); // Position AFTER flush curs_set(1); // Show cursor } key = getch(); // if (key == 'q' || key == 'Q') // { // // if (editor.getMode() == EditorMode::NORMAL) // // { // running = false; // continue; // // } // } InputHandler::KeyResult result = inputHandler.handleKey(key); switch (result) { case InputHandler::KeyResult::QUIT: running = false; break; case InputHandler::KeyResult::REDRAW: case InputHandler::KeyResult::HANDLED: curs_set(0); // Hide during render editor.display(); wnoutrefresh(stdscr); doupdate(); editor.positionCursor(); // Position AFTER flush curs_set(1); // Show immediately break; case InputHandler::KeyResult::NOT_HANDLED: // std::cerr << "Input not handled" << std::endl; break; } // if (result != InputHandler::KeyResult::NOT_HANDLED) // { // doupdate(); // } } // Cleanup cleanupMouse(); attrset(A_NORMAL); curs_set(1); endwin(); return 0; } bool initializeNcurses() { initscr(); // cbreak(); raw(); keypad(stdscr, TRUE); // This MUST be set for arrow keys noecho(); curs_set(1); #ifdef _WIN32 // CRITICAL: Don't use timeout on Windows - causes ERR spam nodelay(stdscr, FALSE); // Blocking mode PDC_set_blink(FALSE); PDC_return_key_modifiers(TRUE); PDC_save_key_modifiers(TRUE); scrollok(stdscr, FALSE); leaveok(stdscr, FALSE); raw(); meta(stdscr, TRUE); intrflush(stdscr, FALSE); #else timeout(50); #endif if (!has_colors()) { endwin(); std::cerr << "Error: Terminal does not support colors" << std::endl; return false; } if (start_color() == ERR) { endwin(); std::cerr << "Error: Could not initialize colors" << std::endl; return false; } if (use_default_colors() == ERR) { assume_default_colors(COLOR_WHITE, COLOR_BLACK); } return true; } bool initializeThemes() { g_style_manager.initialize(); // Register the callback for future config changes ConfigManager::registerReloadCallback( []() { // 1. Get current theme from the *newly loaded* config std::string active_theme = ConfigManager::getActiveTheme(); std::string theme_file = ConfigManager::getThemeFile(active_theme); if (!theme_file.empty()) { // 2. Load the theme if (!g_style_manager.load_theme_from_file(theme_file)) { std::cerr << "ERROR: Theme reload failed for: " << active_theme << std::endl; } } }); // NOTE: Initial theme load logic removed from here return true; } void flushInputQueue() { nodelay(stdscr, TRUE); // Make getch() non-blocking while (getch() != ERR) { // Drain all pending input } nodelay(stdscr, FALSE); // Restore blocking mode timeout(50); // Restore your timeout } void setupMouse() { mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL); #ifndef _WIN32 printf("\033[?1003h"); fflush(stdout); #endif } void cleanupMouse() { #ifndef _WIN32 printf("\033[?1003l"); fflush(stdout); #endif } ``` -------------------------------------------------------------------------------- /deps/tree-sitter-markdown/tree-sitter-markdown-inline/src/node-types.json: -------------------------------------------------------------------------------- ```json [ { "type": "backslash_escape", "named": true, "fields": {} }, { "type": "code_span", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "code_span_delimiter", "named": true } ] } }, { "type": "collapsed_reference_link", "named": true, "fields": {}, "children": { "multiple": false, "required": false, "types": [ { "type": "link_text", "named": true } ] } }, { "type": "emphasis", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "backslash_escape", "named": true }, { "type": "code_span", "named": true }, { "type": "collapsed_reference_link", "named": true }, { "type": "email_autolink", "named": true }, { "type": "emphasis", "named": true }, { "type": "emphasis_delimiter", "named": true }, { "type": "entity_reference", "named": true }, { "type": "full_reference_link", "named": true }, { "type": "hard_line_break", "named": true }, { "type": "html_tag", "named": true }, { "type": "image", "named": true }, { "type": "inline_link", "named": true }, { "type": "latex_block", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "shortcut_link", "named": true }, { "type": "strikethrough", "named": true }, { "type": "strong_emphasis", "named": true }, { "type": "uri_autolink", "named": true } ] } }, { "type": "full_reference_link", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "link_label", "named": true }, { "type": "link_text", "named": true } ] } }, { "type": "hard_line_break", "named": true, "fields": {} }, { "type": "html_tag", "named": true, "fields": {} }, { "type": "image", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "image_description", "named": true }, { "type": "link_destination", "named": true }, { "type": "link_label", "named": true }, { "type": "link_title", "named": true } ] } }, { "type": "image_description", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "code_span", "named": true }, { "type": "collapsed_reference_link", "named": true }, { "type": "email_autolink", "named": true }, { "type": "emphasis", "named": true }, { "type": "entity_reference", "named": true }, { "type": "full_reference_link", "named": true }, { "type": "hard_line_break", "named": true }, { "type": "html_tag", "named": true }, { "type": "image", "named": true }, { "type": "inline_link", "named": true }, { "type": "latex_block", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "shortcut_link", "named": true }, { "type": "strikethrough", "named": true }, { "type": "strong_emphasis", "named": true }, { "type": "uri_autolink", "named": true } ] } }, { "type": "inline", "named": true, "root": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "code_span", "named": true }, { "type": "collapsed_reference_link", "named": true }, { "type": "email_autolink", "named": true }, { "type": "emphasis", "named": true }, { "type": "entity_reference", "named": true }, { "type": "full_reference_link", "named": true }, { "type": "hard_line_break", "named": true }, { "type": "html_tag", "named": true }, { "type": "image", "named": true }, { "type": "inline_link", "named": true }, { "type": "latex_block", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "shortcut_link", "named": true }, { "type": "strikethrough", "named": true }, { "type": "strong_emphasis", "named": true }, { "type": "uri_autolink", "named": true } ] } }, { "type": "inline_link", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "link_destination", "named": true }, { "type": "link_text", "named": true }, { "type": "link_title", "named": true } ] } }, { "type": "latex_block", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "backslash_escape", "named": true }, { "type": "latex_span_delimiter", "named": true } ] } }, { "type": "link_destination", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "entity_reference", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "link_label", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "entity_reference", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "link_text", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "code_span", "named": true }, { "type": "email_autolink", "named": true }, { "type": "emphasis", "named": true }, { "type": "entity_reference", "named": true }, { "type": "hard_line_break", "named": true }, { "type": "html_tag", "named": true }, { "type": "image", "named": true }, { "type": "latex_block", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "strikethrough", "named": true }, { "type": "strong_emphasis", "named": true }, { "type": "uri_autolink", "named": true } ] } }, { "type": "link_title", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "entity_reference", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "shortcut_link", "named": true, "fields": {}, "children": { "multiple": false, "required": true, "types": [ { "type": "link_text", "named": true } ] } }, { "type": "strikethrough", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "backslash_escape", "named": true }, { "type": "code_span", "named": true }, { "type": "collapsed_reference_link", "named": true }, { "type": "email_autolink", "named": true }, { "type": "emphasis", "named": true }, { "type": "emphasis_delimiter", "named": true }, { "type": "entity_reference", "named": true }, { "type": "full_reference_link", "named": true }, { "type": "hard_line_break", "named": true }, { "type": "html_tag", "named": true }, { "type": "image", "named": true }, { "type": "inline_link", "named": true }, { "type": "latex_block", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "shortcut_link", "named": true }, { "type": "strikethrough", "named": true }, { "type": "strong_emphasis", "named": true }, { "type": "uri_autolink", "named": true } ] } }, { "type": "strong_emphasis", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "backslash_escape", "named": true }, { "type": "code_span", "named": true }, { "type": "collapsed_reference_link", "named": true }, { "type": "email_autolink", "named": true }, { "type": "emphasis", "named": true }, { "type": "emphasis_delimiter", "named": true }, { "type": "entity_reference", "named": true }, { "type": "full_reference_link", "named": true }, { "type": "hard_line_break", "named": true }, { "type": "html_tag", "named": true }, { "type": "image", "named": true }, { "type": "inline_link", "named": true }, { "type": "latex_block", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "shortcut_link", "named": true }, { "type": "strikethrough", "named": true }, { "type": "strong_emphasis", "named": true }, { "type": "uri_autolink", "named": true } ] } }, { "type": "!", "named": false }, { "type": "\"", "named": false }, { "type": "#", "named": false }, { "type": "$", "named": false }, { "type": "%", "named": false }, { "type": "&", "named": false }, { "type": "'", "named": false }, { "type": "(", "named": false }, { "type": ")", "named": false }, { "type": "*", "named": false }, { "type": "+", "named": false }, { "type": ",", "named": false }, { "type": "-", "named": false }, { "type": "-->", "named": false }, { "type": ".", "named": false }, { "type": "/", "named": false }, { "type": ":", "named": false }, { "type": ";", "named": false }, { "type": "<", "named": false }, { "type": "<!--", "named": false }, { "type": "<![CDATA[", "named": false }, { "type": "<?", "named": false }, { "type": "=", "named": false }, { "type": ">", "named": false }, { "type": "?", "named": false }, { "type": "?>", "named": false }, { "type": "@", "named": false }, { "type": "[", "named": false }, { "type": "\\", "named": false }, { "type": "]", "named": false }, { "type": "]]>", "named": false }, { "type": "^", "named": false }, { "type": "_", "named": false }, { "type": "`", "named": false }, { "type": "code_span_delimiter", "named": true }, { "type": "email_autolink", "named": true }, { "type": "emphasis_delimiter", "named": true }, { "type": "entity_reference", "named": true }, { "type": "latex_span_delimiter", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "uri_autolink", "named": true }, { "type": "{", "named": false }, { "type": "|", "named": false }, { "type": "}", "named": false }, { "type": "~", "named": false } ] ``` -------------------------------------------------------------------------------- /deps/tree-sitter-markdown/tree-sitter-markdown-inline/src/scanner.c: -------------------------------------------------------------------------------- ```cpp #include "tree_sitter/parser.h" #ifdef _MSC_VER #define UNUSED __pragma(warning(suppress : 4101)) #else #define UNUSED __attribute__((unused)) #endif // For explanation of the tokens see grammar.js typedef enum { ERROR, TRIGGER_ERROR, CODE_SPAN_START, CODE_SPAN_CLOSE, EMPHASIS_OPEN_STAR, EMPHASIS_OPEN_UNDERSCORE, EMPHASIS_CLOSE_STAR, EMPHASIS_CLOSE_UNDERSCORE, LAST_TOKEN_WHITESPACE, LAST_TOKEN_PUNCTUATION, STRIKETHROUGH_OPEN, STRIKETHROUGH_CLOSE, LATEX_SPAN_START, LATEX_SPAN_CLOSE, UNCLOSED_SPAN } TokenType; // Determines if a character is punctuation as defined by the markdown spec. static bool is_punctuation(int32_t chr) { return (chr >= '!' && chr <= '/') || (chr >= ':' && chr <= '@') || (chr >= '[' && chr <= '`') || (chr >= '{' && chr <= '~'); } // State bitflags used with `Scanner.state` // TODO static UNUSED const uint8_t STATE_EMPHASIS_DELIMITER_MOD_3 = 0x3; // Current delimiter run is opening static const uint8_t STATE_EMPHASIS_DELIMITER_IS_OPEN = 0x1 << 2; // Convenience function to emit the error token. This is done to stop invalid // parse branches. Specifically: // 1. When encountering a newline after a line break that ended a paragraph, and // no new block // has been opened. // 2. When encountering a new block after a soft line break. // 3. When a `$._trigger_error` token is valid, which is used to stop parse // branches through // normal tree-sitter grammar rules. // // See also the `$._soft_line_break` and `$._paragraph_end_newline` tokens in // grammar.js static bool error(TSLexer *lexer) { lexer->result_symbol = ERROR; return true; } typedef struct { // Parser state flags uint8_t state; uint8_t code_span_delimiter_length; uint8_t latex_span_delimiter_length; // The number of characters remaining in the currrent emphasis delimiter // run. uint8_t num_emphasis_delimiters_left; } Scanner; // Write the whole state of a Scanner to a byte buffer static unsigned serialize(Scanner *s, char *buffer) { unsigned size = 0; buffer[size++] = (char)s->state; buffer[size++] = (char)s->code_span_delimiter_length; buffer[size++] = (char)s->latex_span_delimiter_length; buffer[size++] = (char)s->num_emphasis_delimiters_left; return size; } // Read the whole state of a Scanner from a byte buffer // `serizalize` and `deserialize` should be fully symmetric. static void deserialize(Scanner *s, const char *buffer, unsigned length) { s->state = 0; s->code_span_delimiter_length = 0; s->latex_span_delimiter_length = 0; s->num_emphasis_delimiters_left = 0; if (length > 0) { size_t size = 0; s->state = (uint8_t)buffer[size++]; s->code_span_delimiter_length = (uint8_t)buffer[size++]; s->latex_span_delimiter_length = (uint8_t)buffer[size++]; s->num_emphasis_delimiters_left = (uint8_t)buffer[size++]; } } static bool parse_leaf_delimiter(TSLexer *lexer, uint8_t *delimiter_length, const bool *valid_symbols, const char delimiter, const TokenType open_token, const TokenType close_token) { uint8_t level = 0; while (lexer->lookahead == delimiter) { lexer->advance(lexer, false); level++; } lexer->mark_end(lexer); if (level == *delimiter_length && valid_symbols[close_token]) { *delimiter_length = 0; lexer->result_symbol = close_token; return true; } if (valid_symbols[open_token]) { // Parse ahead to check if there is a closing delimiter size_t close_level = 0; while (!lexer->eof(lexer)) { if (lexer->lookahead == delimiter) { close_level++; } else { if (close_level == level) { // Found a matching delimiter break; } close_level = 0; } lexer->advance(lexer, false); } if (close_level == level) { *delimiter_length = level; lexer->result_symbol = open_token; return true; } if (valid_symbols[UNCLOSED_SPAN]) { lexer->result_symbol = UNCLOSED_SPAN; return true; } } return false; } static bool parse_backtick(Scanner *s, TSLexer *lexer, const bool *valid_symbols) { return parse_leaf_delimiter(lexer, &s->code_span_delimiter_length, valid_symbols, '`', CODE_SPAN_START, CODE_SPAN_CLOSE); } static bool parse_dollar(Scanner *s, TSLexer *lexer, const bool *valid_symbols) { return parse_leaf_delimiter(lexer, &s->latex_span_delimiter_length, valid_symbols, '$', LATEX_SPAN_START, LATEX_SPAN_CLOSE); } static bool parse_star(Scanner *s, TSLexer *lexer, const bool *valid_symbols) { lexer->advance(lexer, false); // If `num_emphasis_delimiters_left` is not zero then we already decided // that this should be part of an emphasis delimiter run, so interpret it as // such. if (s->num_emphasis_delimiters_left > 0) { // The `STATE_EMPHASIS_DELIMITER_IS_OPEN` state flag tells us wether it // should be open or close. if ((s->state & STATE_EMPHASIS_DELIMITER_IS_OPEN) && valid_symbols[EMPHASIS_OPEN_STAR]) { s->state &= (~STATE_EMPHASIS_DELIMITER_IS_OPEN); lexer->result_symbol = EMPHASIS_OPEN_STAR; s->num_emphasis_delimiters_left--; return true; } if (valid_symbols[EMPHASIS_CLOSE_STAR]) { lexer->result_symbol = EMPHASIS_CLOSE_STAR; s->num_emphasis_delimiters_left--; return true; } } lexer->mark_end(lexer); // Otherwise count the number of stars uint8_t star_count = 1; while (lexer->lookahead == '*') { star_count++; lexer->advance(lexer, false); } bool line_end = lexer->lookahead == '\n' || lexer->lookahead == '\r' || lexer->eof(lexer); if (valid_symbols[EMPHASIS_OPEN_STAR] || valid_symbols[EMPHASIS_CLOSE_STAR]) { // The desicion made for the first star also counts for all the // following stars in the delimiter run. Rembemer how many there are. s->num_emphasis_delimiters_left = star_count - 1; // Look ahead to the next symbol (after the last star) to find out if it // is whitespace punctuation or other. bool next_symbol_whitespace = line_end || lexer->lookahead == ' ' || lexer->lookahead == '\t'; bool next_symbol_punctuation = is_punctuation(lexer->lookahead); // Information about the last token is in valid_symbols. See grammar.js // for these tokens for how this is done. if (valid_symbols[EMPHASIS_CLOSE_STAR] && !valid_symbols[LAST_TOKEN_WHITESPACE] && (!valid_symbols[LAST_TOKEN_PUNCTUATION] || next_symbol_punctuation || next_symbol_whitespace)) { // Closing delimiters take precedence s->state &= ~STATE_EMPHASIS_DELIMITER_IS_OPEN; lexer->result_symbol = EMPHASIS_CLOSE_STAR; return true; } if (!next_symbol_whitespace && (!next_symbol_punctuation || valid_symbols[LAST_TOKEN_PUNCTUATION] || valid_symbols[LAST_TOKEN_WHITESPACE])) { s->state |= STATE_EMPHASIS_DELIMITER_IS_OPEN; lexer->result_symbol = EMPHASIS_OPEN_STAR; return true; } } return false; } static bool parse_tilde(Scanner *s, TSLexer *lexer, const bool *valid_symbols) { lexer->advance(lexer, false); // If `num_emphasis_delimiters_left` is not zero then we already decided // that this should be part of an emphasis delimiter run, so interpret it as // such. if (s->num_emphasis_delimiters_left > 0) { // The `STATE_EMPHASIS_DELIMITER_IS_OPEN` state flag tells us wether it // should be open or close. if ((s->state & STATE_EMPHASIS_DELIMITER_IS_OPEN) && valid_symbols[STRIKETHROUGH_OPEN]) { s->state &= (~STATE_EMPHASIS_DELIMITER_IS_OPEN); lexer->result_symbol = STRIKETHROUGH_OPEN; s->num_emphasis_delimiters_left--; return true; } if (valid_symbols[STRIKETHROUGH_CLOSE]) { lexer->result_symbol = STRIKETHROUGH_CLOSE; s->num_emphasis_delimiters_left--; return true; } } lexer->mark_end(lexer); // Otherwise count the number of tildes uint8_t star_count = 1; while (lexer->lookahead == '~') { star_count++; lexer->advance(lexer, false); } bool line_end = lexer->lookahead == '\n' || lexer->lookahead == '\r' || lexer->eof(lexer); if (valid_symbols[STRIKETHROUGH_OPEN] || valid_symbols[STRIKETHROUGH_CLOSE]) { // The desicion made for the first star also counts for all the // following stars in the delimiter run. Rembemer how many there are. s->num_emphasis_delimiters_left = star_count - 1; // Look ahead to the next symbol (after the last star) to find out if it // is whitespace punctuation or other. bool next_symbol_whitespace = line_end || lexer->lookahead == ' ' || lexer->lookahead == '\t'; bool next_symbol_punctuation = is_punctuation(lexer->lookahead); // Information about the last token is in valid_symbols. See grammar.js // for these tokens for how this is done. if (valid_symbols[STRIKETHROUGH_CLOSE] && !valid_symbols[LAST_TOKEN_WHITESPACE] && (!valid_symbols[LAST_TOKEN_PUNCTUATION] || next_symbol_punctuation || next_symbol_whitespace)) { // Closing delimiters take precedence s->state &= ~STATE_EMPHASIS_DELIMITER_IS_OPEN; lexer->result_symbol = STRIKETHROUGH_CLOSE; return true; } if (!next_symbol_whitespace && (!next_symbol_punctuation || valid_symbols[LAST_TOKEN_PUNCTUATION] || valid_symbols[LAST_TOKEN_WHITESPACE])) { s->state |= STATE_EMPHASIS_DELIMITER_IS_OPEN; lexer->result_symbol = STRIKETHROUGH_OPEN; return true; } } return false; } static bool parse_underscore(Scanner *s, TSLexer *lexer, const bool *valid_symbols) { lexer->advance(lexer, false); // If `num_emphasis_delimiters_left` is not zero then we already decided // that this should be part of an emphasis delimiter run, so interpret it as // such. if (s->num_emphasis_delimiters_left > 0) { // The `STATE_EMPHASIS_DELIMITER_IS_OPEN` state flag tells us wether it // should be open or close. if ((s->state & STATE_EMPHASIS_DELIMITER_IS_OPEN) && valid_symbols[EMPHASIS_OPEN_UNDERSCORE]) { s->state &= (~STATE_EMPHASIS_DELIMITER_IS_OPEN); lexer->result_symbol = EMPHASIS_OPEN_UNDERSCORE; s->num_emphasis_delimiters_left--; return true; } if (valid_symbols[EMPHASIS_CLOSE_UNDERSCORE]) { lexer->result_symbol = EMPHASIS_CLOSE_UNDERSCORE; s->num_emphasis_delimiters_left--; return true; } } lexer->mark_end(lexer); // Otherwise count the number of stars uint8_t underscore_count = 1; while (lexer->lookahead == '_') { underscore_count++; lexer->advance(lexer, false); } bool line_end = lexer->lookahead == '\n' || lexer->lookahead == '\r' || lexer->eof(lexer); if (valid_symbols[EMPHASIS_OPEN_UNDERSCORE] || valid_symbols[EMPHASIS_CLOSE_UNDERSCORE]) { // The desicion made for the first underscore also counts for all the // following underscores in the delimiter run. Rembemer how many there are. s->num_emphasis_delimiters_left = underscore_count - 1; // Look ahead to the next symbol (after the last underscore) to find out if it // is whitespace punctuation or other. bool next_symbol_whitespace = line_end || lexer->lookahead == ' ' || lexer->lookahead == '\t'; bool next_symbol_punctuation = is_punctuation(lexer->lookahead); // Information about the last token is in valid_symbols. See grammar.js // for these tokens for how this is done. if (valid_symbols[EMPHASIS_CLOSE_UNDERSCORE] && !valid_symbols[LAST_TOKEN_WHITESPACE] && (!valid_symbols[LAST_TOKEN_PUNCTUATION] || next_symbol_punctuation || next_symbol_whitespace)) { // Closing delimiters take precedence s->state &= ~STATE_EMPHASIS_DELIMITER_IS_OPEN; lexer->result_symbol = EMPHASIS_CLOSE_UNDERSCORE; return true; } if (!next_symbol_whitespace && (!next_symbol_punctuation || valid_symbols[LAST_TOKEN_PUNCTUATION] || valid_symbols[LAST_TOKEN_WHITESPACE])) { s->state |= STATE_EMPHASIS_DELIMITER_IS_OPEN; lexer->result_symbol = EMPHASIS_OPEN_UNDERSCORE; return true; } } return false; } static bool scan(Scanner *s, TSLexer *lexer, const bool *valid_symbols) { // A normal tree-sitter rule decided that the current branch is invalid and // now "requests" an error to stop the branch if (valid_symbols[TRIGGER_ERROR]) { return error(lexer); } // Decide which tokens to consider based on the first non-whitespace // character switch (lexer->lookahead) { case '`': // A backtick could mark the beginning or ending of a code span or a // fenced code block. return parse_backtick(s, lexer, valid_symbols); case '$': return parse_dollar(s, lexer, valid_symbols); case '*': // A star could either mark the beginning or ending of emphasis, a // list item or thematic break. This code is similar to the code for // '_' and '+'. return parse_star(s, lexer, valid_symbols); case '_': return parse_underscore(s, lexer, valid_symbols); case '~': return parse_tilde(s, lexer, valid_symbols); } return false; } void *tree_sitter_markdown_inline_external_scanner_create() { Scanner *s = (Scanner *)malloc(sizeof(Scanner)); deserialize(s, NULL, 0); return s; } bool tree_sitter_markdown_inline_external_scanner_scan( void *payload, TSLexer *lexer, const bool *valid_symbols) { Scanner *scanner = (Scanner *)payload; return scan(scanner, lexer, valid_symbols); } unsigned tree_sitter_markdown_inline_external_scanner_serialize(void *payload, char *buffer) { Scanner *scanner = (Scanner *)payload; return serialize(scanner, buffer); } void tree_sitter_markdown_inline_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) { Scanner *scanner = (Scanner *)payload; deserialize(scanner, buffer, length); } void tree_sitter_markdown_inline_external_scanner_destroy(void *payload) { Scanner *scanner = (Scanner *)payload; free(scanner); } ``` -------------------------------------------------------------------------------- /deps/tree-sitter-markdown/tree-sitter-markdown/src/node-types.json: -------------------------------------------------------------------------------- ```json [ { "type": "atx_heading", "named": true, "fields": { "heading_content": { "multiple": false, "required": false, "types": [ { "type": "inline", "named": true } ] } }, "children": { "multiple": true, "required": true, "types": [ { "type": "atx_h1_marker", "named": true }, { "type": "atx_h2_marker", "named": true }, { "type": "atx_h3_marker", "named": true }, { "type": "atx_h4_marker", "named": true }, { "type": "atx_h5_marker", "named": true }, { "type": "atx_h6_marker", "named": true }, { "type": "block_continuation", "named": true } ] } }, { "type": "backslash_escape", "named": true, "fields": {} }, { "type": "block_quote", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "block_continuation", "named": true }, { "type": "block_quote", "named": true }, { "type": "block_quote_marker", "named": true }, { "type": "fenced_code_block", "named": true }, { "type": "html_block", "named": true }, { "type": "indented_code_block", "named": true }, { "type": "link_reference_definition", "named": true }, { "type": "list", "named": true }, { "type": "paragraph", "named": true }, { "type": "pipe_table", "named": true }, { "type": "section", "named": true }, { "type": "setext_heading", "named": true }, { "type": "thematic_break", "named": true } ] } }, { "type": "code_fence_content", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "block_continuation", "named": true } ] } }, { "type": "document", "named": true, "root": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "minus_metadata", "named": true }, { "type": "plus_metadata", "named": true }, { "type": "section", "named": true } ] } }, { "type": "fenced_code_block", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "block_continuation", "named": true }, { "type": "code_fence_content", "named": true }, { "type": "fenced_code_block_delimiter", "named": true }, { "type": "info_string", "named": true } ] } }, { "type": "html_block", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "block_continuation", "named": true } ] } }, { "type": "indented_code_block", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "block_continuation", "named": true } ] } }, { "type": "info_string", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "entity_reference", "named": true }, { "type": "language", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "inline", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "block_continuation", "named": true } ] } }, { "type": "language", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "entity_reference", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "link_destination", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "entity_reference", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "link_label", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "block_continuation", "named": true }, { "type": "entity_reference", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "link_reference_definition", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "block_continuation", "named": true }, { "type": "link_destination", "named": true }, { "type": "link_label", "named": true }, { "type": "link_title", "named": true } ] } }, { "type": "link_title", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "backslash_escape", "named": true }, { "type": "block_continuation", "named": true }, { "type": "entity_reference", "named": true }, { "type": "numeric_character_reference", "named": true } ] } }, { "type": "list", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "list_item", "named": true } ] } }, { "type": "list_item", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "block_continuation", "named": true }, { "type": "block_quote", "named": true }, { "type": "fenced_code_block", "named": true }, { "type": "html_block", "named": true }, { "type": "indented_code_block", "named": true }, { "type": "link_reference_definition", "named": true }, { "type": "list", "named": true }, { "type": "list_marker_dot", "named": true }, { "type": "list_marker_minus", "named": true }, { "type": "list_marker_parenthesis", "named": true }, { "type": "list_marker_plus", "named": true }, { "type": "list_marker_star", "named": true }, { "type": "paragraph", "named": true }, { "type": "pipe_table", "named": true }, { "type": "section", "named": true }, { "type": "setext_heading", "named": true }, { "type": "task_list_marker_checked", "named": true }, { "type": "task_list_marker_unchecked", "named": true }, { "type": "thematic_break", "named": true } ] } }, { "type": "list_marker_dot", "named": true, "fields": {} }, { "type": "list_marker_minus", "named": true, "fields": {} }, { "type": "list_marker_parenthesis", "named": true, "fields": {} }, { "type": "list_marker_plus", "named": true, "fields": {} }, { "type": "list_marker_star", "named": true, "fields": {} }, { "type": "paragraph", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "block_continuation", "named": true }, { "type": "inline", "named": true } ] } }, { "type": "pipe_table", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "block_continuation", "named": true }, { "type": "pipe_table_delimiter_row", "named": true }, { "type": "pipe_table_header", "named": true }, { "type": "pipe_table_row", "named": true } ] } }, { "type": "pipe_table_cell", "named": true, "fields": {} }, { "type": "pipe_table_delimiter_cell", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "pipe_table_align_left", "named": true }, { "type": "pipe_table_align_right", "named": true } ] } }, { "type": "pipe_table_delimiter_row", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "pipe_table_delimiter_cell", "named": true } ] } }, { "type": "pipe_table_header", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "pipe_table_cell", "named": true } ] } }, { "type": "pipe_table_row", "named": true, "fields": {}, "children": { "multiple": true, "required": true, "types": [ { "type": "pipe_table_cell", "named": true } ] } }, { "type": "section", "named": true, "fields": {}, "children": { "multiple": true, "required": false, "types": [ { "type": "atx_heading", "named": true }, { "type": "block_continuation", "named": true }, { "type": "block_quote", "named": true }, { "type": "fenced_code_block", "named": true }, { "type": "html_block", "named": true }, { "type": "indented_code_block", "named": true }, { "type": "link_reference_definition", "named": true }, { "type": "list", "named": true }, { "type": "paragraph", "named": true }, { "type": "pipe_table", "named": true }, { "type": "section", "named": true }, { "type": "setext_heading", "named": true }, { "type": "thematic_break", "named": true } ] } }, { "type": "setext_heading", "named": true, "fields": { "heading_content": { "multiple": false, "required": true, "types": [ { "type": "paragraph", "named": true } ] } }, "children": { "multiple": true, "required": true, "types": [ { "type": "block_continuation", "named": true }, { "type": "setext_h1_underline", "named": true }, { "type": "setext_h2_underline", "named": true } ] } }, { "type": "task_list_marker_checked", "named": true, "fields": {} }, { "type": "task_list_marker_unchecked", "named": true, "fields": {} }, { "type": "thematic_break", "named": true, "fields": {}, "children": { "multiple": false, "required": false, "types": [ { "type": "block_continuation", "named": true } ] } }, { "type": "!", "named": false }, { "type": "\"", "named": false }, { "type": "#", "named": false }, { "type": "$", "named": false }, { "type": "%", "named": false }, { "type": "&", "named": false }, { "type": "'", "named": false }, { "type": "(", "named": false }, { "type": ")", "named": false }, { "type": "*", "named": false }, { "type": "+", "named": false }, { "type": ",", "named": false }, { "type": "-", "named": false }, { "type": "-->", "named": false }, { "type": ".", "named": false }, { "type": "/", "named": false }, { "type": ":", "named": false }, { "type": ";", "named": false }, { "type": "<", "named": false }, { "type": "=", "named": false }, { "type": ">", "named": false }, { "type": "?", "named": false }, { "type": "?>", "named": false }, { "type": "@", "named": false }, { "type": "[", "named": false }, { "type": "\\", "named": false }, { "type": "]", "named": false }, { "type": "]]>", "named": false }, { "type": "^", "named": false }, { "type": "_", "named": false }, { "type": "`", "named": false }, { "type": "atx_h1_marker", "named": true }, { "type": "atx_h2_marker", "named": true }, { "type": "atx_h3_marker", "named": true }, { "type": "atx_h4_marker", "named": true }, { "type": "atx_h5_marker", "named": true }, { "type": "atx_h6_marker", "named": true }, { "type": "block_continuation", "named": true }, { "type": "block_quote_marker", "named": true }, { "type": "entity_reference", "named": true }, { "type": "fenced_code_block_delimiter", "named": true }, { "type": "minus_metadata", "named": true }, { "type": "numeric_character_reference", "named": true }, { "type": "pipe_table_align_left", "named": true }, { "type": "pipe_table_align_right", "named": true }, { "type": "plus_metadata", "named": true }, { "type": "setext_h1_underline", "named": true }, { "type": "setext_h2_underline", "named": true }, { "type": "{", "named": false }, { "type": "|", "named": false }, { "type": "}", "named": false }, { "type": "~", "named": false } ] ``` -------------------------------------------------------------------------------- /src/core/config_manager.cpp: -------------------------------------------------------------------------------- ```cpp #include "config_manager.h" #include <algorithm> #include <cstdlib> #include <filesystem> #include <fstream> #include <functional> #include <iostream> #include <memory> #include <sstream> #include <yaml-cpp/yaml.h> // EFSW includes #include <efsw/efsw.h> #include <efsw/efsw.hpp> namespace fs = std::filesystem; // Static initialization std::string ConfigManager::config_dir_cache_; std::string ConfigManager::active_theme_ = "default"; std::vector<ConfigReloadCallback> ConfigManager::reload_callbacks_; std::unique_ptr<efsw::FileWatchListener> ConfigManager::watcher_listener_ = nullptr; std::unique_ptr<efsw::FileWatcher> ConfigManager::watcher_instance_ = nullptr; std::atomic<bool> ConfigManager::reload_pending_ = false; EditorConfig ConfigManager::editor_config_; SyntaxConfig ConfigManager::syntax_config_; // --- EFSW Listener Class --- // We define a custom listener that efsw will use to communicate events. class ConfigFileListener : public efsw::FileWatchListener { public: void handleFileAction(efsw::WatchID watchid, const std::string &dir, const std::string &filename, efsw::Action action, std::string oldFilename) override { // We are interested in Modification and Renaming (e.g., atomic save by // editor) if (filename == "config.yaml" && (action == efsw::Action::Modified || action == efsw::Action::Moved)) { // Call the static handler in ConfigManager ConfigManager::handleFileChange(); } } }; // ----------------------------------------------------------------- // CONFIG DIRECTORY AND PATH GETTERS // ----------------------------------------------------------------- std::string ConfigManager::getConfigDir() { // Return cached value if already determined if (!config_dir_cache_.empty()) { return config_dir_cache_; } std::vector<std::string> search_paths; #ifdef _WIN32 // Windows: %APPDATA%\arceditor const char *appdata = std::getenv("APPDATA"); if (appdata) { search_paths.push_back(std::string(appdata) + "\\arceditor"); } // Fallback: %USERPROFILE%\.config\arceditor const char *userprofile = std::getenv("USERPROFILE"); if (userprofile) { search_paths.push_back(std::string(userprofile) + "\\.config\\arceditor"); } #else // Linux/macOS: XDG_CONFIG_HOME or ~/.config const char *xdg_config = std::getenv("XDG_CONFIG_HOME"); if (xdg_config) { search_paths.push_back(std::string(xdg_config) + "/arceditor"); } const char *home = std::getenv("HOME"); if (home) { search_paths.push_back(std::string(home) + "/.config/arceditor"); } #endif // Development fallback to use ./config/arceditor std::string cwd = fs::current_path().string(); std::string dev_config_path = cwd + "/.config/arceditor"; if (fs::exists(dev_config_path) && fs::is_directory(dev_config_path)) { // Insert the local development path as the highest priority search_paths.insert(search_paths.begin(), dev_config_path); } // Find first existing config directory for (const auto &path : search_paths) { if (fs::exists(path) && fs::is_directory(path)) { config_dir_cache_ = path; // std::cerr << "Using config directory: " << config_dir_cache_ << // std::endl; return config_dir_cache_; } } // If no config dir exists, create one in the standard location (first in // search_paths) if (!search_paths.empty()) { std::string target_dir = search_paths[0]; try { fs::create_directories(target_dir); config_dir_cache_ = target_dir; std::cerr << "Created config directory: " << config_dir_cache_ << std::endl; return config_dir_cache_; } catch (const fs::filesystem_error &e) { std::cerr << "Failed to create config directory: " << e.what() << std::endl; } } // Last resort: use current directory config_dir_cache_ = cwd; std::cerr << "Warning: Using current directory as config dir" << std::endl; return config_dir_cache_; } std::string ConfigManager::getThemesDir() { return getConfigDir() + "/themes"; } std::string ConfigManager::getSyntaxRulesDir() { return getConfigDir() + "/syntax_rules"; } std::string ConfigManager::getConfigFile() { return getConfigDir() + "/config.yaml"; } bool ConfigManager::ensureConfigStructure() { try { std::string config_dir = getConfigDir(); // Create main config directory (already handled in getConfigDir, but safety // check) if (!fs::exists(config_dir)) { fs::create_directories(config_dir); } // Create subdirectories std::string themes_dir = config_dir + "/themes"; // std::string syntax_dir = config_dir + "/syntax_rules"; if (!fs::exists(themes_dir)) { fs::create_directories(themes_dir); std::cerr << "Created themes directory: " << themes_dir << std::endl; } // if (!fs::exists(syntax_dir)) // { // fs::create_directories(syntax_dir); // std::cerr << "Created syntax_rules directory: " << syntax_dir // << std::endl; // } // Create default config file if it doesn't exist std::string config_file = getConfigFile(); if (!fs::exists(config_file)) { createDefaultConfig(config_file); } return true; } catch (const fs::filesystem_error &e) { std::cerr << "Error ensuring config structure: " << e.what() << std::endl; return false; } } // ----------------------------------------------------------------- // CONFIGURATION (YAML) MANAGEMENT // ----------------------------------------------------------------- bool ConfigManager::createDefaultConfig(const std::string &config_file) { try { YAML::Node config; config["appearance"]["theme"] = "default"; config["editor"]["tab_size"] = 4; config["editor"]["line_numbers"] = true; config["editor"]["cursor_style"] = "auto"; config["syntax"]["highlighting"] = "viewport"; // Changed to string std::ofstream file(config_file); if (!file.is_open()) { std::cerr << "Failed to create default config file" << std::endl; return false; } file << "# arceditor Configuration File\n"; file << "# This file is automatically generated\n\n"; file << config; file.close(); std::cerr << "Created default config: " << config_file << std::endl; return true; } catch (const std::exception &e) { std::cerr << "Failed to create config: " << e.what() << std::endl; return false; } } bool ConfigManager::loadConfig() { std::string config_file = getConfigFile(); if (!fs::exists(config_file)) { std::cerr << "Config file not found, using defaults" << std::endl; return false; } try { YAML::Node config = YAML::LoadFile(config_file); // Load appearance section if (config["appearance"] && config["appearance"]["theme"]) { active_theme_ = config["appearance"]["theme"].as<std::string>(); } // Load editor section if (config["editor"]) { if (config["editor"]["tab_size"]) { editor_config_.tab_size = config["editor"]["tab_size"].as<int>(); } if (config["editor"]["line_numbers"]) { editor_config_.line_numbers = config["editor"]["line_numbers"].as<bool>(); } if (config["editor"]["cursor_style"]) { editor_config_.cursor_style = config["editor"]["cursor_style"].as<std::string>(); } } // Load syntax section if (config["syntax"] && config["syntax"]["highlighting"]) { std::string mode_str = config["syntax"]["highlighting"].as<std::string>(); syntax_config_.highlighting = parseSyntaxMode(mode_str); } return true; } catch (const YAML::Exception &e) { std::cerr << "Failed to load config: " << e.what() << std::endl; return false; } } bool ConfigManager::saveConfig() { std::string config_file = getConfigFile(); YAML::Node config; try { // Try to load existing config to preserve comments/structure config = YAML::LoadFile(config_file); } catch (const YAML::BadFile &) { // File doesn't exist, create new structure } // Update all sections config["appearance"]["theme"] = active_theme_; config["editor"]["tab_size"] = editor_config_.tab_size; config["editor"]["line_numbers"] = editor_config_.line_numbers; config["editor"]["cursor_style"] = editor_config_.cursor_style; config["syntax"]["highlighting"] = syntaxModeToString(syntax_config_.highlighting); try { std::ofstream file(config_file); if (!file.is_open()) { std::cerr << "Failed to open config file for saving" << std::endl; return false; } file << "# arceditor Configuration File\n\n"; file << config; file.close(); return true; } catch (const std::exception &e) { std::cerr << "Failed to save config: " << e.what() << std::endl; return false; } } // ----------------------------------------------------------------- // LIVE RELOAD IMPLEMENTATION (EFSW) // ----------------------------------------------------------------- void ConfigManager::registerReloadCallback(ConfigReloadCallback callback) { reload_callbacks_.push_back(callback); } void ConfigManager::handleFileChange() { std::cerr << "Config file modified. Attempting hot reload..." << std::endl; // 1. Re-read the configuration file if (!loadConfig()) { std::cerr << "Failed to hot reload configuration. File may be invalid." << std::endl; return; } // 2. Notify all subscribed components for (const auto &callback : reload_callbacks_) { // NOTE: In a multi-threaded app, this should be marshaled to the main // thread. callback(); } reload_pending_.store(true); // std::cerr << "Configuration hot reload complete." << std::endl; } bool ConfigManager::isReloadPending() { // Atomically check the flag and reset it to false in one operation return reload_pending_.exchange(false); } bool ConfigManager::startWatchingConfig() { // 1. Check if watching is already active if (watcher_instance_ && watcher_listener_) { return true; // Already watching } // 2. Instantiate the FileWatcher and Listener try { // The FileWatcher instance is heavy and should be long-lived watcher_instance_ = std::make_unique<efsw::FileWatcher>(); // The listener is the custom class we defined earlier watcher_listener_ = std::make_unique<ConfigFileListener>(); // 3. Get the directory to watch (the config directory) std::string configDir = getConfigDir(); // 4. Add the watch. The 'true' is for recursive watching, // but the listener only cares about 'config.yaml' anyway. efsw::WatchID watchID = watcher_instance_->addWatch( configDir, watcher_listener_.get(), false // false for non-recursive ); if (watchID < 0) { std::cerr << "Error: EFSW failed to add watch for config directory: " << configDir << std::endl; // Cleanup pointers if the watch failed watcher_instance_.reset(); watcher_listener_.reset(); return false; } // 5. Start the watcher thread watcher_instance_->watch(); // This starts the background thread // std::cerr << "Config watching started for: " << configDir << std::endl; return true; } catch (const std::exception &e) { std::cerr << "Fatal Error starting EFSW watcher: " << e.what() << std::endl; watcher_instance_.reset(); watcher_listener_.reset(); return false; } } // ----------------------------------------------------------------- // THEME AND SYNTAX MANAGEMENT // ----------------------------------------------------------------- std::string ConfigManager::getThemeFile(const std::string &theme_name) { std::string themes_dir = getThemesDir(); std::string theme_file = themes_dir + "/" + theme_name + ".theme"; if (fs::exists(theme_file)) { return theme_file; } // Fallback: Check project's "themes" directory for default files std::string dev_theme = "themes/" + theme_name + ".theme"; if (fs::exists(dev_theme)) { return dev_theme; } std::cerr << "Theme file not found: " << theme_name << std::endl; return ""; } std::string ConfigManager::getSyntaxFile(const std::string &language) { std::string syntax_dir = getSyntaxRulesDir(); std::string syntax_file = syntax_dir + "/" + language + ".yaml"; if (fs::exists(syntax_file)) { return syntax_file; } // Fallback: Check project's "syntax_rules" directory for default files std::string dev_syntax = "treesitter/" + language + ".yaml"; if (fs::exists(dev_syntax)) { return dev_syntax; } return ""; // Not found } std::string ConfigManager::getActiveTheme() { return active_theme_; } bool ConfigManager::setActiveTheme(const std::string &theme_name) { // Verify theme exists std::string theme_file = getThemeFile(theme_name); if (theme_file.empty()) { std::cerr << "Cannot set theme, file not found: " << theme_name << std::endl; return false; } active_theme_ = theme_name; saveConfig(); // Persist the change return true; } bool ConfigManager::copyProjectFilesToConfig() { try { std::string config_dir = getConfigDir(); // Copy themes if (fs::exists("themes") && fs::is_directory("themes")) { std::string target_themes = config_dir + "/themes"; fs::create_directories(target_themes); for (const auto &entry : fs::directory_iterator("themes")) { if (entry.is_regular_file() && entry.path().extension() == ".theme") { std::string filename = entry.path().filename().string(); std::string target = target_themes + "/" + filename; // Only copy if doesn't exist (don't overwrite user themes) if (!fs::exists(target)) { fs::copy_file(entry.path(), target); std::cerr << "Copied theme: " << filename << std::endl; } } } } // Copy syntax rules if (fs::exists("syntax_rules") && fs::is_directory("syntax_rules")) { std::string target_syntax = config_dir + "/syntax_rules"; fs::create_directories(target_syntax); for (const auto &entry : fs::directory_iterator("syntax_rules")) { if (entry.is_regular_file() && entry.path().extension() == ".yaml") { std::string filename = entry.path().filename().string(); std::string target = target_syntax + "/" + filename; // Only copy if doesn't exist if (!fs::exists(target)) { fs::copy_file(entry.path(), target); std::cerr << "Copied syntax rule: " << filename << std::endl; } } } } return true; } catch (const fs::filesystem_error &e) { std::cerr << "Error copying project files: " << e.what() << std::endl; return false; } } SyntaxMode ConfigManager::parseSyntaxMode(const std::string &mode_str) { std::string lower = mode_str; std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); if (lower == "none" || lower == "false" || lower == "off") return SyntaxMode::NONE; else if (lower == "viewport" || lower == "lazy" || lower == "dynamic") return SyntaxMode::VIEWPORT; else if (lower == "full" || lower == "immediate" || lower == "true") return SyntaxMode::FULL; std::cerr << "Unknown syntax mode '" << mode_str << "', using viewport" << std::endl; return SyntaxMode::VIEWPORT; } std::string ConfigManager::syntaxModeToString(SyntaxMode mode) { switch (mode) { case SyntaxMode::NONE: return "none"; case SyntaxMode::VIEWPORT: return "viewport"; case SyntaxMode::FULL: return "full"; default: return "viewport"; } } // NEW: Setters void ConfigManager::setTabSize(int size) { if (size < 1) size = 1; if (size > 16) size = 16; editor_config_.tab_size = size; saveConfig(); } void ConfigManager::setLineNumbers(bool enabled) { editor_config_.line_numbers = enabled; saveConfig(); } void ConfigManager::setCursorStyle(const std::string &style) { editor_config_.cursor_style = style; saveConfig(); } void ConfigManager::setSyntaxMode(SyntaxMode mode) { syntax_config_.highlighting = mode; saveConfig(); } ``` -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- ``` cmake_minimum_required(VERSION 3.16) project(arc VERSION 0.0.1 LANGUAGES CXX C) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Define a central location for external dependencies set(DEPS_DIR ${CMAKE_SOURCE_DIR}/deps) # Initialize Tree-sitter as enabled by default set(TREE_SITTER_ENABLED TRUE) # Debug Delta # add_compile_definitions(DEBUG_DELTA_UNDO) # ---------------------------------------------------- # 1. Tree-sitter Core Library # ---------------------------------------------------- # Manually list core source files set(TS_CORE_SOURCES ${DEPS_DIR}/tree-sitter-core/lib/src/language.c ${DEPS_DIR}/tree-sitter-core/lib/src/lexer.c ${DEPS_DIR}/tree-sitter-core/lib/src/node.c ${DEPS_DIR}/tree-sitter-core/lib/src/parser.c ${DEPS_DIR}/tree-sitter-core/lib/src/query.c ${DEPS_DIR}/tree-sitter-core/lib/src/tree.c ${DEPS_DIR}/tree-sitter-core/lib/src/tree_cursor.c ${DEPS_DIR}/tree-sitter-core/lib/src/alloc.c ${DEPS_DIR}/tree-sitter-core/lib/src/get_changed_ranges.c ${DEPS_DIR}/tree-sitter-core/lib/src/stack.c ${DEPS_DIR}/tree-sitter-core/lib/src/subtree.c ${DEPS_DIR}/tree-sitter-core/lib/src/point.c ${DEPS_DIR}/tree-sitter-core/lib/src/wasm_store.c ) # Check if Tree-sitter headers exist if(NOT EXISTS ${DEPS_DIR}/tree-sitter-core/lib/include/tree_sitter/api.h) message(WARNING "Tree-sitter header files not found at ${DEPS_DIR}/tree-sitter-core/lib/include/tree_sitter/api.h") set(TREE_SITTER_ENABLED FALSE) endif() if(TREE_SITTER_ENABLED) # Verify core files exist set(MISSING_FILES "") foreach(file ${TS_CORE_SOURCES}) if(NOT EXISTS ${file}) list(APPEND MISSING_FILES ${file}) endif() endforeach() if(MISSING_FILES) message(WARNING "Missing Tree-sitter core files: ${MISSING_FILES}") set(TREE_SITTER_ENABLED FALSE) else() message(STATUS "Tree-sitter core sources detected: ${TS_CORE_SOURCES}") message(STATUS "Building Tree-sitter core library.") add_library(tree-sitter-core STATIC ${TS_CORE_SOURCES}) # Explicitly set these as C sources set_source_files_properties(${TS_CORE_SOURCES} PROPERTIES LANGUAGE C) # The public API headers are in lib/include/ target_include_directories(tree-sitter-core PUBLIC ${DEPS_DIR}/tree-sitter-core/lib/include ) # Explicitly set C standard for C files target_compile_features(tree-sitter-core PUBLIC c_std_99) # Force static runtime for Tree-sitter to match main executable if(MSVC) target_compile_options(tree-sitter-core PRIVATE $<$<CONFIG:Debug>:/MTd> $<$<CONFIG:Release>:/MT> ) endif() set(TS_LIBRARIES tree-sitter-core) set(TS_INCLUDES ${DEPS_DIR}/tree-sitter-core/lib/include) message(STATUS "Tree-sitter enabled with include path: ${TS_INCLUDES}") endif() else() message(WARNING "Tree-sitter core sources or headers not found. Disabling Tree-sitter features.") set(TREE_SITTER_ENABLED FALSE) set(TS_LIBRARIES "") set(TS_INCLUDES "") endif() # ---------------------------------------------------- # 2. Language Parsers (Auto-Discovery) - FIXED FOR WINDOWS # ---------------------------------------------------- if(TREE_SITTER_ENABLED) message(STATUS "=== Tree-sitter Auto-Discovery ===") # Define parsers - use CMake lists instead of colon-separated strings # Format: list of pairs (lang_name, parser_path) set(PARSER_NAMES "python" "c" "cpp" "rust" "markdown" "javascript" "typescript" "tsx" "zig" "go" ) set(PARSER_PATHS "${DEPS_DIR}/tree-sitter-python" "${DEPS_DIR}/tree-sitter-c" "${DEPS_DIR}/tree-sitter-cpp" "${DEPS_DIR}/tree-sitter-rust" "${DEPS_DIR}/tree-sitter-markdown/tree-sitter-markdown" "${DEPS_DIR}/tree-sitter-javascript" "${DEPS_DIR}/tree-sitter-typescript/typescript" "${DEPS_DIR}/tree-sitter-typescript/tsx" "${DEPS_DIR}/tree-sitter-zig" "${DEPS_DIR}/tree-sitter-go" ) set(DISCOVERED_PARSERS "") # Iterate using indices list(LENGTH PARSER_NAMES parser_count) math(EXPR parser_count "${parser_count} - 1") foreach(i RANGE ${parser_count}) list(GET PARSER_NAMES ${i} lang_name) list(GET PARSER_PATHS ${i} parser_dir) if(NOT EXISTS ${parser_dir}) message(STATUS " ✗ Skipping ${lang_name}: directory not found at ${parser_dir}") continue() endif() # Collect source files set(PARSER_SOURCES "") # Check for parser.c (required) if(EXISTS ${parser_dir}/src/parser.c) list(APPEND PARSER_SOURCES ${parser_dir}/src/parser.c) else() message(STATUS " ✗ Skipping ${lang_name}: no parser.c at ${parser_dir}/src/") continue() endif() # Check for scanner files (optional) if(EXISTS ${parser_dir}/src/scanner.c) list(APPEND PARSER_SOURCES ${parser_dir}/src/scanner.c) endif() if(EXISTS ${parser_dir}/src/scanner.cc) list(APPEND PARSER_SOURCES ${parser_dir}/src/scanner.cc) endif() # Create library add_library(tree-sitter-${lang_name} STATIC ${PARSER_SOURCES}) # Set as C sources set_source_files_properties(${PARSER_SOURCES} PROPERTIES LANGUAGE C) # Set C99 standard target_compile_features(tree-sitter-${lang_name} PUBLIC c_std_99) # Force static runtime for parsers to match main executable if(MSVC) target_compile_options(tree-sitter-${lang_name} PRIVATE $<$<CONFIG:Debug>:/MTd> $<$<CONFIG:Release>:/MT> ) endif() # Include directories for scanner files target_include_directories(tree-sitter-${lang_name} PRIVATE ${parser_dir}/src ) # Add to libraries list list(APPEND TS_LIBRARIES tree-sitter-${lang_name}) list(APPEND DISCOVERED_PARSERS ${lang_name}) message(STATUS " ✓ Built parser: ${lang_name}") endforeach() # ---------------------------------------------------- # 3. Generate Language Registry Header (Auto-registration) # ---------------------------------------------------- if(DISCOVERED_PARSERS) set(LANG_REGISTRY_FILE "${CMAKE_BINARY_DIR}/generated/language_registry.h") file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/generated") # Build the header file content set(REGISTRY_CONTENT "// Auto-generated by CMake - DO NOT EDIT MANUALLY\n") string(APPEND REGISTRY_CONTENT "// Generated from: ${CMAKE_CURRENT_LIST_FILE}\n\n") string(APPEND REGISTRY_CONTENT "#pragma once\n\n") string(APPEND REGISTRY_CONTENT "#ifdef TREE_SITTER_ENABLED\n\n") string(APPEND REGISTRY_CONTENT "#include <tree_sitter/api.h>\n") string(APPEND REGISTRY_CONTENT "#include <unordered_map>\n") string(APPEND REGISTRY_CONTENT "#include <string>\n\n") # Extern declarations for all discovered languages string(APPEND REGISTRY_CONTENT "// External language function declarations\n") string(APPEND REGISTRY_CONTENT "extern \"C\" {\n") foreach(lang ${DISCOVERED_PARSERS}) string(APPEND REGISTRY_CONTENT " const TSLanguage *tree_sitter_${lang}();\n") endforeach() string(APPEND REGISTRY_CONTENT "}\n\n") # Registration function string(APPEND REGISTRY_CONTENT "// Auto-register all available languages\n") string(APPEND REGISTRY_CONTENT "inline void registerAllLanguages(std::unordered_map<std::string, const TSLanguage* (*)()>& registry) {\n") foreach(lang ${DISCOVERED_PARSERS}) string(APPEND REGISTRY_CONTENT " registry[\"${lang}\"] = tree_sitter_${lang};\n") endforeach() string(APPEND REGISTRY_CONTENT "}\n\n") # List of available languages as a comment string(APPEND REGISTRY_CONTENT "// Available languages: ") string(JOIN DISCOVERED_PARSERS ", " LANG_LIST) string(APPEND REGISTRY_CONTENT "${LANG_LIST}\n\n") string(APPEND REGISTRY_CONTENT "#endif // TREE_SITTER_ENABLED\n") # Write the file file(WRITE ${LANG_REGISTRY_FILE} "${REGISTRY_CONTENT}") message(STATUS "Generated language registry: ${LANG_REGISTRY_FILE}") message(STATUS " Registered parsers: ${DISCOVERED_PARSERS}") else() message(WARNING "No parsers discovered, skipping registry generation") set(TREE_SITTER_ENABLED FALSE) endif() message(STATUS "=== End Tree-sitter Auto-Discovery ===") endif() if(NOT TREE_SITTER_ENABLED) message(STATUS "Tree-sitter disabled - using fallback syntax highlighting") set(TS_LIBRARIES "") set(TS_INCLUDES "") endif() # ---------------------------------------------------- # 4. EFSW (Event File System Watcher) - For Live Reloading # ---------------------------------------------------- set(EFSW_BASE_DIR ${DEPS_DIR}/efsw) set(EFSW_SOURCES "") # List ALL required core files from the flattened structure (src/efsw/) list(APPEND EFSW_SOURCES # Core Files (Unconditional) ${EFSW_BASE_DIR}/src/efsw/Debug.cpp ${EFSW_BASE_DIR}/src/efsw/DirWatcherGeneric.cpp ${EFSW_BASE_DIR}/src/efsw/DirectorySnapshot.cpp ${EFSW_BASE_DIR}/src/efsw/DirectorySnapshotDiff.cpp ${EFSW_BASE_DIR}/src/efsw/FileInfo.cpp ${EFSW_BASE_DIR}/src/efsw/FileSystem.cpp ${EFSW_BASE_DIR}/src/efsw/FileWatcher.cpp ${EFSW_BASE_DIR}/src/efsw/FileWatcherCWrapper.cpp ${EFSW_BASE_DIR}/src/efsw/FileWatcherImpl.cpp ${EFSW_BASE_DIR}/src/efsw/Log.cpp ${EFSW_BASE_DIR}/src/efsw/String.cpp ${EFSW_BASE_DIR}/src/efsw/System.cpp ${EFSW_BASE_DIR}/src/efsw/Watcher.cpp # CRITICAL FIX: Add Generic implementation for default constructors ${EFSW_BASE_DIR}/src/efsw/FileWatcherGeneric.cpp ${EFSW_BASE_DIR}/src/efsw/WatcherGeneric.cpp ) # Conditionally add the correct platform backend if(CMAKE_SYSTEM_NAME MATCHES "Linux") list(APPEND EFSW_SOURCES ${EFSW_BASE_DIR}/src/efsw/FileWatcherInotify.cpp ${EFSW_BASE_DIR}/src/efsw/WatcherInotify.cpp ${EFSW_BASE_DIR}/src/efsw/platform/posix/FileSystemImpl.cpp ${EFSW_BASE_DIR}/src/efsw/platform/posix/SystemImpl.cpp ) elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") # macOS list(APPEND EFSW_SOURCES ${EFSW_BASE_DIR}/src/efsw/FileWatcherFSEvents.cpp ${EFSW_BASE_DIR}/src/efsw/WatcherFSEvents.cpp ${EFSW_BASE_DIR}/src/efsw/platform/posix/FileSystemImpl.cpp ${EFSW_BASE_DIR}/src/efsw/platform/posix/SystemImpl.cpp ) elseif(WIN32) list(APPEND EFSW_SOURCES ${EFSW_BASE_DIR}/src/efsw/FileWatcherWin32.cpp ${EFSW_BASE_DIR}/src/efsw/WatcherWin32.cpp ${EFSW_BASE_DIR}/src/efsw/platform/win/FileSystemImpl.cpp ${EFSW_BASE_DIR}/src/efsw/platform/win/SystemImpl.cpp ) else() list(APPEND EFSW_SOURCES ${EFSW_BASE_DIR}/src/efsw/FileWatcherGeneric.cpp ${EFSW_BASE_DIR}/src/efsw/WatcherGeneric.cpp ) endif() if(EFSW_SOURCES) add_library(efsw STATIC ${EFSW_SOURCES}) target_include_directories(efsw PUBLIC ${EFSW_BASE_DIR}/include ${EFSW_BASE_DIR}/src ) # Force static runtime for EFSW to match main executable if(MSVC) target_compile_options(efsw PRIVATE $<$<CONFIG:Debug>:/MTd> $<$<CONFIG:Release>:/MT> ) endif() find_package(Threads REQUIRED) target_link_libraries(efsw PRIVATE Threads::Threads) if(CMAKE_SYSTEM_NAME MATCHES "Linux") target_link_libraries(efsw PRIVATE rt) elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") target_link_libraries(efsw PRIVATE CoreServices) endif() message(STATUS "Added EFSW from deps folder.") set(EFSW_LIBRARIES efsw) else() message(WARNING "EFSW sources missing. Live reload feature disabled.") set(EFSW_LIBRARIES "") endif() # ---------------------------------------------------- # 5. Platform-Specific Settings # ---------------------------------------------------- # Windows-specific optimizations if(WIN32) set(VCPKG_APPLOCAL_DEPS OFF) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF) set(CMAKE_COLOR_MAKEFILE OFF) if(MINGW) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++ -static") endif() if(MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>") endif() endif() # Find packages - platform-specific approach if(WIN32) set(PDCURSESMOD_DIR ${DEPS_DIR}/PDCursesMod/wincon) if(EXISTS "${PDCURSESMOD_DIR}/pdcurses.lib") message(STATUS "Using locally built PDCursesMod from ${PDCURSESMOD_DIR}") add_library(pdcursesmod STATIC IMPORTED) set_target_properties(pdcursesmod PROPERTIES IMPORTED_LOCATION "${PDCURSESMOD_DIR}/pdcurses.lib" INTERFACE_INCLUDE_DIRECTORIES "${DEPS_DIR}/PDCursesMod" ) set(CURSES_LIBRARIES pdcursesmod) set(CURSES_INCLUDE_DIRS "${DEPS_DIR}/PDCursesMod") else() message(FATAL_ERROR "PDCursesMod not found at ${PDCURSESMOD_DIR}. Please build it manually with nmake -f Makefile.vc HAVE_VT=Y") endif() elseif(ANDROID) find_package(PkgConfig REQUIRED) pkg_check_modules(NCURSES REQUIRED ncurses) set(CURSES_LIBRARIES ${NCURSES_LIBRARIES}) set(CURSES_INCLUDE_DIRS ${NCURSES_INCLUDE_DIRS}) else() find_package(Curses REQUIRED) set(CURSES_LIBRARIES ${CURSES_LIBRARIES}) set(CURSES_INCLUDE_DIRS ${CURSES_INCLUDE_DIR}) endif() find_package(yaml-cpp CONFIG REQUIRED) # ---------------------------------------------------- # 6. Main Executable # ---------------------------------------------------- # Source files set(SOURCES src/main.cpp src/core/editor.cpp src/core/buffer.cpp src/core/config_manager.cpp src/ui/input_handler.cpp # src/ui/renderer.cpp src/ui/style_manager.cpp src/features/syntax_config_loader.cpp src/features/syntax_highlighter.cpp ) # Create executable add_executable(arc ${SOURCES}) # Conditionally add Tree-sitter compile definition if(TREE_SITTER_ENABLED) target_compile_definitions(arc PRIVATE TREE_SITTER_ENABLED) message(STATUS "Compiling with Tree-sitter support enabled") else() message(STATUS "Compiling without Tree-sitter support") endif() # Link libraries target_link_libraries(arc PRIVATE yaml-cpp::yaml-cpp ${CURSES_LIBRARIES} ${TS_LIBRARIES} ${EFSW_LIBRARIES} ) if(WIN32) # Link the Windows Multimedia library required by PDCursesMod's beep() target_link_libraries(arc PRIVATE winmm) endif() # Include directories target_include_directories(arc PRIVATE . ${CURSES_INCLUDE_DIRS} ${TS_INCLUDES} ) # Add generated headers directory if Tree-sitter is enabled if(TREE_SITTER_ENABLED) target_include_directories(arc PRIVATE ${CMAKE_BINARY_DIR}/generated) endif() # Compiler flags with optimizations if(MSVC) target_compile_options(arc PRIVATE /W4 /MP) target_compile_options(arc PRIVATE $<$<CONFIG:Debug>:/MTd> $<$<CONFIG:Release>:/MT>) else() target_compile_options(arc PRIVATE -Wall -Wextra) if(ANDROID) target_link_libraries(arc PRIVATE ${NCURSES_LINK_LIBRARIES}) endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(arc PRIVATE -g1 -O0 -fno-omit-frame-pointer) else() target_compile_options(arc PRIVATE -O2) endif() endif() # ---------------------------------------------------- # 7. Build Summary # ---------------------------------------------------- message(STATUS "") message(STATUS "========================================") message(STATUS "Arc Editor Build Configuration") message(STATUS "========================================") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID}") message(STATUS "Platform: ${CMAKE_SYSTEM_NAME}") message(STATUS "Curses library: ${CURSES_LIBRARIES}") message(STATUS "Tree-sitter enabled: ${TREE_SITTER_ENABLED}") if(TREE_SITTER_ENABLED) message(STATUS " Tree-sitter libraries: ${TS_LIBRARIES}") message(STATUS " Tree-sitter includes: ${TS_INCLUDES}") message(STATUS " Discovered parsers: ${DISCOVERED_PARSERS}") endif() message(STATUS "EFSW enabled: ${EFSW_LIBRARIES}") if(WIN32) message(STATUS "VCPKG_APPLOCAL_DEPS: ${VCPKG_APPLOCAL_DEPS}") endif() message(STATUS "========================================") message(STATUS "") ``` -------------------------------------------------------------------------------- /deps/tree-sitter-markdown/bindings/rust/parser.rs: -------------------------------------------------------------------------------- ```rust use std::collections::HashMap; use std::num::NonZeroU16; use tree_sitter::{InputEdit, Language, Node, Parser, Point, Range, Tree, TreeCursor}; use crate::{INLINE_LANGUAGE, LANGUAGE}; /// A parser that produces [`MarkdownTree`]s. /// /// This is a convenience wrapper around [`LANGUAGE`] and [`INLINE_LANGUAGE`]. pub struct MarkdownParser { parser: Parser, block_language: Language, inline_language: Language, } /// A stateful object for walking a [`MarkdownTree`] efficiently. /// /// This exposes the same methdos as [`TreeCursor`], but abstracts away the /// double block / inline structure of [`MarkdownTree`]. pub struct MarkdownCursor<'a> { markdown_tree: &'a MarkdownTree, block_cursor: TreeCursor<'a>, inline_cursor: Option<TreeCursor<'a>>, } impl<'a> MarkdownCursor<'a> { /// Get the cursor's current [`Node`]. pub fn node(&self) -> Node<'a> { match &self.inline_cursor { Some(cursor) => cursor.node(), None => self.block_cursor.node(), } } /// Returns `true` if the current node is from the (inline language)[INLINE_LANGUAGE] /// /// This information is needed to handle "tree-sitter internal" data like /// [`field_id`](Self::field_id) correctly. pub fn is_inline(&self) -> bool { self.inline_cursor.is_some() } /// Get the numerical field id of this tree cursor’s current node. /// /// You will need to call [`is_inline`](Self::is_inline) to find out if the /// current node is an inline or block node. /// /// See also [`field_name`](Self::field_name). pub fn field_id(&self) -> Option<NonZeroU16> { match &self.inline_cursor { Some(cursor) => cursor.field_id(), None => self.block_cursor.field_id(), } } /// Get the field name of this tree cursor’s current node. /// /// You will need to call [`is_inline`](Self::is_inline) to find out if the /// current node is an inline or block node. pub fn field_name(&self) -> Option<&'static str> { match &self.inline_cursor { Some(cursor) => cursor.field_name(), None => self.block_cursor.field_name(), } } fn move_to_inline_tree(&mut self) -> bool { let node = self.block_cursor.node(); match node.kind() { "inline" | "pipe_table_cell" => { if let Some(inline_tree) = self.markdown_tree.inline_tree(&node) { self.inline_cursor = Some(inline_tree.walk()); return true; } } _ => (), } false } fn move_to_block_tree(&mut self) { self.inline_cursor = None; } /// Move this cursor to the first child of its current node. /// /// This returns `true` if the cursor successfully moved, and returns `false` if there were no /// children. /// If the cursor is currently at a node in the block tree and it has an associated inline tree, it /// will descend into the inline tree. pub fn goto_first_child(&mut self) -> bool { match &mut self.inline_cursor { Some(cursor) => cursor.goto_first_child(), None => { if self.move_to_inline_tree() { if !self.inline_cursor.as_mut().unwrap().goto_first_child() { self.move_to_block_tree(); false } else { true } } else { self.block_cursor.goto_first_child() } } } } /// Move this cursor to the parent of its current node. /// /// This returns true if the cursor successfully moved, and returns false if there was no /// parent node (the cursor was already on the root node). /// If the cursor moves to the root node of an inline tree, the it ascents to the associated /// node in the block tree. pub fn goto_parent(&mut self) -> bool { match &mut self.inline_cursor { Some(inline_cursor) => { inline_cursor.goto_parent(); if inline_cursor.node().parent().is_none() { self.move_to_block_tree(); } true } None => self.block_cursor.goto_parent(), } } /// Move this cursor to the next sibling of its current node. /// /// This returns true if the cursor successfully moved, and returns false if there was no next /// sibling node. pub fn goto_next_sibling(&mut self) -> bool { match &mut self.inline_cursor { Some(inline_cursor) => inline_cursor.goto_next_sibling(), None => self.block_cursor.goto_next_sibling(), } } /// Move this cursor to the first child of its current node that extends beyond the given byte offset. /// /// This returns the index of the child node if one was found, and returns None if no such child was found. /// If the cursor is currently at a node in the block tree and it has an associated inline tree, it /// will descend into the inline tree. pub fn goto_first_child_for_byte(&mut self, index: usize) -> Option<usize> { match &mut self.inline_cursor { Some(cursor) => cursor.goto_first_child_for_byte(index), None => { if self.move_to_inline_tree() { self.inline_cursor .as_mut() .unwrap() .goto_first_child_for_byte(index) } else { self.block_cursor.goto_first_child_for_byte(index) } } } } /// Move this cursor to the first child of its current node that extends beyond the given point. /// /// This returns the index of the child node if one was found, and returns None if no such child was found. /// If the cursor is currently at a node in the block tree and it has an associated inline tree, it /// will descend into the inline tree. pub fn goto_first_child_for_point(&mut self, index: Point) -> Option<usize> { match &mut self.inline_cursor { Some(cursor) => cursor.goto_first_child_for_point(index), None => { if self.move_to_inline_tree() { self.inline_cursor .as_mut() .unwrap() .goto_first_child_for_point(index) } else { self.block_cursor.goto_first_child_for_point(index) } } } } } /// An object that holds a combined markdown tree. #[derive(Debug, Clone)] pub struct MarkdownTree { block_tree: Tree, inline_trees: Vec<Tree>, inline_indices: HashMap<usize, usize>, } impl MarkdownTree { /// Edit the block tree and inline trees to keep them in sync with source code that has been /// edited. /// /// You must describe the edit both in terms of byte offsets and in terms of /// row/column coordinates. pub fn edit(&mut self, edit: &InputEdit) { self.block_tree.edit(edit); for inline_tree in self.inline_trees.iter_mut() { inline_tree.edit(edit); } } /// Returns the block tree for the parsed document pub fn block_tree(&self) -> &Tree { &self.block_tree } /// Returns the inline tree for the given inline node. /// /// Returns `None` if the given node does not have an associated inline tree. Either because /// the nodes type is not `inline` or because the inline content is empty. pub fn inline_tree(&self, parent: &Node) -> Option<&Tree> { let index = *self.inline_indices.get(&parent.id())?; Some(&self.inline_trees[index]) } /// Returns the list of all inline trees pub fn inline_trees(&self) -> &[Tree] { &self.inline_trees } /// Create a new [`MarkdownCursor`] starting from the root of the tree. pub fn walk(&self) -> MarkdownCursor { MarkdownCursor { markdown_tree: self, block_cursor: self.block_tree.walk(), inline_cursor: None, } } } impl Default for MarkdownParser { fn default() -> Self { let block_language = LANGUAGE.into(); let inline_language = INLINE_LANGUAGE.into(); let parser = Parser::new(); MarkdownParser { parser, block_language, inline_language, } } } impl MarkdownParser { /// Parse a slice of UTF8 text. /// /// # Arguments: /// * `text` The UTF8-encoded text to parse. /// * `old_tree` A previous syntax tree parsed from the same document. /// If the text of the document has changed since `old_tree` was /// created, then you must edit `old_tree` to match the new text using /// [MarkdownTree::edit]. /// /// Returns a [MarkdownTree] if parsing succeeded, or `None` if: /// * The timeout set with [tree_sitter::Parser::set_timeout_micros] expired /// * The cancellation flag set with [tree_sitter::Parser::set_cancellation_flag] was flipped pub fn parse_with<T: AsRef<[u8]>, F: FnMut(usize, Point) -> T>( &mut self, callback: &mut F, old_tree: Option<&MarkdownTree>, ) -> Option<MarkdownTree> { let MarkdownParser { parser, block_language, inline_language, } = self; parser .set_included_ranges(&[]) .expect("Can not set included ranges to whole document"); parser .set_language(block_language) .expect("Could not load block grammar"); let block_tree = parser.parse_with(callback, old_tree.map(|tree| &tree.block_tree))?; let (mut inline_trees, mut inline_indices) = if let Some(old_tree) = old_tree { let len = old_tree.inline_trees.len(); (Vec::with_capacity(len), HashMap::with_capacity(len)) } else { (Vec::new(), HashMap::new()) }; parser .set_language(inline_language) .expect("Could not load inline grammar"); let mut tree_cursor = block_tree.walk(); let mut i = 0; 'outer: loop { let node = loop { let kind = tree_cursor.node().kind(); if kind == "inline" || kind == "pipe_table_cell" || !tree_cursor.goto_first_child() { while !tree_cursor.goto_next_sibling() { if !tree_cursor.goto_parent() { break 'outer; } } } let kind = tree_cursor.node().kind(); if kind == "inline" || kind == "pipe_table_cell" { break tree_cursor.node(); } }; let mut range = node.range(); let mut ranges = Vec::new(); if tree_cursor.goto_first_child() { while tree_cursor.goto_next_sibling() { if !tree_cursor.node().is_named() { continue; } let child_range = tree_cursor.node().range(); ranges.push(Range { start_byte: range.start_byte, start_point: range.start_point, end_byte: child_range.start_byte, end_point: child_range.start_point, }); range.start_byte = child_range.end_byte; range.start_point = child_range.end_point; } tree_cursor.goto_parent(); } ranges.push(range); parser.set_included_ranges(&ranges).ok()?; let inline_tree = parser.parse_with( callback, old_tree.and_then(|old_tree| old_tree.inline_trees.get(i)), )?; inline_trees.push(inline_tree); inline_indices.insert(node.id(), i); i += 1; } drop(tree_cursor); inline_trees.shrink_to_fit(); inline_indices.shrink_to_fit(); Some(MarkdownTree { block_tree, inline_trees, inline_indices, }) } /// Parse a slice of UTF8 text. /// /// # Arguments: /// * `text` The UTF8-encoded text to parse. /// * `old_tree` A previous syntax tree parsed from the same document. /// If the text of the document has changed since `old_tree` was /// created, then you must edit `old_tree` to match the new text using /// [MarkdownTree::edit]. /// /// Returns a [MarkdownTree] if parsing succeeded, or `None` if: /// * The timeout set with [tree_sitter::Parser::set_timeout_micros] expired /// * The cancellation flag set with [tree_sitter::Parser::set_cancellation_flag] was flipped pub fn parse(&mut self, text: &[u8], old_tree: Option<&MarkdownTree>) -> Option<MarkdownTree> { self.parse_with(&mut |byte, _| &text[byte..], old_tree) } } #[cfg(test)] mod tests { use tree_sitter::{InputEdit, Point}; use super::*; #[test] fn inline_ranges() { let code = "# title\n\nInline [content].\n"; let mut parser = MarkdownParser::default(); let mut tree = parser.parse(code.as_bytes(), None).unwrap(); let section = tree.block_tree().root_node().child(0).unwrap(); assert_eq!(section.kind(), "section"); let heading = section.child(0).unwrap(); assert_eq!(heading.kind(), "atx_heading"); let paragraph = section.child(1).unwrap(); assert_eq!(paragraph.kind(), "paragraph"); let inline = paragraph.child(0).unwrap(); assert_eq!(inline.kind(), "inline"); assert_eq!( tree.inline_tree(&inline) .unwrap() .root_node() .child(0) .unwrap() .kind(), "shortcut_link" ); let code = "# Title\n\nInline [content].\n"; tree.edit(&InputEdit { start_byte: 2, old_end_byte: 3, new_end_byte: 3, start_position: Point { row: 0, column: 2 }, old_end_position: Point { row: 0, column: 3 }, new_end_position: Point { row: 0, column: 3 }, }); let tree = parser.parse(code.as_bytes(), Some(&tree)).unwrap(); let section = tree.block_tree().root_node().child(0).unwrap(); assert_eq!(section.kind(), "section"); let heading = section.child(0).unwrap(); assert_eq!(heading.kind(), "atx_heading"); let paragraph = section.child(1).unwrap(); assert_eq!(paragraph.kind(), "paragraph"); let inline = paragraph.child(0).unwrap(); assert_eq!(inline.kind(), "inline"); assert_eq!( tree.inline_tree(&inline) .unwrap() .root_node() .named_child(0) .unwrap() .kind(), "shortcut_link" ); } #[test] fn markdown_cursor() { let code = "# title\n\nInline [content].\n"; let mut parser = MarkdownParser::default(); let tree = parser.parse(code.as_bytes(), None).unwrap(); let mut cursor = tree.walk(); assert_eq!(cursor.node().kind(), "document"); assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "section"); assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "atx_heading"); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "paragraph"); assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "inline"); assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "shortcut_link"); assert!(cursor.goto_parent()); assert!(cursor.goto_parent()); assert!(cursor.goto_parent()); assert!(cursor.goto_parent()); assert_eq!(cursor.node().kind(), "document"); } #[test] fn table() { let code = "| foo |\n| --- |\n| *bar*|\n"; let mut parser = MarkdownParser::default(); let tree = parser.parse(code.as_bytes(), None).unwrap(); dbg!(&tree.inline_trees()); let mut cursor = tree.walk(); assert_eq!(cursor.node().kind(), "document"); assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "section"); assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "pipe_table"); assert!(cursor.goto_first_child()); assert!(cursor.goto_next_sibling()); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "pipe_table_row"); assert!(cursor.goto_first_child()); assert!(cursor.goto_next_sibling()); assert_eq!(cursor.node().kind(), "pipe_table_cell"); assert!(cursor.goto_first_child()); assert_eq!(cursor.node().kind(), "emphasis"); } } ``` -------------------------------------------------------------------------------- /deps/tree-sitter-markdown/tree-sitter-markdown-inline/grammar.js: -------------------------------------------------------------------------------- ```javascript // This grammar only concerns the inline structure according to the CommonMark Spec // (https://spec.commonmark.org/0.30/#inlines) // For more information see README.md /// <reference types="tree-sitter-cli/dsl" /> const common = require('../common/common'); // Levels used for dynmic precedence. Ideally // n * PRECEDENCE_LEVEL_EMPHASIS > PRECEDENCE_LEVEL_LINK for any n, so maybe the // maginuted of these values should be increased in the future const PRECEDENCE_LEVEL_EMPHASIS = 1; const PRECEDENCE_LEVEL_LINK = 10; const PRECEDENCE_LEVEL_HTML = 100; // Punctuation characters as specified in // https://github.github.com/gfm/#ascii-punctuation-character const PUNCTUATION_CHARACTERS_REGEX = '!-/:-@\\[-`\\{-~'; // !!! // Notice the call to `add_inline_rules` which generates some additional rules related to parsing // inline contents in different contexts. // !!! module.exports = grammar(add_inline_rules({ name: 'markdown_inline', externals: $ => [ // An `$._error` token is never valid and gets emmited to kill invalid parse branches. Concretely // this is used to decide wether a newline closes a paragraph and together and it gets emitted // when trying to parse the `$._trigger_error` token in `$.link_title`. $._error, $._trigger_error, // Opening and closing delimiters for code spans. These are sequences of one or more backticks. // An opening token does not mean the text after has to be a code span if there is no closing token $._code_span_start, $._code_span_close, // Opening and closing delimiters for emphasis. $._emphasis_open_star, $._emphasis_open_underscore, $._emphasis_close_star, $._emphasis_close_underscore, // For emphasis we need to tell the parser if the last character was a whitespace (or the // beginning of a line) or a punctuation. These tokens never actually get emitted. $._last_token_whitespace, $._last_token_punctuation, $._strikethrough_open, $._strikethrough_close, // Opening and closing delimiters for latex. These are sequences of one or more dollar signs. // An opening token does not mean the text after has to be latex if there is no closing token $._latex_span_start, $._latex_span_close, // Token emmited when encountering opening delimiters for a leaf span // e.g. a code span, that does not have a matching closing span $._unclosed_span ], precedences: $ => [ // [$._strong_emphasis_star, $._inline_element_no_star], [$._strong_emphasis_star_no_link, $._inline_element_no_star_no_link], // [$._strong_emphasis_underscore, $._inline_element_no_underscore], [$._strong_emphasis_underscore_no_link, $._inline_element_no_underscore_no_link], [$.hard_line_break, $._whitespace], [$.hard_line_break, $._text_base], ], // More conflicts are defined in `add_inline_rules` conflicts: $ => [ [$._closing_tag, $._text_base], [$._open_tag, $._text_base], [$._html_comment, $._text_base], [$._processing_instruction, $._text_base], [$._declaration, $._text_base], [$._cdata_section, $._text_base], [$._link_text_non_empty, $._inline_element], [$._link_text_non_empty, $._inline_element_no_star], [$._link_text_non_empty, $._inline_element_no_underscore], [$._link_text_non_empty, $._inline_element_no_tilde], [$._link_text, $._inline_element], [$._link_text, $._inline_element_no_star], [$._link_text, $._inline_element_no_underscore], [$._link_text, $._inline_element_no_tilde], [$._image_description, $._image_description_non_empty, $._text_base], // [$._image_description, $._image_description_non_empty, $._text_inline], // [$._image_description, $._image_description_non_empty, $._text_inline_no_star], // [$._image_description, $._image_description_non_empty, $._text_inline_no_underscore], [$._image_shortcut_link, $._image_description], [$.shortcut_link, $._link_text], [$.link_destination, $.link_title], [$._link_destination_parenthesis, $.link_title], [$.wiki_link, $._inline_element], [$.wiki_link, $._inline_element_no_star], [$.wiki_link, $._inline_element_no_underscore], [$.wiki_link, $._inline_element_no_tilde], ], extras: $ => [], rules: { inline: $ => seq(optional($._last_token_whitespace), $._inline), ...common.rules, // A lot of inlines are defined in `add_inline_rules`, including: // // * collections of inlines // * emphasis // * textual content // // This is done to reduce code duplication, as some inlines need to be parsed differently // depending on the context. For example inlines in ATX headings may not contain newlines. code_span: $ => seq( alias($._code_span_start, $.code_span_delimiter), repeat(choice($._text_base, '[', ']', $._soft_line_break, $._html_tag)), alias($._code_span_close, $.code_span_delimiter) ), latex_block: $ => seq( alias($._latex_span_start, $.latex_span_delimiter), repeat(choice($._text_base, '[', ']', $._soft_line_break, $._html_tag, $.backslash_escape)), alias($._latex_span_close, $.latex_span_delimiter), ), // Different kinds of links: // * inline links (https://github.github.com/gfm/#inline-link) // * full reference links (https://github.github.com/gfm/#full-reference-link) // * collapsed reference links (https://github.github.com/gfm/#collapsed-reference-link) // * shortcut links (https://github.github.com/gfm/#shortcut-reference-link) // // Dynamic precedence is distributed as granular as possible to help the parser decide // while parsing which branch is the most important. // // https://github.github.com/gfm/#links _link_text: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, choice( $._link_text_non_empty, seq('[', ']') )), _link_text_non_empty: $ => seq('[', alias($._inline_no_link, $.link_text), ']'), shortcut_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, $._link_text_non_empty), full_reference_link: $ => prec.dynamic(2 * PRECEDENCE_LEVEL_LINK, seq( $._link_text, $.link_label )), collapsed_reference_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq( $._link_text, '[', ']' )), inline_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq( $._link_text, '(', repeat(choice($._whitespace, $._soft_line_break)), optional(seq( choice( seq( $.link_destination, optional(seq( repeat1(choice($._whitespace, $._soft_line_break)), $.link_title )) ), $.link_title, ), repeat(choice($._whitespace, $._soft_line_break)), )), ')' )), wiki_link: $ => prec.dynamic(2 * PRECEDENCE_LEVEL_LINK, seq( '[', '[', alias($._wiki_link_destination, $.link_destination), optional(seq( '|', alias($._wiki_link_text, $.link_text) )), ']', ']' ) ), _wiki_link_destination: $ => repeat1(choice( $._word, common.punctuation_without($, ['[',']', '|']), $._whitespace, )), _wiki_link_text: $ => repeat1(choice( $._word, common.punctuation_without($, ['[',']']), $._whitespace, )), // Images work exactly like links with a '!' added in front. // // https://github.github.com/gfm/#images image: $ => choice( $._image_inline_link, $._image_shortcut_link, $._image_full_reference_link, $._image_collapsed_reference_link ), _image_inline_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq( $._image_description, '(', repeat(choice($._whitespace, $._soft_line_break)), optional(seq( choice( seq( $.link_destination, optional(seq( repeat1(choice($._whitespace, $._soft_line_break)), $.link_title )) ), $.link_title, ), repeat(choice($._whitespace, $._soft_line_break)), )), ')' )), _image_shortcut_link: $ => prec.dynamic(3 * PRECEDENCE_LEVEL_LINK, $._image_description_non_empty), _image_full_reference_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq($._image_description, $.link_label)), _image_collapsed_reference_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq($._image_description, '[', ']')), _image_description: $ => prec.dynamic(3 * PRECEDENCE_LEVEL_LINK, choice($._image_description_non_empty, seq('!', '[', prec(1, ']')))), _image_description_non_empty: $ => seq('!', '[', alias($._inline, $.image_description), prec(1, ']')), // Autolinks. Uri autolinks actually accept protocolls of arbitrary length which does not // align with the spec. This is because the binary for the grammar gets to large if done // otherwise as tree-sitters code generation is not very concise for this type of regex. // // Email autolinks do not match every valid email (emails normally should not be parsed // using regexes), but this is how they are defined in the spec. // // https://github.github.com/gfm/#autolinks uri_autolink: $ => /<[a-zA-Z][a-zA-Z0-9+\.\-][a-zA-Z0-9+\.\-]*:[^ \t\r\n<>]*>/, email_autolink: $ => /<[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>/, // Raw html. As with html blocks we do not emit additional information as this is best done // by a proper html tree-sitter grammar. // // https://github.github.com/gfm/#raw-html _html_tag: $ => choice($._open_tag, $._closing_tag, $._html_comment, $._processing_instruction, $._declaration, $._cdata_section), _open_tag: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq('<', $._tag_name, repeat($._attribute), repeat(choice($._whitespace, $._soft_line_break)), optional('/'), '>')), _closing_tag: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq('<', '/', $._tag_name, repeat(choice($._whitespace, $._soft_line_break)), '>')), _tag_name: $ => seq($._word_no_digit, repeat(choice($._word_no_digit, $._digits, '-'))), _attribute: $ => seq(repeat1(choice($._whitespace, $._soft_line_break)), $._attribute_name, repeat(choice($._whitespace, $._soft_line_break)), '=', repeat(choice($._whitespace, $._soft_line_break)), $._attribute_value), _attribute_name: $ => /[a-zA-Z_:][a-zA-Z0-9_\.:\-]*/, _attribute_value: $ => choice( /[^ \t\r\n"'=<>`]+/, seq("'", repeat(choice($._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ["'"]))), "'"), seq('"', repeat(choice($._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ['"']))), '"'), ), _html_comment: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq( '<!--', optional(seq( choice( $._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ['-', '>']), seq( '-', common.punctuation_without($, ['>']), ) ), repeat(prec.right(choice( $._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ['-']), seq( '-', choice( $._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ['-']), ) ) ))), )), '-->' )), _processing_instruction: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq( '<?', repeat(prec.right(choice( $._word, $._whitespace, $._soft_line_break, common.punctuation_without($, []), ))), '?>' )), _declaration: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq( /<![A-Z]+/, choice( $._whitespace, $._soft_line_break, ), repeat(prec.right(choice( $._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ['>']), ))), '>' )), _cdata_section: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq( '<![CDATA[', repeat(prec.right(choice( $._word, $._whitespace, $._soft_line_break, common.punctuation_without($, []), ))), ']]>' )), // A hard line break. // // https://github.github.com/gfm/#hard-line-breaks hard_line_break: $ => seq(choice('\\', $._whitespace_ge_2), $._soft_line_break), _text: $ => choice($._word, common.punctuation_without($, []), $._whitespace), // Whitespace is divided into single whitespaces and multiple whitespaces as wee need this // information for hard line breaks. _whitespace_ge_2: $ => /\t| [ \t]+/, _whitespace: $ => seq(choice($._whitespace_ge_2, / /), optional($._last_token_whitespace)), // Other than whitespace we tokenize into strings of digits, punctuation characters // (handled by `common.punctuation_without`) and strings of any other characters. This way the // lexer does not have to many different states, which makes it a lot easier to make // conflicts work. _word: $ => choice($._word_no_digit, $._digits), _word_no_digit: $ => new RegExp('[^' + PUNCTUATION_CHARACTERS_REGEX + ' \\t\\n\\r0-9]+(_+[^' + PUNCTUATION_CHARACTERS_REGEX + ' \\t\\n\\r0-9]+)*'), _digits: $ => /[0-9][0-9_]*/, _soft_line_break: $ => seq($._newline_token, optional($._last_token_whitespace)), _inline_base: $ => prec.right(repeat1(choice( $.image, $._soft_line_break, $.backslash_escape, $.hard_line_break, $.uri_autolink, $.email_autolink, $.entity_reference, $.numeric_character_reference, (common.EXTENSION_LATEX ? $.latex_block : choice()), $.code_span, alias($._html_tag, $.html_tag), $._text_base, common.EXTENSION_TAGS ? $.tag : choice(), $._unclosed_span, ))), _text_base: $ => choice( $._word, common.punctuation_without($, ['[', ']']), $._whitespace, '<!--', /<![A-Z]+/, '<?', '<![CDATA[', ), _text_inline_no_link: $ => choice( $._text_base, $._emphasis_open_star, $._emphasis_open_underscore, $._unclosed_span, ), ...(common.EXTENSION_TAGS ? { tag: $ => /#[0-9]*[a-zA-Z_\-\/][a-zA-Z_\-\/0-9]*/, } : {}), }, })); // This function adds some extra inline rules. This is done to reduce code duplication, as some // rules may not contain newlines, characters like '*' and '_', ... depending on the context. // // This is by far the most ugly part of this code and should be cleaned up. function add_inline_rules(grammar) { let conflicts = []; for (let link of [true, false]) { let suffix_link = link ? "" : "_no_link"; for (let delimiter of [false, "star", "underscore", "tilde"]) { let suffix_delimiter = delimiter ? "_no_" + delimiter : ""; let suffix = suffix_delimiter + suffix_link; grammar.rules["_inline_element" + suffix] = $ => { let elements = [ $._inline_base, alias($['_emphasis_star' + suffix_link], $.emphasis), alias($['_strong_emphasis_star' + suffix_link], $.strong_emphasis), alias($['_emphasis_underscore' + suffix_link], $.emphasis), alias($['_strong_emphasis_underscore' + suffix_link], $.strong_emphasis), ]; if (common.EXTENSION_STRIKETHROUGH) { elements.push(alias($['_strikethrough' + suffix_link], $.strikethrough)); } if (delimiter !== "star") { elements.push($._emphasis_open_star); } if (delimiter !== "underscore") { elements.push($._emphasis_open_underscore); } if (delimiter !== "tilde") { elements.push($._strikethrough_open); } if (link) { elements = elements.concat([ $.shortcut_link, $.full_reference_link, $.collapsed_reference_link, $.inline_link, // (common.EXTENSION_WIKI_LINK && $.wiki_link), seq(choice('[', ']'), optional($._last_token_punctuation)), ]); if (common.EXTENSION_WIKI_LINK) { elements.push($.wiki_link); } } return choice(...elements); }; grammar.rules["_inline" + suffix] = $ => repeat1($["_inline_element" + suffix]); if (delimiter !== "star") { conflicts.push(['_emphasis_star' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]); conflicts.push(['_emphasis_star' + suffix_link, '_strong_emphasis_star' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]); } if (delimiter == 'star' || delimiter == 'underscore') { conflicts.push(['_strong_emphasis_' + delimiter + suffix_link, '_inline_element_no_' + delimiter]); } if (delimiter !== "underscore") { conflicts.push(['_emphasis_underscore' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]); conflicts.push(['_emphasis_underscore' + suffix_link, '_strong_emphasis_underscore' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]); } if (delimiter !== "tilde") { conflicts.push(['_strikethrough' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]); } } if (common.EXTENSION_STRIKETHROUGH) { grammar.rules['_strikethrough' + suffix_link] = $ => prec.dynamic(PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._strikethrough_open, $.emphasis_delimiter), optional($._last_token_punctuation), $['_inline' + '_no_tilde' + suffix_link], alias($._strikethrough_close, $.emphasis_delimiter))); } grammar.rules['_emphasis_star' + suffix_link] = $ => prec.dynamic(PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_star, $.emphasis_delimiter), optional($._last_token_punctuation), $['_inline' + '_no_star' + suffix_link], alias($._emphasis_close_star, $.emphasis_delimiter))); grammar.rules['_strong_emphasis_star' + suffix_link] = $ => prec.dynamic(2 * PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_star, $.emphasis_delimiter), $['_emphasis_star' + suffix_link], alias($._emphasis_close_star, $.emphasis_delimiter))); grammar.rules['_emphasis_underscore' + suffix_link] = $ => prec.dynamic(PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_underscore, $.emphasis_delimiter), optional($._last_token_punctuation), $['_inline' + '_no_underscore' + suffix_link], alias($._emphasis_close_underscore, $.emphasis_delimiter))); grammar.rules['_strong_emphasis_underscore' + suffix_link] = $ => prec.dynamic(2 * PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_underscore, $.emphasis_delimiter), $['_emphasis_underscore' + suffix_link], alias($._emphasis_close_underscore, $.emphasis_delimiter))); } let old = grammar.conflicts grammar.conflicts = $ => { let cs = old($); for (let conflict of conflicts) { let c = []; for (let rule of conflict) { c.push($[rule]); } cs.push(c); } return cs; } return grammar; } ``` -------------------------------------------------------------------------------- /src/ui/style_manager.cpp: -------------------------------------------------------------------------------- ```cpp #include "style_manager.h" #include <algorithm> #include <cctype> #include <cstring> #include <fstream> #include <iostream> #ifdef _WIN32 #include <curses.h> inline int setenv(const char *name, const char *value, int overwrite) { if (!overwrite) { size_t envsize = 0; getenv_s(&envsize, nullptr, 0, name); if (envsize != 0) return 0; // Variable exists, don't overwrite } return _putenv_s(name, value); } #else #include <ncurses.h> #endif #include <sstream> StyleManager::StyleManager() : initialized(false), supports_256_colors_cache(false), next_custom_color_id(16) { } void StyleManager::initialize() { if (initialized) { std::cerr << "StyleManager already initialized" << std::endl; return; } // Critical: Check if ncurses has been initialized first if (!stdscr) { std::cerr << "ERROR: ncurses not initialized. Call initscr() first!" << std::endl; return; } if (!has_colors()) { std::cerr << "Terminal does not support colors" << std::endl; return; } // Initialize color support if (start_color() == ERR) { std::cerr << "Failed to start color support" << std::endl; return; } // Use default terminal colors - CRITICAL for transparent support if (use_default_colors() == ERR) { std::cerr << "Warning: use_default_colors() failed, using fallback" << std::endl; // Fallback: assume black background, white foreground assume_default_colors(COLOR_WHITE, COLOR_BLACK); } // Initialize color capability cache and custom color tracking supports_256_colors_cache = supports_256_colors(); next_custom_color_id = 16; // Start custom colors at ID 16 color_cache.clear(); // std::cerr << "=== UNIFIED THEME SYSTEM WITH HEX SUPPORT ===" << std::endl; // std::cerr << "COLORS: " << COLORS << std::endl; // std::cerr << "COLOR_PAIRS: " << COLOR_PAIRS << std::endl; // std::cerr << "256-color support: " // << (supports_256_colors_cache ? "YES" : "NO") << std::endl; // std::cerr << "TERM: " << (getenv("TERM") ? getenv("TERM") : "not set") // << std::endl; // Check for WSL-specific issues const char *wsl_distro = getenv("WSL_DISTRO_NAME"); if (wsl_distro) { // std::cerr << "WSL detected: " << wsl_distro << std::endl; // WSL sometimes has color issues, force TERM if needed if (!getenv("TERM") || strcmp(getenv("TERM"), "dumb") == 0) { // std::cerr << "Setting TERM=xterm-256color for WSL" << std::endl; setenv("TERM", "xterm-256color", 1); } } load_default_theme(); apply_theme(); initialized = true; // std::cerr << "Unified theme system initialized successfully" << std::endl; } short StyleManager::resolve_theme_color(const std::string &config_value) { // Handle transparent/default colors if (config_value.empty() || config_value == "transparent" || config_value == "default") { return -1; // Use terminal default } // Check if it's a hex color if (config_value.length() == 7 && config_value[0] == '#') { // Check cache first auto cache_it = color_cache.find(config_value); if (cache_it != color_cache.end()) { return cache_it->second; } // Parse the hex color RGB rgb = parse_hex_color(config_value); if (supports_256_colors_cache && next_custom_color_id < COLORS) { // 256-color mode: use init_color for true color mapping // Scale R, G, B from 0-255 to ncurses' 0-1000 range short r_1000 = (rgb.r * 1000) / 255; short g_1000 = (rgb.g * 1000) / 255; short b_1000 = (rgb.b * 1000) / 255; short color_id = next_custom_color_id; if (init_color(color_id, r_1000, g_1000, b_1000) == OK) { // Cache the mapping and increment for next time auto cache_it = color_cache.find(config_value); if (cache_it != color_cache.end()) { return cache_it->second; // OK: using iterator } color_cache[config_value] = color_id; const_cast<StyleManager *>(this)->next_custom_color_id++; // std::cerr << "Created custom color " << color_id << " for " // << config_value << " (RGB: " << rgb.r << "," << rgb.g << // "," // << rgb.b << ")" << std::endl; return color_id; } else { std::cerr << "Failed to create custom color for " << config_value << ", falling back to closest 8-color" << std::endl; } } // Fallback to legacy 8-color mode return find_closest_8color(rgb); } // Legacy named color support (for backward compatibility) // This handles old theme files that might still use color names ThemeColor legacy_color = string_to_theme_color(config_value); return theme_color_to_ncurses_color(legacy_color); } // NEW: Hex color parsing utility RGB StyleManager::parse_hex_color(const std::string &hex_str) const { RGB rgb; if (hex_str.length() != 7 || hex_str[0] != '#') { std::cerr << "Invalid hex color format: " << hex_str << ", using black" << std::endl; return RGB(0, 0, 0); } try { // Parse each component (skip the '#') std::string r_str = hex_str.substr(1, 2); std::string g_str = hex_str.substr(3, 2); std::string b_str = hex_str.substr(5, 2); rgb.r = std::stoi(r_str, nullptr, 16); rgb.g = std::stoi(g_str, nullptr, 16); rgb.b = std::stoi(b_str, nullptr, 16); // Clamp to valid range rgb.r = std::max(0, std::min(255, rgb.r)); rgb.g = std::max(0, std::min(255, rgb.g)); rgb.b = std::max(0, std::min(255, rgb.b)); } catch (const std::exception &e) { std::cerr << "Error parsing hex color " << hex_str << ": " << e.what() << std::endl; return RGB(0, 0, 0); } return rgb; } // NEW: Find closest 8-color match for fallback short StyleManager::find_closest_8color(const RGB &rgb) const { // Define the standard 8 colors in RGB struct ColorMapping { short ncurses_color; RGB rgb; const char *name; }; static const ColorMapping basic_colors[] = { {COLOR_BLACK, RGB(0, 0, 0), "black"}, {COLOR_RED, RGB(128, 0, 0), "red"}, {COLOR_GREEN, RGB(0, 128, 0), "green"}, {COLOR_YELLOW, RGB(128, 128, 0), "yellow"}, {COLOR_BLUE, RGB(0, 0, 128), "blue"}, {COLOR_MAGENTA, RGB(128, 0, 128), "magenta"}, {COLOR_CYAN, RGB(0, 128, 128), "cyan"}, {COLOR_WHITE, RGB(192, 192, 192), "white"}}; // Find the closest color using simple Euclidean distance double min_distance = 1000000; short closest_color = COLOR_WHITE; for (const auto &color : basic_colors) { double dr = rgb.r - color.rgb.r; double dg = rgb.g - color.rgb.g; double db = rgb.b - color.rgb.b; double distance = dr * dr + dg * dg + db * db; if (distance < min_distance) { min_distance = distance; closest_color = color.ncurses_color; } } return closest_color; } // Legacy function - kept only for load_default_theme compatibility ThemeColor StyleManager::string_to_theme_color(const std::string &color_name) const { std::string lower_name = color_name; std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower); if (lower_name == "black") return ThemeColor::BLACK; if (lower_name == "dark_gray" || lower_name == "dark_grey") return ThemeColor::DARK_GRAY; if (lower_name == "gray" || lower_name == "grey") return ThemeColor::GRAY; if (lower_name == "light_gray" || lower_name == "light_grey") return ThemeColor::LIGHT_GRAY; if (lower_name == "white") return ThemeColor::WHITE; if (lower_name == "red") return ThemeColor::RED; if (lower_name == "green") return ThemeColor::GREEN; if (lower_name == "blue") return ThemeColor::BLUE; if (lower_name == "yellow") return ThemeColor::YELLOW; if (lower_name == "magenta") return ThemeColor::MAGENTA; if (lower_name == "cyan") return ThemeColor::CYAN; if (lower_name == "bright_red") return ThemeColor::BRIGHT_RED; if (lower_name == "bright_green") return ThemeColor::BRIGHT_GREEN; if (lower_name == "bright_blue") return ThemeColor::BRIGHT_BLUE; if (lower_name == "bright_yellow") return ThemeColor::BRIGHT_YELLOW; if (lower_name == "bright_magenta") return ThemeColor::BRIGHT_MAGENTA; if (lower_name == "bright_cyan") return ThemeColor::BRIGHT_CYAN; std::cerr << "Unknown color name: " << color_name << ", using white" << std::endl; return ThemeColor::WHITE; } // Legacy function - kept for backward compatibility int StyleManager::theme_color_to_ncurses_color(ThemeColor color) const { switch (color) { case ThemeColor::BLACK: return COLOR_BLACK; case ThemeColor::DARK_GRAY: return COLOR_BLACK; // Will use A_BOLD attribute for brighter black case ThemeColor::GRAY: return COLOR_WHITE; // Will use A_DIM attribute for dimmed white case ThemeColor::LIGHT_GRAY: return COLOR_WHITE; // Will use A_DIM + A_BOLD for medium brightness case ThemeColor::WHITE: return COLOR_WHITE; case ThemeColor::RED: return COLOR_RED; case ThemeColor::GREEN: return COLOR_GREEN; case ThemeColor::BLUE: return COLOR_BLUE; case ThemeColor::YELLOW: return COLOR_YELLOW; case ThemeColor::MAGENTA: return COLOR_MAGENTA; case ThemeColor::CYAN: return COLOR_CYAN; case ThemeColor::BRIGHT_RED: return COLOR_RED; // Will use A_BOLD attribute case ThemeColor::BRIGHT_GREEN: return COLOR_GREEN; // Will use A_BOLD attribute case ThemeColor::BRIGHT_BLUE: return COLOR_BLUE; // Will use A_BOLD attribute case ThemeColor::BRIGHT_YELLOW: return COLOR_YELLOW; // Will use A_BOLD attribute case ThemeColor::BRIGHT_MAGENTA: return COLOR_MAGENTA; // Will use A_BOLD attribute case ThemeColor::BRIGHT_CYAN: return COLOR_CYAN; // Will use A_BOLD attribute case ThemeColor::TERMINAL: return COLOR_RED; default: return COLOR_WHITE; } } // Legacy function - simplified since 256-color provides enough fidelity int StyleManager::theme_color_to_ncurses_attr(ThemeColor color) const { // With 256-color support, we can rely more on actual colors than attributes // Keep only essential attributes switch (color) { case ThemeColor::BRIGHT_RED: case ThemeColor::BRIGHT_GREEN: case ThemeColor::BRIGHT_BLUE: case ThemeColor::BRIGHT_YELLOW: case ThemeColor::BRIGHT_MAGENTA: case ThemeColor::BRIGHT_CYAN: return A_BOLD; // Keep bold for legacy compatibility default: return A_NORMAL; } } bool StyleManager::is_light_theme() const { // Check if background is a light hex color or legacy light theme if (current_theme.background[0] == '#') { RGB bg_rgb = parse_hex_color(current_theme.background); // Consider it light if the average RGB value is > 128 return (bg_rgb.r + bg_rgb.g + bg_rgb.b) / 3 > 128; } // Legacy check for named colors ThemeColor legacy_bg = string_to_theme_color(current_theme.background); return (legacy_bg == ThemeColor::WHITE || legacy_bg == ThemeColor::LIGHT_GRAY); } void StyleManager::load_default_theme() { current_theme = { "Default Dark (Hex)", "#000000", // background "#FFFFFF", // foreground "#FFFFFF", // cursor "#0000FF", // selection "#333333", // line_highlight "#FFFF00", // line_numbers "#FFFF99", // line_numbers_active "#000080", // status_bar_bg "#FFFFFF", // status_bar_fg "#00FFFF", // status_bar_active // Semantic categories "#569CD6", // keyword "#CE9178", // string_literal "#B5CEA8", // number "#6A9955", // comment "#DCDCAA", // function_name "#9CDCFE", // variable "#4EC9B0", // type "#D4D4D4", // operator "#D4D4D4", // punctuation "#4FC1FF", // constant "#4EC9B0", // namespace "#9CDCFE", // property "#DCDCAA", // decorator "#C586C0", // macro "#569CD6", // label // Markup "#569CD6", // markup_heading "#D4D4D4", // markup_bold "#CE9178", // markup_italic "#CE9178", // markup_code "#CE9178", // markup_code_block "#3794FF", // markup_link "#3794FF", // markup_url "#6A9955", // markup_list "#6A9955", // markup_blockquote "#FF6B6B", // markup_strikethrough "#6A9955" // markup_quote }; } void StyleManager::apply_theme() { if (!initialized) { // std::cerr << "StyleManager not initialized, cannot apply theme" // << std::endl; return; } // std::cerr << "Applying theme: " << current_theme.name << std::endl; short terminal_bg = resolve_theme_color(current_theme.background); short terminal_fg = resolve_theme_color(current_theme.foreground); init_pair(0, terminal_fg, terminal_bg); const int BACKGROUND_PAIR_ID = 100; init_pair(BACKGROUND_PAIR_ID, terminal_fg, terminal_bg); auto init_pair_enhanced = [&](int pair_id, const std::string &fg_color_str, const std::string &bg_color_str) -> bool { if (pair_id >= COLOR_PAIRS) return false; short fg = resolve_theme_color(fg_color_str); short bg = resolve_theme_color(bg_color_str); int result = init_pair(pair_id, fg, bg); return (result == OK); }; // UI Elements init_pair_enhanced(2, current_theme.line_numbers, current_theme.background); init_pair_enhanced(3, current_theme.line_numbers_active, current_theme.background); init_pair_enhanced(4, "#808080", current_theme.background); // Status bar init_pair_enhanced(5, current_theme.status_bar_fg, current_theme.status_bar_bg); init_pair_enhanced(6, current_theme.status_bar_fg, current_theme.status_bar_bg); init_pair_enhanced(7, current_theme.status_bar_active, current_theme.status_bar_bg); init_pair_enhanced(8, "#00FFFF", current_theme.status_bar_bg); init_pair_enhanced(9, "#FFFF00", current_theme.status_bar_bg); init_pair_enhanced(10, "#00FF00", current_theme.status_bar_bg); init_pair_enhanced(11, "#FF00FF", current_theme.status_bar_bg); init_pair_enhanced(12, "#808080", current_theme.status_bar_bg); // Selection and cursor init_pair_enhanced(13, current_theme.cursor, current_theme.background); init_pair_enhanced(14, current_theme.foreground, current_theme.selection); init_pair_enhanced(15, current_theme.foreground, current_theme.line_highlight); // Semantic categories (20-39) init_pair_enhanced(20, current_theme.keyword, current_theme.background); init_pair_enhanced(21, current_theme.string_literal, current_theme.background); init_pair_enhanced(22, current_theme.number, current_theme.background); init_pair_enhanced(23, current_theme.comment, current_theme.background); init_pair_enhanced(24, current_theme.function_name, current_theme.background); init_pair_enhanced(25, current_theme.variable, current_theme.background); init_pair_enhanced(26, current_theme.type, current_theme.background); init_pair_enhanced(27, current_theme.operator_color, current_theme.background); init_pair_enhanced(28, current_theme.punctuation, current_theme.background); init_pair_enhanced(29, current_theme.constant, current_theme.background); init_pair_enhanced(30, current_theme.namespace_color, current_theme.background); init_pair_enhanced(31, current_theme.property, current_theme.background); init_pair_enhanced(32, current_theme.decorator, current_theme.background); init_pair_enhanced(33, current_theme.macro, current_theme.background); init_pair_enhanced(34, current_theme.label, current_theme.background); // Markup (50-61) init_pair_enhanced(50, current_theme.markup_heading, current_theme.background); init_pair_enhanced(51, current_theme.markup_bold, current_theme.background); init_pair_enhanced(52, current_theme.markup_italic, current_theme.background); init_pair_enhanced(53, current_theme.markup_code, current_theme.background); init_pair_enhanced(54, current_theme.markup_code_block, current_theme.background); init_pair_enhanced(55, current_theme.markup_link, current_theme.background); init_pair_enhanced(56, current_theme.markup_url, current_theme.background); init_pair_enhanced(57, current_theme.markup_blockquote, current_theme.background); init_pair_enhanced(58, current_theme.markup_list, current_theme.background); init_pair_enhanced(59, current_theme.operator_color, current_theme.background); init_pair_enhanced(60, current_theme.markup_strikethrough, current_theme.background); init_pair_enhanced(61, current_theme.markup_quote, current_theme.background); // Special pairs init_pair_enhanced(70, current_theme.foreground, current_theme.line_highlight); bkgdset(' ' | COLOR_PAIR(BACKGROUND_PAIR_ID)); clear(); // refresh(); } // Also add this helper function to detect and optimize for WSL bool StyleManager::is_wsl_environment() const { return getenv("WSL_DISTRO_NAME") != nullptr; } // Enhanced color initialization for WSL void StyleManager::optimize_for_wsl() { if (!is_wsl_environment()) return; std::cerr << "WSL environment detected - applying optimizations" << std::endl; // Check Windows Terminal version and capabilities const char *wt_session = getenv("WT_SESSION"); if (wt_session) { std::cerr << "Windows Terminal detected" << std::endl; // Windows Terminal supports true color if (supports_true_color()) { std::cerr << "True color support detected" << std::endl; } // Force TERM to get better colors if (!getenv("TERM") || strcmp(getenv("TERM"), "xterm-256color") != 0) { std::cerr << "Setting TERM=xterm-256color for better WSL colors" << std::endl; setenv("TERM", "xterm-256color", 1); } } } // Helper function to apply color with attributes (simplified for 256-color) void StyleManager::apply_color_pair(int pair_id, ThemeColor theme_color) const { int attrs = COLOR_PAIR(pair_id) | theme_color_to_ncurses_attr(theme_color); attrset(attrs); } // Legacy compatibility functions // Terminal capability detection bool StyleManager::supports_256_colors() const { return COLORS >= 256; } bool StyleManager::supports_true_color() const { const char *colorterm = getenv("COLORTERM"); return (colorterm && (std::strcmp(colorterm, "truecolor") == 0 || std::strcmp(colorterm, "24bit") == 0)); } void StyleManager::apply_legacy_theme(const Theme &theme) { if (initialized) { apply_theme(); } } // YAML parsing utilities remain the same std::string StyleManager::trim(const std::string &str) { size_t start = str.find_first_not_of(" \t\n\r"); if (start == std::string::npos) return ""; size_t end = str.find_last_not_of(" \t\n\r"); return str.substr(start, end - start + 1); } std::string StyleManager::remove_quotes(const std::string &str) { std::string trimmed = trim(str); if (trimmed.length() >= 2 && ((trimmed.front() == '"' && trimmed.back() == '"') || (trimmed.front() == '\'' && trimmed.back() == '\''))) { return trimmed.substr(1, trimmed.length() - 2); } return trimmed; } std::map<std::string, std::string> StyleManager::parse_yaml(const std::string &yaml_content) { std::map<std::string, std::string> result; std::istringstream stream(yaml_content); std::string line; while (std::getline(stream, line)) { std::string trimmed_line = trim(line); if (trimmed_line.empty() || trimmed_line[0] == '#') continue; size_t colon_pos = line.find(':'); if (colon_pos == std::string::npos) continue; std::string key = trim(line.substr(0, colon_pos)); std::string value = trim(line.substr(colon_pos + 1)); value = remove_quotes(value); if (!key.empty() && !value.empty()) { result[key] = value; } } return result; } bool StyleManager::load_theme_from_yaml(const std::string &yaml_content) { try { auto config = parse_yaml(yaml_content); NamedTheme theme; auto get_color = [&](const std::string &key, const std::string &default_color) -> std::string { auto it = config.find(key); return (it != config.end()) ? it->second : default_color; }; theme.name = config.count("name") ? config["name"] : "Custom Theme"; theme.background = get_color("background", "#000000"); theme.foreground = get_color("foreground", "#FFFFFF"); theme.cursor = get_color("cursor", "#FFFFFF"); theme.selection = get_color("selection", "#0000FF"); theme.line_highlight = get_color("line_highlight", "#333333"); theme.line_numbers = get_color("line_numbers", "#808080"); theme.line_numbers_active = get_color("line_numbers_active", "#FFFFFF"); theme.status_bar_bg = get_color("status_bar_bg", "#000080"); theme.status_bar_fg = get_color("status_bar_fg", "#FFFFFF"); theme.status_bar_active = get_color("status_bar_active", "#00FFFF"); // Semantic categories theme.keyword = get_color("keyword", "#569CD6"); theme.string_literal = get_color("string_literal", "#CE9178"); theme.number = get_color("number", "#B5CEA8"); theme.comment = get_color("comment", "#6A9955"); theme.function_name = get_color("function_name", "#DCDCAA"); theme.variable = get_color("variable", "#9CDCFE"); theme.type = get_color("type", "#4EC9B0"); theme.operator_color = get_color("operator", "#D4D4D4"); theme.punctuation = get_color("punctuation", "#D4D4D4"); theme.constant = get_color("constant", "#4FC1FF"); theme.namespace_color = get_color("namespace", "#4EC9B0"); theme.property = get_color("property", "#9CDCFE"); theme.decorator = get_color("decorator", "#DCDCAA"); theme.macro = get_color("macro", "#C586C0"); theme.label = get_color("label", "#569CD6"); // Markup theme.markup_heading = get_color("markup_heading", "#569CD6"); theme.markup_bold = get_color("markup_bold", "#D4D4D4"); theme.markup_italic = get_color("markup_italic", "#CE9178"); theme.markup_code = get_color("markup_code", "#CE9178"); theme.markup_code_block = get_color("markup_code_block", "#CE9178"); theme.markup_link = get_color("markup_link", "#3794FF"); theme.markup_url = get_color("markup_url", "#3794FF"); theme.markup_list = get_color("markup_list", "#6A9955"); theme.markup_blockquote = get_color("markup_blockquote", "#6A9955"); theme.markup_strikethrough = get_color("markup_strikethrough", "#FF6B6B"); theme.markup_quote = get_color("markup_quote", "#6A9955"); current_theme = theme; if (initialized) { apply_theme(); } return true; } catch (const std::exception &e) { std::cerr << "Error parsing theme: " << e.what() << std::endl; load_default_theme(); return false; } } bool StyleManager::load_theme_from_file(const std::string &file_path) { try { std::ifstream file(file_path); if (!file.is_open()) { std::cerr << "Failed to open theme file: " << file_path << std::endl; load_default_theme(); return false; } std::stringstream buffer; buffer << file.rdbuf(); file.close(); return load_theme_from_yaml(buffer.str()); } catch (const std::exception &e) { std::cerr << "Error reading theme file " << file_path << ": " << e.what() << std::endl; load_default_theme(); return false; } } // Global instance StyleManager g_style_manager; // Legacy API functions for backward compatibility void init_colors() { g_style_manager.initialize(); } void load_default_theme() { if (!g_style_manager.is_initialized()) { g_style_manager.initialize(); } } void apply_theme(const Theme &theme) { g_style_manager.apply_legacy_theme(theme); } ```