meltygui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import time
|
|
3
|
+
import weakref
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import meltygui_imgui as imgui
|
|
7
|
+
|
|
8
|
+
from meltygui.state.model_enums import RelaxedEnum
|
|
9
|
+
from meltygui.core.rendering.modes import Modes
|
|
10
|
+
from meltygui.core.rendering.render_funcs import RenderFuncs
|
|
11
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
12
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
13
|
+
from meltygui.core.rendering.window_decoration import window
|
|
14
|
+
from meltygui.view.header_view import draw_header
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Change:
|
|
18
|
+
"""One recorded edit (or coalesced burst): a draw_state's value moving from
|
|
19
|
+
`old` to `new`. `ui` is a snapshot of the draw_state's transient UI state
|
|
20
|
+
(caret/selection/scroll) captured before the edit, restored alongside `old`
|
|
21
|
+
on undo. While a burst keeps coalescing, `new`/`t`/`ui_after` advance but
|
|
22
|
+
`old`/`ui`/`direction` stay pinned to the group's start, so one undo reverts
|
|
23
|
+
the whole burst and drops the caret where it began. `ui_after` is the caret
|
|
24
|
+
snapshot at the group's current end, used by redo so re-applying lands the
|
|
25
|
+
caret after the redone text. `t` is the wall-clock time of the last edit
|
|
26
|
+
folded in; `direction` is 'insert'/'delete'/'replace'/None (None for non-text
|
|
27
|
+
values). Typing runs (UndoManager._can_coalesce) also track `edit_end` —
|
|
28
|
+
the buffer offset where the run currently ends, the caret edge the next
|
|
29
|
+
keystroke must touch to fold in — and `edge_char`, the last character the
|
|
30
|
+
run inserted or removed in typing order (the word-step test reads it).
|
|
31
|
+
`sealed` closes a change to further folding: a standalone edit (paste,
|
|
32
|
+
Enter, a replaced selection) is born sealed, and UndoStack.seal_top seals
|
|
33
|
+
whatever sits on top after an undo/redo."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, draw_state, old, new, ui=None, t=0.0, direction=None, ui_after=None,
|
|
36
|
+
group_id=0, frame=0, edit_end=None, edge_char="", sealed=False):
|
|
37
|
+
self.draw_state = draw_state
|
|
38
|
+
self.old = old
|
|
39
|
+
self.new = new
|
|
40
|
+
self.ui = ui
|
|
41
|
+
self.ui_after = ui_after
|
|
42
|
+
self.t = t
|
|
43
|
+
self.direction = direction
|
|
44
|
+
self.edit_end = edit_end
|
|
45
|
+
self.edge_char = edge_char
|
|
46
|
+
self.sealed = sealed
|
|
47
|
+
# Undo group: changes from one user action (e.g. multiple views reacting to
|
|
48
|
+
# the same edit, recorded within a frame or two) share a group_id and are
|
|
49
|
+
# undone/redone together. `frame` is the frame_count the change last
|
|
50
|
+
# touched, used to decide group membership.
|
|
51
|
+
self.group_id = group_id
|
|
52
|
+
self.frame = frame
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def display_name(self):
|
|
56
|
+
return getattr(self.draw_state, "name", None) or "?"
|
|
57
|
+
|
|
58
|
+
def apply(self, undo):
|
|
59
|
+
"""Re-apply one side of this change (undo → `old`, redo → `new`).
|
|
60
|
+
Subclasses override this — it's the ONLY kind-specific hook, so
|
|
61
|
+
UndoStack never inspects change types."""
|
|
62
|
+
if undo:
|
|
63
|
+
UndoManager._request(self.draw_state, self.old, self.ui)
|
|
64
|
+
else:
|
|
65
|
+
UndoManager._request(self.draw_state, self.new, self.ui_after)
|
|
66
|
+
#
|
|
67
|
+
# def __repr__(self):
|
|
68
|
+
# name = getattr(self.draw_state, "name", "?")
|
|
69
|
+
# return f"Change({name}: {self.old!r} -> {self.new!r})"
|
|
70
|
+
|
|
71
|
+
class SetterChange(Change):
|
|
72
|
+
"""A value change whose owner has no wrapper of its own to land an undo
|
|
73
|
+
through — draw_tuple_fast's colour chips (an immediate-mode chip in a
|
|
74
|
+
host body: several share the host's draw_state, told apart by `key`,
|
|
75
|
+
the chip's view_id). Replay calls `setter(value)`, the write the caller
|
|
76
|
+
would have made, instead of routing through Melty.undo_requests."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, draw_state, old, new, setter, key=None, label=None, **kw):
|
|
79
|
+
super().__init__(draw_state, old, new, **kw)
|
|
80
|
+
self.setter = setter
|
|
81
|
+
self.key = key
|
|
82
|
+
self.label = label
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def display_name(self):
|
|
86
|
+
if self.label:
|
|
87
|
+
return self.label
|
|
88
|
+
base = getattr(self.draw_state, "name", None) or "?"
|
|
89
|
+
return f"{base}/{self.key}" if self.key else base
|
|
90
|
+
|
|
91
|
+
def apply(self, undo):
|
|
92
|
+
try:
|
|
93
|
+
self.setter(self.old if undo else self.new)
|
|
94
|
+
finally:
|
|
95
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
96
|
+
cache = getattr(Core.melty, "cache", None)
|
|
97
|
+
if cache is not None and getattr(self.draw_state, "_tile_id", None) is not None:
|
|
98
|
+
cache.invalidate_up(self.draw_state._tile_id, force=True)
|
|
99
|
+
request_render()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class UndoStack:
|
|
103
|
+
"""One independent undo/redo timeline. The edit stack (UndoManager.stack)
|
|
104
|
+
and the navigation stack (NavUndo.stack) are instances; adding another
|
|
105
|
+
timeline is: make an UndoStack, push Change subclasses into it, and wire
|
|
106
|
+
something to its undo()/redo(). Changes re-apply THEMSELVES (Change.apply)
|
|
107
|
+
— a stack never inspects change kinds. Grouping (several changes from one
|
|
108
|
+
user action undoing as a unit) rides group_id, same as before the split."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, name, maxlen=128):
|
|
111
|
+
self.name = name
|
|
112
|
+
self.history = collections.deque(maxlen=maxlen)
|
|
113
|
+
# undo() moves a popped group here; redo() moves it back. Any fresh
|
|
114
|
+
# push clears this - you can't redo after diverging.
|
|
115
|
+
self.redo_stack = collections.deque(maxlen=maxlen)
|
|
116
|
+
self._next_group_id = 0
|
|
117
|
+
|
|
118
|
+
def new_group_id(self):
|
|
119
|
+
self._next_group_id += 1
|
|
120
|
+
return self._next_group_id
|
|
121
|
+
|
|
122
|
+
def push(self, change):
|
|
123
|
+
self.redo_stack.clear()
|
|
124
|
+
self.history.append(change)
|
|
125
|
+
|
|
126
|
+
def can_undo(self):
|
|
127
|
+
return bool(self.history)
|
|
128
|
+
|
|
129
|
+
def can_redo(self):
|
|
130
|
+
return bool(self.redo_stack)
|
|
131
|
+
|
|
132
|
+
def _pop_group(self):
|
|
133
|
+
"""Pop the newest group (all trailing changes sharing the top group_id)
|
|
134
|
+
off `history`, newest-first."""
|
|
135
|
+
if not self.history:
|
|
136
|
+
return []
|
|
137
|
+
gid = self.history[-1].group_id
|
|
138
|
+
group = []
|
|
139
|
+
while self.history and self.history[-1].group_id == gid:
|
|
140
|
+
group.append(self.history.pop())
|
|
141
|
+
return group
|
|
142
|
+
|
|
143
|
+
def seal_top(self):
|
|
144
|
+
"""Close the newest group to further folding. Called after every
|
|
145
|
+
undo/redo: the next keystroke is a fresh step, never a continuation
|
|
146
|
+
of the step the replay just exposed or restored (typing right after
|
|
147
|
+
an undo must not grow the older word — IntelliJ flushes its command
|
|
148
|
+
merger the same way)."""
|
|
149
|
+
if not self.history:
|
|
150
|
+
return
|
|
151
|
+
gid = self.history[-1].group_id
|
|
152
|
+
for change in reversed(self.history):
|
|
153
|
+
if change.group_id != gid:
|
|
154
|
+
break
|
|
155
|
+
change.sealed = True
|
|
156
|
+
|
|
157
|
+
def undo(self):
|
|
158
|
+
group = self._pop_group()
|
|
159
|
+
if not group:
|
|
160
|
+
return
|
|
161
|
+
self.redo_stack.append(group)
|
|
162
|
+
for change in group:
|
|
163
|
+
change.apply(undo=True)
|
|
164
|
+
self.seal_top()
|
|
165
|
+
|
|
166
|
+
def redo(self):
|
|
167
|
+
if not self.redo_stack:
|
|
168
|
+
return
|
|
169
|
+
group = self.redo_stack.pop()
|
|
170
|
+
for change in reversed(group): # restore original append order
|
|
171
|
+
self.history.append(change)
|
|
172
|
+
# Replay in the order the changes were recorded (undo walked them
|
|
173
|
+
# newest-first). Two changes in one group that affect the same target
|
|
174
|
+
# (a tab switch and the caret placed by it) must end where the LATER one
|
|
175
|
+
# left it, so the last applied is the newest.
|
|
176
|
+
for change in reversed(group):
|
|
177
|
+
change.apply(undo=False)
|
|
178
|
+
self.seal_top()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class NavChange(Change):
|
|
182
|
+
"""A file-navigation step (editor tab switch / jump-to). `old`/`new` are
|
|
183
|
+
(path, line, instance) locations — not values — and there is no
|
|
184
|
+
draw_state: replay navigates (open_in_editor / tab select) instead of
|
|
185
|
+
writing a value back through the wrapper. line None means "wherever that
|
|
186
|
+
file's editor last left its caret" (each file's draw_text keeps its own
|
|
187
|
+
caret/scroll on its persistent draw_state); instance is the code-editor
|
|
188
|
+
window the step happened in, so replay lands in the same window."""
|
|
189
|
+
|
|
190
|
+
def __init__(self, old_loc, new_loc, t=0.0, group_id=0, frame=0):
|
|
191
|
+
super().__init__(None, old_loc, new_loc, t=t, group_id=group_id,
|
|
192
|
+
frame=frame)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def display_name(self):
|
|
196
|
+
return "goto"
|
|
197
|
+
|
|
198
|
+
def apply(self, undo):
|
|
199
|
+
NavUndo._apply_location(self.old if undo else self.new)
|
|
200
|
+
|
|
201
|
+
def file_location(self, undo):
|
|
202
|
+
"""The (path, line, instance) this side lands in (dock tooltips /
|
|
203
|
+
tints), or None."""
|
|
204
|
+
return self.old if undo else self.new
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class CaretLocation:
|
|
208
|
+
"""Where the text caret sits: a focused draw_text and its caret /
|
|
209
|
+
selection offsets. `tile_id` is the identity used for equality and for
|
|
210
|
+
resolving the LIVE draw_state on replay (a rebuilt tile hands out a fresh
|
|
211
|
+
draw_state object for the same tile — the weakref is only the fallback).
|
|
212
|
+
`path`/`instance` are set when the view is a code-editor pane (the tab
|
|
213
|
+
to select and the editor window to raise on replay); `line` is the
|
|
214
|
+
FULL-buffer caret line for coalescing and the dock's target tooltip."""
|
|
215
|
+
|
|
216
|
+
__slots__ = ("draw_state_ref", "tile_id", "cursor", "selection_start",
|
|
217
|
+
"selection_end", "path", "instance", "line")
|
|
218
|
+
|
|
219
|
+
def __init__(self, draw_state, cursor, selection_start, selection_end,
|
|
220
|
+
path=None, instance=0, line=None):
|
|
221
|
+
self.draw_state_ref = weakref.ref(draw_state)
|
|
222
|
+
self.tile_id = draw_state._tile_id
|
|
223
|
+
self.cursor = cursor
|
|
224
|
+
self.selection_start = selection_start
|
|
225
|
+
self.selection_end = selection_end
|
|
226
|
+
self.path = path
|
|
227
|
+
self.instance = instance
|
|
228
|
+
self.line = line
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def key(self):
|
|
232
|
+
return (self.tile_id, self.cursor, self.selection_start,
|
|
233
|
+
self.selection_end)
|
|
234
|
+
|
|
235
|
+
def __eq__(self, other):
|
|
236
|
+
return isinstance(other, CaretLocation) and self.key == other.key
|
|
237
|
+
|
|
238
|
+
def __hash__(self):
|
|
239
|
+
return hash(self.key)
|
|
240
|
+
|
|
241
|
+
def draw_state(self):
|
|
242
|
+
"""The live draw_state for this tile — the cache's current object
|
|
243
|
+
first (rebuilt tiles), the recorded one as fallback, None when the
|
|
244
|
+
view is gone."""
|
|
245
|
+
cache = getattr(Core.melty, "cache", None)
|
|
246
|
+
live = None
|
|
247
|
+
if cache is not None and self.tile_id is not None:
|
|
248
|
+
live = cache.key_to_draw_state.get(self.tile_id)
|
|
249
|
+
return live if live is not None else self.draw_state_ref()
|
|
250
|
+
|
|
251
|
+
def __repr__(self):
|
|
252
|
+
if self.path:
|
|
253
|
+
where = self.path.rsplit("/", 1)[-1]
|
|
254
|
+
return f"{where}:{self.line + 1}" if self.line is not None else where
|
|
255
|
+
draw_state = self.draw_state_ref()
|
|
256
|
+
name = str(getattr(draw_state, "name", "?") or "?").split("##")[0]
|
|
257
|
+
return f"{name}@{self.cursor}"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CaretChange(Change):
|
|
261
|
+
"""A caret / text-focus step: `old`/`new` are CaretLocations — the
|
|
262
|
+
focused draw_text and where its caret sat. Consecutive small moves in the
|
|
263
|
+
same view fold into one change (NavUndo.record_caret), so an arrow-key
|
|
264
|
+
walk is one step back. No draw_state on the change itself: replay resolves
|
|
265
|
+
the LIVE view through the location (NavUndo._apply_caret) and writes the
|
|
266
|
+
caret + focus directly, summoning the view's tab / window first."""
|
|
267
|
+
|
|
268
|
+
def __init__(self, old_loc, new_loc, t=0.0, group_id=0, frame=0):
|
|
269
|
+
super().__init__(None, old_loc, new_loc, t=t, group_id=group_id,
|
|
270
|
+
frame=frame)
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def display_name(self):
|
|
274
|
+
return "caret"
|
|
275
|
+
|
|
276
|
+
def apply(self, undo):
|
|
277
|
+
NavUndo._apply_caret(self.old if undo else self.new)
|
|
278
|
+
|
|
279
|
+
def file_location(self, undo):
|
|
280
|
+
location = self.old if undo else self.new
|
|
281
|
+
if location is None or not location.path:
|
|
282
|
+
return None
|
|
283
|
+
return (location.path,
|
|
284
|
+
None if location.line is None else location.line + 1,
|
|
285
|
+
location.instance)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class WindowChange(Change):
|
|
289
|
+
"""A window open/close step. `draw_state` is the WINDOW's draw_state;
|
|
290
|
+
`old`/`new` are the `closed` flag before/after the user's toggle. Replay
|
|
291
|
+
just writes the flag back (raising the window when it reopens)."""
|
|
292
|
+
|
|
293
|
+
def __init__(self, window_ds, closed_before, closed_after, t=0.0,
|
|
294
|
+
group_id=0, frame=0):
|
|
295
|
+
super().__init__(window_ds, closed_before, closed_after, t=t,
|
|
296
|
+
group_id=group_id, frame=frame)
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def display_name(self):
|
|
300
|
+
base = str(getattr(self.draw_state, "name", "?")).split("##")[0]
|
|
301
|
+
return f"{'close' if self.new else 'open'} {base}"
|
|
302
|
+
|
|
303
|
+
def apply(self, undo):
|
|
304
|
+
wds = self.draw_state
|
|
305
|
+
wds.closed = self.old if undo else self.new
|
|
306
|
+
if not wds.closed:
|
|
307
|
+
Core.melty.move_window_to_front(wds)
|
|
308
|
+
# Repaint the window's own subtree (a reopen must redraw its content,
|
|
309
|
+
# a close must clear its blit from the compositor) ...
|
|
310
|
+
if Core.melty.cache is not None and wds._tile_id is not None:
|
|
311
|
+
Core.melty.cache.invalidate_up(wds._tile_id, force=True, max_depth=4)
|
|
312
|
+
# ... and the dock/window list rows, same frame for interactive paths.
|
|
313
|
+
Core.melty.cache.invalidate_up_by_obj(Core.melty.registered_windows)
|
|
314
|
+
request_render()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class WindowMoveChange(Change):
|
|
318
|
+
"""A window move step: `draw_state` is the WINDOW's draw_state, `old`/`new`
|
|
319
|
+
its window_pos before/after one hand drag (recorded at gesture END by
|
|
320
|
+
core_render's window_move block, so a whole drag is one step). Replay
|
|
321
|
+
writes the position back — window_pos is parent-relative for nested
|
|
322
|
+
windows, and the recorded value is in that same space. A window closed
|
|
323
|
+
since the move still takes the write (invisible until reopened); replay
|
|
324
|
+
never reopens or raises for a move alone."""
|
|
325
|
+
|
|
326
|
+
def __init__(self, window_ds, old_pos, new_pos, t=0.0, group_id=0,
|
|
327
|
+
frame=0):
|
|
328
|
+
super().__init__(window_ds, (old_pos[0], old_pos[1]),
|
|
329
|
+
(new_pos[0], new_pos[1]), t=t, group_id=group_id,
|
|
330
|
+
frame=frame)
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def display_name(self):
|
|
334
|
+
base = str(getattr(self.draw_state, "name", "?")).split("##")[0]
|
|
335
|
+
return f"move {base}"
|
|
336
|
+
|
|
337
|
+
def apply(self, undo):
|
|
338
|
+
wds = self.draw_state
|
|
339
|
+
wds.window_pos = self.old if undo else self.new
|
|
340
|
+
if Core.melty.cache is not None and wds._tile_id is not None:
|
|
341
|
+
Core.melty.cache.invalidate_up(wds._tile_id, force=True, max_depth=4)
|
|
342
|
+
request_render()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@window(view_func=RenderFuncs.draw_undo_manager, live=True)
|
|
348
|
+
class UndoManager:
|
|
349
|
+
# draw_state -> ordered list of Changes recorded for that node. Safe to key
|
|
350
|
+
# on the draw_state object: DrawState uses identity equality and hashes on
|
|
351
|
+
# its unique id, so distinct nodes never collide as keys.
|
|
352
|
+
change_history = {}
|
|
353
|
+
MAX_HISTORY = 128
|
|
354
|
+
|
|
355
|
+
# Only record a change when both old and new are one of these immutable
|
|
356
|
+
# primitives. Snapshotting a mutable object by reference is unsound - it
|
|
357
|
+
# could be aliased and mutated after the fact, so undo would restore the
|
|
358
|
+
# wrong value. Start with types that are safe to keep by reference; widen
|
|
359
|
+
# as snapshotting for richer types is implemented.
|
|
360
|
+
APPROVED_TYPES = (float, int, str, bool, tuple, Enum, RelaxedEnum)
|
|
361
|
+
|
|
362
|
+
# The EDIT timeline (value changes). Navigation lives on its own stack -
|
|
363
|
+
# NavUndo.stack - so Ctrl+Z never yanks the viewport and Ctrl+Shift+arrows
|
|
364
|
+
# never deletes text. `history`/`redo_stack` alias the stack's deques (same
|
|
365
|
+
# objects) for the render func and older call sites.
|
|
366
|
+
stack = UndoStack("edits", maxlen=MAX_HISTORY)
|
|
367
|
+
history = stack.history
|
|
368
|
+
redo_stack = stack.redo_stack
|
|
369
|
+
|
|
370
|
+
# Text coalescing mirrors IntelliJ's undo merge (see _can_coalesce, knobs
|
|
371
|
+
# in Tweak.CodeEditor.max_word_wrap and undo_typing_max_chars). Each
|
|
372
|
+
# keystroke folds into the previous step only while the value stays
|
|
373
|
+
# contiguous in VALUE (the step's `new` is this edit's `old` - an undo or
|
|
374
|
+
# an external write breaks it) and in POSITION (the edit lands on the
|
|
375
|
+
# run's caret edge - typing somewhere else breaks it), keeps its
|
|
376
|
+
# insert/delete direction, and doesn't start a new word (a non-space
|
|
377
|
+
# right after whitespace; backspace runs mirror it). There is deliberately
|
|
378
|
+
# NO pause timeout for text - a step stores what was typed in one place,
|
|
379
|
+
# however slowly - but nothing folds across an undo/redo (UndoStack.seal_top).
|
|
380
|
+
# Edits that aren't keystroke-sized (a newline, more than
|
|
381
|
+
# undo_typing_max_chars characters, a replaced selection) are standalone
|
|
382
|
+
# steps: paste, Enter + auto-indent, Tab, completions, comment/un etc.
|
|
383
|
+
# Non-text values (floats/ints) keep the timer: a held widget streams a
|
|
384
|
+
# value per frame, and COALESCE_WINDOW + _imgui_is_active make the drag
|
|
385
|
+
# one undo.
|
|
386
|
+
COALESCE_WINDOW = 0.6
|
|
387
|
+
|
|
388
|
+
# Cross-view grouping: one user action can make several different views record
|
|
389
|
+
# a change in the same frame (or a frame or two apart, when a derived view
|
|
390
|
+
# updates a tick later). Changes whose frames are within GROUP_FRAME_WINDOW of
|
|
391
|
+
# the previous change join the same group and undo/redo as a unit, so the user
|
|
392
|
+
# doesn't see edits alternating back and forth between views. (Same-draw_state
|
|
393
|
+
# bursts still fold in _can_coalesce regardless of frame distance.)
|
|
394
|
+
GROUP_FRAME_WINDOW = 2
|
|
395
|
+
|
|
396
|
+
settle_for = 2 # 2 frame at start
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def _request(cls, ds, value, ui):
|
|
400
|
+
# Register a (value, ui) request with Melty for `ds`. next_render
|
|
401
|
+
# intercepts that draw_state's return next frame and reports (True, value)
|
|
402
|
+
# with the caret/selection restored. Its parent then writes `value` back
|
|
403
|
+
# into the model, exactly as if the user had typed it.
|
|
404
|
+
Core.melty.undo_requests[ds] = (value, ui)
|
|
405
|
+
# Force the target and its parent wrapper to re-render this frame so the
|
|
406
|
+
# restored data actually propagates: a blitted parent would otherwise never
|
|
407
|
+
# call the child wrapper that performs the interception.
|
|
408
|
+
cache = getattr(Core.melty, "cache", None)
|
|
409
|
+
if cache is not None:
|
|
410
|
+
cache.invalidate_up(ds._tile_id, force=True)
|
|
411
|
+
if ds._parent is not None:
|
|
412
|
+
cache.invalidate_up(ds._parent._tile_id, force=True)
|
|
413
|
+
request_render()
|
|
414
|
+
|
|
415
|
+
@classmethod
|
|
416
|
+
def undo(cls):
|
|
417
|
+
cls.stack.undo()
|
|
418
|
+
|
|
419
|
+
@classmethod
|
|
420
|
+
def redo(cls):
|
|
421
|
+
cls.stack.redo()
|
|
422
|
+
|
|
423
|
+
@classmethod
|
|
424
|
+
def _can_coalesce(cls, last, draw_state, old, new, now, edit):
|
|
425
|
+
"""Whether this edit (old -> new) should fold into `last` instead of
|
|
426
|
+
starting a new step. `edit` is the _TypingEdit for keystroke-sized
|
|
427
|
+
text edits; None for non-text values and for standalone text edits."""
|
|
428
|
+
if last is None or last.draw_state is not draw_state or last.sealed:
|
|
429
|
+
return False
|
|
430
|
+
if isinstance(old, str):
|
|
431
|
+
if edit is None: # paste / Enter / replace
|
|
432
|
+
return False
|
|
433
|
+
return cls._typing_continues(last, old, edit) is not None
|
|
434
|
+
if now - last.t > cls.COALESCE_WINDOW: # pause -> commit group
|
|
435
|
+
return False
|
|
436
|
+
# Non-text (numbers/tuples): one for a continuous drag - a held
|
|
437
|
+
# widget streams a value every frame. Exact value-contiguity is the
|
|
438
|
+
# wrong test here: a drag_float returns float32-precision values
|
|
439
|
+
# (3.828000068664551) that read back next frame as a clean 3.828, so
|
|
440
|
+
# `last.new == old` on every frame and each frame becomes its own
|
|
441
|
+
# undo step. The reliable "one gesture" signal is the widget being
|
|
442
|
+
# actively dragged; discrete clicks/taps aren't held, so they
|
|
443
|
+
# stay discrete undo steps.
|
|
444
|
+
return _gesture_live(draw_state)
|
|
445
|
+
|
|
446
|
+
@classmethod
|
|
447
|
+
def _typing_continues(cls, last, old, edit):
|
|
448
|
+
"""The text rule: `edit` extends the typing run `last` holds. Value
|
|
449
|
+
contiguity (an undo / external write in between shows as a mismatch),
|
|
450
|
+
same direction, position contiguity (the edit touches the run's caret
|
|
451
|
+
edge — an insert right at it, a Backspace ending at it or a Delete
|
|
452
|
+
starting at it), and no new word starting inside `edge_char + typed`
|
|
453
|
+
(typed in typing order, so a Backspace run reads its removed text
|
|
454
|
+
reversed). Returns None to start a new step, else the resolved
|
|
455
|
+
buffer offset the edit really happened at (the diff's span is the
|
|
456
|
+
rightmost of its equivalent placements — deleting one 'l' of "ll"
|
|
457
|
+
reads at the second 'l' — and `_span_reaches` slides it back to the
|
|
458
|
+
run's edge)."""
|
|
459
|
+
if last.new != old or last.direction != edit.kind or last.edit_end is None:
|
|
460
|
+
return None
|
|
461
|
+
if edit.kind == "insert":
|
|
462
|
+
if not _span_reaches(old, edit, last.edit_end):
|
|
463
|
+
return None
|
|
464
|
+
pos, typed = last.edit_end, edit.text
|
|
465
|
+
elif _span_reaches(old, edit, last.edit_end - len(edit.text)): # Backspace
|
|
466
|
+
pos, typed = last.edit_end - len(edit.text), edit.text[::-1]
|
|
467
|
+
elif _span_reaches(old, edit, last.edit_end): # Delete key
|
|
468
|
+
pos, typed = last.edit_end, edit.text
|
|
469
|
+
else:
|
|
470
|
+
return None # edited elsewhere
|
|
471
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
472
|
+
if Toggles.CodeEditor.undo_word_steps and _word_starts(last.edge_char + typed):
|
|
473
|
+
return None
|
|
474
|
+
return pos
|
|
475
|
+
|
|
476
|
+
@classmethod
|
|
477
|
+
def record(cls, draw_state, old, new, setter=None, key=None, label=None):
|
|
478
|
+
"""Log one edit. `setter` (with `key`, `label`) makes it a
|
|
479
|
+
SetterChange — an editor with no wrapper of its own (draw_tuple_fast)
|
|
480
|
+
supplies the write undo/redo must make; `key` tells the chips that
|
|
481
|
+
share one draw_state apart for coalescing."""
|
|
482
|
+
if Core.melty.frame_count < cls.settle_for:
|
|
483
|
+
return
|
|
484
|
+
# Collection mutations (drag-drop reorders, see drag_drop.py) are
|
|
485
|
+
# frozen "insert x at key y"-style records - safe to hold by
|
|
486
|
+
# reference like the approved primitives, and the whole point of
|
|
487
|
+
# them is not snapshotting the dict they edit. `old` is the
|
|
488
|
+
# inverse mutation, `new` the applied one; undo/redo apply either
|
|
489
|
+
# side to the live collection via the wrapper-tail interception.
|
|
490
|
+
is_mutation = (getattr(old, "__collection_mutation__", False)
|
|
491
|
+
and getattr(new, "__collection_mutation__", False))
|
|
492
|
+
if not is_mutation and not (isinstance(old, cls.APPROVED_TYPES)
|
|
493
|
+
and isinstance(new, cls.APPROVED_TYPES)):
|
|
494
|
+
return
|
|
495
|
+
# Skip no- change. Several renderers (draw_text, draw_collection, the
|
|
496
|
+
# @window source views) report changed=True every frame with old == new.
|
|
497
|
+
# Logging those floods the bounded history deque and evicts the real
|
|
498
|
+
# edits, so undo ends up restoring an identical value (a visible no-op).
|
|
499
|
+
# An undo entry where nothing changed is pointless by definition.
|
|
500
|
+
try:
|
|
501
|
+
if old == new:
|
|
502
|
+
return
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
# A genuine new edit (only origins reach here; undo/redo restores bypass
|
|
507
|
+
# record().) diverges from any undone changes - drop redo.
|
|
508
|
+
cls.redo_stack.clear()
|
|
509
|
+
|
|
510
|
+
now = time.time()
|
|
511
|
+
frame = Core.melty.frame_count
|
|
512
|
+
is_text = isinstance(old, str) and isinstance(new, str)
|
|
513
|
+
edit = _typing_edit(old, new) if is_text else None
|
|
514
|
+
if edit is not None:
|
|
515
|
+
direction = edit.kind
|
|
516
|
+
else:
|
|
517
|
+
direction = _edit_direction(old, new) if is_text else None
|
|
518
|
+
# Post-edit caret: record() runs in the wrapper frame (after the body), so
|
|
519
|
+
# the draw_state's caret now reflects the result of this edit. Redo needs it.
|
|
520
|
+
ui_after = draw_state.capture_undo_state() if hasattr(draw_state, "capture_undo_state") else None
|
|
521
|
+
|
|
522
|
+
# Fold target: the change for THIS draw_state in the TOP group. Only the
|
|
523
|
+
# top group: any other user action landing on the stack (another page's
|
|
524
|
+
# step, a non-text change) closes the typing run, as in Excel. Views
|
|
525
|
+
# responding to the SAME keystroke share its group (GROUP_FRAME_WINDOW), so
|
|
526
|
+
# a cascade landing between two keystrokes doesn't fold them - searching
|
|
527
|
+
# the group rather than just history[-1] is what keeps that in happening.
|
|
528
|
+
target = None
|
|
529
|
+
top_gid = cls.history[-1].group_id if cls.history else None
|
|
530
|
+
for c in reversed(cls.history):
|
|
531
|
+
if c.group_id != top_gid:
|
|
532
|
+
break
|
|
533
|
+
if c.draw_state is draw_state and getattr(c, "key", None) == key:
|
|
534
|
+
target = c
|
|
535
|
+
break
|
|
536
|
+
if target is not None and cls._can_coalesce(target, draw_state, old, new, now, edit):
|
|
537
|
+
if edit is not None:
|
|
538
|
+
pos = cls._typing_continues(target, old, edit)
|
|
539
|
+
target.edit_end, target.edge_char = _run_edge(edit, pos, target.edit_end)
|
|
540
|
+
target.new = new
|
|
541
|
+
target.t = now
|
|
542
|
+
target.ui_after = ui_after
|
|
543
|
+
target.frame = frame
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
# New change. Join the previous change's group when they're within
|
|
547
|
+
# GROUP_FRAME_WINDOW frames (same user action / cascade) - unless that
|
|
548
|
+
# group already holds a change for this draw_state, in which case this is a
|
|
549
|
+
# fresh semantic step (e.g. a new word boundary) and gets its own group.
|
|
550
|
+
gid = None
|
|
551
|
+
last = cls.history[-1] if cls.history else None
|
|
552
|
+
if last is not None and (frame - last.frame) <= cls.GROUP_FRAME_WINDOW:
|
|
553
|
+
gid = last.group_id
|
|
554
|
+
for c in reversed(cls.history):
|
|
555
|
+
if c.group_id != gid:
|
|
556
|
+
break
|
|
557
|
+
if c.draw_state is draw_state and getattr(c, "key", None) == key:
|
|
558
|
+
gid = None # ds already in this group -> new group
|
|
559
|
+
break
|
|
560
|
+
if gid is None:
|
|
561
|
+
gid = cls.stack.new_group_id()
|
|
562
|
+
|
|
563
|
+
if edit is not None:
|
|
564
|
+
# A run's first edit: the diff's span is ambiguous next to repeated
|
|
565
|
+
# characters, so trust the post-edit caret when it names one of the
|
|
566
|
+
# equivalent placements (rendered display text can put the caret in
|
|
567
|
+
# other coordinates - then it's none and the diff's own stands).
|
|
568
|
+
caret = ui_after.get("text_cursor_pos") if ui_after else None
|
|
569
|
+
pos = edit.pos
|
|
570
|
+
if isinstance(caret, int):
|
|
571
|
+
want = caret - len(edit.text) if edit.kind == "insert" else caret
|
|
572
|
+
if _span_reaches(old, edit, want):
|
|
573
|
+
pos = want
|
|
574
|
+
edit_end, edge_char = _run_edge(edit, pos, None)
|
|
575
|
+
else:
|
|
576
|
+
edit_end, edge_char = None, ""
|
|
577
|
+
if setter is not None:
|
|
578
|
+
change = SetterChange(draw_state, old, new, setter, key=key, label=label,
|
|
579
|
+
t=now, group_id=gid, frame=frame)
|
|
580
|
+
else:
|
|
581
|
+
change = Change(draw_state, old, new, ui=getattr(draw_state, "_undo_pre", None),
|
|
582
|
+
t=now, direction=direction, ui_after=ui_after, group_id=gid, frame=frame,
|
|
583
|
+
edit_end=edit_end, edge_char=edge_char,
|
|
584
|
+
sealed=is_text and edit is None) # paste / Enter / replace: own group
|
|
585
|
+
cls.history.append(change)
|
|
586
|
+
|
|
587
|
+
# while len(cls.history) > cls.MAX_HISTORY:
|
|
588
|
+
# evicted = cls.history.popleft()
|
|
589
|
+
# per_node = cls.change_history.get(evicted.draw_state)
|
|
590
|
+
# if per_node:
|
|
591
|
+
# per_node.remove(evicted)
|
|
592
|
+
# if not per_node:
|
|
593
|
+
# del cls.change_history[evicted.draw_state]
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class NavUndo:
|
|
597
|
+
"""The navigation timeline — a separate UndoStack from text edits, so
|
|
598
|
+
stepping back through WHERE you were never touches WHAT you typed.
|
|
599
|
+
Records location moves (editor tab switches, jump-tos), window
|
|
600
|
+
open/close toggles, and caret / text-focus steps (poll_caret, once per
|
|
601
|
+
frame from Melty.end_frame). Driven by Ctrl+Shift+Left/Right (root
|
|
602
|
+
handler in new_core_view) and the Fast Dock back/forward buttons. Gated
|
|
603
|
+
on Toggles.CodeEditor.undo_navigation (caret steps additionally on
|
|
604
|
+
undo_navigation_caret)."""
|
|
605
|
+
|
|
606
|
+
stack = UndoStack("navigation")
|
|
607
|
+
|
|
608
|
+
# Reentrancy guard: while undo/redo replays, the navigation it triggers
|
|
609
|
+
# (open_in_editor → tab select + jump-to, window closed writes) should not
|
|
610
|
+
# record fresh entries.
|
|
611
|
+
_restoring = False
|
|
612
|
+
|
|
613
|
+
# Caret detector state: where the focused draw_text's caret was at the
|
|
614
|
+
# last poll (a CaretLocation), and the frame up to which caret moves are
|
|
615
|
+
# NOT recorded - armed by every other record_* (a jump / tab switch moves
|
|
616
|
+
# the caret itself; the nav step already covers it), by the jump
|
|
617
|
+
# consumer in draw_code_editor (quiet_caret), and by undo/redo (a replay
|
|
618
|
+
# moves the caret too).
|
|
619
|
+
_last_caret = None
|
|
620
|
+
_caret_quiet_until = -1
|
|
621
|
+
|
|
622
|
+
@classmethod
|
|
623
|
+
def _recordable(cls):
|
|
624
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
625
|
+
return (Toggles.CodeEditor.undo_navigation and not cls._restoring
|
|
626
|
+
and Core.melty.frame_count >= UndoManager.settle_for)
|
|
627
|
+
|
|
628
|
+
@classmethod
|
|
629
|
+
def quiet_caret(cls, frames=None):
|
|
630
|
+
"""Don't record caret moves for the next `frames` frames (default
|
|
631
|
+
UndoManager.GROUP_FRAME_WINDOW): the move about to happen belongs to
|
|
632
|
+
a navigation step that is recorded (or replayed) on its own."""
|
|
633
|
+
if frames is None:
|
|
634
|
+
frames = UndoManager.GROUP_FRAME_WINDOW
|
|
635
|
+
cls._caret_quiet_until = max(cls._caret_quiet_until,
|
|
636
|
+
Core.melty.frame_count + frames)
|
|
637
|
+
|
|
638
|
+
@classmethod
|
|
639
|
+
def record_location(cls, old_loc, new_loc):
|
|
640
|
+
"""Push a location step. Locations are (path, line, instance) tuples
|
|
641
|
+
(line may be None; instance is which code-editor window — replay must
|
|
642
|
+
land in the SAME window, not the primary). Each step is its own
|
|
643
|
+
group."""
|
|
644
|
+
cls.quiet_caret()
|
|
645
|
+
if not cls._recordable():
|
|
646
|
+
return
|
|
647
|
+
if old_loc == new_loc:
|
|
648
|
+
return
|
|
649
|
+
# Re-selecting the already-selected file with no target line moves
|
|
650
|
+
# nothing - not worth an undo step. Same file in the OTHER editor
|
|
651
|
+
# instance is a real move (the [2:] slice keeps pre-instance 2-tuples
|
|
652
|
+
# comparing equal).
|
|
653
|
+
if (new_loc[1] is None and old_loc[0] == new_loc[0]
|
|
654
|
+
and old_loc[2:] == new_loc[2:]):
|
|
655
|
+
return
|
|
656
|
+
cls.stack.push(NavChange(old_loc, new_loc, t=time.time(),
|
|
657
|
+
group_id=cls.stack.new_group_id(),
|
|
658
|
+
frame=Core.melty.frame_count))
|
|
659
|
+
|
|
660
|
+
@classmethod
|
|
661
|
+
def record_compare(cls, instance, old_token, new_token, repo_root=None):
|
|
662
|
+
"""Push a Compare-With selection step (editor dropdown / clear ×)."""
|
|
663
|
+
from meltygui.core.runtime.extensions import call
|
|
664
|
+
call('compare_history', instance, old_token, new_token, repo_root=repo_root)
|
|
665
|
+
|
|
666
|
+
@classmethod
|
|
667
|
+
def record_window(cls, window_ds, closed_before, closed_after):
|
|
668
|
+
"""Push a window open/close step (the user toggled `closed` — dock row
|
|
669
|
+
click, chrome ×)."""
|
|
670
|
+
cls.quiet_caret()
|
|
671
|
+
if not cls._recordable() or closed_before == closed_after:
|
|
672
|
+
return
|
|
673
|
+
cls.stack.push(WindowChange(window_ds, closed_before, closed_after,
|
|
674
|
+
t=time.time(),
|
|
675
|
+
group_id=cls.stack.new_group_id(),
|
|
676
|
+
frame=Core.melty.frame_count))
|
|
677
|
+
|
|
678
|
+
@classmethod
|
|
679
|
+
def record_window_move(cls, window_ds, old_pos, new_pos):
|
|
680
|
+
"""Push a window-move step (the end of one hand drag). Consecutive
|
|
681
|
+
drags of the same window each get their own step — a drag is already
|
|
682
|
+
the natural gesture unit, no coalescing."""
|
|
683
|
+
cls.quiet_caret()
|
|
684
|
+
if not cls._recordable():
|
|
685
|
+
return
|
|
686
|
+
if (old_pos[0], old_pos[1]) == (new_pos[0], new_pos[1]):
|
|
687
|
+
return
|
|
688
|
+
cls.stack.push(WindowMoveChange(window_ds, old_pos, new_pos,
|
|
689
|
+
t=time.time(),
|
|
690
|
+
group_id=cls.stack.new_group_id(),
|
|
691
|
+
frame=Core.melty.frame_count))
|
|
692
|
+
|
|
693
|
+
# ── Caret / text-focus detection ─────────────────────────────────────────
|
|
694
|
+
|
|
695
|
+
@classmethod
|
|
696
|
+
def poll_caret(cls):
|
|
697
|
+
"""Once per frame (Melty.end_frame, after apply_move_to_front settled
|
|
698
|
+
text focus): compare the focused draw_text's caret with the last
|
|
699
|
+
poll and record a CaretChange when it moved — a caret move inside
|
|
700
|
+
one view, or text focus hopping to another view (the old side is
|
|
701
|
+
the view that HELD focus last, even if focus was cleared meanwhile).
|
|
702
|
+
Cheap on the steady state: one 4-tuple compare. Never records
|
|
703
|
+
during the quiet frames a jump / tab switch / replay armed, nor the
|
|
704
|
+
caret move of a text EDIT (the edit stack owns that — typing would
|
|
705
|
+
otherwise leave a nav step at every pause)."""
|
|
706
|
+
draw_state = Core.melty.text_focused_ds
|
|
707
|
+
if draw_state is None or getattr(draw_state, "is_search_box", False):
|
|
708
|
+
return
|
|
709
|
+
last = cls._last_caret
|
|
710
|
+
key = (draw_state._tile_id, draw_state.text_cursor_pos,
|
|
711
|
+
draw_state.text_selection_start, draw_state.text_selection_end)
|
|
712
|
+
if last is not None and last.key == key:
|
|
713
|
+
return
|
|
714
|
+
current = cls._caret_location(draw_state)
|
|
715
|
+
cls._last_caret = current
|
|
716
|
+
if last is None or not cls._recordable():
|
|
717
|
+
return
|
|
718
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
719
|
+
if not Toggles.CodeEditor.undo_navigation_caret:
|
|
720
|
+
return
|
|
721
|
+
frame = Core.melty.frame_count
|
|
722
|
+
if frame <= cls._caret_quiet_until:
|
|
723
|
+
return
|
|
724
|
+
if cls._edited_recently(draw_state, frame):
|
|
725
|
+
return
|
|
726
|
+
cls.record_caret(last, current)
|
|
727
|
+
|
|
728
|
+
@classmethod
|
|
729
|
+
def _caret_location(cls, draw_state):
|
|
730
|
+
"""Snapshot `draw_state`'s caret as a CaretLocation. A code-editor pane (the
|
|
731
|
+
selected tab of an editor instance, per open_files._active_editors)
|
|
732
|
+
carries its path/instance so replay can re-select the tab, and its
|
|
733
|
+
FULL-buffer caret line (the caret offset lives in fold display
|
|
734
|
+
space). Other draw_texts count lines in their raw input when it's a
|
|
735
|
+
string; line None = coalesce on time alone."""
|
|
736
|
+
pos = draw_state.text_cursor_pos
|
|
737
|
+
path, instance, text = None, 0, None
|
|
738
|
+
from meltygui.core.runtime.extensions import source_views
|
|
739
|
+
_active_editors = source_views()
|
|
740
|
+
for editor_instance, (editor_path, pane, held_text) in _active_editors.items():
|
|
741
|
+
if pane is draw_state:
|
|
742
|
+
path, instance, text = editor_path, editor_instance, held_text
|
|
743
|
+
break
|
|
744
|
+
line = None
|
|
745
|
+
if path is not None and isinstance(text, str):
|
|
746
|
+
from meltygui.editor.text_editor import fold_buffer_line_at
|
|
747
|
+
line = fold_buffer_line_at(draw_state, text, max(0, min(pos, len(text))))
|
|
748
|
+
else:
|
|
749
|
+
raw = getattr(draw_state, "_raw_input_value", None)
|
|
750
|
+
if isinstance(raw, str):
|
|
751
|
+
line = raw.count("\n", 0, max(0, min(pos, len(raw))))
|
|
752
|
+
return CaretLocation(draw_state, pos, draw_state.text_selection_start,
|
|
753
|
+
draw_state.text_selection_end, path=path,
|
|
754
|
+
instance=instance, line=line)
|
|
755
|
+
|
|
756
|
+
@classmethod
|
|
757
|
+
def _edited_recently(cls, draw_state, frame):
|
|
758
|
+
"""Whether the edit stack recorded a change to `draw_state` within the last
|
|
759
|
+
GROUP_FRAME_WINDOW frames — i.e. this caret move came from typing /
|
|
760
|
+
deleting, not from navigating."""
|
|
761
|
+
seen = 0
|
|
762
|
+
for change in reversed(UndoManager.history):
|
|
763
|
+
if frame - change.frame > UndoManager.GROUP_FRAME_WINDOW:
|
|
764
|
+
break
|
|
765
|
+
if change.draw_state is draw_state:
|
|
766
|
+
return True
|
|
767
|
+
seen += 1
|
|
768
|
+
if seen >= 8:
|
|
769
|
+
break
|
|
770
|
+
return False
|
|
771
|
+
|
|
772
|
+
@classmethod
|
|
773
|
+
def record_caret(cls, old_loc, new_loc):
|
|
774
|
+
"""Push a caret step, or fold it into the newest one: consecutive
|
|
775
|
+
moves in the SAME view within Toggles.CodeEditor.nav_caret_coalesce_s
|
|
776
|
+
seconds of each other that stay within nav_caret_step_lines lines of
|
|
777
|
+
where the step already ended advance that step's `new` (an arrow-key
|
|
778
|
+
walk is one step back), while `old` stays pinned to where the walk
|
|
779
|
+
began. A far move (a click elsewhere, Ctrl+End) or a pause starts a
|
|
780
|
+
new step; so does any move after an undo (a fold would keep a
|
|
781
|
+
diverged redo alive)."""
|
|
782
|
+
if old_loc == new_loc:
|
|
783
|
+
return
|
|
784
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
785
|
+
now = time.time()
|
|
786
|
+
frame = Core.melty.frame_count
|
|
787
|
+
top = cls.stack.history[-1] if cls.stack.history else None
|
|
788
|
+
if (isinstance(top, CaretChange) and not cls.stack.redo_stack
|
|
789
|
+
and top.new.tile_id == new_loc.tile_id == old_loc.tile_id
|
|
790
|
+
and now - top.t <= Toggles.CodeEditor.nav_caret_coalesce_s
|
|
791
|
+
and (top.new.line is None or new_loc.line is None
|
|
792
|
+
or abs(top.new.line - new_loc.line)
|
|
793
|
+
<= Toggles.CodeEditor.nav_caret_step_lines)):
|
|
794
|
+
top.new = new_loc
|
|
795
|
+
top.t = now
|
|
796
|
+
top.frame = frame
|
|
797
|
+
return
|
|
798
|
+
cls.stack.push(CaretChange(old_loc, new_loc, t=now,
|
|
799
|
+
group_id=cls.stack.new_group_id(),
|
|
800
|
+
frame=frame))
|
|
801
|
+
|
|
802
|
+
@classmethod
|
|
803
|
+
def _apply_caret(cls, location):
|
|
804
|
+
"""Replay one side of a CaretChange: bring the view on screen (its
|
|
805
|
+
editor tab / window), put the caret back and hand it text focus.
|
|
806
|
+
The view's own caret-follow scroll brings the caret into view on its
|
|
807
|
+
next body run (text_cursor_pos != text_prev_cursor_pos)."""
|
|
808
|
+
draw_state = location.draw_state()
|
|
809
|
+
if draw_state is None:
|
|
810
|
+
return
|
|
811
|
+
from meltygui.core.runtime.extensions import source_views
|
|
812
|
+
_active_editors = source_views()
|
|
813
|
+
from meltygui.core.runtime.extensions import source_window as editor_window_draw_state
|
|
814
|
+
if location.path:
|
|
815
|
+
active = _active_editors.get(location.instance)
|
|
816
|
+
if active is None or active[0] != location.path:
|
|
817
|
+
# Another tab was selected in that editor instance: a line-less
|
|
818
|
+
# location replay selects it (and raises its window) without
|
|
819
|
+
# touching the caret - the write below is what lands it.
|
|
820
|
+
cls._apply_location((location.path, None, location.instance))
|
|
821
|
+
else:
|
|
822
|
+
win = editor_window_draw_state(location.instance)
|
|
823
|
+
if win is not None:
|
|
824
|
+
win.closed = False
|
|
825
|
+
Core.melty.move_window_to_front(win)
|
|
826
|
+
else:
|
|
827
|
+
# Reopen any closed window on the view's parent chain, then raise
|
|
828
|
+
# the innermost one (move_window_to_front raises its whole chain).
|
|
829
|
+
innermost = None
|
|
830
|
+
node, steps = draw_state.parent_window, 0
|
|
831
|
+
while node is not None and steps < 16:
|
|
832
|
+
if innermost is None:
|
|
833
|
+
innermost = node
|
|
834
|
+
if node.closed:
|
|
835
|
+
node.closed = False
|
|
836
|
+
if Core.melty.cache is not None and node._tile_id is not None:
|
|
837
|
+
Core.melty.cache.invalidate_up(node._tile_id, force=True,
|
|
838
|
+
max_depth=4)
|
|
839
|
+
next_window = node.parent_window
|
|
840
|
+
if next_window is node:
|
|
841
|
+
break
|
|
842
|
+
node, steps = next_window, steps + 1
|
|
843
|
+
if innermost is not None:
|
|
844
|
+
Core.melty.move_window_to_front(innermost)
|
|
845
|
+
draw_state.text_cursor_pos = location.cursor
|
|
846
|
+
draw_state.text_selection_start = location.selection_start
|
|
847
|
+
draw_state.text_selection_end = location.selection_end
|
|
848
|
+
draw_state.text_cursor_blink_time = time.time()
|
|
849
|
+
Core.melty.text_focused_ds = draw_state
|
|
850
|
+
Core.melty._text_focus_grant_frame = Core.melty.frame_count
|
|
851
|
+
# The replayed position is the new location - next poll must not read
|
|
852
|
+
# it back as a fresh move (quiet only all the frames in between).
|
|
853
|
+
cls._last_caret = location
|
|
854
|
+
cache = getattr(Core.melty, "cache", None)
|
|
855
|
+
if cache is not None and draw_state._tile_id is not None:
|
|
856
|
+
cache.invalidate_up(draw_state._tile_id, force=True)
|
|
857
|
+
request_render()
|
|
858
|
+
|
|
859
|
+
@classmethod
|
|
860
|
+
def _apply_location(cls, loc):
|
|
861
|
+
from meltygui.core.runtime.extensions import get, open_source
|
|
862
|
+
provider = get('source_location')
|
|
863
|
+
if provider is not None:
|
|
864
|
+
return provider(loc)
|
|
865
|
+
if loc and loc[0]:
|
|
866
|
+
return open_source(loc[0], line_number=loc[1] if len(loc) > 1 else None)
|
|
867
|
+
|
|
868
|
+
@classmethod
|
|
869
|
+
def undo(cls):
|
|
870
|
+
cls._restoring = True
|
|
871
|
+
cls.quiet_caret()
|
|
872
|
+
try:
|
|
873
|
+
cls.stack.undo()
|
|
874
|
+
finally:
|
|
875
|
+
cls._restoring = False
|
|
876
|
+
|
|
877
|
+
@classmethod
|
|
878
|
+
def redo(cls):
|
|
879
|
+
cls._restoring = True
|
|
880
|
+
cls.quiet_caret()
|
|
881
|
+
try:
|
|
882
|
+
cls.stack.redo()
|
|
883
|
+
finally:
|
|
884
|
+
cls._restoring = False
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _edit_direction(old, new):
|
|
888
|
+
"""Classify a text edit by length: 'insert' grew, 'delete' shrank, 'replace'
|
|
889
|
+
kept the same length (e.g. overwriting a selection)."""
|
|
890
|
+
if len(new) > len(old):
|
|
891
|
+
return "insert"
|
|
892
|
+
if len(new) < len(old):
|
|
893
|
+
return "delete"
|
|
894
|
+
return "replace"
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _diff_span(old, new):
|
|
898
|
+
"""Minimal differing span as (prefix_len, removed_text, inserted_text).
|
|
899
|
+
Strips the common prefix and suffix so `removed` is the run `old` loses
|
|
900
|
+
and `inserted` the run `new` adds at offset `prefix_len`.
|
|
901
|
+
|
|
902
|
+
Chunked: equal 4KB slices skip at C memcmp speed, per-char refinement only
|
|
903
|
+
inside the first mismatching chunk. The original per-char Python walk was
|
|
904
|
+
O(buffer) per FOLDED INSERT — record() runs it via _typing_edit on
|
|
905
|
+
every keystroke, and on a ~166KB buffer that was a
|
|
906
|
+
measured ~20ms slice of the edited-frame wrapper epilogue (held-Enter
|
|
907
|
+
bursts; alternating insert/delete never reached it, which is why the cost
|
|
908
|
+
came and went between sessions)."""
|
|
909
|
+
lo, ln = len(old), len(new)
|
|
910
|
+
m = min(lo, ln)
|
|
911
|
+
chunk = 4096
|
|
912
|
+
p = 0
|
|
913
|
+
while p < m:
|
|
914
|
+
step = min(chunk, m - p)
|
|
915
|
+
if old[p:p + step] == new[p:p + step]:
|
|
916
|
+
p += step
|
|
917
|
+
continue
|
|
918
|
+
e = p + step
|
|
919
|
+
while p < e and old[p] == new[p]:
|
|
920
|
+
p += 1
|
|
921
|
+
break
|
|
922
|
+
s = 0
|
|
923
|
+
ms = m - p
|
|
924
|
+
while s < ms:
|
|
925
|
+
step = min(chunk, ms - s)
|
|
926
|
+
if old[lo - s - step:lo - s] == new[ln - s - step:ln - s]:
|
|
927
|
+
s += step
|
|
928
|
+
continue
|
|
929
|
+
e = s + step
|
|
930
|
+
while s < e and old[lo - 1 - s] == new[ln - 1 - s]:
|
|
931
|
+
s += 1
|
|
932
|
+
break
|
|
933
|
+
return p, old[p:lo - s], new[p:ln - s]
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
class _TypingEdit:
|
|
937
|
+
"""A keystroke-sized text edit: `kind` 'insert' or 'delete', `pos` the
|
|
938
|
+
buffer offset it starts at (in `old`), `text` the run inserted or removed
|
|
939
|
+
in BUFFER order (a Backspace run is read reversed where typing order
|
|
940
|
+
matters)."""
|
|
941
|
+
|
|
942
|
+
__slots__ = ("kind", "pos", "text")
|
|
943
|
+
|
|
944
|
+
def __init__(self, kind, pos, text):
|
|
945
|
+
self.kind = kind
|
|
946
|
+
self.pos = pos
|
|
947
|
+
self.text = text
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _typing_edit(old, new):
|
|
951
|
+
"""Classify a text edit as typing, or None for a standalone step. Typing
|
|
952
|
+
is a pure insert or a pure delete of at most
|
|
953
|
+
Toggles.CodeEditor.undo_typing_max_chars characters with no newline in
|
|
954
|
+
it. Everything else is its own undo step, like the separate editor
|
|
955
|
+
commands they come from in IntelliJ: Enter (+ auto-indent), Tab, a
|
|
956
|
+
paste, a completion, a comment toggle, a selection typed over or cut."""
|
|
957
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
958
|
+
pos, removed, inserted = _diff_span(old, new)
|
|
959
|
+
if removed and inserted:
|
|
960
|
+
return None
|
|
961
|
+
text = inserted or removed
|
|
962
|
+
if not text or "\n" in text or len(text) > Toggles.CodeEditor.undo_typing_max_chars:
|
|
963
|
+
return None
|
|
964
|
+
return _TypingEdit("insert" if inserted else "delete", pos, text)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _span_reaches(old, edit, pos):
|
|
968
|
+
"""Whether `edit`, whose diff span sits at `edit.pos`, could equally have
|
|
969
|
+
happened at `pos`. The common-prefix diff reports the RIGHTMOST
|
|
970
|
+
equivalent placement: inserting 'l' before the 'l' of "helo" reads as an
|
|
971
|
+
insert after it, deleting the first 'l' of "hello" as deleting the
|
|
972
|
+
second. Sliding the span left by one is equivalent whenever the character
|
|
973
|
+
it slides over equals the run's last (cyclically, for a multi-character
|
|
974
|
+
run), so the check walks back from edit.pos to `pos` — O(shift), and the
|
|
975
|
+
shift is the length of the repeated-character run."""
|
|
976
|
+
if pos == edit.pos:
|
|
977
|
+
return True
|
|
978
|
+
if pos < 0 or pos > edit.pos:
|
|
979
|
+
return False
|
|
980
|
+
text = edit.text
|
|
981
|
+
length = len(text)
|
|
982
|
+
for j in range(edit.pos - pos):
|
|
983
|
+
if old[edit.pos - 1 - j] != text[-1 - (j % length)]:
|
|
984
|
+
return False
|
|
985
|
+
return True
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def _run_edge(edit, pos, prev_end):
|
|
989
|
+
"""Where a typing run ends once `edit` (resolved to buffer offset `pos`)
|
|
990
|
+
joins it — (edit_end, edge_char). `edit_end` is the caret offset after
|
|
991
|
+
the edit (an insert ends past its text; a delete leaves the caret at its
|
|
992
|
+
start either way). `edge_char` is the last character typed in typing
|
|
993
|
+
order: an insert's last, a Delete-key run's last removed, a Backspace
|
|
994
|
+
run's FIRST removed (it removes backwards). A run's first delete has no
|
|
995
|
+
edge to compare against and reads as Backspace, the common case."""
|
|
996
|
+
if edit.kind == "insert":
|
|
997
|
+
return pos + len(edit.text), edit.text[-1]
|
|
998
|
+
backspace = prev_end is None or pos + len(edit.text) == prev_end
|
|
999
|
+
return pos, (edit.text[0] if backspace else edit.text[-1])
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _word_starts(run):
|
|
1003
|
+
"""True when a new word starts anywhere inside `run` — a non-space right
|
|
1004
|
+
after whitespace — which closes the current step so each word is its own
|
|
1005
|
+
undo (typing 'hello world' -> 'hello ' | 'world'; backspacing it from the
|
|
1006
|
+
end -> ' world' | 'hello'). `run` is the step's edge char followed by the
|
|
1007
|
+
new keystrokes in typing order."""
|
|
1008
|
+
for i in range(1, len(run)):
|
|
1009
|
+
if run[i - 1].isspace() and not run[i].isspace():
|
|
1010
|
+
return True
|
|
1011
|
+
return False
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _gesture_live(draw_state):
|
|
1015
|
+
"""Whether `draw_state`'s value is mid-gesture: its own imgui item is
|
|
1016
|
+
active (a held drag_float), or it OWNS the open popover in which an
|
|
1017
|
+
imgui item is active — draw_tuple's colour picker, draw_tuple_fast's:
|
|
1018
|
+
the picker window is parented under the editor (Melty.popover_focused_ds
|
|
1019
|
+
names the editor, or the picker whose parent chain reaches it), and a
|
|
1020
|
+
hue/SV drag there is one gesture on the editor's value."""
|
|
1021
|
+
if getattr(draw_state, "_imgui_is_active", False):
|
|
1022
|
+
return True
|
|
1023
|
+
if not getattr(Core.melty, "imgui_any_item_active", False):
|
|
1024
|
+
return False
|
|
1025
|
+
node, steps = getattr(Core.melty, "popover_focused_ds", None), 0
|
|
1026
|
+
while node is not None and steps < 64:
|
|
1027
|
+
if node is draw_state:
|
|
1028
|
+
return True
|
|
1029
|
+
parent = getattr(node, "parent_window", None)
|
|
1030
|
+
if parent is None or parent is node:
|
|
1031
|
+
return False
|
|
1032
|
+
node, steps = parent, steps + 1
|
|
1033
|
+
return False
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
# The primitive value editors - the genuine leaves of the render tree. Their
|
|
1037
|
+
# changes originate from direct user interaction (not bubbled up from a child),
|
|
1038
|
+
# so they're always valid undo origins. Needed because imgui status flags aren't
|
|
1039
|
+
# reliable across widget types: drag_float sets is_item_edited but checkbox does
|
|
1040
|
+
# not, so a flag-only origin check silently dropped every boolean toggle.
|
|
1041
|
+
LEAF_EDITOR_FUNCS = frozenset({"draw_bool", "draw_str", "draw_float", "draw_int", "draw_text",
|
|
1042
|
+
"draw_tuple"})
|
|
1043
|
+
# Sub-widgets a leaf editor owns: their draw_state reports the same change
|
|
1044
|
+
# a frame earlier (the colour picker popover under draw_tuple) and must never
|
|
1045
|
+
# be the origin - the owning leaf records first, so undo lands on the leaf.
|
|
1046
|
+
NEVER_ORIGIN_FUNCS = frozenset({"draw_color_picker"})
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _is_leaf_editor(draw_state):
|
|
1050
|
+
return getattr(getattr(draw_state, "_view_func", None), "__name__", None) in LEAF_EDITOR_FUNCS
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _never_origin(draw_state):
|
|
1054
|
+
return getattr(getattr(draw_state, "_view_func", None), "__name__", None) in NEVER_ORIGIN_FUNCS
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def handle_undo(changed, old_value, new_value, draw_state):
|
|
1058
|
+
if not changed:
|
|
1059
|
+
return
|
|
1060
|
+
# Only the widget the user directly interacted with may record an undo entry.
|
|
1061
|
+
# A single edit bubbles up through every render_func chain - the focused text
|
|
1062
|
+
# editor (draw_text) and its converter/wrapper ancestors (code_to_io_wrapped,
|
|
1063
|
+
# collections) all report the same string change, often on different frames
|
|
1064
|
+
# because conversion runs async. Those pass-through draw_states must never
|
|
1065
|
+
# become undo-stack elements; only the interaction origin may: a primitive
|
|
1066
|
+
# value editor, the focused text editor, or the imgui-active widget this frame.
|
|
1067
|
+
# A container/converter is none of these.
|
|
1068
|
+
is_origin = (_is_leaf_editor(draw_state)
|
|
1069
|
+
or draw_state is Core.melty.text_focused_ds
|
|
1070
|
+
or getattr(draw_state, "_imgui_is_edited", False))
|
|
1071
|
+
if not is_origin or _never_origin(draw_state):
|
|
1072
|
+
return
|
|
1073
|
+
UndoManager.record(draw_state, old_value, new_value)
|
|
1074
|
+
|
|
1075
|
+
|