diff --git a/PROGRESS.md b/PROGRESS.md index 764b1fe..f6d948b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -547,4 +547,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step | 2026-02-10 | Codex | Step 197: Keyboard navigation audit (F6 panel cycle, Esc focus return, focus ring defaults, nav focus enabled on panels). 1/1 tests pass (step197_test). | | 2026-02-10 | Codex | Step 198: Virtual scrolling for large files (viewport + buffered rendering, fold hiding optimized). 1/1 tests pass (step198_test). | | 2026-02-10 | Codex | Step 199: Large file handling (size thresholds, text-mode fallback, large file prompt, highlight disable, memory indicator). 1/1 tests pass (step199_test). | +| 2026-02-10 | Codex | Step 200: Startup/perf (LSP & highlight debounce, deferred AST sync on session restore, debug frame budget warnings). 1/1 tests pass (step200_test). | | 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 1e04bf1..c5829b0 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1136,6 +1136,9 @@ target_include_directories(step198_test PRIVATE src) add_executable(step199_test tests/step199_test.cpp) target_include_directories(step199_test PRIVATE src) + +add_executable(step200_test tests/step200_test.cpp) +target_include_directories(step200_test PRIVATE src) find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index b0bf18c..50edc49 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -158,6 +158,9 @@ struct BufferState { size_t fileSizeBytes = 0; bool largeFileMode = false; bool disableSyntaxHighlight = false; + double highlightRequestTime = 0.0; + bool pendingAstSync = false; + double pendingAstStart = 0.0; }; struct EditorState { @@ -205,6 +208,9 @@ struct EditorState { bool completionVisible = false; int completionSelected = 0; double completionLastChange = 0.0; + bool lspChangePending = false; + double lspChangeLast = 0.0; + std::string lspChangePath; bool hoverPending = false; double hoverLastMove = 0.0; ImVec2 hoverLastMouse = ImVec2(-1.0f, -1.0f); @@ -213,6 +219,7 @@ struct EditorState { int definitionRequestLine = 0; int definitionRequestCol = 0; std::string definitionRequestPath; + double lastFrameBudgetWarning = 0.0; LSPClient::DefinitionLocation definitionPreview; bool definitionPreviewValid = false; Pipeline pipeline; @@ -592,7 +599,8 @@ struct EditorState { BufferManager::BufferMode mode = BufferManager::BufferMode::Structured, size_t fileSizeBytes = 0, bool largeFileMode = false, - bool disableSyntaxHighlight = false) { + bool disableSyntaxHighlight = false, + bool deferAstSync = false) { BufferManager::BufferMode effectiveMode = mode; if (language == "org") effectiveMode = BufferManager::BufferMode::Text; if (buffers.hasBuffer(path)) { @@ -611,9 +619,14 @@ struct EditorState { state->generatedMode.setLanguage(language); state->editor.setContent(content, language); if (effectiveMode == BufferManager::BufferMode::Structured) { - state->sync.setText(content, language); - state->sync.syncNow(); - state->incrementalOptimizer.setRoot(state->sync.getAST()); + if (deferAstSync) { + state->pendingAstSync = true; + state->pendingAstStart = ImGui::GetTime(); + } else { + state->sync.setText(content, language); + state->sync.syncNow(); + state->incrementalOptimizer.setRoot(state->sync.getAST()); + } } state->editBuf = content; state->highlightsDirty = true; @@ -924,7 +937,7 @@ struct EditorState { } ui.layoutPreset = LayoutManager::presetFromName(session.layoutPreset); for (const auto& buf : session.buffers) { - doOpen(buf.path, bufferModeFromString(buf.mode)); + doOpen(buf.path, bufferModeFromString(buf.mode), true); auto it = bufferStates.find(buf.path); if (it != bufferStates.end()) { auto* bs = it->second.get(); @@ -960,6 +973,44 @@ struct EditorState { } } + void queueLspDidChange() { + if (!lsp || !active()) return; + if (active()->path.rfind("(untitled", 0) == 0) return; + active()->lspVersion += 1; + lspChangePending = true; + lspChangeLast = ImGui::GetTime(); + lspChangePath = active()->path; + } + + void flushLspDidChange(double nowSeconds) { + if (!lsp || !lspChangePending) return; + double delay = settings.getLspDebounceMs() / 1000.0; + if ((nowSeconds - lspChangeLast) < delay) return; + auto it = bufferStates.find(lspChangePath); + if (it == bufferStates.end()) { + lspChangePending = false; + return; + } + auto* buf = it->second.get(); + lsp->didChange(toFileUri(buf->path), buf->editBuf, buf->lspVersion); + lspChangePending = false; + } + + void flushDeferredAstSync(double nowSeconds) { + (void)nowSeconds; + for (auto& kv : bufferStates) { + auto* buf = kv.second.get(); + if (!buf->pendingAstSync) continue; + buf->sync.setText(buf->editBuf, buf->language); + buf->sync.syncNow(); + buf->incrementalOptimizer.setRoot(buf->sync.getAST()); + buf->pendingAstSync = false; + buf->highlightsDirty = true; + buf->generatedHighlightsDirty = true; + break; + } + } + bool hasSidePanels() const { if (ui.showOutline) return true; if (library.showDependencyPanel) return true; @@ -1791,12 +1842,10 @@ struct EditorState { } active()->orchestratorDirty = false; active()->highlightsDirty = true; + active()->highlightRequestTime = ImGui::GetTime(); active()->generatedHighlightsDirty = true; active()->modified = true; - if (lsp && active()->path.rfind("(untitled", 0) != 0) { - active()->lspVersion += 1; - lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion); - } + queueLspDidChange(); } void registerCommand(const std::string& id, @@ -2176,12 +2225,10 @@ struct EditorState { } active()->orchestratorDirty = true; active()->highlightsDirty = true; + active()->highlightRequestTime = ImGui::GetTime(); active()->generatedHighlightsDirty = true; active()->modified = true; - if (lsp && active()->path.rfind("(untitled", 0) != 0) { - active()->lspVersion += 1; - lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion); - } + queueLspDidChange(); events.publish(UIEventType::FileModified, active()->path, {}, ImGui::GetTime()); events.publishDebounced(UIEventType::ASTChanged, active()->path, @@ -2408,7 +2455,8 @@ struct EditorState { } void doOpen(const std::string& path, - BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Structured) { + BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Structured, + bool deferAstSync = false) { std::error_code sizeErr; size_t fileSizeBytes = 0; if (!path.empty()) { @@ -2455,7 +2503,7 @@ struct EditorState { else if (path.size() > 4 && path.substr(path.size() - 4) == ".org") language = "org"; createBuffer(path, content, language, effectiveMode, - fileSizeBytes, largeFileMode, disableHighlight); + fileSizeBytes, largeFileMode, disableHighlight, deferAstSync); welcome.addRecentFile(path, language, bufferModeToString(effectiveMode)); saveRecentFiles(); if (autoText && modeOverride == BufferManager::BufferMode::Structured) { @@ -2489,6 +2537,7 @@ struct EditorState { buf->incrementalOptimizer.setRoot(buf->sync.getAST()); } buf->highlightsDirty = true; + buf->highlightRequestTime = ImGui::GetTime(); buf->generatedHighlightsDirty = true; buf->modified = false; notify(NotificationLevel::Info, "Reloaded: " + path); @@ -2511,6 +2560,12 @@ struct EditorState { void updateHighlights() { if (!active()) return; if (!active()->highlightsDirty) return; + double now = ImGui::GetTime(); + double delay = settings.getHighlightDebounceMs() / 1000.0; + if (active()->highlightRequestTime > 0.0 && + (now - active()->highlightRequestTime) < delay) { + return; + } if (active()->disableSyntaxHighlight) { active()->highlights.clear(); active()->highlightsDirty = false; @@ -2599,11 +2654,9 @@ struct EditorState { active()->editBuf = active()->sync.getText(); active()->editor.setContent(active()->editBuf, active()->language); active()->highlightsDirty = true; + active()->highlightRequestTime = ImGui::GetTime(); active()->modified = true; - if (lsp && active()->path.rfind("(untitled", 0) != 0) { - active()->lspVersion += 1; - lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion); - } + queueLspDidChange(); analysisPending = true; analysisLastChange = ImGui::GetTime(); } diff --git a/editor/src/SettingsManager.h b/editor/src/SettingsManager.h index 48c125e..585072d 100644 --- a/editor/src/SettingsManager.h +++ b/editor/src/SettingsManager.h @@ -68,6 +68,12 @@ public: void setLargeFileTextMB(int value) { largeFileTextMB_ = value; } int getLargeFileDisableHighlightMB() const { return largeFileDisableHighlightMB_; } void setLargeFileDisableHighlightMB(int value) { largeFileDisableHighlightMB_ = value; } + int getLspDebounceMs() const { return lspDebounceMs_; } + void setLspDebounceMs(int value) { lspDebounceMs_ = value; } + int getDiagnosticsDebounceMs() const { return diagnosticsDebounceMs_; } + void setDiagnosticsDebounceMs(int value) { diagnosticsDebounceMs_ = value; } + int getHighlightDebounceMs() const { return highlightDebounceMs_; } + void setHighlightDebounceMs(int value) { highlightDebounceMs_ = value; } const std::string& getLayoutPreset() const { return layoutPreset_; } void setLayoutPreset(const std::string& name) { layoutPreset_ = name; } const std::string& getKeybindingProfile() const { return keybindingProfile_; } @@ -98,6 +104,9 @@ public: largeFileWarnMB_ = j.value("largeFileWarnMB", largeFileWarnMB_); largeFileTextMB_ = j.value("largeFileTextMB", largeFileTextMB_); largeFileDisableHighlightMB_ = j.value("largeFileDisableHighlightMB", largeFileDisableHighlightMB_); + lspDebounceMs_ = j.value("lspDebounceMs", lspDebounceMs_); + diagnosticsDebounceMs_ = j.value("diagnosticsDebounceMs", diagnosticsDebounceMs_); + highlightDebounceMs_ = j.value("highlightDebounceMs", highlightDebounceMs_); layoutPreset_ = j.value("layoutPreset", layoutPreset_); keybindingProfile_ = j.value("keybindingProfile", keybindingProfile_); emacsConfigPath_ = j.value("emacsConfigPath", emacsConfigPath_); @@ -141,6 +150,9 @@ public: j["largeFileWarnMB"] = largeFileWarnMB_; j["largeFileTextMB"] = largeFileTextMB_; j["largeFileDisableHighlightMB"] = largeFileDisableHighlightMB_; + j["lspDebounceMs"] = lspDebounceMs_; + j["diagnosticsDebounceMs"] = diagnosticsDebounceMs_; + j["highlightDebounceMs"] = highlightDebounceMs_; j["layoutPreset"] = layoutPreset_; j["keybindingProfile"] = keybindingProfile_; j["emacsConfigPath"] = emacsConfigPath_; @@ -221,6 +233,9 @@ private: int largeFileWarnMB_ = 1; int largeFileTextMB_ = 5; int largeFileDisableHighlightMB_ = 10; + int lspDebounceMs_ = 120; + int diagnosticsDebounceMs_ = 500; + int highlightDebounceMs_ = 120; std::string layoutPreset_ = "VSCode"; std::string keybindingProfile_ = "VSCode"; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index e2c0220..3b4ec89 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -330,12 +330,22 @@ int main(int, char**) { } } state.pollLspMessages(); + state.flushLspDidChange(ImGui::GetTime()); + state.flushDeferredAstSync(ImGui::GetTime()); state.processLibraryIndexResponses(); if (state.emacsState.emacsFunctionIndexDirty) { state.updateEmacsFunctionIndex(); } state.refreshEmacsModeLine(ImGui::GetTime()); state.events.tick(ImGui::GetTime()); +#ifdef _DEBUG + double now = ImGui::GetTime(); + if (io.DeltaTime > 0.016 && (now - state.lastFrameBudgetWarning) > 1.0) { + state.notify(NotificationLevel::Warning, + "[perf] Frame time exceeded 16ms budget."); + state.lastFrameBudgetWarning = now; + } +#endif themes.refreshWatchedThemes(); if (state.fontsDirty) { reloadFonts(); diff --git a/editor/src/panels/EditorPanel.h b/editor/src/panels/EditorPanel.h index 7c2b1df..cf2660c 100644 --- a/editor/src/panels/EditorPanel.h +++ b/editor/src/panels/EditorPanel.h @@ -372,7 +372,8 @@ static void renderEditorPanel(EditorState& state) { state.symbolsPending = false; } - if (state.analysisPending && (now - state.analysisLastChange) > 0.5) { + double diagDelay = state.settings.getDiagnosticsDebounceMs() / 1000.0; + if (state.analysisPending && (now - state.analysisLastChange) > diagDelay) { if (state.isStructured()) { auto result = state.pipeline.run(state.active()->editBuf, state.active()->language, diff --git a/editor/src/panels/SettingsPanel.h b/editor/src/panels/SettingsPanel.h index f05920b..8658dc4 100644 --- a/editor/src/panels/SettingsPanel.h +++ b/editor/src/panels/SettingsPanel.h @@ -381,6 +381,27 @@ static void renderSettingsPanel(EditorState& state) { settingsChanged = true; } + ImGui::Separator(); + ImGui::TextUnformatted("Debounce (ms)"); + int lspDebounce = state.settings.getLspDebounceMs(); + if (ImGui::InputInt("LSP Change", &lspDebounce)) { + lspDebounce = std::max(0, lspDebounce); + state.settings.setLspDebounceMs(lspDebounce); + settingsChanged = true; + } + int diagDebounce = state.settings.getDiagnosticsDebounceMs(); + if (ImGui::InputInt("Diagnostics", &diagDebounce)) { + diagDebounce = std::max(0, diagDebounce); + state.settings.setDiagnosticsDebounceMs(diagDebounce); + settingsChanged = true; + } + int highlightDebounce = state.settings.getHighlightDebounceMs(); + if (ImGui::InputInt("Highlighting", &highlightDebounce)) { + highlightDebounce = std::max(0, highlightDebounce); + state.settings.setHighlightDebounceMs(highlightDebounce); + settingsChanged = true; + } + LayoutPreset preset = state.ui.layoutPreset; int presetIndex = 0; if (preset == LayoutPreset::Emacs) presetIndex = 1; diff --git a/editor/tests/step200_test.cpp b/editor/tests/step200_test.cpp new file mode 100644 index 0000000..2b28084 --- /dev/null +++ b/editor/tests/step200_test.cpp @@ -0,0 +1,37 @@ +// Step 200: Startup and panel performance. + +#include +#include +#include + +static std::string readFile(const std::string& path) { + std::ifstream f(path); + if (!f.is_open()) return {}; + return std::string((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); +} + +static void assertContains(const std::string& text, const std::string& needle) { + assert(text.find(needle) != std::string::npos); +} + +int main() { + const std::string settings = readFile("src/SettingsManager.h"); + const std::string editorState = readFile("src/EditorState.h"); + const std::string mainSrc = readFile("src/main.cpp"); + const std::string editorPanel = readFile("src/panels/EditorPanel.h"); + + assertContains(settings, "getLspDebounceMs"); + assertContains(settings, "getDiagnosticsDebounceMs"); + assertContains(settings, "getHighlightDebounceMs"); + + assertContains(editorState, "flushDeferredAstSync"); + assertContains(editorState, "flushLspDidChange"); + assertContains(editorState, "pendingAstSync"); + + assertContains(editorPanel, "getDiagnosticsDebounceMs"); + assertContains(mainSrc, "Frame time exceeded 16ms"); + + printf("step200_test: all assertions passed\n"); + return 0; +} diff --git a/sprint6_plan.md b/sprint6_plan.md index a8d1f3a..360e644 100644 --- a/sprint6_plan.md +++ b/sprint6_plan.md @@ -424,7 +424,7 @@ Make Whetstone usable by everyone and fast with large files. - "Reopen in Structured Mode" option if user wants full features *Modifies:* buffer opening logic, `panels/StatusBarPanel.h` -- [ ] **Step 200: Startup and panel performance** +- [x] **Step 200: Startup and panel performance** Faster startup and smoother runtime: - Lazy-load panels: only parse/render on first show - Background grammar loading (don't block editor open for tree-sitter init)