This is page 5 of 10. Use http://codebase.md/moisnx/arc?lines=true&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/core/editor.cpp: -------------------------------------------------------------------------------- ```cpp 1 | #include "editor.h" 2 | // #include "src/ui/colors.h" 3 | #include "src/core/config_manager.h" 4 | #include "src/ui/style_manager.h" 5 | #include <algorithm> 6 | #include <cstdlib> 7 | #include <cstring> 8 | #include <fstream> 9 | #include <iostream> 10 | #ifdef _WIN32 11 | #include <curses.h> 12 | #else 13 | #include <ncurses.h> 14 | #endif 15 | #include <iostream> 16 | #include <sstream> 17 | #include <string> 18 | #include <utility> 19 | 20 | // Windows-specific mouse codes 21 | #ifndef BUTTON4_PRESSED 22 | #define BUTTON4_PRESSED 0x00200000L 23 | #endif 24 | #ifndef BUTTON5_PRESSED 25 | #define BUTTON5_PRESSED 0x00100000L 26 | #endif 27 | 28 | // ================================================================= 29 | // Constructor 30 | // ================================================================= 31 | 32 | Editor::Editor(SyntaxHighlighter *highlighter) : syntaxHighlighter(highlighter) 33 | { 34 | tabSize = ConfigManager::getTabSize(); 35 | } 36 | 37 | EditorSnapshot Editor::captureSnapshot() const 38 | { 39 | EditorSnapshot snap; 40 | snap.lineCount = buffer.getLineCount(); 41 | snap.cursorLine = cursorLine; 42 | snap.cursorCol = cursorCol; 43 | snap.viewportTop = viewportTop; 44 | snap.viewportLeft = viewportLeft; 45 | snap.bufferSize = buffer.size(); 46 | 47 | if (snap.lineCount > 0) 48 | { 49 | snap.firstLine = buffer.getLine(0); 50 | snap.lastLine = buffer.getLine(snap.lineCount - 1); 51 | if (cursorLine < snap.lineCount) 52 | { 53 | snap.cursorLineContent = buffer.getLine(cursorLine); 54 | } 55 | } 56 | 57 | return snap; 58 | } 59 | 60 | ValidationResult Editor::validateState(const std::string &context) const 61 | { 62 | // Check buffer is not empty 63 | if (buffer.getLineCount() == 0) 64 | { 65 | return ValidationResult("Buffer has 0 lines at: " + context); 66 | } 67 | 68 | // Check cursor line bounds 69 | if (cursorLine < 0 || cursorLine >= buffer.getLineCount()) 70 | { 71 | std::ostringstream oss; 72 | oss << "Cursor line " << cursorLine << " out of bounds [0, " 73 | << buffer.getLineCount() - 1 << "] at: " << context; 74 | return ValidationResult(oss.str()); 75 | } 76 | 77 | // Check cursor column bounds 78 | std::string line = buffer.getLine(cursorLine); 79 | if (cursorCol < 0 || cursorCol > static_cast<int>(line.length())) 80 | { 81 | std::ostringstream oss; 82 | oss << "Cursor col " << cursorCol << " out of bounds [0, " << line.length() 83 | << "] at: " << context; 84 | return ValidationResult(oss.str()); 85 | } 86 | 87 | // Check viewport bounds 88 | if (viewportTop < 0) 89 | { 90 | return ValidationResult("Viewport top negative at: " + context); 91 | } 92 | 93 | if (viewportLeft < 0) 94 | { 95 | return ValidationResult("Viewport left negative at: " + context); 96 | } 97 | 98 | // Check viewport can contain cursor 99 | if (cursorLine < viewportTop) 100 | { 101 | std::ostringstream oss; 102 | oss << "Cursor line " << cursorLine << " above viewport " << viewportTop 103 | << " at: " + context; 104 | return ValidationResult(oss.str()); 105 | } 106 | 107 | return ValidationResult(); // All valid 108 | } 109 | 110 | // Compare two snapshots and report differences 111 | std::string Editor::compareSnapshots(const EditorSnapshot &before, 112 | const EditorSnapshot &after) const 113 | { 114 | std::ostringstream oss; 115 | 116 | if (before.lineCount != after.lineCount) 117 | { 118 | oss << "LineCount: " << before.lineCount << " -> " << after.lineCount 119 | << "\n"; 120 | } 121 | if (before.cursorLine != after.cursorLine) 122 | { 123 | oss << "CursorLine: " << before.cursorLine << " -> " << after.cursorLine 124 | << "\n"; 125 | } 126 | if (before.cursorCol != after.cursorCol) 127 | { 128 | oss << "CursorCol: " << before.cursorCol << " -> " << after.cursorCol 129 | << "\n"; 130 | } 131 | if (before.bufferSize != after.bufferSize) 132 | { 133 | oss << "BufferSize: " << before.bufferSize << " -> " << after.bufferSize 134 | << "\n"; 135 | } 136 | if (before.cursorLineContent != after.cursorLineContent) 137 | { 138 | oss << "CursorLine content changed\n"; 139 | oss << " Before: '" << before.cursorLineContent << "'\n"; 140 | oss << " After: '" << after.cursorLineContent << "'\n"; 141 | } 142 | 143 | return oss.str(); 144 | } 145 | 146 | void Editor::reloadConfig() 147 | { 148 | tabSize = ConfigManager::getTabSize(); 149 | // Trigger redisplay to reflect changes 150 | } 151 | 152 | // ================================================================= 153 | // Mode Management 154 | // ================================================================= 155 | 156 | // ================================================================= 157 | // Private Helper Methods (from original code) 158 | // ================================================================= 159 | 160 | std::string Editor::expandTabs(const std::string &line, int tabSize) 161 | { 162 | std::string result; 163 | for (char c : line) 164 | { 165 | if (c == '\t') 166 | { 167 | int spacesToAdd = tabSize - (result.length() % tabSize); 168 | result.append(spacesToAdd, ' '); 169 | } 170 | else if (c >= 32 && c <= 126) 171 | { 172 | result += c; 173 | } 174 | else 175 | { 176 | result += ' '; 177 | } 178 | } 179 | return result; 180 | } 181 | 182 | std::string Editor::getFileExtension() 183 | { 184 | if (filename.empty()) 185 | return ""; 186 | 187 | size_t dot = filename.find_last_of("."); 188 | if (dot == std::string::npos) 189 | return ""; 190 | 191 | std::string ext = filename.substr(dot + 1); 192 | std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); 193 | 194 | return ext; 195 | } 196 | 197 | bool Editor::isPositionSelected(int line, int col) 198 | { 199 | if (!hasSelection && !isSelecting) 200 | return false; 201 | 202 | int startL = selectionStartLine; 203 | int startC = selectionStartCol; 204 | int endL = selectionEndLine; 205 | int endC = selectionEndCol; 206 | 207 | if (startL > endL || (startL == endL && startC > endC)) 208 | { 209 | std::swap(startL, endL); 210 | std::swap(startC, endC); 211 | } 212 | 213 | if (line < startL || line > endL) 214 | return false; 215 | 216 | if (startL == endL) 217 | { 218 | return col >= startC && col < endC; 219 | } 220 | else if (line == startL) 221 | { 222 | return col >= startC; 223 | } 224 | else if (line == endL) 225 | { 226 | return col < endC; 227 | } 228 | else 229 | { 230 | return true; 231 | } 232 | } 233 | 234 | void Editor::positionCursor() 235 | { 236 | int rows, cols; 237 | getmaxyx(stdscr, rows, cols); 238 | 239 | int screenRow = cursorLine - viewportTop; 240 | if (screenRow >= 0 && screenRow < viewportHeight) 241 | { 242 | bool show_line_numbers = ConfigManager::getLineNumbers(); 243 | int lineNumWidth = 244 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 245 | int contentStartCol = show_line_numbers ? (lineNumWidth + 3) : 0; 246 | int screenCol = contentStartCol + cursorCol - viewportLeft; 247 | 248 | if (screenCol >= contentStartCol && screenCol < cols) 249 | { 250 | move(screenRow, screenCol); 251 | } 252 | else 253 | { 254 | move(screenRow, contentStartCol); 255 | } 256 | } 257 | // REMOVED: All #ifdef _WIN32 refresh() calls 258 | } 259 | 260 | bool Editor::mouseToFilePos(int mouseRow, int mouseCol, int &fileRow, 261 | int &fileCol) 262 | { 263 | int rows, cols; 264 | getmaxyx(stdscr, rows, cols); 265 | 266 | if (mouseRow >= rows - 1) 267 | return false; 268 | 269 | bool show_line_numbers = ConfigManager::getLineNumbers(); 270 | int lineNumWidth = 271 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 272 | int contentStartCol = show_line_numbers ? (lineNumWidth + 3) : 0; 273 | 274 | if (mouseCol < contentStartCol) 275 | { 276 | mouseCol = contentStartCol; 277 | } 278 | 279 | fileRow = viewportTop + mouseRow; 280 | if (fileRow < 0) 281 | fileRow = 0; 282 | if (fileRow >= buffer.getLineCount()) 283 | fileRow = buffer.getLineCount() - 1; 284 | 285 | fileCol = viewportLeft + (mouseCol - contentStartCol); 286 | if (fileCol < 0) 287 | fileCol = 0; 288 | 289 | return true; 290 | } 291 | 292 | void Editor::updateCursorAndViewport(int newLine, int newCol) 293 | { 294 | cursorLine = newLine; 295 | 296 | int currentTabSize = ConfigManager::getTabSize(); 297 | std::string expandedLine = 298 | expandTabs(buffer.getLine(cursorLine), currentTabSize); 299 | cursorCol = std::min(newCol, static_cast<int>(expandedLine.length())); 300 | 301 | if (cursorLine < viewportTop) 302 | { 303 | viewportTop = cursorLine; 304 | } 305 | else if (cursorLine >= viewportTop + viewportHeight) 306 | { 307 | viewportTop = cursorLine - viewportHeight + 1; 308 | } 309 | 310 | int rows, cols; 311 | getmaxyx(stdscr, rows, cols); 312 | bool show_line_numbers = ConfigManager::getLineNumbers(); 313 | int lineNumWidth = 314 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 315 | int contentWidth = cols - (show_line_numbers ? (lineNumWidth + 3) : 0); 316 | 317 | if (cursorCol < viewportLeft) 318 | { 319 | viewportLeft = cursorCol; 320 | } 321 | else if (cursorCol >= viewportLeft + contentWidth) 322 | { 323 | viewportLeft = cursorCol - contentWidth + 1; 324 | } 325 | } 326 | 327 | // ================================================================= 328 | // Public API Methods 329 | // ================================================================= 330 | 331 | void Editor::setSyntaxHighlighter(SyntaxHighlighter *highlighter) 332 | { 333 | syntaxHighlighter = highlighter; 334 | } 335 | 336 | void Editor::display() 337 | { 338 | // Validate state 339 | if (!validateEditorState()) 340 | { 341 | validateCursorAndViewport(); 342 | if (!validateEditorState()) 343 | return; 344 | } 345 | 346 | int rows, cols; 347 | getmaxyx(stdscr, rows, cols); 348 | viewportHeight = rows - 1; 349 | 350 | bool show_line_numbers = ConfigManager::getLineNumbers(); 351 | int lineNumWidth = 352 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 353 | int contentStartCol = show_line_numbers ? (lineNumWidth + 3) : 0; 354 | int contentWidth = cols - contentStartCol; 355 | 356 | int endLine = std::min(viewportTop + viewportHeight, buffer.getLineCount()); 357 | 358 | // OPTIMIZATION: Pre-mark viewport lines for priority parsing 359 | if (syntaxHighlighter) 360 | { 361 | syntaxHighlighter->markViewportLines(viewportTop, endLine - 1); 362 | } 363 | 364 | // Pre-compute selection (unchanged) 365 | bool hasActiveSelection = (hasSelection || isSelecting); 366 | int sel_start_line = -1, sel_start_col = -1; 367 | int sel_end_line = -1, sel_end_col = -1; 368 | 369 | if (hasActiveSelection) 370 | { 371 | auto [start, end] = getNormalizedSelection(); 372 | sel_start_line = start.first; 373 | sel_start_col = start.second; 374 | sel_end_line = end.first; 375 | sel_end_col = end.second; 376 | } 377 | 378 | int currentTabSize = ConfigManager::getTabSize(); 379 | 380 | // OPTIMIZATION: Batch render - minimize attribute changes 381 | for (int i = viewportTop; i < endLine; i++) 382 | { 383 | int screenRow = i - viewportTop; 384 | bool isCurrentLine = (cursorLine == i); 385 | 386 | move(screenRow, 0); 387 | attrset(COLOR_PAIR(0)); 388 | 389 | // Render line numbers 390 | if (show_line_numbers) 391 | { 392 | int ln_colorPair = isCurrentLine ? 3 : 2; 393 | attron(COLOR_PAIR(ln_colorPair)); 394 | printw("%*d ", lineNumWidth, i + 1); 395 | attroff(COLOR_PAIR(ln_colorPair)); 396 | 397 | attron(COLOR_PAIR(4)); 398 | addch(' '); 399 | attroff(COLOR_PAIR(4)); 400 | addch(' '); 401 | } 402 | 403 | // Get line content 404 | std::string expandedLine = expandTabs(buffer.getLine(i), currentTabSize); 405 | 406 | // OPTIMIZATION: Get highlighting spans (cached if available) 407 | std::vector<ColorSpan> currentLineSpans; 408 | if (syntaxHighlighter) 409 | { 410 | try 411 | { 412 | currentLineSpans = 413 | syntaxHighlighter->getHighlightSpans(expandedLine, i, buffer); 414 | } 415 | catch (...) 416 | { 417 | currentLineSpans.clear(); 418 | } 419 | } 420 | 421 | // Render line content (unchanged logic, but faster due to cached spans) 422 | bool lineHasSelection = 423 | hasActiveSelection && i >= sel_start_line && i <= sel_end_line; 424 | int current_span_idx = 0; 425 | int num_spans = currentLineSpans.size(); 426 | 427 | for (int screenCol = 0; screenCol < contentWidth; screenCol++) 428 | { 429 | int fileCol = viewportLeft + screenCol; 430 | bool charExists = 431 | (fileCol >= 0 && fileCol < static_cast<int>(expandedLine.length())); 432 | char ch = charExists ? expandedLine[fileCol] : ' '; 433 | 434 | if (charExists && (ch < 32 || ch > 126)) 435 | ch = ' '; 436 | 437 | // Selection check 438 | bool isSelected = false; 439 | if (lineHasSelection && charExists) 440 | { 441 | if (sel_start_line == sel_end_line) 442 | { 443 | isSelected = (fileCol >= sel_start_col && fileCol < sel_end_col); 444 | } 445 | else if (i == sel_start_line) 446 | { 447 | isSelected = (fileCol >= sel_start_col); 448 | } 449 | else if (i == sel_end_line) 450 | { 451 | isSelected = (fileCol < sel_end_col); 452 | } 453 | else 454 | { 455 | isSelected = true; 456 | } 457 | } 458 | 459 | if (isSelected) 460 | { 461 | attron(COLOR_PAIR(14) | A_REVERSE); 462 | addch(ch); 463 | attroff(COLOR_PAIR(14) | A_REVERSE); 464 | } 465 | else 466 | { 467 | bool colorApplied = false; 468 | 469 | if (charExists && num_spans > 0) 470 | { 471 | while (current_span_idx < num_spans && 472 | currentLineSpans[current_span_idx].end <= fileCol) 473 | { 474 | current_span_idx++; 475 | } 476 | 477 | if (current_span_idx < num_spans) 478 | { 479 | const auto &span = currentLineSpans[current_span_idx]; 480 | if (fileCol >= span.start && fileCol < span.end) 481 | { 482 | if (span.colorPair >= 0 && span.colorPair < COLOR_PAIRS) 483 | { 484 | attron(COLOR_PAIR(span.colorPair)); 485 | if (span.attribute != 0) 486 | attron(span.attribute); 487 | addch(ch); 488 | if (span.attribute != 0) 489 | attroff(span.attribute); 490 | attroff(COLOR_PAIR(span.colorPair)); 491 | colorApplied = true; 492 | } 493 | } 494 | } 495 | } 496 | 497 | if (!colorApplied) 498 | { 499 | attrset(COLOR_PAIR(0)); 500 | addch(ch); 501 | } 502 | } 503 | } 504 | 505 | attrset(COLOR_PAIR(0)); 506 | clrtoeol(); 507 | } 508 | 509 | // Clear remaining lines 510 | attrset(COLOR_PAIR(0)); 511 | for (int i = endLine - viewportTop; i < viewportHeight; i++) 512 | { 513 | move(i, 0); 514 | clrtoeol(); 515 | } 516 | 517 | drawStatusBar(); 518 | positionCursor(); 519 | } 520 | 521 | void Editor::drawStatusBar() 522 | { 523 | int rows, cols; 524 | getmaxyx(stdscr, rows, cols); 525 | int statusRow = rows - 1; 526 | 527 | move(statusRow, 0); 528 | attrset(COLOR_PAIR(STATUS_BAR)); 529 | clrtoeol(); 530 | 531 | move(statusRow, 0); 532 | attron(COLOR_PAIR(STATUS_BAR)); 533 | 534 | // Show filename 535 | attron(COLOR_PAIR(STATUS_BAR_CYAN) | A_BOLD); 536 | if (filename.empty()) 537 | { 538 | printw("[No Name]"); 539 | } 540 | else 541 | { 542 | size_t lastSlash = filename.find_last_of("/\\"); 543 | std::string displayName = (lastSlash != std::string::npos) 544 | ? filename.substr(lastSlash + 1) 545 | : filename; 546 | printw("%s", displayName.c_str()); 547 | } 548 | attroff(COLOR_PAIR(STATUS_BAR_CYAN) | A_BOLD); 549 | 550 | // Show modified indicator 551 | if (isModified) 552 | { 553 | attron(COLOR_PAIR(STATUS_BAR_ACTIVE) | A_BOLD); 554 | printw(" [+]"); 555 | attroff(COLOR_PAIR(STATUS_BAR_ACTIVE) | A_BOLD); 556 | } 557 | 558 | // Show file extension 559 | std::string ext = getFileExtension(); 560 | if (!ext.empty()) 561 | { 562 | attron(COLOR_PAIR(STATUS_BAR_ACTIVE)); 563 | printw(" [%s]", ext.c_str()); 564 | attroff(COLOR_PAIR(STATUS_BAR_ACTIVE)); 565 | } 566 | 567 | // Right section with position info 568 | char rightSection[256]; 569 | if (hasSelection) 570 | { 571 | auto [start, end] = getNormalizedSelection(); 572 | int startL = start.first, startC = start.second; 573 | int endL = end.first, endC = end.second; 574 | 575 | if (startL == endL) 576 | { 577 | int selectionSize = endC - startC; 578 | snprintf(rightSection, sizeof(rightSection), 579 | "[%d chars] %d:%d %d/%d %d%% ", selectionSize, cursorLine + 1, 580 | cursorCol + 1, cursorLine + 1, buffer.getLineCount(), 581 | buffer.getLineCount() == 0 582 | ? 0 583 | : ((cursorLine + 1) * 100 / buffer.getLineCount())); 584 | } 585 | else 586 | { 587 | int lineCount = endL - startL + 1; 588 | snprintf(rightSection, sizeof(rightSection), 589 | "[%d lines] %d:%d %d/%d %d%% ", lineCount, cursorLine + 1, 590 | cursorCol + 1, cursorLine + 1, buffer.getLineCount(), 591 | buffer.getLineCount() == 0 592 | ? 0 593 | : ((cursorLine + 1) * 100 / buffer.getLineCount())); 594 | } 595 | } 596 | else 597 | { 598 | snprintf(rightSection, sizeof(rightSection), "%d:%d %d/%d %d%% ", 599 | cursorLine + 1, cursorCol + 1, cursorLine + 1, 600 | buffer.getLineCount(), 601 | buffer.getLineCount() == 0 602 | ? 0 603 | : ((cursorLine + 1) * 100 / buffer.getLineCount())); 604 | } 605 | 606 | int rightLen = strlen(rightSection); 607 | int currentPos = getcurx(stdscr); 608 | int rightStart = cols - rightLen; 609 | 610 | if (rightStart <= currentPos) 611 | { 612 | rightStart = currentPos + 2; 613 | } 614 | 615 | // Fill middle space 616 | attron(COLOR_PAIR(STATUS_BAR)); 617 | for (int i = currentPos; i < rightStart && i < cols; i++) 618 | { 619 | move(statusRow, i); 620 | addch(' '); 621 | } 622 | 623 | // Right section 624 | if (rightStart < cols) 625 | { 626 | move(statusRow, rightStart); 627 | attron(COLOR_PAIR(STATUS_BAR_YELLOW) | A_BOLD); 628 | printw("%s", rightSection); 629 | attroff(COLOR_PAIR(STATUS_BAR_YELLOW) | A_BOLD); 630 | } 631 | 632 | attroff(COLOR_PAIR(STATUS_BAR)); 633 | } 634 | 635 | void Editor::handleResize() 636 | { 637 | int rows, cols; 638 | getmaxyx(stdscr, rows, cols); 639 | viewportHeight = rows - 1; 640 | 641 | if (cursorLine >= viewportTop + viewportHeight) 642 | { 643 | viewportTop = cursorLine - viewportHeight + 1; 644 | } 645 | if (viewportTop < 0) 646 | { 647 | viewportTop = 0; 648 | } 649 | clear(); 650 | display(); 651 | 652 | wnoutrefresh(stdscr); // Mark stdscr as ready 653 | doupdate(); // Execute the single, clean flush 654 | } 655 | 656 | void Editor::handleMouse(MEVENT &event) 657 | { 658 | if (event.bstate & BUTTON1_PRESSED) 659 | { 660 | int fileRow, fileCol; 661 | if (mouseToFilePos(event.y, event.x, fileRow, fileCol)) 662 | { 663 | // Start a new selection on mouse press 664 | clearSelection(); 665 | isSelecting = true; 666 | selectionStartLine = fileRow; 667 | selectionStartCol = fileCol; 668 | selectionEndLine = fileRow; 669 | selectionEndCol = fileCol; 670 | updateCursorAndViewport(fileRow, fileCol); 671 | } 672 | } 673 | else if (event.bstate & BUTTON1_RELEASED) 674 | { 675 | if (isSelecting) 676 | { 677 | int fileRow, fileCol; 678 | if (mouseToFilePos(event.y, event.x, fileRow, fileCol)) 679 | { 680 | selectionEndLine = fileRow; 681 | selectionEndCol = fileCol; 682 | // Only keep selection if it's not just a click (start != end) 683 | if (selectionStartLine != selectionEndLine || 684 | selectionStartCol != selectionEndCol) 685 | { 686 | hasSelection = true; 687 | } 688 | else 689 | { 690 | // Just a click, no drag - clear selection 691 | clearSelection(); 692 | } 693 | updateCursorAndViewport(fileRow, fileCol); 694 | } 695 | isSelecting = false; 696 | } 697 | } 698 | else if ((event.bstate & REPORT_MOUSE_POSITION) && isSelecting) 699 | { 700 | // Mouse drag - extend selection 701 | int fileRow, fileCol; 702 | if (mouseToFilePos(event.y, event.x, fileRow, fileCol)) 703 | { 704 | selectionEndLine = fileRow; 705 | selectionEndCol = fileCol; 706 | updateCursorAndViewport(fileRow, fileCol); 707 | } 708 | } 709 | else if (event.bstate & BUTTON1_CLICKED) 710 | { 711 | // Single click - move cursor and clear selection 712 | int fileRow, fileCol; 713 | if (mouseToFilePos(event.y, event.x, fileRow, fileCol)) 714 | { 715 | clearSelection(); 716 | updateCursorAndViewport(fileRow, fileCol); 717 | } 718 | } 719 | else if (event.bstate & BUTTON4_PRESSED) 720 | { 721 | // Scroll up 722 | scrollUp(); 723 | } 724 | else if (event.bstate & BUTTON5_PRESSED) 725 | { 726 | // Scroll down 727 | scrollDown(); 728 | } 729 | } 730 | 731 | void Editor::clearSelection() 732 | { 733 | hasSelection = false; 734 | isSelecting = false; 735 | selectionStartLine = 0; 736 | selectionStartCol = 0; 737 | selectionEndLine = 0; 738 | selectionEndCol = 0; 739 | } 740 | 741 | void Editor::moveCursorUp() 742 | { 743 | if (cursorLine > 0) 744 | { 745 | cursorLine--; 746 | if (cursorLine < viewportTop) 747 | { 748 | viewportTop = cursorLine; 749 | } 750 | 751 | if (cursorCol > 0) 752 | { 753 | std::string line = buffer.getLine(cursorLine); 754 | int lineLen = static_cast<int>(line.length()); 755 | if (cursorCol > lineLen) 756 | { 757 | std::string expandedLine = expandTabs(line, tabSize); 758 | cursorCol = 759 | std::min(cursorCol, static_cast<int>(expandedLine.length())); 760 | } 761 | } 762 | } 763 | // Note: Selection handling now done in InputHandler 764 | } 765 | 766 | void Editor::moveCursorDown() 767 | { 768 | int maxLine = buffer.getLineCount() - 1; 769 | if (cursorLine < maxLine) 770 | { 771 | cursorLine++; 772 | if (cursorLine >= viewportTop + viewportHeight) 773 | { 774 | viewportTop = cursorLine - viewportHeight + 1; 775 | } 776 | 777 | if (cursorCol > 0) 778 | { 779 | std::string line = buffer.getLine(cursorLine); 780 | int lineLen = static_cast<int>(line.length()); 781 | if (cursorCol > lineLen) 782 | { 783 | std::string expandedLine = expandTabs(line, tabSize); 784 | cursorCol = 785 | std::min(cursorCol, static_cast<int>(expandedLine.length())); 786 | } 787 | } 788 | } 789 | } 790 | 791 | void Editor::moveCursorLeft() 792 | { 793 | if (cursorCol > 0) 794 | { 795 | cursorCol--; 796 | if (cursorCol < viewportLeft) 797 | { 798 | viewportLeft = cursorCol; 799 | } 800 | } 801 | else if (cursorLine > 0) 802 | { 803 | cursorLine--; 804 | int currentTabSize = ConfigManager::getTabSize(); 805 | std::string expandedLine = 806 | expandTabs(buffer.getLine(cursorLine), currentTabSize); 807 | cursorCol = expandedLine.length(); 808 | 809 | if (cursorLine < viewportTop) 810 | { 811 | viewportTop = cursorLine; 812 | } 813 | 814 | int rows, cols; 815 | getmaxyx(stdscr, rows, cols); 816 | bool show_line_numbers = ConfigManager::getLineNumbers(); 817 | int lineNumWidth = 818 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 819 | int contentWidth = cols - (show_line_numbers ? (lineNumWidth + 3) : 0); 820 | 821 | if (contentWidth > 0 && cursorCol >= viewportLeft + contentWidth) 822 | { 823 | viewportLeft = cursorCol - contentWidth + 1; 824 | if (viewportLeft < 0) 825 | viewportLeft = 0; 826 | } 827 | } 828 | } 829 | 830 | void Editor::moveCursorRight() 831 | { 832 | std::string line = buffer.getLine(cursorLine); 833 | 834 | if (cursorCol < static_cast<int>(line.length())) 835 | { 836 | if (line[cursorCol] != '\t') 837 | { 838 | cursorCol++; 839 | } 840 | else 841 | { 842 | int currentTabSize = ConfigManager::getTabSize(); 843 | std::string expandedLine = expandTabs(line, currentTabSize); 844 | if (cursorCol < static_cast<int>(expandedLine.length())) 845 | { 846 | cursorCol++; 847 | } 848 | } 849 | 850 | int rows, cols; 851 | getmaxyx(stdscr, rows, cols); 852 | bool show_line_numbers = ConfigManager::getLineNumbers(); 853 | int lineNumWidth = 854 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 855 | int contentWidth = cols - (show_line_numbers ? (lineNumWidth + 3) : 0); 856 | 857 | if (contentWidth > 0 && cursorCol >= viewportLeft + contentWidth) 858 | { 859 | viewportLeft = cursorCol - contentWidth + 1; 860 | } 861 | } 862 | else if (cursorLine < buffer.getLineCount() - 1) 863 | { 864 | cursorLine++; 865 | cursorCol = 0; 866 | 867 | if (cursorLine >= viewportTop + viewportHeight) 868 | { 869 | viewportTop = cursorLine - viewportHeight + 1; 870 | } 871 | 872 | viewportLeft = 0; 873 | } 874 | } 875 | 876 | void Editor::pageUp() 877 | { 878 | for (int i = 0; i < 10; i++) 879 | { 880 | moveCursorUp(); 881 | } 882 | } 883 | 884 | void Editor::pageDown() 885 | { 886 | for (int i = 0; i < 10; i++) 887 | { 888 | moveCursorDown(); 889 | } 890 | } 891 | 892 | void Editor::moveCursorToLineStart() 893 | { 894 | cursorCol = 0; 895 | if (cursorCol < viewportLeft) 896 | { 897 | viewportLeft = 0; 898 | } 899 | 900 | // Selection handling is done in InputHandler, not here 901 | // This method just moves the cursor 902 | } 903 | 904 | void Editor::moveCursorToLineEnd() 905 | { 906 | int currentTabSize = ConfigManager::getTabSize(); 907 | std::string expandedLine = 908 | expandTabs(buffer.getLine(cursorLine), currentTabSize); 909 | cursorCol = static_cast<int>(expandedLine.length()); 910 | 911 | int rows, cols; 912 | getmaxyx(stdscr, rows, cols); 913 | bool show_line_numbers = ConfigManager::getLineNumbers(); 914 | int lineNumWidth = 915 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 916 | int contentWidth = cols - (show_line_numbers ? (lineNumWidth + 3) : 0); 917 | 918 | if (contentWidth > 0 && cursorCol >= viewportLeft + contentWidth) 919 | { 920 | viewportLeft = cursorCol - contentWidth + 1; 921 | if (viewportLeft < 0) 922 | viewportLeft = 0; 923 | } 924 | 925 | // Selection handling is done in InputHandler, not here 926 | // This method just moves the cursor 927 | } 928 | 929 | void Editor::scrollUp(int linesToScroll) 930 | { 931 | viewportTop -= linesToScroll; 932 | if (viewportTop < 0) 933 | viewportTop = 0; 934 | 935 | if (cursorLine < viewportTop) 936 | { 937 | cursorLine = viewportTop; 938 | if (cursorLine < 0) 939 | cursorLine = 0; 940 | if (cursorLine >= buffer.getLineCount()) 941 | { 942 | cursorLine = buffer.getLineCount() - 1; 943 | } 944 | 945 | std::string expandedLine = expandTabs(buffer.getLine(cursorLine), tabSize); 946 | cursorCol = std::min(cursorCol, static_cast<int>(expandedLine.length())); 947 | } 948 | } 949 | 950 | void Editor::scrollDown(int linesToScroll) 951 | { 952 | int maxViewportTop = buffer.getLineCount() - viewportHeight; 953 | if (maxViewportTop < 0) 954 | maxViewportTop = 0; 955 | 956 | viewportTop += linesToScroll; 957 | if (viewportTop > maxViewportTop) 958 | viewportTop = maxViewportTop; 959 | if (viewportTop < 0) 960 | viewportTop = 0; 961 | 962 | if (cursorLine >= viewportTop + viewportHeight) 963 | { 964 | cursorLine = viewportTop + viewportHeight - 1; 965 | 966 | int maxLine = buffer.getLineCount() - 1; 967 | if (cursorLine > maxLine) 968 | cursorLine = maxLine; 969 | if (cursorLine < 0) 970 | cursorLine = 0; 971 | 972 | std::string expandedLine = expandTabs(buffer.getLine(cursorLine), tabSize); 973 | cursorCol = std::min(cursorCol, static_cast<int>(expandedLine.length())); 974 | } 975 | } 976 | 977 | void Editor::validateCursorAndViewport() 978 | { 979 | if (buffer.getLineCount() == 0) 980 | return; 981 | 982 | int maxLine = buffer.getLineCount() - 1; 983 | if (cursorLine < 0) 984 | cursorLine = 0; 985 | if (cursorLine > maxLine) 986 | cursorLine = maxLine; 987 | 988 | std::string expandedLine = expandTabs(buffer.getLine(cursorLine), tabSize); 989 | if (cursorCol < 0) 990 | cursorCol = 0; 991 | if (cursorCol > static_cast<int>(expandedLine.length())) 992 | { 993 | cursorCol = static_cast<int>(expandedLine.length()); 994 | } 995 | 996 | int maxViewportTop = buffer.getLineCount() - viewportHeight; 997 | if (maxViewportTop < 0) 998 | maxViewportTop = 0; 999 | 1000 | if (viewportTop < 0) 1001 | viewportTop = 0; 1002 | if (viewportTop > maxViewportTop) 1003 | viewportTop = maxViewportTop; 1004 | if (viewportLeft < 0) 1005 | viewportLeft = 0; 1006 | 1007 | if (cursorLine < viewportTop) 1008 | { 1009 | viewportTop = cursorLine; 1010 | } 1011 | else if (cursorLine >= viewportTop + viewportHeight) 1012 | { 1013 | viewportTop = cursorLine - viewportHeight + 1; 1014 | if (viewportTop < 0) 1015 | viewportTop = 0; 1016 | if (viewportTop > maxViewportTop) 1017 | viewportTop = maxViewportTop; 1018 | } 1019 | } 1020 | 1021 | // ================================================================= 1022 | // File Operations 1023 | // ================================================================= 1024 | 1025 | void Editor::debugPrintState(const std::string &context) 1026 | { 1027 | std::cerr << "=== EDITOR STATE DEBUG: " << context << " ===" << std::endl; 1028 | std::cerr << "cursorLine: " << cursorLine << std::endl; 1029 | std::cerr << "cursorCol: " << cursorCol << std::endl; 1030 | std::cerr << "viewportTop: " << viewportTop << std::endl; 1031 | std::cerr << "viewportLeft: " << viewportLeft << std::endl; 1032 | std::cerr << "buffer.getLineCount(): " << buffer.getLineCount() << std::endl; 1033 | std::cerr << "buffer.size(): " << buffer.size() << std::endl; 1034 | std::cerr << "isModified: " << isModified << std::endl; 1035 | // std::cerr << "currentMode: " << (int)currentMode << std::endl; 1036 | 1037 | if (cursorLine < buffer.getLineCount()) 1038 | { 1039 | std::string currentLine = buffer.getLine(cursorLine); 1040 | std::cerr << "currentLine length: " << currentLine.length() << std::endl; 1041 | std::cerr << "currentLine content: '" << currentLine << "'" << std::endl; 1042 | } 1043 | else 1044 | { 1045 | std::cerr << "ERROR: cursorLine out of bounds!" << std::endl; 1046 | } 1047 | 1048 | std::cerr << "hasSelection: " << hasSelection << std::endl; 1049 | std::cerr << "isSelecting: " << isSelecting << std::endl; 1050 | std::cerr << "undoStack.size(): " << undoStack.size() << std::endl; 1051 | std::cerr << "redoStack.size(): " << redoStack.size() << std::endl; 1052 | std::cerr << "=== END DEBUG ===" << std::endl; 1053 | } 1054 | 1055 | bool Editor::validateEditorState() 1056 | { 1057 | bool valid = true; 1058 | 1059 | if (cursorLine < 0 || cursorLine >= buffer.getLineCount()) 1060 | { 1061 | std::cerr << "INVALID: cursorLine out of bounds: " << cursorLine 1062 | << " (max: " << buffer.getLineCount() - 1 << ")" << std::endl; 1063 | valid = false; 1064 | } 1065 | 1066 | if (cursorCol < 0) 1067 | { 1068 | std::cerr << "INVALID: cursorCol negative: " << cursorCol << std::endl; 1069 | valid = false; 1070 | } 1071 | 1072 | if (cursorLine >= 0 && cursorLine < buffer.getLineCount()) 1073 | { 1074 | std::string line = buffer.getLine(cursorLine); 1075 | if (cursorCol > static_cast<int>(line.length())) 1076 | { 1077 | std::cerr << "INVALID: cursorCol past end of line: " << cursorCol 1078 | << " (line length: " << line.length() << ")" << std::endl; 1079 | valid = false; 1080 | } 1081 | } 1082 | 1083 | if (viewportTop < 0) 1084 | { 1085 | std::cerr << "INVALID: viewportTop negative: " << viewportTop << std::endl; 1086 | valid = false; 1087 | } 1088 | 1089 | if (viewportLeft < 0) 1090 | { 1091 | std::cerr << "INVALID: viewportLeft negative: " << viewportLeft 1092 | << std::endl; 1093 | valid = false; 1094 | } 1095 | 1096 | return valid; 1097 | } 1098 | 1099 | bool Editor::loadFile(const std::string &fname) 1100 | { 1101 | filename = fname; 1102 | 1103 | if (syntaxHighlighter) 1104 | { 1105 | std::string extension = getFileExtension(); 1106 | syntaxHighlighter->setLanguage(extension); 1107 | } 1108 | 1109 | if (!buffer.loadFromFile(filename)) 1110 | { 1111 | buffer.clear(); 1112 | buffer.insertLine(0, ""); 1113 | return false; 1114 | } 1115 | 1116 | // Set language but DON'T parse yet - parsing happens on first display 1117 | 1118 | isModified = false; 1119 | return true; 1120 | } 1121 | 1122 | bool Editor::saveFile() 1123 | { 1124 | if (filename.empty()) 1125 | { 1126 | return false; 1127 | } 1128 | 1129 | // Set flag to prevent saveState() during file operations 1130 | isSaving = true; 1131 | 1132 | bool success = buffer.saveToFile(filename); 1133 | 1134 | if (success) 1135 | { 1136 | isModified = false; 1137 | } 1138 | 1139 | isSaving = false; // Reset flag 1140 | return success; 1141 | } 1142 | // ================================================================= 1143 | // Text Editing Operations 1144 | // ================================================================= 1145 | 1146 | void Editor::insertChar(char ch) 1147 | { 1148 | if (cursorLine < 0 || cursorLine >= buffer.getLineCount()) 1149 | return; 1150 | 1151 | if (useDeltaUndo_ && !isUndoRedoing) 1152 | { 1153 | EditDelta delta = createDeltaForInsertChar(ch); 1154 | std::string line = buffer.getLine(cursorLine); 1155 | if (cursorCol > static_cast<int>(line.length())) 1156 | cursorCol = line.length(); 1157 | if (cursorCol < 0) 1158 | cursorCol = 0; 1159 | 1160 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol); 1161 | line.insert(cursorCol, 1, ch); 1162 | buffer.replaceLine(cursorLine, line); 1163 | cursorCol++; 1164 | 1165 | if (syntaxHighlighter && !isUndoRedoing) 1166 | { 1167 | syntaxHighlighter->updateTreeAfterEdit( 1168 | buffer, byte_pos, 0, 1, cursorLine, cursorCol - 1, cursorLine, 1169 | cursorCol - 1, cursorLine, cursorCol); 1170 | 1171 | // NEW: Always invalidate cache after edit 1172 | syntaxHighlighter->invalidateLineCache(cursorLine); 1173 | } 1174 | 1175 | // Update viewport 1176 | int rows, cols; 1177 | getmaxyx(stdscr, rows, cols); 1178 | bool show_line_numbers = ConfigManager::getLineNumbers(); 1179 | int lineNumWidth = 1180 | show_line_numbers ? std::to_string(buffer.getLineCount()).length() : 0; 1181 | int contentWidth = cols - (show_line_numbers ? (lineNumWidth + 3) : 0); 1182 | 1183 | if (contentWidth > 0 && cursorCol >= viewportLeft + contentWidth) 1184 | { 1185 | viewportLeft = cursorCol - contentWidth + 1; 1186 | } 1187 | 1188 | // Complete delta 1189 | delta.postCursorLine = cursorLine; 1190 | delta.postCursorCol = cursorCol; 1191 | delta.postViewportTop = viewportTop; 1192 | delta.postViewportLeft = viewportLeft; 1193 | 1194 | addDelta(delta); 1195 | 1196 | // FIX: Auto-commit on timeout OR boundary characters for immediate 1197 | // highlighting 1198 | auto now = std::chrono::steady_clock::now(); 1199 | auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( 1200 | now - currentDeltaGroup_.timestamp) 1201 | .count(); 1202 | 1203 | // Boundary characters that should trigger immediate commit 1204 | bool is_boundary_char = 1205 | (ch == '>' || ch == ')' || ch == '}' || ch == ']' || ch == ';' || 1206 | ch == ',' || ch == ' ' || ch == '\t' || ch == '<' || ch == '(' || 1207 | ch == '{' || ch == '['); 1208 | 1209 | if (elapsed > UNDO_GROUP_TIMEOUT_MS || is_boundary_char) 1210 | { 1211 | commitDeltaGroup(); 1212 | beginDeltaGroup(); 1213 | } 1214 | 1215 | markModified(); 1216 | } 1217 | else if (!isUndoRedoing) 1218 | { 1219 | // OLD: Full-state undo (fallback) 1220 | saveState(); 1221 | 1222 | std::string line = buffer.getLine(cursorLine); 1223 | if (cursorCol > static_cast<int>(line.length())) 1224 | cursorCol = line.length(); 1225 | if (cursorCol < 0) 1226 | cursorCol = 0; 1227 | 1228 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol); 1229 | line.insert(cursorCol, 1, ch); 1230 | buffer.replaceLine(cursorLine, line); 1231 | 1232 | if (syntaxHighlighter && !isUndoRedoing) 1233 | { 1234 | syntaxHighlighter->updateTreeAfterEdit(buffer, byte_pos, 0, 1, cursorLine, 1235 | cursorCol, cursorLine, cursorCol, 1236 | cursorLine, cursorCol + 1); 1237 | syntaxHighlighter->invalidateLineRange(cursorLine, cursorLine); 1238 | } 1239 | 1240 | cursorCol++; 1241 | markModified(); 1242 | 1243 | int rows, cols; 1244 | getmaxyx(stdscr, rows, cols); 1245 | int lineNumWidth = std::to_string(buffer.getLineCount()).length(); 1246 | int contentWidth = cols - lineNumWidth - 3; 1247 | if (contentWidth > 0 && cursorCol >= viewportLeft + contentWidth) 1248 | { 1249 | viewportLeft = cursorCol - contentWidth + 1; 1250 | } 1251 | } 1252 | } 1253 | 1254 | void Editor::insertNewline() 1255 | { 1256 | if (useDeltaUndo_ && !isUndoRedoing) 1257 | { 1258 | EditorSnapshot before = captureSnapshot(); 1259 | EditDelta delta = createDeltaForNewline(); 1260 | 1261 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol); 1262 | 1263 | // 1. MODIFY BUFFER FIRST 1264 | splitLineAtCursor(); 1265 | cursorLine++; 1266 | cursorCol = 0; 1267 | 1268 | // 2. THEN notify Tree-sitter AFTER buffer change 1269 | if (syntaxHighlighter && !isUndoRedoing) 1270 | { 1271 | syntaxHighlighter->updateTreeAfterEdit( 1272 | buffer, byte_pos, 0, 1, // Inserted 1 byte (newline) 1273 | delta.preCursorLine, delta.preCursorCol, // OLD position 1274 | delta.preCursorLine, delta.preCursorCol, cursorLine, 1275 | 0); // NEW position 1276 | 1277 | // Invalidate from split point onwards 1278 | syntaxHighlighter->invalidateLineRange(cursorLine - 1, 1279 | buffer.getLineCount() - 1); 1280 | } 1281 | 1282 | if (cursorLine >= viewportTop + viewportHeight) 1283 | { 1284 | viewportTop = cursorLine - viewportHeight + 1; 1285 | } 1286 | viewportLeft = 0; 1287 | 1288 | // Complete delta 1289 | delta.postCursorLine = cursorLine; 1290 | delta.postCursorCol = cursorCol; 1291 | delta.postViewportTop = viewportTop; 1292 | delta.postViewportLeft = viewportLeft; 1293 | 1294 | ValidationResult valid = validateState("After insertNewline"); 1295 | if (valid) 1296 | { 1297 | addDelta(delta); 1298 | // Newlines always commit the current group 1299 | commitDeltaGroup(); 1300 | beginDeltaGroup(); 1301 | } 1302 | else 1303 | { 1304 | std::cerr << "VALIDATION FAILED in insertNewline\n"; 1305 | std::cerr << valid.error << "\n"; 1306 | } 1307 | 1308 | markModified(); 1309 | } 1310 | else if (!isUndoRedoing) 1311 | { 1312 | // OLD: Full-state undo 1313 | saveState(); 1314 | 1315 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol); 1316 | 1317 | splitLineAtCursor(); 1318 | cursorLine++; 1319 | cursorCol = 0; 1320 | 1321 | if (syntaxHighlighter && !isUndoRedoing) 1322 | { 1323 | syntaxHighlighter->updateTreeAfterEdit(buffer, byte_pos, 0, 1, 1324 | cursorLine - 1, 0, cursorLine - 1, 1325 | 0, cursorLine, 0); 1326 | syntaxHighlighter->invalidateLineRange(cursorLine - 1, 1327 | buffer.getLineCount() - 1); 1328 | } 1329 | 1330 | if (cursorLine >= viewportTop + viewportHeight) 1331 | { 1332 | viewportTop = cursorLine - viewportHeight + 1; 1333 | } 1334 | viewportLeft = 0; 1335 | markModified(); 1336 | } 1337 | } 1338 | 1339 | void Editor::deleteChar() 1340 | { 1341 | if (useDeltaUndo_ && !isUndoRedoing) 1342 | { 1343 | EditorSnapshot before = captureSnapshot(); 1344 | EditDelta delta = createDeltaForDeleteChar(); 1345 | std::string line = buffer.getLine(cursorLine); 1346 | 1347 | if (cursorCol < static_cast<int>(line.length())) 1348 | { 1349 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol); 1350 | line.erase(cursorCol, 1); 1351 | buffer.replaceLine(cursorLine, line); 1352 | 1353 | if (syntaxHighlighter && !isUndoRedoing) 1354 | { 1355 | syntaxHighlighter->updateTreeAfterEdit( 1356 | buffer, byte_pos, 1, 0, cursorLine, cursorCol, cursorLine, 1357 | cursorCol + 1, cursorLine, cursorCol); 1358 | 1359 | // NEW: Always invalidate cache after edit 1360 | syntaxHighlighter->invalidateLineCache(cursorLine); 1361 | } 1362 | } 1363 | else if (cursorLine < buffer.getLineCount() - 1) 1364 | { 1365 | size_t byte_pos = buffer.lineColToPos(cursorLine, line.length()); 1366 | std::string nextLine = buffer.getLine(cursorLine + 1); 1367 | buffer.replaceLine(cursorLine, line + nextLine); 1368 | buffer.deleteLine(cursorLine + 1); 1369 | 1370 | if (syntaxHighlighter && !isUndoRedoing) 1371 | { 1372 | syntaxHighlighter->updateTreeAfterEdit( 1373 | buffer, byte_pos, 1, 0, cursorLine, (uint32_t)line.length(), 1374 | cursorLine + 1, 0, cursorLine, (uint32_t)line.length()); 1375 | 1376 | syntaxHighlighter->invalidateLineRange(cursorLine, 1377 | buffer.getLineCount() - 1); 1378 | } 1379 | } 1380 | 1381 | delta.postCursorLine = cursorLine; 1382 | delta.postCursorCol = cursorCol; 1383 | delta.postViewportTop = viewportTop; 1384 | delta.postViewportLeft = viewportLeft; 1385 | 1386 | ValidationResult valid = validateState("After deleteChar"); 1387 | if (valid) 1388 | { 1389 | addDelta(delta); 1390 | if (delta.operation == EditDelta::JOIN_LINES) 1391 | { 1392 | commitDeltaGroup(); 1393 | beginDeltaGroup(); 1394 | } 1395 | } 1396 | else 1397 | { 1398 | std::cerr << "VALIDATION FAILED in deleteChar\n"; 1399 | std::cerr << valid.error << "\n"; 1400 | } 1401 | 1402 | markModified(); 1403 | } 1404 | else if (!isUndoRedoing) 1405 | { 1406 | saveState(); 1407 | std::string line = buffer.getLine(cursorLine); 1408 | 1409 | if (cursorCol < static_cast<int>(line.length())) 1410 | { 1411 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol); 1412 | line.erase(cursorCol, 1); 1413 | buffer.replaceLine(cursorLine, line); 1414 | 1415 | if (syntaxHighlighter && !isUndoRedoing) 1416 | { 1417 | syntaxHighlighter->updateTreeAfterEdit( 1418 | buffer, byte_pos, 1, 0, cursorLine, cursorCol, cursorLine, 1419 | cursorCol + 1, cursorLine, cursorCol); 1420 | // NEW: Always invalidate cache after edit 1421 | syntaxHighlighter->invalidateLineCache(cursorLine); 1422 | } 1423 | 1424 | markModified(); 1425 | } 1426 | else if (cursorLine < buffer.getLineCount() - 1) 1427 | { 1428 | size_t byte_pos = buffer.lineColToPos(cursorLine, line.length()); 1429 | std::string nextLine = buffer.getLine(cursorLine + 1); 1430 | buffer.replaceLine(cursorLine, line + nextLine); 1431 | buffer.deleteLine(cursorLine + 1); 1432 | 1433 | if (syntaxHighlighter && !isUndoRedoing) 1434 | { 1435 | syntaxHighlighter->updateTreeAfterEdit( 1436 | buffer, byte_pos, 1, 0, cursorLine, (uint32_t)line.length(), 1437 | cursorLine + 1, 0, cursorLine, (uint32_t)line.length()); 1438 | syntaxHighlighter->invalidateLineRange(cursorLine, 1439 | buffer.getLineCount() - 1); 1440 | } 1441 | 1442 | markModified(); 1443 | } 1444 | } 1445 | } 1446 | 1447 | void Editor::backspace() 1448 | { 1449 | if (useDeltaUndo_ && !isUndoRedoing) 1450 | { 1451 | EditorSnapshot before = captureSnapshot(); 1452 | EditDelta delta = createDeltaForBackspace(); 1453 | 1454 | if (cursorCol > 0) 1455 | { 1456 | std::string line = buffer.getLine(cursorLine); 1457 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol - 1); 1458 | line.erase(cursorCol - 1, 1); 1459 | buffer.replaceLine(cursorLine, line); 1460 | cursorCol--; 1461 | 1462 | if (syntaxHighlighter && !isUndoRedoing) 1463 | { 1464 | syntaxHighlighter->updateTreeAfterEdit( 1465 | buffer, byte_pos, 1, 0, cursorLine, cursorCol, cursorLine, 1466 | cursorCol + 1, cursorLine, cursorCol); 1467 | 1468 | // NEW: Always invalidate cache after edit 1469 | syntaxHighlighter->invalidateLineCache(cursorLine); 1470 | } 1471 | 1472 | if (cursorCol < viewportLeft) 1473 | { 1474 | viewportLeft = cursorCol; 1475 | } 1476 | } 1477 | else if (cursorLine > 0) 1478 | { 1479 | std::string currentLine = buffer.getLine(cursorLine); 1480 | std::string prevLine = buffer.getLine(cursorLine - 1); 1481 | size_t byte_pos = buffer.lineColToPos(cursorLine - 1, prevLine.length()); 1482 | 1483 | int oldCursorLine = cursorLine; 1484 | cursorCol = static_cast<int>(prevLine.length()); 1485 | cursorLine--; 1486 | 1487 | buffer.replaceLine(cursorLine, prevLine + currentLine); 1488 | buffer.deleteLine(cursorLine + 1); 1489 | 1490 | if (syntaxHighlighter && !isUndoRedoing) 1491 | { 1492 | syntaxHighlighter->updateTreeAfterEdit( 1493 | buffer, byte_pos, 1, 0, cursorLine, cursorCol, oldCursorLine, 0, 1494 | cursorLine, cursorCol); 1495 | 1496 | syntaxHighlighter->invalidateLineRange(cursorLine, 1497 | buffer.getLineCount() - 1); 1498 | } 1499 | } 1500 | 1501 | delta.postCursorLine = cursorLine; 1502 | delta.postCursorCol = cursorCol; 1503 | delta.postViewportTop = viewportTop; 1504 | delta.postViewportLeft = viewportLeft; 1505 | 1506 | ValidationResult valid = validateState("After backspace"); 1507 | if (valid) 1508 | { 1509 | addDelta(delta); 1510 | if (delta.operation == EditDelta::JOIN_LINES) 1511 | { 1512 | commitDeltaGroup(); 1513 | beginDeltaGroup(); 1514 | } 1515 | } 1516 | else 1517 | { 1518 | std::cerr << "VALIDATION FAILED in backspace\n"; 1519 | std::cerr << valid.error << "\n"; 1520 | } 1521 | 1522 | markModified(); 1523 | } 1524 | else if (!isUndoRedoing) 1525 | { 1526 | saveState(); 1527 | 1528 | if (cursorCol > 0) 1529 | { 1530 | size_t byte_pos = buffer.lineColToPos(cursorLine, cursorCol - 1); 1531 | std::string line = buffer.getLine(cursorLine); 1532 | line.erase(cursorCol - 1, 1); 1533 | buffer.replaceLine(cursorLine, line); 1534 | cursorCol--; 1535 | 1536 | if (syntaxHighlighter && !isUndoRedoing) 1537 | { 1538 | syntaxHighlighter->updateTreeAfterEdit( 1539 | buffer, byte_pos, 1, 0, cursorLine, cursorCol, cursorLine, 1540 | cursorCol + 1, cursorLine, cursorCol); 1541 | // NEW: Always invalidate cache after edit 1542 | syntaxHighlighter->invalidateLineCache(cursorLine); 1543 | } 1544 | 1545 | if (cursorCol < viewportLeft) 1546 | { 1547 | viewportLeft = cursorCol; 1548 | } 1549 | 1550 | markModified(); 1551 | } 1552 | else if (cursorLine > 0) 1553 | { 1554 | std::string currentLine = buffer.getLine(cursorLine); 1555 | std::string prevLine = buffer.getLine(cursorLine - 1); 1556 | size_t byte_pos = buffer.lineColToPos(cursorLine - 1, prevLine.length()); 1557 | 1558 | cursorCol = static_cast<int>(prevLine.length()); 1559 | cursorLine--; 1560 | 1561 | buffer.replaceLine(cursorLine, prevLine + currentLine); 1562 | buffer.deleteLine(cursorLine + 1); 1563 | 1564 | if (syntaxHighlighter && !isUndoRedoing) 1565 | { 1566 | syntaxHighlighter->updateTreeAfterEdit( 1567 | buffer, byte_pos, 1, 0, cursorLine, cursorCol, cursorLine + 1, 0, 1568 | cursorLine, cursorCol); 1569 | syntaxHighlighter->invalidateLineRange(cursorLine, 1570 | buffer.getLineCount() - 1); 1571 | } 1572 | 1573 | markModified(); 1574 | } 1575 | } 1576 | } 1577 | 1578 | void Editor::deleteLine() 1579 | { 1580 | // SAVE STATE BEFORE MODIFICATION 1581 | if (!isUndoRedoing) 1582 | { 1583 | saveState(); 1584 | } 1585 | 1586 | if (buffer.getLineCount() == 1) 1587 | { 1588 | std::string line = buffer.getLine(0); 1589 | size_t byte_pos = 0; 1590 | 1591 | buffer.replaceLine(0, ""); 1592 | cursorCol = 0; 1593 | 1594 | if (syntaxHighlighter && !isUndoRedoing) 1595 | { 1596 | syntaxHighlighter->updateTreeAfterEdit(buffer, byte_pos, line.length(), 0, 1597 | 0, 0, 0, (uint32_t)line.length(), 1598 | 0, 0); 1599 | syntaxHighlighter->invalidateLineRange(0, 0); 1600 | } 1601 | 1602 | // buffer.replaceLine(0, ""); 1603 | // cursorCol = 0; 1604 | } 1605 | else 1606 | { 1607 | size_t byte_pos = buffer.lineColToPos(cursorLine, 0); 1608 | std::string line = buffer.getLine(cursorLine); 1609 | size_t line_length = line.length(); 1610 | 1611 | bool has_newline = (cursorLine < buffer.getLineCount() - 1); 1612 | size_t delete_bytes = line_length + (has_newline ? 1 : 0); 1613 | 1614 | buffer.deleteLine(cursorLine); 1615 | 1616 | if (syntaxHighlighter && !isUndoRedoing) 1617 | { 1618 | syntaxHighlighter->updateTreeAfterEdit( 1619 | buffer, byte_pos, delete_bytes, 0, cursorLine, 0, 1620 | cursorLine + (has_newline ? 1 : 0), 1621 | has_newline ? 0 : (uint32_t)line_length, cursorLine, 0); 1622 | syntaxHighlighter->invalidateLineRange(cursorLine, 1623 | buffer.getLineCount() - 1); 1624 | } 1625 | 1626 | if (cursorLine >= buffer.getLineCount()) 1627 | { 1628 | cursorLine = buffer.getLineCount() - 1; 1629 | } 1630 | 1631 | line = buffer.getLine(cursorLine); 1632 | if (cursorCol > static_cast<int>(line.length())) 1633 | { 1634 | cursorCol = static_cast<int>(line.length()); 1635 | } 1636 | } 1637 | 1638 | validateCursorAndViewport(); 1639 | markModified(); 1640 | } 1641 | 1642 | // ================================================================= 1643 | // Undo/Redo System 1644 | // ================================================================= 1645 | 1646 | void Editor::deleteSelection() 1647 | { 1648 | if (!hasSelection && !isSelecting) 1649 | return; 1650 | 1651 | if (useDeltaUndo_ && !isUndoRedoing) 1652 | { 1653 | EditorSnapshot before = captureSnapshot(); 1654 | EditDelta delta = createDeltaForDeleteSelection(); 1655 | 1656 | auto selection = getNormalizedSelection(); 1657 | int startLine = selection.first.first; 1658 | int startCol = selection.first.second; 1659 | int endLine = selection.second.first; 1660 | int endCol = selection.second.second; 1661 | 1662 | size_t start_byte = buffer.lineColToPos(startLine, startCol); 1663 | size_t end_byte = buffer.lineColToPos(endLine, endCol); 1664 | size_t delete_bytes = end_byte - start_byte; 1665 | 1666 | // 1. MODIFY BUFFER FIRST 1667 | if (startLine == endLine) 1668 | { 1669 | std::string line = buffer.getLine(startLine); 1670 | line.erase(startCol, endCol - startCol); 1671 | buffer.replaceLine(startLine, line); 1672 | } 1673 | else 1674 | { 1675 | std::string firstLine = buffer.getLine(startLine); 1676 | std::string lastLine = buffer.getLine(endLine); 1677 | 1678 | std::string newLine = 1679 | firstLine.substr(0, startCol) + lastLine.substr(endCol); 1680 | buffer.replaceLine(startLine, newLine); 1681 | 1682 | for (int i = endLine; i > startLine; i--) 1683 | { 1684 | buffer.deleteLine(i); 1685 | } 1686 | } 1687 | 1688 | // 2. THEN notify Tree-sitter AFTER buffer change 1689 | if (syntaxHighlighter && !isUndoRedoing) 1690 | { 1691 | syntaxHighlighter->updateTreeAfterEdit( 1692 | buffer, start_byte, delete_bytes, 0, // Deleted bytes 1693 | startLine, startCol, endLine, endCol, startLine, startCol); 1694 | 1695 | syntaxHighlighter->invalidateLineRange(startLine, 1696 | buffer.getLineCount() - 1); 1697 | } 1698 | 1699 | updateCursorAndViewport(startLine, startCol); 1700 | clearSelection(); 1701 | 1702 | // Complete delta 1703 | delta.postCursorLine = cursorLine; 1704 | delta.postCursorCol = cursorCol; 1705 | delta.postViewportTop = viewportTop; 1706 | delta.postViewportLeft = viewportLeft; 1707 | 1708 | ValidationResult valid = validateState("After deleteSelection"); 1709 | if (valid) 1710 | { 1711 | addDelta(delta); 1712 | commitDeltaGroup(); 1713 | beginDeltaGroup(); 1714 | } 1715 | else 1716 | { 1717 | std::cerr << "VALIDATION FAILED in deleteSelection\n"; 1718 | std::cerr << valid.error << "\n"; 1719 | } 1720 | 1721 | markModified(); 1722 | } 1723 | else if (!isUndoRedoing) 1724 | { 1725 | // OLD: Full-state undo 1726 | saveState(); 1727 | 1728 | auto selection = getNormalizedSelection(); 1729 | int startLine = selection.first.first; 1730 | int startCol = selection.first.second; 1731 | int endLine = selection.second.first; 1732 | int endCol = selection.second.second; 1733 | 1734 | size_t start_byte = buffer.lineColToPos(startLine, startCol); 1735 | size_t end_byte = buffer.lineColToPos(endLine, endCol); 1736 | size_t delete_bytes = end_byte - start_byte; 1737 | 1738 | if (startLine == endLine) 1739 | { 1740 | std::string line = buffer.getLine(startLine); 1741 | line.erase(startCol, endCol - startCol); 1742 | buffer.replaceLine(startLine, line); 1743 | } 1744 | else 1745 | { 1746 | std::string firstLine = buffer.getLine(startLine); 1747 | std::string lastLine = buffer.getLine(endLine); 1748 | 1749 | std::string newLine = 1750 | firstLine.substr(0, startCol) + lastLine.substr(endCol); 1751 | buffer.replaceLine(startLine, newLine); 1752 | 1753 | for (int i = endLine; i > startLine; i--) 1754 | { 1755 | buffer.deleteLine(i); 1756 | } 1757 | } 1758 | 1759 | if (syntaxHighlighter && !isUndoRedoing) 1760 | { 1761 | syntaxHighlighter->updateTreeAfterEdit(buffer, start_byte, delete_bytes, 1762 | 0, startLine, startCol, endLine, 1763 | endCol, startLine, startCol); 1764 | syntaxHighlighter->invalidateLineRange(startLine, 1765 | buffer.getLineCount() - 1); 1766 | } 1767 | 1768 | updateCursorAndViewport(startLine, startCol); 1769 | clearSelection(); 1770 | markModified(); 1771 | } 1772 | } 1773 | 1774 | void Editor::undo() 1775 | { 1776 | if (useDeltaUndo_) 1777 | { 1778 | // Commit any pending delta group first 1779 | if (!currentDeltaGroup_.isEmpty()) 1780 | { 1781 | commitDeltaGroup(); 1782 | } 1783 | 1784 | if (deltaUndoStack_.empty()) 1785 | { 1786 | return; 1787 | } 1788 | 1789 | #ifdef DEBUG_DELTA_UNDO 1790 | std::cerr << "\n=== UNDO START ===\n"; 1791 | EditorSnapshot beforeUndo = captureSnapshot(); 1792 | #endif 1793 | 1794 | // Get the delta group to undo 1795 | DeltaGroup group = deltaUndoStack_.top(); 1796 | deltaUndoStack_.pop(); 1797 | 1798 | #ifdef DEBUG_DELTA_UNDO 1799 | std::cerr << "Undoing group:\n" << group.toString() << "\n"; 1800 | #endif 1801 | 1802 | // Track affected line range for incremental highlighting 1803 | int minAffectedLine = buffer.getLineCount(); 1804 | int maxAffectedLine = 0; 1805 | 1806 | // Apply deltas in REVERSE order 1807 | for (auto it = group.deltas.rbegin(); it != group.deltas.rend(); ++it) 1808 | { 1809 | // Track which lines are affected 1810 | minAffectedLine = 1811 | std::min(minAffectedLine, std::min(it->startLine, it->preCursorLine)); 1812 | maxAffectedLine = 1813 | std::max(maxAffectedLine, std::max(it->endLine, it->postCursorLine)); 1814 | 1815 | applyDeltaReverse(*it); 1816 | 1817 | #ifdef DEBUG_DELTA_UNDO 1818 | ValidationResult valid = validateState("After undo delta"); 1819 | if (!valid) 1820 | { 1821 | std::cerr << "CRITICAL: Validation failed during undo!\n"; 1822 | std::cerr << valid.error << "\n"; 1823 | } 1824 | #endif 1825 | } 1826 | 1827 | // Save to redo stack 1828 | deltaRedoStack_.push(group); 1829 | 1830 | // FIXED: Incremental syntax update instead of full reparse 1831 | if (syntaxHighlighter) 1832 | { 1833 | // Only invalidate affected line range, not entire cache 1834 | syntaxHighlighter->invalidateLineRange(minAffectedLine, 1835 | buffer.getLineCount() - 1); 1836 | 1837 | // Use viewport-only parsing for immediate visual update 1838 | syntaxHighlighter->parseViewportOnly(buffer, viewportTop); 1839 | 1840 | // Schedule background full reparse (non-blocking) 1841 | syntaxHighlighter->scheduleBackgroundParse(buffer); 1842 | } 1843 | 1844 | isModified = true; 1845 | 1846 | #ifdef DEBUG_DELTA_UNDO 1847 | EditorSnapshot afterUndo = captureSnapshot(); 1848 | std::cerr << "Affected lines: " << minAffectedLine << " to " 1849 | << maxAffectedLine << "\n"; 1850 | std::cerr << "=== UNDO END ===\n\n"; 1851 | #endif 1852 | } 1853 | else 1854 | { 1855 | // OLD: Full-state undo (fallback) 1856 | if (undoStack.empty()) 1857 | return; 1858 | 1859 | isUndoRedoing = true; 1860 | redoStack.push(getCurrentState()); 1861 | EditorState state = undoStack.top(); 1862 | undoStack.pop(); 1863 | restoreState(state); 1864 | 1865 | if (syntaxHighlighter) 1866 | { 1867 | syntaxHighlighter->bufferChanged(buffer); 1868 | } 1869 | 1870 | isModified = true; 1871 | isUndoRedoing = false; 1872 | } 1873 | } 1874 | 1875 | void Editor::redo() 1876 | { 1877 | if (useDeltaUndo_) 1878 | { 1879 | if (deltaRedoStack_.empty()) 1880 | { 1881 | return; 1882 | } 1883 | 1884 | #ifdef DEBUG_DELTA_UNDO 1885 | std::cerr << "\n=== REDO START ===\n"; 1886 | EditorSnapshot beforeRedo = captureSnapshot(); 1887 | #endif 1888 | 1889 | // Get the delta group to redo 1890 | DeltaGroup group = deltaRedoStack_.top(); 1891 | deltaRedoStack_.pop(); 1892 | 1893 | #ifdef DEBUG_DELTA_UNDO 1894 | std::cerr << "Redoing group:\n" << group.toString() << "\n"; 1895 | #endif 1896 | 1897 | // Track affected line range 1898 | int minAffectedLine = buffer.getLineCount(); 1899 | int maxAffectedLine = 0; 1900 | 1901 | // Apply deltas in FORWARD order 1902 | for (const auto &delta : group.deltas) 1903 | { 1904 | minAffectedLine = std::min( 1905 | minAffectedLine, std::min(delta.startLine, delta.preCursorLine)); 1906 | maxAffectedLine = std::max(maxAffectedLine, 1907 | std::max(delta.endLine, delta.postCursorLine)); 1908 | 1909 | applyDeltaForward(delta); 1910 | 1911 | #ifdef DEBUG_DELTA_UNDO 1912 | ValidationResult valid = validateState("After redo delta"); 1913 | if (!valid) 1914 | { 1915 | std::cerr << "CRITICAL: Validation failed during redo!\n"; 1916 | std::cerr << valid.error << "\n"; 1917 | } 1918 | #endif 1919 | } 1920 | 1921 | // Save to undo stack 1922 | deltaUndoStack_.push(group); 1923 | 1924 | // FIXED: Incremental syntax update 1925 | if (syntaxHighlighter) 1926 | { 1927 | syntaxHighlighter->invalidateLineRange(minAffectedLine, 1928 | buffer.getLineCount() - 1); 1929 | syntaxHighlighter->parseViewportOnly(buffer, viewportTop); 1930 | syntaxHighlighter->scheduleBackgroundParse(buffer); 1931 | } 1932 | 1933 | isModified = true; 1934 | 1935 | #ifdef DEBUG_DELTA_UNDO 1936 | EditorSnapshot afterRedo = captureSnapshot(); 1937 | std::cerr << "Affected lines: " << minAffectedLine << " to " 1938 | << maxAffectedLine << "\n"; 1939 | std::cerr << "=== REDO END ===\n\n"; 1940 | #endif 1941 | } 1942 | else 1943 | { 1944 | // OLD: Full-state redo (fallback) 1945 | if (redoStack.empty()) 1946 | return; 1947 | 1948 | isUndoRedoing = true; 1949 | undoStack.push(getCurrentState()); 1950 | EditorState state = redoStack.top(); 1951 | redoStack.pop(); 1952 | restoreState(state); 1953 | 1954 | if (syntaxHighlighter) 1955 | { 1956 | syntaxHighlighter->bufferChanged(buffer); 1957 | } 1958 | 1959 | isModified = true; 1960 | isUndoRedoing = false; 1961 | } 1962 | } 1963 | 1964 | EditorState Editor::getCurrentState() 1965 | { 1966 | EditorState state; 1967 | 1968 | // Your existing serialization code 1969 | std::ostringstream oss; 1970 | for (int i = 0; i < buffer.getLineCount(); i++) 1971 | { 1972 | oss << buffer.getLine(i); 1973 | if (i < buffer.getLineCount() - 1) 1974 | { 1975 | oss << "\n"; 1976 | } 1977 | } 1978 | state.content = oss.str(); 1979 | 1980 | // Save cursor/viewport state regardless 1981 | state.cursorLine = cursorLine; 1982 | state.cursorCol = cursorCol; 1983 | state.viewportTop = viewportTop; 1984 | state.viewportLeft = viewportLeft; 1985 | 1986 | return state; 1987 | } 1988 | 1989 | void Editor::restoreState(const EditorState &state) 1990 | { 1991 | // Clear buffer and reload content 1992 | buffer.clear(); 1993 | 1994 | std::istringstream iss(state.content); 1995 | std::string line; 1996 | int lineNum = 0; 1997 | 1998 | while (std::getline(iss, line)) 1999 | { 2000 | buffer.insertLine(lineNum++, line); 2001 | } 2002 | 2003 | // If no lines were added, add empty line 2004 | if (buffer.getLineCount() == 0) 2005 | { 2006 | buffer.insertLine(0, ""); 2007 | } 2008 | 2009 | // Restore cursor and viewport 2010 | cursorLine = state.cursorLine; 2011 | cursorCol = state.cursorCol; 2012 | viewportTop = state.viewportTop; 2013 | viewportLeft = state.viewportLeft; 2014 | 2015 | validateCursorAndViewport(); 2016 | } 2017 | 2018 | void Editor::limitUndoStack() 2019 | { 2020 | while (undoStack.size() > MAX_UNDO_LEVELS) 2021 | { 2022 | // Remove oldest state (bottom of stack) 2023 | std::stack<EditorState> temp; 2024 | bool first = true; 2025 | 2026 | while (!undoStack.empty()) 2027 | { 2028 | if (first) 2029 | { 2030 | first = false; 2031 | undoStack.pop(); // Skip the oldest 2032 | } 2033 | else 2034 | { 2035 | temp.push(undoStack.top()); 2036 | undoStack.pop(); 2037 | } 2038 | } 2039 | 2040 | // Restore stack in correct order 2041 | while (!temp.empty()) 2042 | { 2043 | undoStack.push(temp.top()); 2044 | temp.pop(); 2045 | } 2046 | } 2047 | } 2048 | 2049 | // ================================================================= 2050 | // Internal Helpers 2051 | // ================================================================= 2052 | 2053 | void Editor::markModified() { isModified = true; } 2054 | 2055 | void Editor::splitLineAtCursor() 2056 | { 2057 | std::string line = buffer.getLine(cursorLine); 2058 | std::string leftPart = line.substr(0, cursorCol); 2059 | std::string rightPart = line.substr(cursorCol); 2060 | 2061 | buffer.replaceLine(cursorLine, leftPart); 2062 | buffer.insertLine(cursorLine + 1, rightPart); 2063 | } 2064 | 2065 | void Editor::joinLineWithNext() 2066 | { 2067 | if (cursorLine < buffer.getLineCount() - 1) 2068 | { 2069 | std::string currentLine = buffer.getLine(cursorLine); 2070 | std::string nextLine = buffer.getLine(cursorLine + 1); 2071 | 2072 | buffer.replaceLine(cursorLine, currentLine + nextLine); 2073 | buffer.deleteLine(cursorLine + 1); 2074 | } 2075 | } 2076 | 2077 | std::pair<std::pair<int, int>, std::pair<int, int>> 2078 | Editor::getNormalizedSelection() 2079 | { 2080 | int startLine = selectionStartLine; 2081 | int startCol = selectionStartCol; 2082 | int endLine = selectionEndLine; 2083 | int endCol = selectionEndCol; 2084 | 2085 | // Always normalize so start < end 2086 | if (startLine > endLine || (startLine == endLine && startCol > endCol)) 2087 | { 2088 | std::swap(startLine, endLine); 2089 | std::swap(startCol, endCol); 2090 | } 2091 | 2092 | return {{startLine, startCol}, {endLine, endCol}}; 2093 | } 2094 | std::string Editor::getSelectedText() 2095 | { 2096 | if (!hasSelection && !isSelecting) 2097 | return ""; 2098 | 2099 | auto [start, end] = getNormalizedSelection(); 2100 | int startLine = start.first, startCol = start.second; 2101 | int endLine = end.first, endCol = end.second; 2102 | 2103 | std::ostringstream result; 2104 | 2105 | if (startLine == endLine) 2106 | { 2107 | // Single line selection 2108 | std::string line = buffer.getLine(startLine); 2109 | result << line.substr(startCol, endCol - startCol); 2110 | } 2111 | else 2112 | { 2113 | // Multi-line selection 2114 | for (int i = startLine; i <= endLine; i++) 2115 | { 2116 | std::string line = buffer.getLine(i); 2117 | 2118 | if (i == startLine) 2119 | { 2120 | result << line.substr(startCol); 2121 | } 2122 | else if (i == endLine) 2123 | { 2124 | result << line.substr(0, endCol); 2125 | } 2126 | else 2127 | { 2128 | result << line; 2129 | } 2130 | 2131 | if (i < endLine) 2132 | { 2133 | result << "\n"; 2134 | } 2135 | } 2136 | } 2137 | 2138 | // CRITICAL FIX: Actually return the result! 2139 | return result.str(); 2140 | } 2141 | 2142 | // Selection management 2143 | void Editor::startSelectionIfNeeded() 2144 | { 2145 | if (!hasSelection && !isSelecting) 2146 | { 2147 | isSelecting = true; 2148 | selectionStartLine = cursorLine; 2149 | selectionStartCol = cursorCol; 2150 | selectionEndLine = cursorLine; 2151 | selectionEndCol = cursorCol; 2152 | } 2153 | } 2154 | 2155 | void Editor::updateSelectionEnd() 2156 | { 2157 | if (isSelecting || hasSelection) 2158 | { 2159 | selectionEndLine = cursorLine; 2160 | selectionEndCol = cursorCol; 2161 | hasSelection = true; 2162 | } 2163 | } 2164 | 2165 | // Clipboard operations 2166 | void Editor::copySelection() 2167 | { 2168 | if (!hasSelection && !isSelecting) 2169 | return; 2170 | 2171 | clipboard = getSelectedText(); 2172 | 2173 | // On Unix, also copy to system clipboard using xclip or xsel 2174 | #ifndef _WIN32 2175 | FILE *pipe = popen("xclip -selection clipboard 2>/dev/null || xsel " 2176 | "--clipboard --input 2>/dev/null", 2177 | "w"); 2178 | if (pipe) 2179 | { 2180 | fwrite(clipboard.c_str(), 1, clipboard.length(), pipe); 2181 | pclose(pipe); 2182 | } 2183 | #endif 2184 | } 2185 | 2186 | void Editor::cutSelection() 2187 | { 2188 | if (!hasSelection && !isSelecting) 2189 | return; 2190 | 2191 | copySelection(); 2192 | deleteSelection(); 2193 | } 2194 | 2195 | void Editor::pasteFromClipboard() 2196 | { 2197 | // Try to get from system clipboard first 2198 | #ifndef _WIN32 2199 | FILE *pipe = popen("xclip -selection clipboard -o 2>/dev/null || xsel " 2200 | "--clipboard --output 2>/dev/null", 2201 | "r"); 2202 | if (pipe) 2203 | { 2204 | char buffer_chars[4096]; 2205 | std::string result; 2206 | while (fgets(buffer_chars, sizeof(buffer_chars), pipe)) 2207 | { 2208 | result += buffer_chars; 2209 | } 2210 | pclose(pipe); 2211 | 2212 | if (!result.empty()) 2213 | { 2214 | clipboard = result; 2215 | } 2216 | } 2217 | #endif 2218 | 2219 | if (clipboard.empty()) 2220 | return; 2221 | 2222 | // SAVE STATE BEFORE PASTE (single undo point for entire paste) 2223 | if (!isUndoRedoing) 2224 | { 2225 | saveState(); 2226 | } 2227 | 2228 | // Delete selection if any 2229 | if (hasSelection || isSelecting) 2230 | { 2231 | deleteSelection(); 2232 | } 2233 | 2234 | // Insert clipboard content character by character 2235 | // Note: Each insertChar/insertNewline will NOT call saveState 2236 | // because we already saved it above 2237 | for (char ch : clipboard) 2238 | { 2239 | if (ch == '\n') 2240 | { 2241 | insertNewline(); 2242 | } 2243 | else 2244 | { 2245 | insertChar(ch); 2246 | } 2247 | } 2248 | } 2249 | 2250 | void Editor::selectAll() 2251 | { 2252 | if (buffer.getLineCount() == 0) 2253 | return; 2254 | 2255 | selectionStartLine = 0; 2256 | selectionStartCol = 0; 2257 | 2258 | selectionEndLine = buffer.getLineCount() - 1; 2259 | std::string lastLine = buffer.getLine(selectionEndLine); 2260 | selectionEndCol = static_cast<int>(lastLine.length()); 2261 | 2262 | hasSelection = true; 2263 | isSelecting = false; 2264 | } 2265 | 2266 | void Editor::initializeViewportHighlighting() 2267 | { 2268 | if (syntaxHighlighter) 2269 | { 2270 | // Pre-parse viewport so first display() is instant 2271 | syntaxHighlighter->parseViewportOnly(buffer, viewportTop); 2272 | } 2273 | } 2274 | 2275 | // Cursor 2276 | void Editor::setCursorMode() 2277 | { 2278 | switch (currentMode) 2279 | { 2280 | case CursorMode::NORMAL: 2281 | // Block cursor (solid block) 2282 | printf("\033[2 q"); 2283 | fflush(stdout); 2284 | break; 2285 | case CursorMode::INSERT: 2286 | // Vertical bar cursor (thin lifne like VSCode/modern editors) 2287 | printf("\033[6 q"); 2288 | fflush(stdout); 2289 | break; 2290 | case CursorMode::VISUAL: 2291 | // Underline cursor for visual mode 2292 | printf("\033[4 q"); 2293 | fflush(stdout); 2294 | break; 2295 | default: 2296 | printf("\033[6 q"); 2297 | fflush(stdout); 2298 | break; 2299 | } 2300 | } 2301 | 2302 | // === Delta Group Management === 2303 | 2304 | void Editor::beginDeltaGroup() 2305 | { 2306 | currentDeltaGroup_ = DeltaGroup(); 2307 | currentDeltaGroup_.initialLineCount = buffer.getLineCount(); 2308 | currentDeltaGroup_.initialBufferSize = buffer.size(); 2309 | currentDeltaGroup_.timestamp = std::chrono::steady_clock::now(); 2310 | } 2311 | 2312 | void Editor::addDelta(const EditDelta &delta) 2313 | { 2314 | currentDeltaGroup_.addDelta(delta); 2315 | 2316 | // DEBUG: Validate after every delta in debug builds 2317 | #ifdef DEBUG_DELTA_UNDO 2318 | ValidationResult valid = validateState("After adding delta"); 2319 | if (!valid) 2320 | { 2321 | std::cerr << "VALIDATION FAILED after delta:\n"; 2322 | std::cerr << delta.toString() << "\n"; 2323 | std::cerr << "Error: " << valid.error << "\n"; 2324 | std::cerr << "Current state: " << captureSnapshot().toString() << "\n"; 2325 | } 2326 | #endif 2327 | } 2328 | 2329 | void Editor::commitDeltaGroup() 2330 | { 2331 | if (currentDeltaGroup_.isEmpty()) 2332 | { 2333 | return; 2334 | } 2335 | 2336 | // Validate before committing 2337 | ValidationResult valid = validateState("Before committing delta group"); 2338 | if (!valid) 2339 | { 2340 | std::cerr << "WARNING: Invalid state before commit, discarding group\n"; 2341 | std::cerr << valid.error << "\n"; 2342 | currentDeltaGroup_ = DeltaGroup(); 2343 | return; 2344 | } 2345 | 2346 | deltaUndoStack_.push(currentDeltaGroup_); 2347 | 2348 | // Clear redo stack on new edit 2349 | while (!deltaRedoStack_.empty()) 2350 | { 2351 | deltaRedoStack_.pop(); 2352 | } 2353 | 2354 | // Limit stack size 2355 | while (deltaUndoStack_.size() > MAX_UNDO_LEVELS) 2356 | { 2357 | // Remove oldest (bottom of stack) 2358 | std::stack<DeltaGroup> temp; 2359 | bool first = true; 2360 | while (!deltaUndoStack_.empty()) 2361 | { 2362 | if (first) 2363 | { 2364 | first = false; 2365 | deltaUndoStack_.pop(); // Discard oldest 2366 | } 2367 | else 2368 | { 2369 | temp.push(deltaUndoStack_.top()); 2370 | deltaUndoStack_.pop(); 2371 | } 2372 | } 2373 | while (!temp.empty()) 2374 | { 2375 | deltaUndoStack_.push(temp.top()); 2376 | temp.pop(); 2377 | } 2378 | } 2379 | 2380 | currentDeltaGroup_ = DeltaGroup(); 2381 | } 2382 | 2383 | // === Delta Creation for Each Operation === 2384 | 2385 | EditDelta Editor::createDeltaForInsertChar(char ch) 2386 | { 2387 | EditDelta delta; 2388 | delta.operation = EditDelta::INSERT_CHAR; 2389 | 2390 | // Capture state BEFORE edit 2391 | delta.preCursorLine = cursorLine; 2392 | delta.preCursorCol = cursorCol; 2393 | delta.preViewportTop = viewportTop; 2394 | delta.preViewportLeft = viewportLeft; 2395 | 2396 | delta.startLine = cursorLine; 2397 | delta.startCol = cursorCol; 2398 | delta.endLine = cursorLine; 2399 | delta.endCol = cursorCol; 2400 | 2401 | // Content: what we're inserting 2402 | delta.insertedContent = std::string(1, ch); 2403 | delta.deletedContent = ""; // Nothing deleted 2404 | 2405 | // No structural change 2406 | delta.lineCountDelta = 0; 2407 | 2408 | // Post-state will be filled after edit completes 2409 | return delta; 2410 | } 2411 | 2412 | EditDelta Editor::createDeltaForDeleteChar() 2413 | { 2414 | EditDelta delta; 2415 | delta.operation = EditDelta::DELETE_CHAR; 2416 | 2417 | // Capture state BEFORE deletion 2418 | delta.preCursorLine = cursorLine; 2419 | delta.preCursorCol = cursorCol; 2420 | delta.preViewportTop = viewportTop; 2421 | delta.preViewportLeft = viewportLeft; 2422 | 2423 | delta.startLine = cursorLine; 2424 | delta.startCol = cursorCol; 2425 | 2426 | // Capture what we're about to delete 2427 | std::string line = buffer.getLine(cursorLine); 2428 | 2429 | if (cursorCol < static_cast<int>(line.length())) 2430 | { 2431 | // Deleting a character on current line 2432 | delta.deletedContent = std::string(1, line[cursorCol]); 2433 | delta.endLine = cursorLine; 2434 | delta.endCol = cursorCol + 1; 2435 | delta.lineCountDelta = 0; 2436 | } 2437 | else if (cursorLine < buffer.getLineCount() - 1) 2438 | { 2439 | // Deleting newline - will join lines 2440 | delta.operation = EditDelta::JOIN_LINES; 2441 | delta.deletedContent = "\n"; 2442 | delta.endLine = cursorLine + 1; 2443 | delta.endCol = 0; 2444 | delta.lineCountDelta = -1; 2445 | 2446 | // Save line contents for reversal 2447 | delta.firstLineBeforeJoin = line; 2448 | delta.secondLineBeforeJoin = buffer.getLine(cursorLine + 1); 2449 | } 2450 | 2451 | delta.insertedContent = ""; // Nothing inserted 2452 | 2453 | return delta; 2454 | } 2455 | 2456 | EditDelta Editor::createDeltaForBackspace() 2457 | { 2458 | EditDelta delta; 2459 | delta.operation = EditDelta::DELETE_CHAR; 2460 | 2461 | // Capture state BEFORE deletion 2462 | delta.preCursorLine = cursorLine; 2463 | delta.preCursorCol = cursorCol; 2464 | delta.preViewportTop = viewportTop; 2465 | delta.preViewportLeft = viewportLeft; 2466 | 2467 | if (cursorCol > 0) 2468 | { 2469 | // Deleting character before cursor on same line 2470 | std::string line = buffer.getLine(cursorLine); 2471 | delta.deletedContent = std::string(1, line[cursorCol - 1]); 2472 | 2473 | delta.startLine = cursorLine; 2474 | delta.startCol = cursorCol - 1; 2475 | delta.endLine = cursorLine; 2476 | delta.endCol = cursorCol; 2477 | delta.lineCountDelta = 0; 2478 | } 2479 | else if (cursorLine > 0) 2480 | { 2481 | // Backspace at line start - join with previous line 2482 | delta.operation = EditDelta::JOIN_LINES; 2483 | delta.deletedContent = "\n"; 2484 | 2485 | std::string prevLine = buffer.getLine(cursorLine - 1); 2486 | std::string currLine = buffer.getLine(cursorLine); 2487 | 2488 | delta.startLine = cursorLine - 1; 2489 | delta.startCol = prevLine.length(); 2490 | delta.endLine = cursorLine; 2491 | delta.endCol = 0; 2492 | delta.lineCountDelta = -1; 2493 | 2494 | // Save line contents for reversal 2495 | delta.firstLineBeforeJoin = prevLine; 2496 | delta.secondLineBeforeJoin = currLine; 2497 | } 2498 | 2499 | delta.insertedContent = ""; // Nothing inserted 2500 | 2501 | return delta; 2502 | } 2503 | 2504 | EditDelta Editor::createDeltaForNewline() 2505 | { 2506 | EditDelta delta; 2507 | delta.operation = EditDelta::SPLIT_LINE; 2508 | 2509 | // Capture state BEFORE split 2510 | delta.preCursorLine = cursorLine; 2511 | delta.preCursorCol = cursorCol; 2512 | delta.preViewportTop = viewportTop; 2513 | delta.preViewportLeft = viewportLeft; 2514 | 2515 | delta.startLine = cursorLine; 2516 | delta.startCol = cursorCol; 2517 | delta.endLine = cursorLine + 1; // New line will be created 2518 | delta.endCol = 0; 2519 | 2520 | // Save the line content before split 2521 | delta.lineBeforeSplit = buffer.getLine(cursorLine); 2522 | 2523 | delta.insertedContent = "\n"; 2524 | delta.deletedContent = ""; 2525 | delta.lineCountDelta = 1; // One new line 2526 | 2527 | return delta; 2528 | } 2529 | 2530 | EditDelta Editor::createDeltaForDeleteSelection() 2531 | { 2532 | EditDelta delta; 2533 | delta.operation = EditDelta::DELETE_TEXT; 2534 | 2535 | // Capture state 2536 | delta.preCursorLine = cursorLine; 2537 | delta.preCursorCol = cursorCol; 2538 | delta.preViewportTop = viewportTop; 2539 | delta.preViewportLeft = viewportLeft; 2540 | 2541 | auto [start, end] = getNormalizedSelection(); 2542 | delta.startLine = start.first; 2543 | delta.startCol = start.second; 2544 | delta.endLine = end.first; 2545 | delta.endCol = end.second; 2546 | 2547 | // Capture the deleted text 2548 | delta.deletedContent = getSelectedText(); 2549 | delta.insertedContent = ""; 2550 | 2551 | // Calculate line count change 2552 | delta.lineCountDelta = -(end.first - start.first); 2553 | 2554 | return delta; 2555 | } 2556 | 2557 | // === Memory Usage Stats === 2558 | 2559 | size_t Editor::getUndoMemoryUsage() const 2560 | { 2561 | size_t total = 0; 2562 | 2563 | if (useDeltaUndo_) 2564 | { 2565 | // Count delta stack 2566 | std::stack<DeltaGroup> temp = deltaUndoStack_; 2567 | while (!temp.empty()) 2568 | { 2569 | total += temp.top().getMemorySize(); 2570 | temp.pop(); 2571 | } 2572 | } 2573 | else 2574 | { 2575 | // Count old state stack (approximate) 2576 | total = undoStack.size() * sizeof(EditorState); 2577 | std::stack<EditorState> temp = undoStack; 2578 | while (!temp.empty()) 2579 | { 2580 | total += temp.top().content.capacity(); 2581 | temp.pop(); 2582 | } 2583 | } 2584 | 2585 | return total; 2586 | } 2587 | 2588 | size_t Editor::getRedoMemoryUsage() const 2589 | { 2590 | size_t total = 0; 2591 | 2592 | if (useDeltaUndo_) 2593 | { 2594 | std::stack<DeltaGroup> temp = deltaRedoStack_; 2595 | while (!temp.empty()) 2596 | { 2597 | total += temp.top().getMemorySize(); 2598 | temp.pop(); 2599 | } 2600 | } 2601 | else 2602 | { 2603 | total = redoStack.size() * sizeof(EditorState); 2604 | std::stack<EditorState> temp = redoStack; 2605 | while (!temp.empty()) 2606 | { 2607 | total += temp.top().content.capacity(); 2608 | temp.pop(); 2609 | } 2610 | } 2611 | 2612 | return total; 2613 | } 2614 | 2615 | void Editor::applyDeltaForward(const EditDelta &delta) 2616 | { 2617 | isUndoRedoing = true; 2618 | 2619 | #ifdef DEBUG_DELTA_UNDO 2620 | std::cerr << "Applying delta forward: " << delta.toString() << "\n"; 2621 | #endif 2622 | 2623 | // Restore cursor to PRE-edit position 2624 | cursorLine = delta.preCursorLine; 2625 | cursorCol = delta.preCursorCol; 2626 | viewportTop = delta.preViewportTop; 2627 | viewportLeft = delta.preViewportLeft; 2628 | 2629 | validateCursorAndViewport(); 2630 | 2631 | // Notify Tree-sitter BEFORE applying changes 2632 | // notifyTreeSitterEdit(delta, false); // false = forward (redo) 2633 | 2634 | switch (delta.operation) 2635 | { 2636 | case EditDelta::INSERT_CHAR: 2637 | case EditDelta::INSERT_TEXT: 2638 | { 2639 | // Re-insert the text 2640 | std::string line = buffer.getLine(cursorLine); 2641 | line.insert(cursorCol, delta.insertedContent); 2642 | buffer.replaceLine(cursorLine, line); 2643 | cursorCol += delta.insertedContent.length(); 2644 | break; 2645 | } 2646 | 2647 | case EditDelta::DELETE_CHAR: 2648 | case EditDelta::DELETE_TEXT: 2649 | { 2650 | // Re-delete the text 2651 | if (delta.startLine == delta.endLine) 2652 | { 2653 | std::string line = buffer.getLine(delta.startLine); 2654 | line.erase(delta.startCol, delta.deletedContent.length()); 2655 | buffer.replaceLine(delta.startLine, line); 2656 | } 2657 | else 2658 | { 2659 | // Multi-line deletion 2660 | std::string firstLine = buffer.getLine(delta.startLine); 2661 | std::string lastLine = buffer.getLine(delta.endLine); 2662 | std::string newLine = 2663 | firstLine.substr(0, delta.startCol) + lastLine.substr(delta.endCol); 2664 | buffer.replaceLine(delta.startLine, newLine); 2665 | 2666 | for (int i = delta.endLine; i > delta.startLine; i--) 2667 | { 2668 | buffer.deleteLine(i); 2669 | } 2670 | } 2671 | break; 2672 | } 2673 | 2674 | case EditDelta::SPLIT_LINE: 2675 | { 2676 | // Re-split the line 2677 | std::string line = buffer.getLine(cursorLine); 2678 | std::string leftPart = line.substr(0, cursorCol); 2679 | std::string rightPart = line.substr(cursorCol); 2680 | 2681 | buffer.replaceLine(cursorLine, leftPart); 2682 | buffer.insertLine(cursorLine + 1, rightPart); 2683 | 2684 | cursorLine++; 2685 | cursorCol = 0; 2686 | break; 2687 | } 2688 | 2689 | case EditDelta::JOIN_LINES: 2690 | { 2691 | // Re-join the lines 2692 | if (delta.startLine + 1 < buffer.getLineCount()) 2693 | { 2694 | std::string firstLine = buffer.getLine(delta.startLine); 2695 | std::string secondLine = buffer.getLine(delta.startLine + 1); 2696 | buffer.replaceLine(delta.startLine, firstLine + secondLine); 2697 | buffer.deleteLine(delta.startLine + 1); 2698 | } 2699 | break; 2700 | } 2701 | 2702 | case EditDelta::REPLACE_LINE: 2703 | { 2704 | if (!delta.insertedContent.empty()) 2705 | { 2706 | buffer.replaceLine(delta.startLine, delta.insertedContent); 2707 | } 2708 | break; 2709 | } 2710 | } 2711 | 2712 | // Restore POST-edit cursor position 2713 | cursorLine = delta.postCursorLine; 2714 | cursorCol = delta.postCursorCol; 2715 | viewportTop = delta.postViewportTop; 2716 | viewportLeft = delta.postViewportLeft; 2717 | 2718 | validateCursorAndViewport(); 2719 | buffer.invalidateLineIndex(); 2720 | 2721 | // Invalidate only affected lines (not entire cache) 2722 | if (syntaxHighlighter) 2723 | { 2724 | int startLine = std::min(delta.startLine, delta.preCursorLine); 2725 | int endLine = std::max(delta.endLine, delta.postCursorLine); 2726 | syntaxHighlighter->invalidateLineRange(startLine, 2727 | buffer.getLineCount() - 1); 2728 | } 2729 | 2730 | isUndoRedoing = false; 2731 | } 2732 | 2733 | // === Apply Delta Reverse (for Undo) === 2734 | 2735 | void Editor::applyDeltaReverse(const EditDelta &delta) 2736 | { 2737 | isUndoRedoing = true; 2738 | 2739 | #ifdef DEBUG_DELTA_UNDO 2740 | std::cerr << "Applying delta reverse: " << delta.toString() << "\n"; 2741 | #endif 2742 | 2743 | // Restore cursor to POST-edit position 2744 | cursorLine = delta.postCursorLine; 2745 | cursorCol = delta.postCursorCol; 2746 | viewportTop = delta.postViewportTop; 2747 | viewportLeft = delta.postViewportLeft; 2748 | 2749 | validateCursorAndViewport(); 2750 | 2751 | switch (delta.operation) 2752 | { 2753 | case EditDelta::INSERT_CHAR: 2754 | case EditDelta::INSERT_TEXT: 2755 | { 2756 | // Reverse of insert is delete 2757 | std::string line = buffer.getLine(delta.startLine); 2758 | if (delta.startCol + delta.insertedContent.length() <= line.length()) 2759 | { 2760 | line.erase(delta.startCol, delta.insertedContent.length()); 2761 | buffer.replaceLine(delta.startLine, line); 2762 | } 2763 | break; 2764 | } 2765 | 2766 | case EditDelta::DELETE_CHAR: 2767 | case EditDelta::DELETE_TEXT: 2768 | { 2769 | // FIXED: Proper multi-line restoration 2770 | if (delta.startLine == delta.endLine) 2771 | { 2772 | // Single line restoration (simple case) 2773 | std::string line = buffer.getLine(delta.startLine); 2774 | line.insert(delta.startCol, delta.deletedContent); 2775 | buffer.replaceLine(delta.startLine, line); 2776 | } 2777 | else 2778 | { 2779 | // Multi-line restoration (the bug was here!) 2780 | 2781 | // Step 1: Get the current line at startLine 2782 | std::string currentLine = buffer.getLine(delta.startLine); 2783 | 2784 | // Step 2: Split current line at insertion point 2785 | std::string beforeInsert = currentLine.substr(0, delta.startCol); 2786 | std::string afterInsert = currentLine.substr(delta.startCol); 2787 | 2788 | // Step 3: Split deletedContent by ACTUAL newlines, preserving them 2789 | std::vector<std::string> linesToRestore; 2790 | size_t pos = 0; 2791 | size_t nextNewline; 2792 | 2793 | while ((nextNewline = delta.deletedContent.find('\n', pos)) != 2794 | std::string::npos) 2795 | { 2796 | // Include everything up to (but not including) the newline 2797 | linesToRestore.push_back( 2798 | delta.deletedContent.substr(pos, nextNewline - pos)); 2799 | pos = nextNewline + 1; 2800 | } 2801 | 2802 | // Add remaining content (after last newline) 2803 | if (pos < delta.deletedContent.length()) 2804 | { 2805 | linesToRestore.push_back(delta.deletedContent.substr(pos)); 2806 | } 2807 | 2808 | // Step 4: Reconstruct lines correctly 2809 | if (!linesToRestore.empty()) 2810 | { 2811 | // First line: beforeInsert + first restored line 2812 | buffer.replaceLine(delta.startLine, beforeInsert + linesToRestore[0]); 2813 | 2814 | // Insert middle lines (if any) 2815 | for (size_t i = 1; i < linesToRestore.size(); ++i) 2816 | { 2817 | buffer.insertLine(delta.startLine + i, linesToRestore[i]); 2818 | } 2819 | 2820 | // Handle the content after insertion point 2821 | if (linesToRestore.size() == 1) 2822 | { 2823 | // Single line case: append afterInsert to same line 2824 | std::string finalLine = buffer.getLine(delta.startLine); 2825 | buffer.replaceLine(delta.startLine, finalLine + afterInsert); 2826 | } 2827 | else 2828 | { 2829 | // Multi-line case: append afterInsert to last restored line 2830 | int lastLineIdx = delta.startLine + linesToRestore.size() - 1; 2831 | std::string lastLine = buffer.getLine(lastLineIdx); 2832 | buffer.replaceLine(lastLineIdx, lastLine + afterInsert); 2833 | } 2834 | } 2835 | else 2836 | { 2837 | // Edge case: deletedContent was empty (shouldn't happen, but be safe) 2838 | std::cerr << "WARNING: Empty deletedContent in multi-line restore\n"; 2839 | } 2840 | } 2841 | break; 2842 | } 2843 | 2844 | case EditDelta::SPLIT_LINE: 2845 | { 2846 | // Reverse of split is join 2847 | if (!delta.lineBeforeSplit.empty()) 2848 | { 2849 | if (delta.startLine + 1 < buffer.getLineCount()) 2850 | { 2851 | buffer.replaceLine(delta.startLine, delta.lineBeforeSplit); 2852 | buffer.deleteLine(delta.startLine + 1); 2853 | } 2854 | } 2855 | break; 2856 | } 2857 | 2858 | case EditDelta::JOIN_LINES: 2859 | { 2860 | // Reverse of join is split 2861 | if (!delta.firstLineBeforeJoin.empty() && 2862 | !delta.secondLineBeforeJoin.empty()) 2863 | { 2864 | buffer.replaceLine(delta.startLine, delta.firstLineBeforeJoin); 2865 | buffer.insertLine(delta.startLine + 1, delta.secondLineBeforeJoin); 2866 | } 2867 | break; 2868 | } 2869 | 2870 | case EditDelta::REPLACE_LINE: 2871 | { 2872 | // Reverse of replace is restore original 2873 | if (!delta.deletedContent.empty()) 2874 | { 2875 | buffer.replaceLine(delta.startLine, delta.deletedContent); 2876 | } 2877 | break; 2878 | } 2879 | } 2880 | 2881 | // Restore PRE-edit cursor position 2882 | cursorLine = delta.preCursorLine; 2883 | cursorCol = delta.preCursorCol; 2884 | viewportTop = delta.preViewportTop; 2885 | viewportLeft = delta.preViewportLeft; 2886 | 2887 | validateCursorAndViewport(); 2888 | buffer.invalidateLineIndex(); 2889 | 2890 | isUndoRedoing = false; 2891 | } 2892 | 2893 | // Fallback 2894 | void Editor::saveState() 2895 | { 2896 | if (isSaving || isUndoRedoing) 2897 | return; 2898 | 2899 | auto now = std::chrono::steady_clock::now(); 2900 | auto elapsed = 2901 | std::chrono::duration_cast<std::chrono::milliseconds>(now - lastEditTime) 2902 | .count(); 2903 | 2904 | // Only save if enough time has passed since last edit 2905 | if (elapsed > UNDO_GROUP_TIMEOUT_MS || undoStack.empty()) 2906 | { 2907 | EditorState state = getCurrentState(); 2908 | undoStack.push(state); 2909 | limitUndoStack(); 2910 | 2911 | while (!redoStack.empty()) 2912 | { 2913 | redoStack.pop(); 2914 | } 2915 | } 2916 | 2917 | lastEditTime = now; 2918 | } 2919 | 2920 | void Editor::optimizedLineInvalidation(int startLine, int endLine) 2921 | { 2922 | if (!syntaxHighlighter) 2923 | { 2924 | return; 2925 | } 2926 | 2927 | // Only invalidate if change is significant 2928 | int changeSize = endLine - startLine + 1; 2929 | 2930 | if (changeSize > 100) 2931 | { 2932 | // Large change: full reparse (but async) 2933 | syntaxHighlighter->clearAllCache(); 2934 | syntaxHighlighter->scheduleBackgroundParse(buffer); 2935 | } 2936 | else if (changeSize > 10) 2937 | { 2938 | // Medium change: invalidate range and reparse viewport 2939 | syntaxHighlighter->invalidateLineRange(startLine, 2940 | buffer.getLineCount() - 1); 2941 | syntaxHighlighter->parseViewportOnly(buffer, viewportTop); 2942 | } 2943 | else 2944 | { 2945 | // Small change: just invalidate the affected lines 2946 | syntaxHighlighter->invalidateLineRange(startLine, endLine); 2947 | } 2948 | } 2949 | 2950 | // ============================================================================ 2951 | // FIX 4: Add Tree-sitter Edit Notification for Delta Operations 2952 | // ============================================================================ 2953 | // Add this method to track Tree-sitter edits during delta apply: 2954 | 2955 | void Editor::notifyTreeSitterEdit(const EditDelta &delta, bool isReverse) 2956 | { 2957 | if (!syntaxHighlighter) 2958 | { 2959 | return; 2960 | } 2961 | 2962 | // Calculate byte positions 2963 | size_t start_byte = buffer.lineColToPos(delta.startLine, delta.startCol); 2964 | 2965 | if (isReverse) 2966 | { 2967 | // Undoing: reverse the original operation 2968 | switch (delta.operation) 2969 | { 2970 | case EditDelta::INSERT_CHAR: 2971 | case EditDelta::INSERT_TEXT: 2972 | { 2973 | // Was an insert, now delete 2974 | size_t len = delta.insertedContent.length(); 2975 | syntaxHighlighter->notifyEdit(start_byte, 0, len, // Inserting back 2976 | delta.startLine, delta.startCol, 2977 | delta.startLine, delta.startCol, 2978 | delta.postCursorLine, delta.postCursorCol); 2979 | break; 2980 | } 2981 | 2982 | case EditDelta::DELETE_CHAR: 2983 | case EditDelta::DELETE_TEXT: 2984 | { 2985 | // Was a delete, now insert 2986 | size_t len = delta.deletedContent.length(); 2987 | syntaxHighlighter->notifyEdit(start_byte, len, 0, // Deleting 2988 | delta.startLine, delta.startCol, 2989 | delta.endLine, delta.endCol, 2990 | delta.startLine, delta.startCol); 2991 | break; 2992 | } 2993 | 2994 | case EditDelta::SPLIT_LINE: 2995 | { 2996 | // Was a split, now join 2997 | syntaxHighlighter->notifyEdit(start_byte, 0, 1, // Remove newline 2998 | delta.startLine, delta.startCol, 2999 | delta.startLine, delta.startCol, 3000 | delta.startLine + 1, 0); 3001 | break; 3002 | } 3003 | 3004 | case EditDelta::JOIN_LINES: 3005 | { 3006 | // Was a join, now split 3007 | syntaxHighlighter->notifyEdit(start_byte, 1, 0, // Add newline 3008 | delta.startLine, delta.startCol, 3009 | delta.startLine + 1, 0, delta.startLine, 3010 | delta.startCol); 3011 | break; 3012 | } 3013 | } 3014 | } 3015 | else 3016 | { 3017 | // Redoing: apply the original operation 3018 | switch (delta.operation) 3019 | { 3020 | case EditDelta::INSERT_CHAR: 3021 | case EditDelta::INSERT_TEXT: 3022 | { 3023 | size_t len = delta.insertedContent.length(); 3024 | syntaxHighlighter->notifyEdit(start_byte, len, 0, delta.startLine, 3025 | delta.startCol, delta.postCursorLine, 3026 | delta.postCursorCol, delta.startLine, 3027 | delta.startCol); 3028 | break; 3029 | } 3030 | 3031 | case EditDelta::DELETE_CHAR: 3032 | case EditDelta::DELETE_TEXT: 3033 | { 3034 | size_t len = delta.deletedContent.length(); 3035 | syntaxHighlighter->notifyEdit( 3036 | start_byte, 0, len, delta.startLine, delta.startCol, delta.startLine, 3037 | delta.startCol, delta.endLine, delta.endCol); 3038 | break; 3039 | } 3040 | 3041 | case EditDelta::SPLIT_LINE: 3042 | { 3043 | syntaxHighlighter->notifyEdit(start_byte, 1, 0, delta.startLine, 3044 | delta.startCol, delta.startLine + 1, 0, 3045 | delta.startLine, delta.startCol); 3046 | break; 3047 | } 3048 | 3049 | case EditDelta::JOIN_LINES: 3050 | { 3051 | syntaxHighlighter->notifyEdit(start_byte, 0, 1, delta.startLine, 3052 | delta.startCol, delta.startLine, 3053 | delta.startCol, delta.startLine + 1, 0); 3054 | break; 3055 | } 3056 | } 3057 | } 3058 | } ```