diff --git a/Makefile b/Makefile index 95a9913..33ac46f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ APP_NAME := graptos APP_ID := io.github.graptos.Editor -VERSION := 0.23.67 +VERSION := 0.23.68 PREFIX ?= /usr/local CC ?= cc BUILD_DIR := build diff --git a/src/app.h b/src/app.h index 87d6a03..b86505d 100644 --- a/src/app.h +++ b/src/app.h @@ -24,6 +24,96 @@ */ #define GRAPTOS_APP_ID "io.github.graptos.Editor" +/** + * @brief Default maximum buffer chars for local live editor features. + */ +#define GRAPTOS_DEFAULT_LIVE_FEATURE_MAX_CHARS (32u * 1024u) + +/** + * @brief Default maximum buffer chars for automatic completion. + */ +#define GRAPTOS_DEFAULT_AUTO_COMPLETION_MAX_CHARS (16u * 1024u) + +/** + * @brief Default maximum buffer chars for LSP document synchronization. + */ +#define GRAPTOS_DEFAULT_LSP_SYNC_MAX_CHARS (256u * 1024u) + +/** + * @brief Default maximum undo snapshots retained per tab. + */ +#define GRAPTOS_DEFAULT_MAX_UNDO_STATES 100u + +/** + * @brief Default maximum chars for full highlighting work. + */ +#define GRAPTOS_DEFAULT_FULL_HIGHLIGHT_MAX_CHARS (256u * 1024u) + +/** + * @brief Default maximum buffer chars copied into undo snapshots. + */ +#define GRAPTOS_DEFAULT_MAX_UNDO_CAPTURE_BYTES (32u * 1024u) + +/** + * @brief Default editor highlight delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_HIGHLIGHT_DELAY_MS 180u + +/** + * @brief Default automatic completion delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_COMPLETION_DELAY_MS 900u + +/** + * @brief Default minimap refresh delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_MINIMAP_DELAY_MS 1500u + +/** + * @brief Default preview refresh delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_PREVIEW_DELAY_MS 1500u + +/** + * @brief Default syntax diagnostics delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_DIAGNOSTICS_DELAY_MS 1500u + +/** + * @brief Default selection match delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_SELECTION_MATCH_DELAY_MS 750u + +/** + * @brief Default color literal delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_COLOR_LITERAL_DELAY_MS 350u + +/** + * @brief Default hover lookup delay in milliseconds. + */ +#define GRAPTOS_DEFAULT_HOVER_DELAY_MS 650u + +/** + * @brief Default maximum chars for syntax diagnostics. + */ +#define GRAPTOS_DEFAULT_DIAGNOSTICS_MAX_CHARS GRAPTOS_DEFAULT_LIVE_FEATURE_MAX_CHARS + +/** + * @brief Default maximum bytes for minimap source extraction. + */ +#define GRAPTOS_DEFAULT_MINIMAP_MAX_BYTES (256u * 1024u) + +/** + * @brief Default maximum chars for selection match scanning. + */ +#define GRAPTOS_DEFAULT_SELECTION_MATCH_MAX_CHARS GRAPTOS_DEFAULT_LIVE_FEATURE_MAX_CHARS + +/** + * @brief Default maximum chars for color literal scanning. + */ +#define GRAPTOS_DEFAULT_COLOR_LITERAL_MAX_CHARS GRAPTOS_DEFAULT_LIVE_FEATURE_MAX_CHARS + /** * @brief App type definition. * @details This is intentionally a large window-owned state object. GTK code is @@ -179,6 +269,24 @@ typedef struct _EditorWindow { guint lsp_completion_retry_delay_ms; /**< Delay before retrying empty LSP completion. */ guint lsp_references_max_results; /**< Maximum LSP reference locations rendered. */ guint lsp_change_delay_ms; /**< Debounce delay before sending didChange to LSP. */ + guint live_feature_max_chars; /**< Maximum buffer chars for local live editor work. */ + guint auto_completion_max_chars; /**< Maximum buffer chars for automatic completion. */ + guint lsp_sync_max_chars; /**< Maximum buffer chars for LSP didChange sync. */ + guint max_undo_states; /**< Maximum undo snapshots retained per tab. */ + guint full_highlight_max_chars; /**< Maximum chars for full highlighting work. */ + guint max_undo_capture_bytes; /**< Maximum buffer chars copied into undo snapshots. */ + guint highlight_delay_ms; /**< Delay before applying editor highlight refresh. */ + guint completion_delay_ms; /**< Delay before automatic completion. */ + guint minimap_delay_ms; /**< Delay before minimap refresh. */ + guint preview_delay_ms; /**< Delay before preview refresh. */ + guint diagnostics_delay_ms; /**< Delay before syntax diagnostics refresh. */ + guint selection_match_delay_ms; /**< Delay before selection match refresh. */ + guint color_literal_delay_ms; /**< Delay before color literal refresh. */ + guint hover_delay_ms; /**< Delay before hover/reference lookup. */ + guint diagnostics_max_chars; /**< Maximum chars for syntax diagnostics. */ + guint minimap_max_bytes; /**< Maximum bytes for minimap source extraction. */ + guint selection_match_max_chars; /**< Maximum chars for selection match scanning. */ + guint color_literal_max_chars; /**< Maximum chars for color literal scanning. */ char *project_root; /**< Project root. */ GPtrArray *project_roots; /**< char*: all open project root. */ guint project_refresh_timeout; /**< Debounced project tree filesystem refresh source. */ @@ -316,7 +424,23 @@ void app_window_add_tab(EditorWindow *win, EditorTab *tab, gboolean switch_to_ta * @param win The win supplied by the caller. * @param tab The editor tab whose buffer or widgets are being inspected. */ -void app_window_close_tab(EditorWindow *win, EditorTab *tab); +gboolean app_window_close_tab(EditorWindow *win, EditorTab *tab); +/** + * @brief Show the tab close context menu. + * @details Tab labels are built by editor tabs, but the window owns tab order + * and close policy. Keeping the menu action here prevents duplicate + * notebook ownership rules. + * @param win The win supplied by the caller. + * @param tab The editor tab whose label was clicked. + * @param parent The widget that should own the popover. + * @param x The click x coordinate in parent coordinates. + * @param y The click y coordinate in parent coordinates. + */ +void app_window_show_tab_context_menu(EditorWindow *win, + EditorTab *tab, + GtkWidget *parent, + double x, + double y); /** * @brief App window close all tabs. * @details Application glue touches actions, tabs, panels, and persistent state. Keeping the contract explicit here makes UI callbacks easier to audit when a later change moves work between the window and child widgets. diff --git a/src/app/app_window_lifecycle.inc.c b/src/app/app_window_lifecycle.inc.c index 35305bb..cf26d05 100644 --- a/src/app/app_window_lifecycle.inc.c +++ b/src/app/app_window_lifecycle.inc.c @@ -197,6 +197,24 @@ EditorWindow *app_window_new(GtkApplication *application) { win->lsp_completion_retry_delay_ms = 120u; win->lsp_references_max_results = 60u; win->lsp_change_delay_ms = 450u; + win->live_feature_max_chars = GRAPTOS_DEFAULT_LIVE_FEATURE_MAX_CHARS; + win->auto_completion_max_chars = GRAPTOS_DEFAULT_AUTO_COMPLETION_MAX_CHARS; + win->lsp_sync_max_chars = GRAPTOS_DEFAULT_LSP_SYNC_MAX_CHARS; + win->max_undo_states = GRAPTOS_DEFAULT_MAX_UNDO_STATES; + win->full_highlight_max_chars = GRAPTOS_DEFAULT_FULL_HIGHLIGHT_MAX_CHARS; + win->max_undo_capture_bytes = GRAPTOS_DEFAULT_MAX_UNDO_CAPTURE_BYTES; + win->highlight_delay_ms = GRAPTOS_DEFAULT_HIGHLIGHT_DELAY_MS; + win->completion_delay_ms = GRAPTOS_DEFAULT_COMPLETION_DELAY_MS; + win->minimap_delay_ms = GRAPTOS_DEFAULT_MINIMAP_DELAY_MS; + win->preview_delay_ms = GRAPTOS_DEFAULT_PREVIEW_DELAY_MS; + win->diagnostics_delay_ms = GRAPTOS_DEFAULT_DIAGNOSTICS_DELAY_MS; + win->selection_match_delay_ms = GRAPTOS_DEFAULT_SELECTION_MATCH_DELAY_MS; + win->color_literal_delay_ms = GRAPTOS_DEFAULT_COLOR_LITERAL_DELAY_MS; + win->hover_delay_ms = GRAPTOS_DEFAULT_HOVER_DELAY_MS; + win->diagnostics_max_chars = GRAPTOS_DEFAULT_DIAGNOSTICS_MAX_CHARS; + win->minimap_max_bytes = GRAPTOS_DEFAULT_MINIMAP_MAX_BYTES; + win->selection_match_max_chars = GRAPTOS_DEFAULT_SELECTION_MATCH_MAX_CHARS; + win->color_literal_max_chars = GRAPTOS_DEFAULT_COLOR_LITERAL_MAX_CHARS; win->project_roots = g_ptr_array_new_with_free_func(g_free); win->tabs = g_ptr_array_new(); win->tile_tabs = g_ptr_array_new(); diff --git a/src/app/app_window_tabs.inc.c b/src/app/app_window_tabs.inc.c index 249dbcf..5d65942 100644 --- a/src/app/app_window_tabs.inc.c +++ b/src/app/app_window_tabs.inc.c @@ -49,6 +49,349 @@ void app_window_add_tab(EditorWindow *win, app_window_update_ui(win); } +/** + * @brief Tab close menu scope. + * @details The context menu closes tabs by browser-style groups. The enum keeps + * callbacks small and makes the warning text match the operation. + */ +typedef enum { + APP_TAB_CLOSE_ALL, /**< Close every tab. */ + APP_TAB_CLOSE_OTHERS, /**< Close every tab except the clicked tab. */ + APP_TAB_CLOSE_RIGHT, /**< Close visible tabs to the right. */ + APP_TAB_CLOSE_LEFT /**< Close visible tabs to the left. */ +} AppTabCloseScope; + +/** + * @brief One tab context-menu action. + * @details Menu buttons outlive the stack frame that builds the popover, so the + * callback keeps the clicked tab, operation, and popover together. + */ +typedef struct { + EditorWindow *win; /**< Window that owns the notebook. */ + EditorTab *tab; /**< Tab whose label opened the menu. */ + GtkWidget *popover; /**< One-shot context popover. */ + AppTabCloseScope scope; /**< Close operation requested by the user. */ +} AppTabCloseAction; + +/** + * @brief Return whether a tab is still registered. + * @details Batch closes can remove saved tile groups at once. Checking the + * registry before each pointer use avoids touching a tab closed by an + * earlier operation in the same batch. + * @param win The win supplied by the caller. + * @param tab The editor tab whose registry state is being inspected. + * @return TRUE when the tab is still open. + */ +static gboolean app_window_tab_is_registered(EditorWindow *win, + EditorTab *tab) { + if (!win || !tab) return FALSE; + guint count = app_window_tab_count(win); + for (guint i = 0u; i < count; i++) { + if (app_window_tab_at(win, i) == tab) return TRUE; + } + return FALSE; +} + +/** + * @brief Append a visible notebook tab to a pointer array. + * @details Visible left/right close operations follow notebook order, not the + * internal registry order that also contains folded tile members. + * @param win The win supplied by the caller. + * @param targets Target array that receives the tab pointer. + * @param page_index Notebook page index. + */ +static void app_window_append_visible_tab(EditorWindow *win, + GPtrArray *targets, + gint page_index) { + if (!win || !targets || page_index < 0) return; + GtkWidget *child = + gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), page_index); + EditorTab *tab = child + ? g_object_get_data(G_OBJECT(child), "graptos-tab") + : NULL; + if (tab) g_ptr_array_add(targets, tab); +} + +/** + * @brief Collect tabs affected by a context-menu close operation. + * @details Close-all uses the registry through its existing path. Other scopes + * return explicit targets so the caller can confirm and close them in + * a stable order. + * @param win The win supplied by the caller. + * @param anchor The tab whose label opened the menu. + * @param scope The close operation requested by the user. + * @return Newly allocated target array. + */ +static GPtrArray *app_window_collect_close_targets(EditorWindow *win, + EditorTab *anchor, + AppTabCloseScope scope) { + GPtrArray *targets = g_ptr_array_new(); + if (!win || !win->notebook || !anchor) return targets; + + if (scope == APP_TAB_CLOSE_OTHERS) { + guint count = app_window_tab_count(win); + for (guint i = 0u; i < count; i++) { + EditorTab *tab = app_window_tab_at(win, i); + if (tab && tab != anchor) g_ptr_array_add(targets, tab); + } + return targets; + } + + gint anchor_page = app_window_page_index_for_tab(win, anchor); + if (anchor_page < 0) return targets; + + if (scope == APP_TAB_CLOSE_LEFT) { + for (gint i = 0; i < anchor_page; i++) { + app_window_append_visible_tab(win, targets, i); + } + } else if (scope == APP_TAB_CLOSE_RIGHT) { + gint pages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)); + for (gint i = anchor_page + 1; i < pages; i++) { + app_window_append_visible_tab(win, targets, i); + } + } + + return targets; +} + +/** + * @brief Return a human-readable close scope name. + * @details The warning dialog and status messages should use the same operation + * names that appear in the context menu. + * @param scope The close operation requested by the user. + * @return Static label for the operation. + */ +static const char *app_window_close_scope_label(AppTabCloseScope scope) { + switch (scope) { + case APP_TAB_CLOSE_ALL: return "Close All Tabs"; + case APP_TAB_CLOSE_OTHERS: return "Close Other Tabs"; + case APP_TAB_CLOSE_RIGHT: return "Close Tabs to the Right"; + case APP_TAB_CLOSE_LEFT: return "Close Tabs to the Left"; + } + return "Close Tabs"; +} + +/** + * @brief Confirm a multi-tab close operation. + * @details This is the browser-style bulk warning. Individual dirty tabs still + * run their normal save/discard prompt after this succeeds. + * @param win The win supplied by the caller. + * @param scope The close operation requested by the user. + * @param count Number of tabs that will be closed. + * @return TRUE when the user wants to continue. + */ +static gboolean app_window_confirm_bulk_close(EditorWindow *win, + AppTabCloseScope scope, + guint count) { + if (!win || count <= 1u) return TRUE; + g_autofree char *message = g_strdup_printf( + "This will close %u tabs. Unsaved tabs will still ask whether to save, " + "discard, or cancel.", + count); + return dialog_confirm_yes_no(app_window_gtk(win), + app_window_close_scope_label(scope), + message); +} + +/** + * @brief Ensure the window still has one editable tab. + * @details Close-all is also exposed from an in-app menu now. Unlike window + * shutdown, the editor should remain usable after the operation. + * @param win The win supplied by the caller. + */ +static void app_window_ensure_editable_tab(EditorWindow *win) { + if (!win || app_window_tab_count(win) > 0u) return; + EditorTab *new_tab = editor_tab_new(win); + app_window_add_tab(win, new_tab, TRUE); +} + +/** + * @brief Run a context-menu tab close action. + * @details The action first shows a bulk warning, then delegates each tab to + * the existing close path so saved tiles and dirty buffers keep their + * normal behavior. + * @param win The win supplied by the caller. + * @param anchor The tab whose label opened the menu. + * @param scope The close operation requested by the user. + */ +static void app_window_run_tab_close_scope(EditorWindow *win, + EditorTab *anchor, + AppTabCloseScope scope) { + if (!win || !anchor) return; + + if (scope == APP_TAB_CLOSE_ALL) { + guint count = app_window_tab_count(win); + if (!app_window_confirm_bulk_close(win, scope, count)) return; + if (app_window_close_all_tabs(win)) { + app_window_ensure_editable_tab(win); + app_window_update_ui(win); + } + return; + } + + g_autoptr(GPtrArray) targets = + app_window_collect_close_targets(win, anchor, scope); + if (!targets || targets->len == 0u) return; + if (!app_window_confirm_bulk_close(win, scope, targets->len)) return; + + for (guint i = 0u; i < targets->len; i++) { + EditorTab *tab = g_ptr_array_index(targets, i); + if (!app_window_tab_is_registered(win, tab)) continue; + if (!app_window_close_tab(win, tab)) break; + } + + app_window_update_ui(win); +} + +/** + * @brief Handle a tab context-menu button click. + * @details The menu is closed before any tab removal starts so GTK does not + * keep a popover attached to a tab label that may be destroyed. + * @param button The button that emitted the callback. + * @param user_data Callback data describing the requested close action. + */ +static void app_window_tab_context_close_clicked(GtkWidget *button, + gpointer user_data) { + (void)button; + AppTabCloseAction *action = user_data; + if (!action) return; + if (action->popover && GTK_IS_POPOVER(action->popover)) { + graptos_popover_hide(action->popover); + } + app_window_run_tab_close_scope(action->win, action->tab, action->scope); +} + +/** + * @brief Clear and destroy a tab context popover. + * @details Tab context popovers are one-shot widgets. The identity check avoids + * clearing a newer menu that replaced this one. + * @param popover The popover supplied by GTK. + * @param user_data The tab label that owns the stored popover pointer. + */ +static void app_window_tab_context_closed(GtkPopover *popover, + gpointer user_data) { + GtkWidget *parent = user_data; + if (parent) { + GtkWidget *stored = + g_object_get_data(G_OBJECT(parent), "graptos-tab-context-popover"); + if (stored == GTK_WIDGET(popover)) { + g_object_set_data(G_OBJECT(parent), + "graptos-tab-context-popover", + NULL); + } + } + graptos_widget_destroy(GTK_WIDGET(popover)); +} + +/** + * @brief Create a tab context-menu button. + * @details Each button owns its action data for as long as the popover keeps + * the button alive. + * @param win The win supplied by the caller. + * @param tab The editor tab whose label opened the menu. + * @param popover The popover that owns the button. + * @param label The label shown to the user. + * @param scope The close operation requested by the button. + * @param sensitive TRUE when the button can be activated. + * @return Newly created menu button. + */ +static GtkWidget *app_window_tab_context_button(EditorWindow *win, + EditorTab *tab, + GtkWidget *popover, + const char *label, + AppTabCloseScope scope, + gboolean sensitive) { + AppTabCloseAction *action = g_new0(AppTabCloseAction, 1); + action->win = win; + action->tab = tab; + action->popover = popover; + action->scope = scope; + + GtkWidget *button = + graptos_flat_button_new(label, NULL, + G_CALLBACK(app_window_tab_context_close_clicked), + action); + g_object_set_data_full(G_OBJECT(button), + "graptos-tab-close-action", + action, + g_free); + gtk_widget_set_sensitive(button, sensitive); + return button; +} + +/** + * @brief Show the tab close context menu. + * @details The menu lives at the window level because it needs notebook order, + * registry order, dirty-tab confirmation, and tile cleanup policy. + * @param win The win supplied by the caller. + * @param tab The editor tab whose label was clicked. + * @param parent The widget that should own the popover. + * @param x The click x coordinate in parent coordinates. + * @param y The click y coordinate in parent coordinates. + */ +void app_window_show_tab_context_menu(EditorWindow *win, + EditorTab *tab, + GtkWidget *parent, + double x, + double y) { + if (!win || !tab || !parent) return; + + GtkWidget *old_popover = + g_object_get_data(G_OBJECT(parent), "graptos-tab-context-popover"); + if (old_popover && GTK_IS_POPOVER(old_popover)) { + graptos_popover_hide(old_popover); + graptos_widget_destroy(old_popover); + } + + GtkWidget *popover = gtk_popover_new(); + graptos_popover_attach(popover, parent); + gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE); + gtk_widget_add_css_class(popover, "graptos-context-popover"); + g_object_set_data(G_OBJECT(parent), "graptos-tab-context-popover", popover); + + guint count = app_window_tab_count(win); + gint page = app_window_page_index_for_tab(win, tab); + gint pages = win->notebook + ? gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)) + : 0; + + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + graptos_set_all_margins(box, 6); + gtk_box_append(GTK_BOX(box), + app_window_tab_context_button(win, tab, popover, + "Close All Tabs", + APP_TAB_CLOSE_ALL, + count > 0u)); + gtk_box_append(GTK_BOX(box), + app_window_tab_context_button(win, tab, popover, + "Close Other Tabs", + APP_TAB_CLOSE_OTHERS, + count > 1u)); + gtk_box_append(GTK_BOX(box), + app_window_tab_context_button(win, tab, popover, + "Close Tabs to the Right", + APP_TAB_CLOSE_RIGHT, + page >= 0 && + page + 1 < pages)); + gtk_box_append(GTK_BOX(box), + app_window_tab_context_button(win, tab, popover, + "Close Tabs to the Left", + APP_TAB_CLOSE_LEFT, + page > 0)); + + GdkRectangle rect = { + .x = (int)x, + .y = (int)y, + .width = 1, + .height = 1 + }; + gtk_popover_set_child(GTK_POPOVER(popover), box); + gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect); + g_signal_connect(popover, "closed", + G_CALLBACK(app_window_tab_context_closed), parent); + gtk_popover_popup(GTK_POPOVER(popover)); +} + /** * @brief App window close tab. @@ -56,11 +399,11 @@ void app_window_add_tab(EditorWindow *win, * @param win The win supplied by the caller. * @param tab The editor tab whose buffer or widgets are being inspected. */ -void app_window_close_tab(EditorWindow *win, EditorTab *tab) { - if (!win || !tab) return; +gboolean app_window_close_tab(EditorWindow *win, EditorTab *tab) { + if (!win || !tab) return FALSE; // Give the tab a chance to stop closing if it has unsaved changes. - if (!editor_tab_confirm_close(tab)) return; + if (!editor_tab_confirm_close(tab)) return FALSE; GPtrArray *group_to_close = NULL; if (tab->tile_group && tab->tile_group->len > 1u) { group_to_close = g_ptr_array_new(); @@ -72,7 +415,7 @@ void app_window_close_tab(EditorWindow *win, EditorTab *tab) { EditorTab *member = g_ptr_array_index(group_to_close, i); if (member && !editor_tab_confirm_close(member)) { g_ptr_array_free(group_to_close, TRUE); - return; + return FALSE; } } } @@ -135,6 +478,7 @@ void app_window_close_tab(EditorWindow *win, EditorTab *tab) { } app_window_update_ui(win); + return TRUE; } diff --git a/src/config.c b/src/config.c index d2d01ad..20ff7e5 100644 --- a/src/config.c +++ b/src/config.c @@ -768,6 +768,24 @@ static void populate_config_key_file(EditorWindow *win, GKeyFile *key_file) { g_key_file_set_integer(key_file, "Editor", "lsp_completion_retry_delay_ms", (gint)win->lsp_completion_retry_delay_ms); g_key_file_set_integer(key_file, "Editor", "lsp_references_max_results", (gint)win->lsp_references_max_results); g_key_file_set_integer(key_file, "Editor", "lsp_change_delay_ms", (gint)win->lsp_change_delay_ms); + g_key_file_set_integer(key_file, "Editor", "live_feature_max_chars", (gint)win->live_feature_max_chars); + g_key_file_set_integer(key_file, "Editor", "auto_completion_max_chars", (gint)win->auto_completion_max_chars); + g_key_file_set_integer(key_file, "Editor", "lsp_sync_max_chars", (gint)win->lsp_sync_max_chars); + g_key_file_set_integer(key_file, "Editor", "max_undo_states", (gint)win->max_undo_states); + g_key_file_set_integer(key_file, "Editor", "full_highlight_max_chars", (gint)win->full_highlight_max_chars); + g_key_file_set_integer(key_file, "Editor", "max_undo_capture_bytes", (gint)win->max_undo_capture_bytes); + g_key_file_set_integer(key_file, "Editor", "highlight_delay_ms", (gint)win->highlight_delay_ms); + g_key_file_set_integer(key_file, "Editor", "completion_delay_ms", (gint)win->completion_delay_ms); + g_key_file_set_integer(key_file, "Editor", "minimap_delay_ms", (gint)win->minimap_delay_ms); + g_key_file_set_integer(key_file, "Editor", "preview_delay_ms", (gint)win->preview_delay_ms); + g_key_file_set_integer(key_file, "Editor", "diagnostics_delay_ms", (gint)win->diagnostics_delay_ms); + g_key_file_set_integer(key_file, "Editor", "selection_match_delay_ms", (gint)win->selection_match_delay_ms); + g_key_file_set_integer(key_file, "Editor", "color_literal_delay_ms", (gint)win->color_literal_delay_ms); + g_key_file_set_integer(key_file, "Editor", "hover_delay_ms", (gint)win->hover_delay_ms); + g_key_file_set_integer(key_file, "Editor", "diagnostics_max_chars", (gint)win->diagnostics_max_chars); + g_key_file_set_integer(key_file, "Editor", "minimap_max_bytes", (gint)win->minimap_max_bytes); + g_key_file_set_integer(key_file, "Editor", "selection_match_max_chars", (gint)win->selection_match_max_chars); + g_key_file_set_integer(key_file, "Editor", "color_literal_max_chars", (gint)win->color_literal_max_chars); g_key_file_set_boolean(key_file, "Editor", "scroll_preview_enabled", win->minimap_enabled); g_key_file_set_boolean(key_file, "Editor", "preview_enabled", win->preview_enabled); g_key_file_set_boolean(key_file, "Editor", "use_gtksourceview_highlighting", TRUE); @@ -955,6 +973,24 @@ void graptos_config_load(EditorWindow *win) { win->lsp_completion_retry_delay_ms = parse_uint(key_file, "lsp_completion_retry_delay_ms", win->lsp_completion_retry_delay_ms, 20u, 2000u); win->lsp_references_max_results = parse_uint(key_file, "lsp_references_max_results", win->lsp_references_max_results, 1u, 1000u); win->lsp_change_delay_ms = parse_uint(key_file, "lsp_change_delay_ms", win->lsp_change_delay_ms, 50u, 5000u); + win->live_feature_max_chars = parse_uint(key_file, "live_feature_max_chars", win->live_feature_max_chars, 1024u, 16u * 1024u * 1024u); + win->auto_completion_max_chars = parse_uint(key_file, "auto_completion_max_chars", win->auto_completion_max_chars, 1024u, 16u * 1024u * 1024u); + win->lsp_sync_max_chars = parse_uint(key_file, "lsp_sync_max_chars", win->lsp_sync_max_chars, 1024u, 16u * 1024u * 1024u); + win->max_undo_states = parse_uint(key_file, "max_undo_states", win->max_undo_states, 1u, 10000u); + win->full_highlight_max_chars = parse_uint(key_file, "full_highlight_max_chars", win->full_highlight_max_chars, 1024u, 16u * 1024u * 1024u); + win->max_undo_capture_bytes = parse_uint(key_file, "max_undo_capture_bytes", win->max_undo_capture_bytes, 1024u, 16u * 1024u * 1024u); + win->highlight_delay_ms = parse_uint(key_file, "highlight_delay_ms", win->highlight_delay_ms, 0u, 60000u); + win->completion_delay_ms = parse_uint(key_file, "completion_delay_ms", win->completion_delay_ms, 0u, 60000u); + win->minimap_delay_ms = parse_uint(key_file, "minimap_delay_ms", win->minimap_delay_ms, 0u, 60000u); + win->preview_delay_ms = parse_uint(key_file, "preview_delay_ms", win->preview_delay_ms, 0u, 60000u); + win->diagnostics_delay_ms = parse_uint(key_file, "diagnostics_delay_ms", win->diagnostics_delay_ms, 0u, 60000u); + win->selection_match_delay_ms = parse_uint(key_file, "selection_match_delay_ms", win->selection_match_delay_ms, 0u, 60000u); + win->color_literal_delay_ms = parse_uint(key_file, "color_literal_delay_ms", win->color_literal_delay_ms, 0u, 60000u); + win->hover_delay_ms = parse_uint(key_file, "hover_delay_ms", win->hover_delay_ms, 0u, 60000u); + win->diagnostics_max_chars = parse_uint(key_file, "diagnostics_max_chars", win->diagnostics_max_chars, 1024u, 16u * 1024u * 1024u); + win->minimap_max_bytes = parse_uint(key_file, "minimap_max_bytes", win->minimap_max_bytes, 1024u, 16u * 1024u * 1024u); + win->selection_match_max_chars = parse_uint(key_file, "selection_match_max_chars", win->selection_match_max_chars, 1024u, 16u * 1024u * 1024u); + win->color_literal_max_chars = parse_uint(key_file, "color_literal_max_chars", win->color_literal_max_chars, 1024u, 16u * 1024u * 1024u); win->minimap_enabled = parse_bool(key_file, "scroll_preview_enabled", win->minimap_enabled); win->preview_enabled = parse_bool(key_file, "preview_enabled", win->preview_enabled); win->use_gtksourceview_highlighting = TRUE; diff --git a/src/editor/editor_tab_highlight_scheduling.inc.c b/src/editor/editor_tab_highlight_scheduling.inc.c index fc4705e..9b4bad8 100644 --- a/src/editor/editor_tab_highlight_scheduling.inc.c +++ b/src/editor/editor_tab_highlight_scheduling.inc.c @@ -44,7 +44,9 @@ void editor_tab_schedule_minimap_update(EditorTab *tab) { if (!tab || !tab->win || !tab->win->minimap_enabled) return; graptos_source_cancel(&tab->minimap_timeout); tab->minimap_timeout = g_timeout_add_full(G_PRIORITY_LOW, - 80u, + tab->win->minimap_delay_ms > 0u + ? tab->win->minimap_delay_ms + : GRAPTOS_MINIMAP_DELAY_MS, minimap_timeout_cb, tab, NULL); @@ -90,7 +92,9 @@ void editor_tab_schedule_preview_update(EditorTab *tab) { return; } tab->preview_timeout = g_timeout_add_full(G_PRIORITY_LOW, - GRAPTOS_PREVIEW_DELAY_MS, + tab->win->preview_delay_ms > 0u + ? tab->win->preview_delay_ms + : GRAPTOS_PREVIEW_DELAY_MS, preview_timeout_cb, tab, NULL); diff --git a/src/editor/editor_tab_hover_events.inc.c b/src/editor/editor_tab_hover_events.inc.c index 3025004..fd3ffbc 100644 --- a/src/editor/editor_tab_hover_events.inc.c +++ b/src/editor/editor_tab_hover_events.inc.c @@ -1355,7 +1355,9 @@ void maybe_show_color_preview(EditorTab *tab) { graptos_source_cancel(&tab->hover_timeout); tab->hover_timeout = g_timeout_add_full(G_PRIORITY_LOW, - GRAPTOS_HOVER_DELAY_MS, + tab->win && tab->win->hover_delay_ms > 0u + ? tab->win->hover_delay_ms + : GRAPTOS_HOVER_DELAY_MS, hover_timeout_cb, tab, NULL); diff --git a/src/editor/editor_tab_hover_refs.inc.c b/src/editor/editor_tab_hover_refs.inc.c index 59cccea..7742c27 100644 --- a/src/editor/editor_tab_hover_refs.inc.c +++ b/src/editor/editor_tab_hover_refs.inc.c @@ -258,9 +258,11 @@ static void schedule_reference_lookup_at_iter(EditorTab *tab, /* * Delay the lookup slightly so normal mouse movement does not cause constant * indexing work. - */ + */ tab->hover_timeout = g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE, - 120u, + tab->win && tab->win->hover_delay_ms > 0u + ? tab->win->hover_delay_ms + : GRAPTOS_HOVER_DELAY_MS, hover_timeout_cb, tab, NULL); diff --git a/src/editor/editor_tab_lifecycle.inc.c b/src/editor/editor_tab_lifecycle.inc.c index c3c2d61..585d114 100644 --- a/src/editor/editor_tab_lifecycle.inc.c +++ b/src/editor/editor_tab_lifecycle.inc.c @@ -81,8 +81,11 @@ gboolean editor_tab_large_file_mode(EditorTab *tab) { * Large buffers skip expensive live features so typing and scrolling stay * responsive. */ + guint max_chars = tab->win && tab->win->live_feature_max_chars > 0u + ? tab->win->live_feature_max_chars + : GRAPTOS_LIVE_FEATURE_MAX_CHARS; return (guint)gtk_text_buffer_get_char_count(tab->buffer) - > GRAPTOS_LIVE_FEATURE_MAX_CHARS; + > max_chars; } /** * @brief Editor tab live features allowed. @@ -94,6 +97,38 @@ gboolean editor_tab_live_features_allowed(EditorTab *tab) { // Live features are disabled for large files to avoid editor lag. return tab && tab->buffer && !editor_tab_large_file_mode(tab); } + +/** + * @brief Editor tab auto completion allowed. + * @details Automatic completion is a typing-time feature, so it keeps the + * smaller autocomplete threshold even when other live features are + * allowed for a larger buffer. + * @param tab The editor tab whose buffer or widgets are being inspected. + * @return TRUE when automatic completion may run. + */ +gboolean editor_tab_auto_completion_allowed(EditorTab *tab) { + if (!tab || !tab->buffer) return FALSE; + guint max_chars = tab->win && tab->win->auto_completion_max_chars > 0u + ? tab->win->auto_completion_max_chars + : GRAPTOS_AUTO_COMPLETION_MAX_CHARS; + return (guint)gtk_text_buffer_get_char_count(tab->buffer) <= max_chars; +} + +/** + * @brief Editor tab LSP sync allowed. + * @details Language servers can remain useful after local live features have + * been reduced. This guard keeps didChange bounded without tying it to + * selection matching, diagnostics, or local completion scanning. + * @param tab The editor tab whose buffer or widgets are being inspected. + * @return TRUE when LSP didChange may be sent. + */ +gboolean editor_tab_lsp_sync_allowed(EditorTab *tab) { + if (!tab || !tab->buffer) return FALSE; + guint max_chars = tab->win && tab->win->lsp_sync_max_chars > 0u + ? tab->win->lsp_sync_max_chars + : GRAPTOS_LSP_SYNC_MAX_CHARS; + return (guint)gtk_text_buffer_get_char_count(tab->buffer) <= max_chars; +} /** * @brief Editor tab highlighting allowed. * @details Editor code runs in response to fast input, delayed timeouts, and background language work. The notes here mark the boundary between immediate GTK state and deferred refresh paths so latency fixes do not turn into stale-widget bugs. diff --git a/src/editor/editor_tab_signals_preferences.inc.c b/src/editor/editor_tab_signals_preferences.inc.c index 3573b73..c8b1e0a 100644 --- a/src/editor/editor_tab_signals_preferences.inc.c +++ b/src/editor/editor_tab_signals_preferences.inc.c @@ -139,18 +139,21 @@ void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data) { if (tab->gutter) gtk_widget_queue_draw(tab->gutter); } tab->last_line_count = line_count; - gboolean large_file = (guint)char_count > GRAPTOS_LIVE_FEATURE_MAX_CHARS; + gboolean large_file = !editor_tab_live_features_allowed(tab); /* * The snapshot undo implementation copies the buffer, so keep it only for * small files. Large files rely on reduced live work rather than spending * time duplicating megabytes of text after each edit. */ - if (!large_file && (guint)char_count <= GRAPTOS_MAX_UNDO_CAPTURE_BYTES) { + guint undo_capture_limit = tab->win && tab->win->max_undo_capture_bytes > 0u + ? tab->win->max_undo_capture_bytes + : GRAPTOS_MAX_UNDO_CAPTURE_BYTES; + if (!large_file && (guint)char_count <= undo_capture_limit) { char *current = buffer_text(tab); if (!current) return; if (tab->last_snapshot && strcmp(tab->last_snapshot, current) != 0) { - push_limited(tab->undo_stack, tab->last_snapshot); + push_limited(tab, tab->undo_stack, tab->last_snapshot); tab->last_snapshot = g_strdup(current); clear_stack(tab->redo_stack); } else if (!tab->last_snapshot) { @@ -179,6 +182,7 @@ void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data) { tab->diagnostic_warnings = 0u; tab->low_latency_mode_active = TRUE; } + editor_tab_schedule_lsp_change(tab); editor_tab_schedule_minimap_update(tab); editor_tab_schedule_lightweight_ui_refresh(tab); if (!tab->manual_syntax_override && !tab->file_path) editor_tab_auto_select_syntax(tab); @@ -226,7 +230,7 @@ gboolean lsp_change_timeout_cb(gpointer user_data) { if (!tab) return G_SOURCE_REMOVE; tab->lsp_change_timeout = 0u; if (tab->win && tab->win->lsp_client && tab->file_path && - editor_tab_live_features_allowed(tab)) { + editor_tab_lsp_sync_allowed(tab)) { lsp_client_document_changed(tab->win->lsp_client, tab); } return G_SOURCE_REMOVE; @@ -242,7 +246,7 @@ gboolean lsp_change_timeout_cb(gpointer user_data) { void editor_tab_schedule_lsp_change(EditorTab *tab) { if (!tab || !tab->win || !tab->win->lsp_client || !tab->file_path) return; graptos_source_cancel(&tab->lsp_change_timeout); - if (!editor_tab_live_features_allowed(tab)) return; + if (!editor_tab_lsp_sync_allowed(tab)) return; tab->lsp_change_timeout = g_timeout_add_full(G_PRIORITY_LOW, tab->win->lsp_change_delay_ms > 0u ? tab->win->lsp_change_delay_ms diff --git a/src/editor_tab_build.c b/src/editor_tab_build.c index 0f8957f..5cab834 100644 --- a/src/editor_tab_build.c +++ b/src/editor_tab_build.c @@ -416,6 +416,30 @@ static void on_tab_label_pressed(GtkGestureClick *gesture, app_window_handle_tab_label_click(tab->win, tab, state); } +/** + * @brief Handle a secondary click on an editor tab label. + * @details Browser-style close actions need notebook ownership, so the editor + * tab only detects the click and delegates the menu to the window. + * @param gesture The gesture supplied by the caller. + * @param n_press The n press supplied by the caller. + * @param x The x supplied by the caller. + * @param y The y supplied by the caller. + * @param user_data The callback context passed through GTK signal data. + */ +static void on_tab_label_right_pressed(GtkGestureClick *gesture, + int n_press, + double x, + double y, + gpointer user_data) { + (void)n_press; + EditorTab *tab = user_data; + GtkWidget *widget = gtk_event_controller_get_widget( + GTK_EVENT_CONTROLLER(gesture)); + if (!tab || !tab->win || !widget) return; + gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED); + app_window_show_tab_context_menu(tab->win, tab, widget, x, y); +} + /** * @brief Tab create tab label. * @details Editor code runs in response to fast input, delayed timeouts, and background language work. The notes here mark the boundary between immediate GTK state and deferred refresh paths so latency fixes do not turn into stale-widget bugs. @@ -464,6 +488,14 @@ static void tab_create_tab_label(EditorTab *tab) { G_CALLBACK(on_tab_label_pressed), tab); gtk_widget_add_controller(tab->tab_widget, GTK_EVENT_CONTROLLER(tab_click)); + + GtkGesture *tab_context_click = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(tab_context_click), + GDK_BUTTON_SECONDARY); + g_signal_connect(tab_context_click, "pressed", + G_CALLBACK(on_tab_label_right_pressed), tab); + gtk_widget_add_controller(tab->tab_widget, + GTK_EVENT_CONTROLLER(tab_context_click)); } /** diff --git a/src/editor_tab_completion.c b/src/editor_tab_completion.c index 127902b..26bd18b 100644 --- a/src/editor_tab_completion.c +++ b/src/editor_tab_completion.c @@ -698,7 +698,7 @@ void completion_add_row(EditorTab *tab, const char *word) { */ void editor_tab_show_completion(EditorTab *tab, gboolean manual) { if (!tab || !tab->autocomplete_enabled) return; - if (!manual && !editor_tab_live_features_allowed(tab)) return; + if (!manual && !editor_tab_auto_completion_allowed(tab)) return; GtkTextIter prefix_start; GtkTextIter cursor; @@ -779,14 +779,15 @@ void editor_tab_schedule_completion(EditorTab *tab) { * touch import-context logic, so keep it for genuinely small buffers only. * Manual completion is still available in larger files. */ - if (!editor_tab_live_features_allowed(tab) || - (guint)gtk_text_buffer_get_char_count(tab->buffer) > GRAPTOS_AUTO_COMPLETION_MAX_CHARS) { + if (!editor_tab_auto_completion_allowed(tab)) { if (tab->completion_popover) graptos_popover_hide(tab->completion_popover); return; } tab->completion_timeout = g_timeout_add_full(G_PRIORITY_LOW, - GRAPTOS_COMPLETION_DELAY_MS, + tab->win && tab->win->completion_delay_ms > 0u + ? tab->win->completion_delay_ms + : GRAPTOS_COMPLETION_DELAY_MS, completion_timeout_cb, tab, NULL); diff --git a/src/editor_tab_diagnostics.c b/src/editor_tab_diagnostics.c index 50ead69..9d466d9 100644 --- a/src/editor_tab_diagnostics.c +++ b/src/editor_tab_diagnostics.c @@ -574,8 +574,11 @@ void editor_tab_apply_syntax_diagnostics(EditorTab *tab) { clear_syntax_diagnostics(tab); + guint diagnostics_max = tab->win && tab->win->diagnostics_max_chars > 0u + ? tab->win->diagnostics_max_chars + : GRAPTOS_DIAGNOSTICS_MAX_CHARS; if (!tab->active_syntax || - (guint)gtk_text_buffer_get_char_count(tab->buffer) > GRAPTOS_DIAGNOSTICS_MAX_CHARS) { + (guint)gtk_text_buffer_get_char_count(tab->buffer) > diagnostics_max) { editor_tab_schedule_lightweight_ui_refresh(tab); return; } @@ -632,7 +635,9 @@ void editor_tab_schedule_syntax_diagnostics(EditorTab *tab) { return; } tab->diagnostics_timeout = g_timeout_add_full(G_PRIORITY_LOW, - GRAPTOS_DIAGNOSTICS_DELAY_MS, + tab->win && tab->win->diagnostics_delay_ms > 0u + ? tab->win->diagnostics_delay_ms + : GRAPTOS_DIAGNOSTICS_DELAY_MS, diagnostics_timeout_cb, tab, NULL); diff --git a/src/editor_tab_gutter.c b/src/editor_tab_gutter.c index c430164..c18adff 100644 --- a/src/editor_tab_gutter.c +++ b/src/editor_tab_gutter.c @@ -318,7 +318,10 @@ void update_selection_matches(EditorTab *tab) { gtk_text_buffer_remove_tag_by_name(tab->buffer, "graptos-selection-match", &start, &end); clear_minimap_matches(tab); - if ((guint)gtk_text_buffer_get_char_count(tab->buffer) > GRAPTOS_SELECTION_MATCH_MAX_CHARS) { + guint selection_match_max = tab->win && tab->win->selection_match_max_chars > 0u + ? tab->win->selection_match_max_chars + : GRAPTOS_SELECTION_MATCH_MAX_CHARS; + if ((guint)gtk_text_buffer_get_char_count(tab->buffer) > selection_match_max) { return; } @@ -391,7 +394,9 @@ void editor_tab_schedule_selection_matches(EditorTab *tab) { } tab->selection_match_timeout = g_timeout_add_full(G_PRIORITY_LOW, - GRAPTOS_SELECTION_MATCH_DELAY_MS, + tab->win && tab->win->selection_match_delay_ms > 0u + ? tab->win->selection_match_delay_ms + : GRAPTOS_SELECTION_MATCH_DELAY_MS, selection_match_timeout_cb, tab, NULL); @@ -575,7 +580,10 @@ static guint maybe_apply_color_token(EditorTab *tab, void update_color_literals(EditorTab *tab) { if (!tab || !tab->buffer) return; clear_color_literals(tab); - if ((guint)gtk_text_buffer_get_char_count(tab->buffer) > GRAPTOS_COLOR_LITERAL_MAX_CHARS) return; + guint color_literal_max = tab->win && tab->win->color_literal_max_chars > 0u + ? tab->win->color_literal_max_chars + : GRAPTOS_COLOR_LITERAL_MAX_CHARS; + if ((guint)gtk_text_buffer_get_char_count(tab->buffer) > color_literal_max) return; GtkTextIter start; GtkTextIter end; @@ -667,7 +675,9 @@ void editor_tab_schedule_color_literals(EditorTab *tab) { } tab->color_literal_timeout = g_timeout_add_full(G_PRIORITY_LOW, - GRAPTOS_COLOR_LITERAL_DELAY_MS, + tab->win && tab->win->color_literal_delay_ms > 0u + ? tab->win->color_literal_delay_ms + : GRAPTOS_COLOR_LITERAL_DELAY_MS, color_literal_timeout_cb, tab, NULL); @@ -762,6 +772,12 @@ void on_minimap_draw(GtkDrawingArea *area, cairo_t *cr, int width, (void)area; EditorTab *tab = user_data; if (!tab || !tab->buffer || width <= 0 || height <= 0) return; + guint minimap_max = tab->win && tab->win->minimap_max_bytes > 0u + ? tab->win->minimap_max_bytes + : GRAPTOS_MINIMAP_MAX_BYTES; + if ((guint)gtk_text_buffer_get_char_count(tab->buffer) > minimap_max) { + return; + } cairo_set_source_rgb(cr, 0.075, 0.082, 0.094); cairo_rectangle(cr, 0.0, 0.0, (double)width, (double)height); diff --git a/src/editor_tab_private.h b/src/editor_tab_private.h index 348a66f..ee137eb 100644 --- a/src/editor_tab_private.h +++ b/src/editor_tab_private.h @@ -30,71 +30,75 @@ /** * @brief Graptoς max undo states macro. */ -#define GRAPTOS_MAX_UNDO_STATES 100u +#define GRAPTOS_MAX_UNDO_STATES GRAPTOS_DEFAULT_MAX_UNDO_STATES /** * @brief Graptoς live feature max chars macro. */ -#define GRAPTOS_LIVE_FEATURE_MAX_CHARS (32u * 1024u) +#define GRAPTOS_LIVE_FEATURE_MAX_CHARS GRAPTOS_DEFAULT_LIVE_FEATURE_MAX_CHARS /** * @brief Graptoς full highlight max chars macro. */ -#define GRAPTOS_FULL_HIGHLIGHT_MAX_CHARS (256u * 1024u) +#define GRAPTOS_FULL_HIGHLIGHT_MAX_CHARS GRAPTOS_DEFAULT_FULL_HIGHLIGHT_MAX_CHARS /** * @brief Graptoς max undo capture bytes macro. */ -#define GRAPTOS_MAX_UNDO_CAPTURE_BYTES (32u * 1024u) +#define GRAPTOS_MAX_UNDO_CAPTURE_BYTES GRAPTOS_DEFAULT_MAX_UNDO_CAPTURE_BYTES /** * @brief Graptoς highlight delay ms macro. */ -#define GRAPTOS_HIGHLIGHT_DELAY_MS 180u +#define GRAPTOS_HIGHLIGHT_DELAY_MS GRAPTOS_DEFAULT_HIGHLIGHT_DELAY_MS /** * @brief Graptoς completion delay ms macro. */ -#define GRAPTOS_COMPLETION_DELAY_MS 900u +#define GRAPTOS_COMPLETION_DELAY_MS GRAPTOS_DEFAULT_COMPLETION_DELAY_MS /** * @brief Graptoς minimap delay ms macro. */ -#define GRAPTOS_MINIMAP_DELAY_MS 1500u +#define GRAPTOS_MINIMAP_DELAY_MS GRAPTOS_DEFAULT_MINIMAP_DELAY_MS /** * @brief Graptoς preview delay ms macro. */ -#define GRAPTOS_PREVIEW_DELAY_MS 1500u +#define GRAPTOS_PREVIEW_DELAY_MS GRAPTOS_DEFAULT_PREVIEW_DELAY_MS /** * @brief Graptoς diagnostics delay ms macro. */ -#define GRAPTOS_DIAGNOSTICS_DELAY_MS 1500u +#define GRAPTOS_DIAGNOSTICS_DELAY_MS GRAPTOS_DEFAULT_DIAGNOSTICS_DELAY_MS /** * @brief Graptoς selection match delay ms macro. */ -#define GRAPTOS_SELECTION_MATCH_DELAY_MS 750u +#define GRAPTOS_SELECTION_MATCH_DELAY_MS GRAPTOS_DEFAULT_SELECTION_MATCH_DELAY_MS /** * @brief Graptoς color literal delay ms macro. */ -#define GRAPTOS_COLOR_LITERAL_DELAY_MS 350u +#define GRAPTOS_COLOR_LITERAL_DELAY_MS GRAPTOS_DEFAULT_COLOR_LITERAL_DELAY_MS /** * @brief Graptoς hover delay ms macro. */ -#define GRAPTOS_HOVER_DELAY_MS 650u +#define GRAPTOS_HOVER_DELAY_MS GRAPTOS_DEFAULT_HOVER_DELAY_MS /** * @brief Graptoς diagnostics max chars macro. */ -#define GRAPTOS_DIAGNOSTICS_MAX_CHARS GRAPTOS_LIVE_FEATURE_MAX_CHARS +#define GRAPTOS_DIAGNOSTICS_MAX_CHARS GRAPTOS_DEFAULT_DIAGNOSTICS_MAX_CHARS /** * @brief Graptoς minimap max bytes macro. */ -#define GRAPTOS_MINIMAP_MAX_BYTES (256u * 1024u) +#define GRAPTOS_MINIMAP_MAX_BYTES GRAPTOS_DEFAULT_MINIMAP_MAX_BYTES /** * @brief Graptoς selection match max chars macro. */ -#define GRAPTOS_SELECTION_MATCH_MAX_CHARS GRAPTOS_LIVE_FEATURE_MAX_CHARS +#define GRAPTOS_SELECTION_MATCH_MAX_CHARS GRAPTOS_DEFAULT_SELECTION_MATCH_MAX_CHARS /** * @brief Graptoς color literal max chars macro. */ -#define GRAPTOS_COLOR_LITERAL_MAX_CHARS GRAPTOS_LIVE_FEATURE_MAX_CHARS +#define GRAPTOS_COLOR_LITERAL_MAX_CHARS GRAPTOS_DEFAULT_COLOR_LITERAL_MAX_CHARS /** * @brief Graptoς auto completion max chars macro. */ -#define GRAPTOS_AUTO_COMPLETION_MAX_CHARS (16u * 1024u) +#define GRAPTOS_AUTO_COMPLETION_MAX_CHARS GRAPTOS_DEFAULT_AUTO_COMPLETION_MAX_CHARS +/** + * @brief Graptoς LSP sync max chars macro. + */ +#define GRAPTOS_LSP_SYNC_MAX_CHARS GRAPTOS_DEFAULT_LSP_SYNC_MAX_CHARS /** * @brief Buffer text. @@ -140,7 +144,7 @@ void clear_stack(GPtrArray *stack); * @param stack The stack supplied by the caller. * @param state The state supplied by the caller. */ -void push_limited(GPtrArray *stack, char *state); +void push_limited(EditorTab *tab, GPtrArray *stack, char *state); /** * @brief Pop stack. * @details Editor code runs in response to fast input, delayed timeouts, and background language work. The notes here mark the boundary between immediate GTK state and deferred refresh paths so latency fixes do not turn into stale-widget bugs. @@ -175,6 +179,23 @@ gboolean editor_tab_large_file_mode(EditorTab *tab); * @return TRUE when the condition is satisfied; otherwise FALSE. */ gboolean editor_tab_live_features_allowed(EditorTab *tab); +/** + * @brief Return whether automatic completion is allowed for a tab. + * @details Automatic completion runs while typing, so it uses its own + * configurable guard instead of the broader live-feature threshold. + * @param tab The editor tab whose buffer or widgets are being inspected. + * @return TRUE when automatic completion may run. + */ +gboolean editor_tab_auto_completion_allowed(EditorTab *tab); +/** + * @brief Return whether LSP document sync is allowed for a tab. + * @details LSP sync can be useful for larger files than local scanning. It has + * a separate configurable threshold so large-file mode does not make + * language servers stale too early. + * @param tab The editor tab whose buffer or widgets are being inspected. + * @return TRUE when LSP didChange may be sent. + */ +gboolean editor_tab_lsp_sync_allowed(EditorTab *tab); /** * @brief Editor tab highlighting allowed. * @details Editor code runs in response to fast input, delayed timeouts, and background language work. The notes here mark the boundary between immediate GTK state and deferred refresh paths so latency fixes do not turn into stale-widget bugs. diff --git a/src/editor_tab_undo.c b/src/editor_tab_undo.c index 563f40f..b21a85a 100644 --- a/src/editor_tab_undo.c +++ b/src/editor_tab_undo.c @@ -24,10 +24,13 @@ void clear_stack(GPtrArray *stack) { * @param stack The stack supplied by the caller. * @param state The state supplied by the caller. */ -void push_limited(GPtrArray *stack, char *state) { +void push_limited(EditorTab *tab, GPtrArray *stack, char *state) { if (!stack || !state) return; + guint max_states = tab && tab->win && tab->win->max_undo_states > 0u + ? tab->win->max_undo_states + : GRAPTOS_MAX_UNDO_STATES; g_ptr_array_add(stack, state); - while (stack->len > GRAPTOS_MAX_UNDO_STATES) g_ptr_array_remove_index(stack, 0); + while (stack->len > max_states) g_ptr_array_remove_index(stack, 0); } @@ -57,7 +60,10 @@ void reset_undo_state(EditorTab *tab) { g_free(tab->last_snapshot); tab->last_snapshot = NULL; if (tab->buffer && - (guint)gtk_text_buffer_get_char_count(tab->buffer) <= GRAPTOS_MAX_UNDO_CAPTURE_BYTES) { + (guint)gtk_text_buffer_get_char_count(tab->buffer) <= + (tab->win && tab->win->max_undo_capture_bytes > 0u + ? tab->win->max_undo_capture_bytes + : GRAPTOS_MAX_UNDO_CAPTURE_BYTES)) { tab->last_snapshot = buffer_text(tab); } } @@ -73,7 +79,7 @@ void editor_tab_undo(EditorTab *tab) { char *prev = pop_stack(tab->undo_stack); if (!prev) return; char *current = buffer_text(tab); - push_limited(tab->redo_stack, current); + push_limited(tab, tab->redo_stack, current); tab->applying_change = TRUE; gtk_text_buffer_set_text(tab->buffer, prev, -1); tab->applying_change = FALSE; @@ -100,7 +106,7 @@ void editor_tab_redo(EditorTab *tab) { char *next = pop_stack(tab->redo_stack); if (!next) return; char *current = buffer_text(tab); - push_limited(tab->undo_stack, current); + push_limited(tab, tab->undo_stack, current); tab->applying_change = TRUE; gtk_text_buffer_set_text(tab->buffer, next, -1); tab->applying_change = FALSE; @@ -115,4 +121,3 @@ void editor_tab_redo(EditorTab *tab) { editor_tab_update_title(tab); editor_tab_update_status(tab); } -