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,1803 @@
|
|
|
1
|
+
"""Editor-side live_view widgets: marker token + anchored value window.
|
|
2
|
+
|
|
3
|
+
The type-keyed token_views overlay in draw_text calls `draw_live_view_overlay`
|
|
4
|
+
for every CallParse node; it bails unless the call is live_view, then resolves
|
|
5
|
+
the SAME (store_obj, key_path) the capture side publishes under
|
|
6
|
+
(live_view.site_for_line — one resolution code path, no drift) and draws a
|
|
7
|
+
box outline around the symbol being visualized. Clicking the box toggles the
|
|
8
|
+
value window: a closable nested window (latched into Melty.root_draw_states,
|
|
9
|
+
so it persists and re-draws every frame even while the editor tile is
|
|
10
|
+
blit-cached) whose swoosh connector anchors back to the marker. The window
|
|
11
|
+
re-reads the store each render and registers itself as a watcher, so the
|
|
12
|
+
publishing thread invalidates exactly this window per publish — never the
|
|
13
|
+
editor tile, never per frame.
|
|
14
|
+
|
|
15
|
+
Only explicit live_view() call tokens auto-open their window the first time a
|
|
16
|
+
marker renders while a value exists — you typed the call, so the value shows
|
|
17
|
+
without a click. SIMPLE builtin values (int/float/str/bool/tuple of ≤4
|
|
18
|
+
scalars/enum members, never None) skip the window entirely and render as an
|
|
19
|
+
inline pill drawn directly over the symbol in the editor's font
|
|
20
|
+
(_inline_value_text + the inline block in draw_live_view_marker) — for
|
|
21
|
+
explicit live_view() tokens AND snapshot markers alike. An inline marker has
|
|
22
|
+
no value window at all (no auto-open, preview, or double-click; the gutter
|
|
23
|
+
shows an inert info glyph instead of the magnifier). Snapshot/param markers (every captured assignment from an
|
|
24
|
+
instrumented run) start closed and open on box click, so a run doesn't bury
|
|
25
|
+
the code under one window per local. Sites the dict conversion can't surface
|
|
26
|
+
(while/with/match bodies — line-keyed fallback stores) have no CallParse token
|
|
27
|
+
to anchor and get no marker yet.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import bisect
|
|
31
|
+
import collections
|
|
32
|
+
import colorsys
|
|
33
|
+
import enum
|
|
34
|
+
import inspect
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
import weakref
|
|
39
|
+
|
|
40
|
+
import meltygui_imgui as imgui
|
|
41
|
+
from meltygui.hdr_color import pack_color
|
|
42
|
+
from meltygui_imgui.core import _DrawList
|
|
43
|
+
|
|
44
|
+
from meltygui.state.new_core_model import Anchor
|
|
45
|
+
from meltygui.state.new_core_model import Pin
|
|
46
|
+
from meltygui.core.styling.fonts import Font
|
|
47
|
+
from meltygui.core.rendering.modes import Modes
|
|
48
|
+
from meltygui.core.core_render import render_func
|
|
49
|
+
from meltygui.code.live_view import live_values_for
|
|
50
|
+
from meltygui.code.live_view import label_for
|
|
51
|
+
from meltygui.code.live_view import site_for_line
|
|
52
|
+
from meltygui.code.live_view import watch
|
|
53
|
+
from meltygui.code.live_view import install_builtin
|
|
54
|
+
from meltygui.code.live_view import auto_dim_names_for
|
|
55
|
+
from meltygui.code.live_view import RerunHint
|
|
56
|
+
from meltygui.core.rendering.core_decoration import Core
|
|
57
|
+
from meltygui.core.rendering.window_decoration import window
|
|
58
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
59
|
+
import meltygui.editor.live_usage as live_usage
|
|
60
|
+
|
|
61
|
+
# The seamless path: any app can call live_view() with no import (like
|
|
62
|
+
# breakpoint()). Installed when the editor side loads - i.e. every studio
|
|
63
|
+
# session - so "type the call, hotswap, run" needs no source changes beyond
|
|
64
|
+
# the call itself.
|
|
65
|
+
install_builtin()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def install_token_views(token_views):
|
|
69
|
+
"""Merge the live_view overlays into a token_views dict (called by
|
|
70
|
+
text_editor right after DEFAULT_TOKEN_VIEWS is defined). Order matters:
|
|
71
|
+
CallParse IS-A GeneralParse and the walk takes the first matching type, so
|
|
72
|
+
the call-token entry must precede the root snapshot entry."""
|
|
73
|
+
from meltygui.code.libcst_conversion import CallParse
|
|
74
|
+
from meltygui.code.libcst_conversion import GeneralParse
|
|
75
|
+
token_views[CallParse] = {"renderer": draw_live_view_overlay,
|
|
76
|
+
"char_width": None}
|
|
77
|
+
token_views[GeneralParse] = {"renderer": draw_snapshot_overlay,
|
|
78
|
+
"char_width": None}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _store_name(obj):
|
|
83
|
+
"""Stable display/name key for a store object (function qualname or
|
|
84
|
+
module name) — window/marker names key on this + the cst key path,
|
|
85
|
+
never on draw_state ids (not stable across sessions/editors)."""
|
|
86
|
+
return getattr(obj, "__qualname__", None) or getattr(obj, "__name__", "?")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_NO_VALUE = object() # "window has received no value yet" sentinel
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _display_key(key):
|
|
93
|
+
"""Human title for one key-path element: a `line:N#name` key (frame
|
|
94
|
+
snapshots / twin_snap stamps) reads as its token name, not the line
|
|
95
|
+
number. Display only — stable IDs keep the raw key."""
|
|
96
|
+
key = str(key)
|
|
97
|
+
if key.startswith("line:") and "#" in key:
|
|
98
|
+
return key.split("#", 1)[1]
|
|
99
|
+
return key
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _split_line_key(seg):
|
|
103
|
+
"""(label, line) for a `line:N#name` segment, (None, None) otherwise."""
|
|
104
|
+
seg = str(seg)
|
|
105
|
+
if not seg.startswith("line:"):
|
|
106
|
+
return None, None
|
|
107
|
+
head, _, label = seg.partition("#")
|
|
108
|
+
try:
|
|
109
|
+
return label, int(head[5:])
|
|
110
|
+
except ValueError:
|
|
111
|
+
return label, 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _stable_key_name(key_path, all_keys=None):
|
|
116
|
+
"""Identity form of a store key path for VIEW NAMES — the LINE NUMBER is
|
|
117
|
+
STRIPPED: `line:12#r` reads as `r`. The name is hashed into the render
|
|
118
|
+
unique ID, so a raw line-keyed name gives the same code line a NEW view
|
|
119
|
+
identity every time a rerun/edit re-stamps its line — leaking the old
|
|
120
|
+
marker/window draw_states and colliding IDs across runs. The label IS the
|
|
121
|
+
code line's stable id; repeat sites of the same label stay sibling-
|
|
122
|
+
distinct via their ordinal among same-label keys in `all_keys` (the
|
|
123
|
+
store's key set), ranked by stamped line — the ORDER survives the line
|
|
124
|
+
shifts the raw numbers don't. Bare `line:N` keys rank under the empty
|
|
125
|
+
label the same way.
|
|
126
|
+
|
|
127
|
+
A plain STATEMENT key with the same name at the same position —
|
|
128
|
+
`('x',)` beside `('line:N#x',)`, a dict-visible assignment plus a
|
|
129
|
+
line-keyed with/while-body site of the same local — claims ordinal 0
|
|
130
|
+
and shifts every line-keyed ordinal up: the two used to collapse to
|
|
131
|
+
ONE name, and colliding view names shared their marker/window
|
|
132
|
+
draw_states (the draw_text garbled-overlay bug)."""
|
|
133
|
+
parts = []
|
|
134
|
+
for i, seg in enumerate(key_path):
|
|
135
|
+
label, line = _split_line_key(seg)
|
|
136
|
+
if label is None:
|
|
137
|
+
parts.append(str(seg))
|
|
138
|
+
continue
|
|
139
|
+
ordinal = 0
|
|
140
|
+
if all_keys is not None:
|
|
141
|
+
lines = []
|
|
142
|
+
statement_twin = False
|
|
143
|
+
for k in all_keys:
|
|
144
|
+
if len(k) > i:
|
|
145
|
+
lb, ln = _split_line_key(k[i])
|
|
146
|
+
if lb == label and ln is not None:
|
|
147
|
+
lines.append(ln)
|
|
148
|
+
elif lb is None and str(k[i]) == label:
|
|
149
|
+
statement_twin = True
|
|
150
|
+
if line in lines:
|
|
151
|
+
ordinal = sorted(lines).index(line)
|
|
152
|
+
if statement_twin:
|
|
153
|
+
ordinal += 1
|
|
154
|
+
parts.append(label if ordinal == 0 else f"{label}~{ordinal}")
|
|
155
|
+
return "/".join(parts)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _stable_key_names(all_keys):
|
|
159
|
+
"""{key_path: line-free name} for a WHOLE store snapshot in one pass.
|
|
160
|
+
The per-key _stable_key_name ordinal scan is O(store), which made naming
|
|
161
|
+
O(store²) per overlay pass on frame-snapshot stores (thousands of keys —
|
|
162
|
+
the draw_text slowdown). Grouping ordinals per (position, label) once
|
|
163
|
+
produces identical names at O(n log n); callers cache the map on their
|
|
164
|
+
existing invalidation signals (the anchor index / a per-store memo), so
|
|
165
|
+
steady-state naming is a dict hit."""
|
|
166
|
+
groups = {}
|
|
167
|
+
statement_twins = set() # (position, literal name) of non-line segs
|
|
168
|
+
for k in all_keys:
|
|
169
|
+
for i, seg in enumerate(k):
|
|
170
|
+
label, line = _split_line_key(seg)
|
|
171
|
+
if label is not None:
|
|
172
|
+
groups.setdefault((i, label), []).append(line)
|
|
173
|
+
else:
|
|
174
|
+
statement_twins.add((i, str(seg)))
|
|
175
|
+
ranks = {}
|
|
176
|
+
for (i, label), lines in groups.items():
|
|
177
|
+
# A statement key with this literal name at this position claims
|
|
178
|
+
# ordinal 0 (see _stable_key_name's docstring) - every line-keyed
|
|
179
|
+
# sibling shifts up so no two key paths share a name.
|
|
180
|
+
base = 1 if (i, label) in statement_twins else 0
|
|
181
|
+
for o, ln in enumerate(sorted(lines)):
|
|
182
|
+
ranks.setdefault((i, label, ln), o + base) # dups keep first rank
|
|
183
|
+
out = {}
|
|
184
|
+
for k in all_keys:
|
|
185
|
+
parts = []
|
|
186
|
+
for i, seg in enumerate(k):
|
|
187
|
+
label, line = _split_line_key(seg)
|
|
188
|
+
if label is None:
|
|
189
|
+
parts.append(str(seg))
|
|
190
|
+
else:
|
|
191
|
+
o = ranks.get((i, label, line), 0)
|
|
192
|
+
parts.append(label if o == 0 else f"{label}~{o}")
|
|
193
|
+
out[k] = "/".join(parts)
|
|
194
|
+
return out
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# One name map per store - see _store_key_names.
|
|
198
|
+
_NAME_MAPS = weakref.WeakKeyDictionary()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _store_key_names(store_obj):
|
|
202
|
+
"""The store's {key_path: view name} map, memoized per KEY-SET
|
|
203
|
+
generation (`__live_keys_gen__`, bumped by live_view on first publish /
|
|
204
|
+
re-key / prune, with len as a backstop). Ordinal names depend on the
|
|
205
|
+
WHOLE key set, so every consumer — snapshot overlay, call-token
|
|
206
|
+
overlay, idle paths — must read the SAME map: consumers holding
|
|
207
|
+
differently-stale private memos minted one name for two different keys
|
|
208
|
+
(duplicate marker/window IDs, the draw_text garbled overlays)."""
|
|
209
|
+
try:
|
|
210
|
+
d = vars(store_obj)
|
|
211
|
+
except TypeError:
|
|
212
|
+
return {}
|
|
213
|
+
store = d.get("__live_values__") or {}
|
|
214
|
+
gen = d.get("__live_keys_gen__", 0)
|
|
215
|
+
try:
|
|
216
|
+
ent = _NAME_MAPS.get(store_obj)
|
|
217
|
+
if ent is None or ent[0] != gen or ent[1] != len(store):
|
|
218
|
+
ent = (gen, len(store), _stable_key_names(list(store.keys())))
|
|
219
|
+
_NAME_MAPS[store_obj] = ent
|
|
220
|
+
return ent[2]
|
|
221
|
+
except TypeError:
|
|
222
|
+
return _stable_key_names(list(store.keys()))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _inline_value_text(value, max_chars=20):
|
|
226
|
+
"""Format a simple builtin value for the marker's INLINE label, or None
|
|
227
|
+
when the value isn't simple enough (those keep the popover window).
|
|
228
|
+
Simple: bool, int, float, str, enum members, and tuples of up to 4 such
|
|
229
|
+
scalars. `max_chars` caps a string's printed length (ellipsis past it) —
|
|
230
|
+
the label floats over code, so it must stay short. Colors (see
|
|
231
|
+
_inline_swatch_rgba) keep their text; the pill adds the swatch."""
|
|
232
|
+
if value is None:
|
|
233
|
+
return None # None is NOT on the supported list - no label
|
|
234
|
+
if isinstance(value, RerunHint):
|
|
235
|
+
# The parked 'Rerun to visualize ...' placeholder is an affordance,
|
|
236
|
+
# not a captured value - it keeps the popover, not a label.
|
|
237
|
+
return None
|
|
238
|
+
# bool before int (bool IS-A int), Enum before int (IntEnum members).
|
|
239
|
+
if isinstance(value, bool):
|
|
240
|
+
return "True" if value else "False"
|
|
241
|
+
if isinstance(value, enum.Enum):
|
|
242
|
+
return value.name
|
|
243
|
+
if isinstance(value, int):
|
|
244
|
+
return str(value)
|
|
245
|
+
if isinstance(value, float):
|
|
246
|
+
return f"{value:.4g}"
|
|
247
|
+
if isinstance(value, str):
|
|
248
|
+
# repr ONLY a max_chars prefix: a frame snapshot's locals include
|
|
249
|
+
# whole file texts (megabytes), and repr(value) on one is ~1 ms
|
|
250
|
+
# per call — 90 markers a frame put the stack-trace pane at 110 ms
|
|
251
|
+
# (cProfile 09-01: 2.13 s of 2.38 s in builtins.repr). Quotes and
|
|
252
|
+
# quotes only lengthen a repr, so the prefix's repr already runs
|
|
253
|
+
# past max_chars whenever the full one would, and the label cut
|
|
254
|
+
# from it is the same text.
|
|
255
|
+
if len(value) > max_chars:
|
|
256
|
+
return repr(value[:max_chars])[:max_chars] + "…"
|
|
257
|
+
text = repr(value)
|
|
258
|
+
if len(text) > max_chars:
|
|
259
|
+
text = text[:max_chars] + "…"
|
|
260
|
+
return text
|
|
261
|
+
if isinstance(value, tuple) and len(value) <= 4:
|
|
262
|
+
parts = []
|
|
263
|
+
for item in value:
|
|
264
|
+
part = (None if isinstance(item, tuple)
|
|
265
|
+
else _inline_value_text(item, max_chars))
|
|
266
|
+
if part is None:
|
|
267
|
+
return None
|
|
268
|
+
parts.append(part)
|
|
269
|
+
tail = "," if len(value) == 1 else ""
|
|
270
|
+
return "(" + ", ".join(parts) + tail + ")"
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
_HEX_COLOR_RE = re.compile(r"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$")
|
|
275
|
+
# Two blank cells the pill text reserves for the swatch - the width math and
|
|
276
|
+
# the left-gap cells below need no special case for it.
|
|
277
|
+
_SWATCH_HOLE = " "
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _inline_swatch_rgba(value):
|
|
281
|
+
"""(r, g, b, a) in 0..1 when a captured value READS as a color — a 3/4
|
|
282
|
+
tuple of numbers all within 0..1 (or all ints within 0..255 with one
|
|
283
|
+
past 1, scaled down), or a hex color string (`'#8888c6'`, `'#fff'`,
|
|
284
|
+
RGBA `'#8888c680'`) — else None. Same shapes the editor's color3 /
|
|
285
|
+
colorhex token widgets swatch."""
|
|
286
|
+
if isinstance(value, str):
|
|
287
|
+
if len(value) > 9 or not _HEX_COLOR_RE.match(value):
|
|
288
|
+
return None
|
|
289
|
+
hex_digits = value[1:]
|
|
290
|
+
if len(hex_digits) == 3:
|
|
291
|
+
hex_digits = "".join(c + c for c in hex_digits)
|
|
292
|
+
channels = [int(hex_digits[i:i + 2], 16) / 255.0
|
|
293
|
+
for i in range(0, len(hex_digits), 2)]
|
|
294
|
+
return (channels[0], channels[1], channels[2],
|
|
295
|
+
channels[3] if len(channels) == 4 else 1.0)
|
|
296
|
+
if not isinstance(value, tuple) or len(value) not in (3, 4):
|
|
297
|
+
return None
|
|
298
|
+
for item in value:
|
|
299
|
+
if isinstance(item, bool) or not isinstance(item, (int, float)):
|
|
300
|
+
return None
|
|
301
|
+
if not 0 <= item <= 255:
|
|
302
|
+
return None
|
|
303
|
+
if all(item <= 1 for item in value):
|
|
304
|
+
channels = tuple(float(c) for c in value)
|
|
305
|
+
elif all(isinstance(item, int) for item in value):
|
|
306
|
+
channels = tuple(c / 255.0 for c in value)
|
|
307
|
+
else:
|
|
308
|
+
return None
|
|
309
|
+
return channels if len(channels) == 4 else channels + (1.0,)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _paint_value_pill(inline_text, span_x, text_y, span_width=None,
|
|
313
|
+
allow_overflow=False, fill=False, tint=None,
|
|
314
|
+
swatch=None):
|
|
315
|
+
"""The shared inline-value pill on the window draw list, in the CURRENT
|
|
316
|
+
(editor) font, over the code span [span_x, span_x + span_width).
|
|
317
|
+
|
|
318
|
+
Placement rules: a value narrower than the span RIGHT-ALIGNS on it, so
|
|
319
|
+
the span's FIRST characters peek through beside it; a wider value
|
|
320
|
+
either grows right past the span (`allow_overflow` — the span is the
|
|
321
|
+
last code on its line, nothing there to cover) or is elided with an
|
|
322
|
+
ellipsis to the span's width. `fill=True` stretches the card over the
|
|
323
|
+
whole span regardless of the value's width (a live_view() call token is
|
|
324
|
+
instrumentation, not code worth peeking at). span_width=None is the
|
|
325
|
+
simple left-anchored unlimited pill (no span geometry known).
|
|
326
|
+
|
|
327
|
+
`tint` is the rgb the pill wears — the background tint under the token
|
|
328
|
+
(see _pill_tint: token → enclosing def/class block → file), so the
|
|
329
|
+
value reads as part of the scope it was captured in; None keeps the
|
|
330
|
+
default forest green. Fill and text derive from it with the SAME
|
|
331
|
+
factors as the green, so an untinted pill looks exactly as before.
|
|
332
|
+
|
|
333
|
+
`swatch` = ((r, g, b, a), hole_index): a color chip painted into the
|
|
334
|
+
_SWATCH_HOLE the text carries at character `hole_index` — the pill's
|
|
335
|
+
layout treats the hole as text, so nothing else changes. The chip is
|
|
336
|
+
split like the editor's color widgets: left half opaque, right half at
|
|
337
|
+
the real alpha over the card."""
|
|
338
|
+
# [tint=(0.36, 0.85, 0.46)] pad_x = 3.0
|
|
339
|
+
pad_x = 3.0
|
|
340
|
+
# [tint=(0.30, 0.52, 0.20)] default_tint = (0.30, 0.52, 0.20)
|
|
341
|
+
default_tint = (0.30, 0.52, 0.20)
|
|
342
|
+
# Card and text are the tint re-saturated at fixed brightness (value),
|
|
343
|
+
# so every pill reads as the same kind of thing whatever hue it wears:
|
|
344
|
+
# a deep, vivid card with equally vivid text - deliberately contrasting the
|
|
345
|
+
# muted code text around it. Raise *_value to lighten, *_saturation
|
|
346
|
+
# toward 1.0 to make the hue purer.
|
|
347
|
+
fill_saturation = 0.92
|
|
348
|
+
fill_value = 0.14
|
|
349
|
+
text_saturation = 0.78
|
|
350
|
+
text_value = 0.88
|
|
351
|
+
# The label's own face: a step below the editor's (JetBrains Mono 18.5)
|
|
352
|
+
# so the value never passes for code. None while the face is still
|
|
353
|
+
# baking (first get() queues it) - the current font stands in.
|
|
354
|
+
label_font = Font.JETBRAINS_MONO_16
|
|
355
|
+
pad_y = 1.0
|
|
356
|
+
shadow_offset = 2.0
|
|
357
|
+
corner_radius = 4.0
|
|
358
|
+
line_text_height = imgui.get_text_line_height() # in editor face
|
|
359
|
+
_font_mgr = Core.melty.font_mgr
|
|
360
|
+
_font_handle = _font_mgr.get(label_font) if _font_mgr is not None else None
|
|
361
|
+
if _font_handle is not None:
|
|
362
|
+
imgui.push_font(_font_handle)
|
|
363
|
+
try:
|
|
364
|
+
_paint_value_pill_body(inline_text, span_x, text_y, span_width,
|
|
365
|
+
allow_overflow, fill, tint, swatch, pad_x,
|
|
366
|
+
pad_y, shadow_offset, corner_radius,
|
|
367
|
+
default_tint, fill_saturation, fill_value,
|
|
368
|
+
text_saturation, text_value, line_text_height)
|
|
369
|
+
finally:
|
|
370
|
+
if _font_handle is not None:
|
|
371
|
+
imgui.pop_font()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _pill_rgb(base, saturation, value):
|
|
375
|
+
"""`base` re-saturated at a fixed brightness — the pill's card / text
|
|
376
|
+
colour for a tint. Hue is all that survives of the base."""
|
|
377
|
+
hue, sat, _val = colorsys.rgb_to_hsv(base[0], base[1], base[2])
|
|
378
|
+
# A grey base has no hue to keep - let it stay grey at the target value.
|
|
379
|
+
return colorsys.hsv_to_rgb(hue, saturation if sat > 0.05 else sat, value)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _paint_value_pill_body(inline_text, span_x, text_y, span_width,
|
|
383
|
+
allow_overflow, fill, tint, swatch, pad_x, pad_y,
|
|
384
|
+
shadow_offset, corner_radius, default_tint,
|
|
385
|
+
fill_saturation, fill_value, text_saturation,
|
|
386
|
+
text_value, line_text_height):
|
|
387
|
+
"""_paint_value_pill's layout + paint, run with the label font pushed
|
|
388
|
+
(every measurement here is in that face). The pill's vertical centre
|
|
389
|
+
stays on the code line: text_y is the LINE's text top, and the smaller
|
|
390
|
+
face is centred within the line's glyph height."""
|
|
391
|
+
text_size = imgui.calc_text_size(inline_text)
|
|
392
|
+
text_y += max(0.0, (line_text_height - text_size.y) * 0.5)
|
|
393
|
+
if (span_width is not None and text_size.x > span_width
|
|
394
|
+
and not allow_overflow):
|
|
395
|
+
while inline_text and imgui.calc_text_size(
|
|
396
|
+
inline_text + "…").x > span_width:
|
|
397
|
+
inline_text = inline_text[:-1]
|
|
398
|
+
if not inline_text:
|
|
399
|
+
return # not even one character fits - draw nothing
|
|
400
|
+
inline_text += "…"
|
|
401
|
+
text_size = imgui.calc_text_size(inline_text)
|
|
402
|
+
if span_width is not None and text_size.x <= span_width:
|
|
403
|
+
text_x = span_x + span_width - text_size.x # right-aligned
|
|
404
|
+
else:
|
|
405
|
+
text_x = span_x
|
|
406
|
+
card_left = span_x if (fill or text_x == span_x) else text_x
|
|
407
|
+
card_right = text_x + text_size.x
|
|
408
|
+
if fill and span_width is not None:
|
|
409
|
+
card_right = max(card_right, span_x + span_width)
|
|
410
|
+
box_x = card_left - pad_x
|
|
411
|
+
box_y = text_y - pad_y
|
|
412
|
+
box_width = (card_right - card_left) + 2 * pad_x
|
|
413
|
+
box_height = text_size.y + 2 * pad_y
|
|
414
|
+
add_shadow((box_x, box_y, box_width, box_height),
|
|
415
|
+
offset=shadow_offset, corner_radius=corner_radius)
|
|
416
|
+
base = tint if tint is not None else default_tint
|
|
417
|
+
fill_rgb = _pill_rgb(base, fill_saturation, fill_value)
|
|
418
|
+
text_rgb = _pill_rgb(base, text_saturation, text_value)
|
|
419
|
+
draw_list: _DrawList = imgui.get_window_draw_list()
|
|
420
|
+
draw_list.add_rect_filled(
|
|
421
|
+
box_x, box_y, box_x + box_width, box_y + box_height,
|
|
422
|
+
pack_color(fill_rgb[0], fill_rgb[1], fill_rgb[2], 0.97),
|
|
423
|
+
rounding=corner_radius)
|
|
424
|
+
draw_list.add_text(text_x, text_y,
|
|
425
|
+
pack_color(text_rgb[0], text_rgb[1],
|
|
426
|
+
text_rgb[2], 0.97),
|
|
427
|
+
inline_text)
|
|
428
|
+
if swatch is not None and len(inline_text) >= swatch[1] + len(_SWATCH_HOLE):
|
|
429
|
+
rgba, hole_index = swatch
|
|
430
|
+
hole_x = text_x + imgui.calc_text_size(inline_text[:hole_index]).x
|
|
431
|
+
hole_width = imgui.calc_text_size(_SWATCH_HOLE).x
|
|
432
|
+
side = max(4.0, min(hole_width - 2.0, text_size.y - 2.0))
|
|
433
|
+
chip_x = hole_x + (hole_width - side) * 0.5
|
|
434
|
+
chip_y = text_y + (text_size.y - side) * 0.5
|
|
435
|
+
chip_mid = chip_x + side * 0.5
|
|
436
|
+
draw_list.add_rect_filled(
|
|
437
|
+
chip_x, chip_y, chip_mid, chip_y + side,
|
|
438
|
+
pack_color(rgba[0], rgba[1], rgba[2], 1.0),
|
|
439
|
+
rounding=2.0, flags=imgui.DRAW_ROUND_CORNERS_LEFT)
|
|
440
|
+
draw_list.add_rect_filled(
|
|
441
|
+
chip_mid, chip_y, chip_x + side, chip_y + side,
|
|
442
|
+
pack_color(rgba[0], rgba[1], rgba[2], rgba[3]),
|
|
443
|
+
rounding=2.0, flags=imgui.DRAW_ROUND_CORNERS_RIGHT)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
_FILE_TINT_FN = None
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _pill_tint(editor_ds, line0, symbol=None, token_tint=None):
|
|
450
|
+
"""RGB a value pill at DISPLAY line `line0` wears: the background tint
|
|
451
|
+
under its token. Highest first: the token's own tint (its `# [tint=…]`
|
|
452
|
+
comment → `token_tint`; else a tinted definition named `symbol`, one
|
|
453
|
+
dict read off the editor's cached def-tint name map), the innermost
|
|
454
|
+
tinted class/def block containing the line (draw_text stamps its
|
|
455
|
+
fold-remapped block list as `_lv_tint_blocks`), the file's FileMeta
|
|
456
|
+
tint, else None (the pill's default green). Cheap by construction: the
|
|
457
|
+
block scan is memoized per line against the block tuple's identity
|
|
458
|
+
(rebuilt only when the def-tint pass rebuilds), everything else is a
|
|
459
|
+
handful of dict reads — this runs once per visible pill per repaint."""
|
|
460
|
+
global _FILE_TINT_FN
|
|
461
|
+
if token_tint is not None:
|
|
462
|
+
return tuple(token_tint[:3])
|
|
463
|
+
if editor_ds is None:
|
|
464
|
+
return None
|
|
465
|
+
_d = editor_ds.__dict__
|
|
466
|
+
if symbol is not None:
|
|
467
|
+
_dt = _d.get("_def_tints")
|
|
468
|
+
if _dt is not None and len(_dt) == 4:
|
|
469
|
+
_t = _dt[3].get(symbol)
|
|
470
|
+
if _t is not None:
|
|
471
|
+
return _t
|
|
472
|
+
blocks = _d.get("_lv_tint_blocks") or ()
|
|
473
|
+
memo = _d.get("_lv_pill_tint_memo")
|
|
474
|
+
if memo is None or memo[0] is not blocks:
|
|
475
|
+
memo = (blocks, {})
|
|
476
|
+
object.__setattr__(editor_ds, "_lv_pill_tint_memo", memo)
|
|
477
|
+
cache = memo[1]
|
|
478
|
+
got = cache.get(line0, _NO_VALUE)
|
|
479
|
+
if got is _NO_VALUE:
|
|
480
|
+
got, best = None, -1
|
|
481
|
+
for _l0, _i0, _e0, _t0 in blocks:
|
|
482
|
+
# Innermost = the containing block that starts LAST.
|
|
483
|
+
if _l0 <= line0 <= _e0 and _l0 > best:
|
|
484
|
+
best, got = _l0, tuple(_t0[:3])
|
|
485
|
+
cache[line0] = got
|
|
486
|
+
if got is not None:
|
|
487
|
+
return got
|
|
488
|
+
# File tint: the same FileMeta color the editor tab wears. The path
|
|
489
|
+
# comes from the editor's jump_to Address, else the editor's file_key.
|
|
490
|
+
_jt = _d.get("jump_to")
|
|
491
|
+
path = getattr(_jt, "path", None) if _jt is not None else None
|
|
492
|
+
if path is None:
|
|
493
|
+
path = _d.get("_file_meta")
|
|
494
|
+
if not isinstance(path, str):
|
|
495
|
+
return None
|
|
496
|
+
if _FILE_TINT_FN is None:
|
|
497
|
+
from meltygui.editor.text_editor import _uj_file_tint
|
|
498
|
+
_FILE_TINT_FN = _uj_file_tint
|
|
499
|
+
return _FILE_TINT_FN(path)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# Assignment operators the inline binding pill replaces the right side of:
|
|
503
|
+
# augmented forms first (so `+=` doesn't read as a bare `=`), walrus, then a
|
|
504
|
+
# bare `=` that is neither ==/<=/>=/!= nor the tail of an augmented form.
|
|
505
|
+
_ASSIGN_OP_RE = re.compile(
|
|
506
|
+
r"\*\*=|//=|>>=|<<=|[+\-*/%&|^@]=|:=|(?<![=<>!+\-*/%&|^@:])=(?!=)")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _code_end_col(line_text):
|
|
510
|
+
"""Column where the line's CODE ends: before an inline # comment
|
|
511
|
+
(quote-aware scan, so a '#' inside a string literal doesn't count) and
|
|
512
|
+
before trailing whitespace."""
|
|
513
|
+
quote = None
|
|
514
|
+
i = 0
|
|
515
|
+
n = len(line_text)
|
|
516
|
+
while i < n:
|
|
517
|
+
ch = line_text[i]
|
|
518
|
+
if quote is not None:
|
|
519
|
+
if ch == "\\":
|
|
520
|
+
i += 2
|
|
521
|
+
continue
|
|
522
|
+
if ch == quote:
|
|
523
|
+
quote = None
|
|
524
|
+
elif ch in "'\"":
|
|
525
|
+
quote = ch
|
|
526
|
+
elif ch == "#":
|
|
527
|
+
return len(line_text[:i].rstrip())
|
|
528
|
+
i += 1
|
|
529
|
+
return len(line_text.rstrip())
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _rhs_span(line_text, from_col):
|
|
533
|
+
"""(start_col, end_col) of the assignment's right-hand side on
|
|
534
|
+
`line_text`, searching for the assignment operator from `from_col` (the
|
|
535
|
+
boxed symbol's end) — so `seq_len = input_ids.shape[1]` pills over the
|
|
536
|
+
RHS and reads `seq_len = 301`. None when the line has no assignment
|
|
537
|
+
after the symbol (bare live_view() calls, expression statements) or the
|
|
538
|
+
RHS continues on the next line with nothing on this one."""
|
|
539
|
+
match = _ASSIGN_OP_RE.search(line_text, from_col or 0)
|
|
540
|
+
if match is None:
|
|
541
|
+
return None
|
|
542
|
+
start = match.end()
|
|
543
|
+
while start < len(line_text) and line_text[start] == " ":
|
|
544
|
+
start += 1
|
|
545
|
+
end = _code_end_col(line_text)
|
|
546
|
+
if end <= start:
|
|
547
|
+
return None
|
|
548
|
+
return start, end
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _stacked_list_value(value, ds):
|
|
552
|
+
"""Display-side stacking: a LIST of ≥2 same-shape tensors/ndarrays
|
|
553
|
+
renders as ONE stacked tensor (leading dim = list index) — covering a
|
|
554
|
+
raw captured list (`hiddens = output.hidden_states`), an accumulator
|
|
555
|
+
that fell back to its list path, and stores built by older code. Dtype/
|
|
556
|
+
device drift is coerced to the first element's. Anything else (ragged,
|
|
557
|
+
mixed kinds, non-tensor lists) passes through untouched. Memoized on
|
|
558
|
+
the marker's draw_state — keyed on the list's identity, length, and
|
|
559
|
+
first/last element identity, so a rollover overwrite or an append
|
|
560
|
+
rebuilds while steady-state re-renders are a tuple compare."""
|
|
561
|
+
if not (isinstance(value, list) and len(value) >= 2):
|
|
562
|
+
return value
|
|
563
|
+
kind = type(value[0]).__name__
|
|
564
|
+
if kind not in ("Tensor", "ndarray"):
|
|
565
|
+
return value
|
|
566
|
+
first = value[0]
|
|
567
|
+
if not all(type(v).__name__ == kind
|
|
568
|
+
and tuple(v.shape) == tuple(first.shape) for v in value):
|
|
569
|
+
return value
|
|
570
|
+
key = (id(value), len(value), id(value[0]), id(value[-1]))
|
|
571
|
+
cached = getattr(ds, "_lv_stack_cache", None)
|
|
572
|
+
if cached is not None and cached[0] == key:
|
|
573
|
+
return cached[1]
|
|
574
|
+
try:
|
|
575
|
+
if kind == "Tensor":
|
|
576
|
+
import torch
|
|
577
|
+
stacked = torch.stack([v.detach().to(first.device, first.dtype)
|
|
578
|
+
for v in value])
|
|
579
|
+
else:
|
|
580
|
+
import numpy as np
|
|
581
|
+
stacked = np.stack(value).astype(first.dtype, copy=False)
|
|
582
|
+
except Exception:
|
|
583
|
+
return value
|
|
584
|
+
ds._lv_stack_cache = (key, stacked)
|
|
585
|
+
return stacked
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _merged_dim_names(auto_dims, user_dims):
|
|
589
|
+
"""Combined dim names for an accumulated loop-site value: the auto loop
|
|
590
|
+
names (leading stacked dims) followed by the site's own `# [dim_names=…]`
|
|
591
|
+
(the per-iteration value's dims) — ('l_idx', 'head', 'query', 'key').
|
|
592
|
+
Round-trip stable: a user list that ALREADY starts with the auto names
|
|
593
|
+
(e.g. a merged list written back into the comment by a panel edit) is
|
|
594
|
+
returned as-is instead of gaining a second copy of the loop dims."""
|
|
595
|
+
user = ([user_dims] if isinstance(user_dims, str)
|
|
596
|
+
else [str(d) for d in (user_dims or ())])
|
|
597
|
+
auto = [str(d) for d in auto_dims]
|
|
598
|
+
if user[:len(auto)] == auto:
|
|
599
|
+
return user
|
|
600
|
+
return auto + user
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _padded_dim_names(user_dims, ndim):
|
|
604
|
+
"""dim_names padded to the value's dim count: a list with too few names
|
|
605
|
+
(or none at all) gains positional `dim<i>` entries for the unnamed
|
|
606
|
+
trailing axes, so every axis still gets a picker tab and an edge label.
|
|
607
|
+
`i` is the actual axis index — matching the `dim{i}` fallbacks the voxel
|
|
608
|
+
view already uses for out-of-range axes. Returns None when the names
|
|
609
|
+
already cover ndim (no change needed)."""
|
|
610
|
+
names = ([user_dims] if isinstance(user_dims, str)
|
|
611
|
+
else [str(d) for d in (user_dims or ())])
|
|
612
|
+
if not ndim or len(names) >= ndim:
|
|
613
|
+
return None
|
|
614
|
+
return names + [f"dim{i}" for i in range(len(names), ndim)]
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _build_owner_index(scope_node):
|
|
618
|
+
"""{surfaced key → owning dict} for a scope, the same breadth-first
|
|
619
|
+
walk _override_owner does, done ONCE: a marker's first render used to
|
|
620
|
+
BFS the whole scope per marker (5.5k markers × a 5.6k-line def = 15 s
|
|
621
|
+
on a diff expand, 09-01). First node in BFS order wins, as before."""
|
|
622
|
+
from meltygui.code.libcst_conversion import _is_block_key
|
|
623
|
+
index = {}
|
|
624
|
+
queue = [scope_node]
|
|
625
|
+
for node in queue:
|
|
626
|
+
if not isinstance(node, dict):
|
|
627
|
+
continue
|
|
628
|
+
ovs = node.get("__overrides__")
|
|
629
|
+
for k in node.keys():
|
|
630
|
+
if isinstance(k, str) and k not in index:
|
|
631
|
+
index[k] = node
|
|
632
|
+
if isinstance(ovs, dict):
|
|
633
|
+
for ok in ovs.keys():
|
|
634
|
+
if (isinstance(ok, str) and len(ok) > 4 and ok.startswith("__")
|
|
635
|
+
and ok.endswith("__") and ok[2:-2] not in index):
|
|
636
|
+
index[ok[2:-2]] = node
|
|
637
|
+
for k, v in node.items():
|
|
638
|
+
if isinstance(v, dict) and _is_block_key(k):
|
|
639
|
+
queue.append(v)
|
|
640
|
+
return index
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _owner_index(host_ds, scope_node, def_key, src):
|
|
644
|
+
"""The owner index of a def scope, memoized on `host_ds` (the editor)
|
|
645
|
+
per source version: `src` is the parse's source string (identity = the
|
|
646
|
+
content-free change signal), `def_key` = (def name, def line)."""
|
|
647
|
+
d = host_ds.__dict__
|
|
648
|
+
memo = d.get("_lv_owner_indexes")
|
|
649
|
+
if memo is None or memo[0] is not src:
|
|
650
|
+
memo = (src, {})
|
|
651
|
+
object.__setattr__(host_ds, "_lv_owner_indexes", memo)
|
|
652
|
+
index = memo[1].get(def_key)
|
|
653
|
+
if index is None:
|
|
654
|
+
index = memo[1][def_key] = _build_owner_index(scope_node)
|
|
655
|
+
return index
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _override_owner(scope_node, lookup_key, index_host=None, def_key=None,
|
|
659
|
+
src=None):
|
|
660
|
+
"""The dict that OWNS statement `lookup_key` — the node whose
|
|
661
|
+
__overrides__ carries the site's `# [...]` comment — searched
|
|
662
|
+
breadth-first through surfaced BLOCK children (for/if/try branches, via
|
|
663
|
+
libcst_conversion's _is_block_key), so a loop-body site links its
|
|
664
|
+
comment exactly like a top-level one: the marker reads comment kwargs
|
|
665
|
+
from it, and live_root points set_anywhere's lazy entry at the same
|
|
666
|
+
level the save patcher writes back. Never descends into non-block dicts
|
|
667
|
+
(nested defs, CallParse args — their names live in other scopes).
|
|
668
|
+
Returns scope_node itself when the key isn't surfaced anywhere (sites
|
|
669
|
+
in with/while bodies — the conversion has no node to hang a comment
|
|
670
|
+
on)."""
|
|
671
|
+
if index_host is not None and def_key is not None and src is not None:
|
|
672
|
+
return _owner_index(index_host, scope_node, def_key, src).get(
|
|
673
|
+
lookup_key, scope_node)
|
|
674
|
+
from meltygui.code.libcst_conversion import _is_block_key
|
|
675
|
+
queue = [scope_node]
|
|
676
|
+
for node in queue:
|
|
677
|
+
if not isinstance(node, dict):
|
|
678
|
+
continue
|
|
679
|
+
ovs = node.get("__overrides__")
|
|
680
|
+
if (lookup_key in node
|
|
681
|
+
or (isinstance(ovs, dict) and f"__{lookup_key}__" in ovs)):
|
|
682
|
+
return node
|
|
683
|
+
for k, v in node.items():
|
|
684
|
+
if isinstance(v, dict) and _is_block_key(k):
|
|
685
|
+
queue.append(v)
|
|
686
|
+
return scope_node
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _is_funcdef_node(node):
|
|
690
|
+
"""A def's parse from EITHER parser: a FunctionParse (core_syntax stamps no
|
|
691
|
+
__cst__) or a libcst FunctionDef-backed dict. isinstance, never the class
|
|
692
|
+
NAME: the code host's held tree is reclassed in place to
|
|
693
|
+
`Bubbling_FunctionParse` (bubbling.py), which is what the overlay walk
|
|
694
|
+
hands this function — a name check returned False for every def and the
|
|
695
|
+
snapshot overlay never drew a single live view (08-25)."""
|
|
696
|
+
if not isinstance(node, dict):
|
|
697
|
+
return False
|
|
698
|
+
from meltygui.code.libcst_conversion import FunctionParse
|
|
699
|
+
if isinstance(node, FunctionParse):
|
|
700
|
+
return True
|
|
701
|
+
return type(node.get("__cst__")).__name__ == "FunctionDef"
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _def_name(node):
|
|
705
|
+
"""The def's name: `def_name` (both parsers stamp it) or the libcst node's."""
|
|
706
|
+
name = getattr(node, "def_name", None)
|
|
707
|
+
if name is not None:
|
|
708
|
+
return name
|
|
709
|
+
cst_node = node.get("__cst__") if isinstance(node, dict) else None
|
|
710
|
+
return getattr(getattr(cst_node, "name", None), "value", None)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _is_def_parse(node, def_name):
|
|
714
|
+
return _is_funcdef_node(node) and _def_name(node) == def_name
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _find_def_node(tree, def_name, near_line):
|
|
718
|
+
"""The def parse named `def_name` in `tree` (a module or span parse).
|
|
719
|
+
Module-level defs are a direct key lookup; otherwise (methods, nested
|
|
720
|
+
defs) a plain dict walk, and when the name repeats (same-named methods
|
|
721
|
+
on two classes) the one whose span starts nearest `near_line`. The
|
|
722
|
+
caller memoizes per tree."""
|
|
723
|
+
direct = tree.get(def_name) if isinstance(tree, dict) else None
|
|
724
|
+
if _is_def_parse(direct, def_name):
|
|
725
|
+
return direct
|
|
726
|
+
best, best_d = None, None
|
|
727
|
+
stack, seen = [tree], set()
|
|
728
|
+
while stack:
|
|
729
|
+
node = stack.pop()
|
|
730
|
+
if not isinstance(node, dict) or id(node) in seen:
|
|
731
|
+
continue
|
|
732
|
+
seen.add(id(node))
|
|
733
|
+
if _is_def_parse(node, def_name):
|
|
734
|
+
sp = getattr(node, "span", None)
|
|
735
|
+
d = abs((getattr(sp, "start_line", 0) or 0) - (near_line or 0))
|
|
736
|
+
if best is None or d < best_d:
|
|
737
|
+
best, best_d = node, d
|
|
738
|
+
for v in node.values():
|
|
739
|
+
if isinstance(v, dict):
|
|
740
|
+
stack.append(v)
|
|
741
|
+
return best
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _host_held_tree(node):
|
|
745
|
+
"""The tree the owning code host CURRENTLY holds — the one its chain_out
|
|
746
|
+
serializes — reached from ANY generation of the site's dict: every
|
|
747
|
+
parse dict keeps `_bubble_root` (the dict RenderHost) after a reparse
|
|
748
|
+
orphans it, and the host's `_held()` is the live tree. None when the
|
|
749
|
+
node isn't host-backed (tests, plain parses)."""
|
|
750
|
+
root = getattr(node, "_bubble_root", None)
|
|
751
|
+
held = getattr(root, "_held", None)
|
|
752
|
+
if callable(held):
|
|
753
|
+
try:
|
|
754
|
+
cur = held()
|
|
755
|
+
except Exception:
|
|
756
|
+
return None
|
|
757
|
+
if isinstance(cur, dict):
|
|
758
|
+
return cur
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def current_live_root(ds):
|
|
763
|
+
"""The dict that owns a live site's `# [...]` comment, resolved against
|
|
764
|
+
the tree the code host CURRENTLY holds — for the marker's own comment
|
|
765
|
+
splat, set_anywhere's `# [<key>]` source row, and the replayed window's
|
|
766
|
+
comment re-splat in Melty.draw.
|
|
767
|
+
|
|
768
|
+
Why not the stamped `live_root` (or the editor's code_dict kwarg): every
|
|
769
|
+
reparse (incremental merges included) REPLACES the def dict, its block
|
|
770
|
+
dicts and their __overrides__. The marker re-stamps only when it renders
|
|
771
|
+
(an open window whose def is scrolled off-viewport is pruned whole by
|
|
772
|
+
the overlay walk), and the editor's code_dict is whatever the cached
|
|
773
|
+
tabs body last captured from the host — both lag the host's held tree
|
|
774
|
+
by an arbitrary number of frames. A panel edit then wrote into an
|
|
775
|
+
orphan: the replay re-splatted the value from it (the UI moved), the
|
|
776
|
+
host serialized its OWN tree without it, and the next fresh render
|
|
777
|
+
handed the window the real tree — the value snapped back and
|
|
778
|
+
anywhere_value's cross-check fired ("settled at 1, not the TensorDim(3)
|
|
779
|
+
that was set"), most often when one write's round trip overlapped the
|
|
780
|
+
next write. Resolving through the host puts the write where the save
|
|
781
|
+
reads, whatever the editor happens to be showing.
|
|
782
|
+
|
|
783
|
+
Anchor: the stamped dict's `_bubble_root` → host → `_held()`; the
|
|
784
|
+
editor's code_dict/code_tree is the fallback when the site isn't
|
|
785
|
+
host-backed, then the stamp itself. Memoized per tree on the ds (tree
|
|
786
|
+
identity + its `source` str — the content-free change signal) and kept
|
|
787
|
+
across renders, so the def lookup runs once per reparse per ds."""
|
|
788
|
+
d = ds.__dict__
|
|
789
|
+
stamped = d.get("live_root")
|
|
790
|
+
loc = d.get("_lv_locator")
|
|
791
|
+
if loc is None:
|
|
792
|
+
return stamped
|
|
793
|
+
editor_ds, def_name, def_line = loc
|
|
794
|
+
tree = _host_held_tree(stamped)
|
|
795
|
+
if tree is None and editor_ds is not None:
|
|
796
|
+
# draw_text params (auto-state markers): the spanned node tree
|
|
797
|
+
# rides code_dict on the non-host route (code_tree rides the error
|
|
798
|
+
# dict case) and code_tree on all chain routes.
|
|
799
|
+
ed = editor_ds.__dict__
|
|
800
|
+
tree = ed.get("code_dict")
|
|
801
|
+
if not isinstance(tree, dict):
|
|
802
|
+
tree = ed.get("code_tree")
|
|
803
|
+
if not isinstance(tree, dict):
|
|
804
|
+
return stamped
|
|
805
|
+
src = getattr(tree, "source", None)
|
|
806
|
+
cache = d.get("_lv_owner_cache")
|
|
807
|
+
if (cache is not None and cache[0] is tree
|
|
808
|
+
and (src is None or cache[1] is src)):
|
|
809
|
+
return cache[2]
|
|
810
|
+
# Def lookup + owner BFS memoized on the EDITOR ds per (tree, source):
|
|
811
|
+
# per marker ds they ran once each — 5.5k first renders on a diff
|
|
812
|
+
# expand walked the def 11k times (09-01).
|
|
813
|
+
if editor_ds is not None:
|
|
814
|
+
_dmemo = editor_ds.__dict__.get("_lv_def_nodes")
|
|
815
|
+
if _dmemo is None or _dmemo[0] is not tree or _dmemo[1] is not src:
|
|
816
|
+
_dmemo = (tree, src, {})
|
|
817
|
+
object.__setattr__(editor_ds, "_lv_def_nodes", _dmemo)
|
|
818
|
+
node = _dmemo[2].get((def_name, def_line), _NO_VALUE)
|
|
819
|
+
if node is _NO_VALUE:
|
|
820
|
+
node = _dmemo[2][(def_name, def_line)] = _find_def_node(
|
|
821
|
+
tree, def_name, def_line)
|
|
822
|
+
else:
|
|
823
|
+
node = _find_def_node(tree, def_name, def_line)
|
|
824
|
+
scope = node.get("locals") if isinstance(node, dict) else None
|
|
825
|
+
key = d.get("live_key")
|
|
826
|
+
owner = stamped
|
|
827
|
+
if isinstance(scope, dict) and isinstance(key, str):
|
|
828
|
+
owner = _override_owner(scope, key, index_host=editor_ds,
|
|
829
|
+
def_key=(def_name, def_line),
|
|
830
|
+
src=src if editor_ds is not None else None)
|
|
831
|
+
if owner is not stamped:
|
|
832
|
+
# Keep the stamp fresh for anything still reading it raw.
|
|
833
|
+
object.__setattr__(ds, "live_root", owner)
|
|
834
|
+
object.__setattr__(ds, "_lv_owner_cache", (tree, src, owner))
|
|
835
|
+
return owner
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
# First-spawn size estimates for the display clamps below: the real size
|
|
839
|
+
# only exists after the window's first render (the spawn must pass it).
|
|
840
|
+
# Height: voxel/value windows both land in this size. Width: matches
|
|
841
|
+
# LIVE_WINDOW's initial width in view.py, which IS the first-spawn width.
|
|
842
|
+
_SPAWN_EST_HEIGHT = 380.0
|
|
843
|
+
_SPAWN_EST_WIDTH = 400.0
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _left_of_window_pos(anchor_left, marker_x, marker_y=None, win_h=None,
|
|
847
|
+
win_w=None, gap=50.0, screen_margin=10.0):
|
|
848
|
+
"""Parent-relative window_pos that opens a spawned live-value window just
|
|
849
|
+
to the LEFT of the draw text, vertically level with the marker — instead
|
|
850
|
+
of on top of the code the marker sits in.
|
|
851
|
+
|
|
852
|
+
`anchor_left` is the absolute x of the draw text's left edge (the editor
|
|
853
|
+
draw_state's abs_left — NOT the enclosing window's edge, which can sit
|
|
854
|
+
far left of the text in multi-pane layouts). The value window's RIGHT
|
|
855
|
+
edge sits `gap` left of it, so its left edge needs `win_w` (the live
|
|
856
|
+
width on reopen, the LIVE_WINDOW initial width on first spawn).
|
|
857
|
+
|
|
858
|
+
window_pos is relative to the spawned window's parent (the same editor
|
|
859
|
+
window), and a marker renders at the cursor (abs_left == marker_x when
|
|
860
|
+
window_pos is 0), so the parent-origin x offset is exactly marker_x:
|
|
861
|
+
subtract it from the target absolute left edge to land there. Keeping
|
|
862
|
+
window_pos parent-relative means the value window then tracks the
|
|
863
|
+
editor window as it moves. Returns None when there's nothing to anchor
|
|
864
|
+
to (caller falls back to the default on-cursor placement).
|
|
865
|
+
|
|
866
|
+
`marker_y` enables the display-bottom clamp: a marker near the screen
|
|
867
|
+
bottom would otherwise spawn its window mostly below the display (the
|
|
868
|
+
pinned-anchor bound in _pinned_base_y clamps to the EDITOR window's box,
|
|
869
|
+
which can itself reach the display bottom, and window_pos is
|
|
870
|
+
deliberately outside that bound). The y offset lifts the window just
|
|
871
|
+
enough that `win_h` (the live height on reopen, an estimate on first
|
|
872
|
+
spawn) fits above the display bottom, floored so the top never leaves
|
|
873
|
+
the screen.
|
|
874
|
+
|
|
875
|
+
The x offset gets the same treatment against the display's LEFT edge:
|
|
876
|
+
a draw text flush against it would spawn the value window entirely
|
|
877
|
+
off-screen. Floored so the left edge stays on screen — when the display
|
|
878
|
+
can't fit both, keeping the left edge visible wins."""
|
|
879
|
+
if anchor_left is None:
|
|
880
|
+
return None
|
|
881
|
+
disp = Core.melty.display_size
|
|
882
|
+
est_w = win_w or _SPAWN_EST_WIDTH
|
|
883
|
+
x_off = anchor_left - gap - est_w - marker_x
|
|
884
|
+
if disp:
|
|
885
|
+
x_off = min(x_off, disp[0] - screen_margin - est_w - marker_x)
|
|
886
|
+
x_off = max(x_off, screen_margin - marker_x) # left edge on screen
|
|
887
|
+
y_off = 0.0
|
|
888
|
+
if marker_y is not None and disp:
|
|
889
|
+
est = win_h or _SPAWN_EST_HEIGHT
|
|
890
|
+
y_off = min(0.0, disp[1] - screen_margin - est - marker_y)
|
|
891
|
+
y_off = max(y_off, -marker_y) # keep the title bar on screen
|
|
892
|
+
return (x_off, y_off)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _token_in_selection(line, start_col, end_col, sel_lo, sel_hi):
|
|
896
|
+
"""True when the editor selection is EXACTLY the token on buffer `line`
|
|
897
|
+
spanning [start_col, end_col) ((line, col) tuples, None when the editor
|
|
898
|
+
has no selection / isn't focused). Exact on purpose: only selecting the
|
|
899
|
+
symbol and JUST the symbol (a double-click select) previews its value
|
|
900
|
+
window — a sweep that happens to contain instrumented tokens must not
|
|
901
|
+
pop windows over the text being selected."""
|
|
902
|
+
if line is None or sel_lo is None or sel_hi is None:
|
|
903
|
+
return False
|
|
904
|
+
return sel_lo == (line, start_col) and sel_hi == (line, end_col)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _line_in_selection(line, sel_lo, sel_hi):
|
|
908
|
+
"""True when buffer `line` (1-based) intersects the editor selection's
|
|
909
|
+
line range — inline value pills hide there so the text being selected
|
|
910
|
+
stays readable."""
|
|
911
|
+
return (line is not None and sel_lo is not None and sel_hi is not None
|
|
912
|
+
and sel_lo[0] <= line <= sel_hi[0])
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _draw_marker_at(editor_ds, pos, cursor_inside, token_span, token,
|
|
916
|
+
**marker_kwargs):
|
|
917
|
+
"""Draw a live-view marker at screen `pos` — unless its token lies inside
|
|
918
|
+
the editor selection, in which case it is QUEUED on the editor and drawn
|
|
919
|
+
by `flush_selected_markers` after the overlay walk. Only ONE selected
|
|
920
|
+
token previews its value window at a time (the last one selected), and
|
|
921
|
+
which one can only be decided once every selected token of the frame is
|
|
922
|
+
known — deferring the calls keeps that decision lag-free (no frame where
|
|
923
|
+
two previews show) and keeps all marker state inside the marker body.
|
|
924
|
+
`token_span` = (buffer line, start_col, end_col) in the selection's
|
|
925
|
+
coordinates; `token` is the marker's positional input_value."""
|
|
926
|
+
if cursor_inside and editor_ds is not None:
|
|
927
|
+
fc = Core.melty.frame_count
|
|
928
|
+
if getattr(editor_ds, "_lv_sel_frame", None) != fc:
|
|
929
|
+
editor_ds._lv_sel_frame = fc
|
|
930
|
+
editor_ds._lv_sel_pending = []
|
|
931
|
+
editor_ds._lv_sel_pending.append(
|
|
932
|
+
(token_span, pos, token, marker_kwargs))
|
|
933
|
+
return
|
|
934
|
+
imgui.set_cursor_screen_pos(pos)
|
|
935
|
+
draw_live_view_marker(token, cursor_inside=cursor_inside,
|
|
936
|
+
editor_ds=editor_ds, **marker_kwargs)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _last_selected(pending, sel_caret):
|
|
940
|
+
"""Index into `pending` of the LAST-selected token: the one nearest the
|
|
941
|
+
caret end of the selection. The caret is the moving end of every
|
|
942
|
+
selection gesture (shift+arrows, drag, shift+click), so the token it
|
|
943
|
+
sits nearest is the one the selection most recently grew over — and
|
|
944
|
+
when the selection shrinks back off a token, the remaining nearest one
|
|
945
|
+
takes over. Stateless, so it needs no memory of entry order. "Nearest" is
|
|
946
|
+
in DOCUMENT order: on the caret's line the closer edge, on a line above
|
|
947
|
+
the later token, on a line below the earlier one (a column gap only
|
|
948
|
+
means something on the caret's own line). A missing caret falls to the
|
|
949
|
+
last token in document order."""
|
|
950
|
+
if sel_caret is None:
|
|
951
|
+
return max(range(len(pending)),
|
|
952
|
+
key=lambda i: pending[i][0][:2])
|
|
953
|
+
cl, cc = sel_caret
|
|
954
|
+
|
|
955
|
+
def _key(i):
|
|
956
|
+
line, c0, c1 = pending[i][0]
|
|
957
|
+
if line == cl:
|
|
958
|
+
return (0, min(abs(c0 - cc), abs(c1 - cc)))
|
|
959
|
+
if line < cl:
|
|
960
|
+
return (cl - line, -c1) # above the caret: later = nearer
|
|
961
|
+
return (line - cl, c0) # below the caret: earlier = nearer
|
|
962
|
+
return min(range(len(pending)), key=_key)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def flush_selected_markers(draw_state=None, sel_caret=None, **kwargs):
|
|
966
|
+
"""text_editor calls this once after the overlay walk (inside its
|
|
967
|
+
cursor-neutral bracket): draw the tokens `_draw_marker_at`
|
|
968
|
+
queued for this frame, passing cursor_inside=True to the elected one only
|
|
969
|
+
— the others render exactly as if the caret had left them, closing any
|
|
970
|
+
preview they were showing. The queue is dropped here whatever happens:
|
|
971
|
+
its entries carry the captured values (tensors), so a lingering queue
|
|
972
|
+
would pin a run's generation."""
|
|
973
|
+
pending = getattr(draw_state, "_lv_sel_pending", None) if draw_state else None
|
|
974
|
+
if draw_state is not None:
|
|
975
|
+
draw_state._lv_sel_pending = None
|
|
976
|
+
if not pending:
|
|
977
|
+
return
|
|
978
|
+
winner = _last_selected(pending, sel_caret)
|
|
979
|
+
for i, (_span, pos, token, mk) in enumerate(pending):
|
|
980
|
+
imgui.set_cursor_screen_pos(pos)
|
|
981
|
+
draw_live_view_marker(token, cursor_inside=(i == winner),
|
|
982
|
+
editor_ds=draw_state, **mk)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _marker_idle_skip(editor_ds, name, x, y, w, h, captured, cursor_inside,
|
|
986
|
+
store_obj, key_path, buffer_line, auto_open):
|
|
987
|
+
"""True when marker `name` is provably a NO-OP this frame, letting the
|
|
988
|
+
overlay skip its ~90µs @render_func call entirely. An idle marker (not
|
|
989
|
+
hovered, no caret inside, no open or pending value window, no hover/
|
|
990
|
+
cursor edge left to clear) draws nothing — its only per-frame work is the
|
|
991
|
+
gutter registration and the first-publish watch, both replicated here
|
|
992
|
+
raw. The marker's draw_state comes from editor_ds._lv_marker_ds (stamped
|
|
993
|
+
by the body), so the FIRST render of each marker always takes the full
|
|
994
|
+
path to create it; steady state is a dict hit + a rect test."""
|
|
995
|
+
reg = getattr(editor_ds, "_lv_marker_ds", None) if editor_ds else None
|
|
996
|
+
mds = reg.get(name) if reg else None
|
|
997
|
+
if mds is None:
|
|
998
|
+
return False
|
|
999
|
+
io = imgui.get_io()
|
|
1000
|
+
if x <= io.mouse_pos.x < x + w and y <= io.mouse_pos.y < y + h:
|
|
1001
|
+
return False
|
|
1002
|
+
if (cursor_inside or getattr(mds, "_lv_hovered", False)
|
|
1003
|
+
or getattr(mds, "_lv_cursor_in", False)):
|
|
1004
|
+
return False # interaction, or cursor edge → body must observe
|
|
1005
|
+
wds = getattr(mds, "_lv_window_ds", None)
|
|
1006
|
+
if getattr(mds, "_lv_open", False) or (wds is not None and not wds.closed):
|
|
1007
|
+
return False # open window streams values through the body
|
|
1008
|
+
if captured and auto_open and getattr(mds, "_lv_open", None) is None:
|
|
1009
|
+
return False # first value seen → body must auto-open
|
|
1010
|
+
# Idle - replicate the body's cheap registrations and bail.
|
|
1011
|
+
# Dismissed-latch reset (the body does this while focused with the caret
|
|
1012
|
+
# outside the symbol - skipping every such frame would leave the cursor
|
|
1013
|
+
# preview permanently suppressed after one X-close).
|
|
1014
|
+
if (editor_ds is not None and Core.melty.text_focused_ds is editor_ds
|
|
1015
|
+
and getattr(mds, "_lv_cursor_dismissed", False)):
|
|
1016
|
+
mds._lv_cursor_dismissed = False
|
|
1017
|
+
if editor_ds is not None and buffer_line is not None:
|
|
1018
|
+
if getattr(editor_ds, "_lv_gutter_frame", None) != Core.melty.frame_count:
|
|
1019
|
+
editor_ds._lv_gutter_frame = Core.melty.frame_count
|
|
1020
|
+
editor_ds._lv_gutter_markers = {}
|
|
1021
|
+
editor_ds._lv_gutter_markers.setdefault(buffer_line, []).append(mds)
|
|
1022
|
+
watch(store_obj, key_path, mds, first_only=True)
|
|
1023
|
+
return True
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
from meltygui.view.code_view import draw_live_view_overlay
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _ds_in_window(ds, win_ds, max_hops=64):
|
|
1030
|
+
"""True when `ds` sits inside `win_ds`'s subtree. Walks BOTH up-links —
|
|
1031
|
+
the render-tree `_parent` chain and `parent_window` — because a deferred
|
|
1032
|
+
satellite (e.g. the voxel controls panel) parents to its window via
|
|
1033
|
+
parent_window while its `_parent` chain, stamped at queue time, isn't
|
|
1034
|
+
guaranteed to pass through the window after a root_draw_states
|
|
1035
|
+
re-dispatch. Identity-set + hop cap bound the walk (chains can
|
|
1036
|
+
self-parent at their root)."""
|
|
1037
|
+
seen, stack = set(), [ds]
|
|
1038
|
+
while stack and len(seen) < max_hops:
|
|
1039
|
+
node = stack.pop()
|
|
1040
|
+
if node is None or id(node) in seen:
|
|
1041
|
+
continue
|
|
1042
|
+
if node is win_ds:
|
|
1043
|
+
return True
|
|
1044
|
+
seen.add(id(node))
|
|
1045
|
+
if node._parent is not node:
|
|
1046
|
+
stack.append(node._parent)
|
|
1047
|
+
stack.append(getattr(node, "parent_window", None))
|
|
1048
|
+
return False
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _mouse_in_window_tree(win_ds, mx, my):
|
|
1052
|
+
"""True when (mx, my) is inside `win_ds`'s rect or any open window
|
|
1053
|
+
parented into its subtree — the value window's satellites (the voxel
|
|
1054
|
+
controls panel, a context menu) are separate root windows positioned
|
|
1055
|
+
OUTSIDE the window's own rect, so a press on them must count as
|
|
1056
|
+
engaging with the window."""
|
|
1057
|
+
def _hit(w):
|
|
1058
|
+
try:
|
|
1059
|
+
x, y = w.abs_left, w.abs_top
|
|
1060
|
+
return (x <= mx < x + (w.width or 0)
|
|
1061
|
+
and y <= my < y + (w.height or 0))
|
|
1062
|
+
except Exception:
|
|
1063
|
+
return False
|
|
1064
|
+
if _hit(win_ds):
|
|
1065
|
+
return True
|
|
1066
|
+
for lst in Core.melty.root_draw_states.values():
|
|
1067
|
+
for w in lst:
|
|
1068
|
+
if (w is not win_ds and not w.closed and _hit(w)
|
|
1069
|
+
and _ds_in_window(w, win_ds)):
|
|
1070
|
+
return True
|
|
1071
|
+
return False
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
# auto_state=False: every named param would otherwise be MIRRORED onto the
|
|
1075
|
+
# draw_state (draw_state.value / .store_obj + the _auto_baseline copy) - so a
|
|
1076
|
+
# marker that stops rendering (culled off-viewport, key pruned) would keep
|
|
1077
|
+
# pinning its last tensor AND its last store-owning function (whose
|
|
1078
|
+
# __live_values__ may hold gigabytes of a superseded run) through those
|
|
1079
|
+
# mirrors. The marker writes none of its params, so it needs no auto-state.
|
|
1080
|
+
from meltygui.view.code_view import draw_live_view_marker
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
_VALUE_ATTRS = ("_raw_input_value", "_input_value", "_original_input_ref")
|
|
1084
|
+
_VALUE_KWARGS = ("input_value", "value")
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def _sever_value_pins(window_ds):
|
|
1088
|
+
"""Under `window_ds`, cut every GLState-cached TORCH reference to the
|
|
1089
|
+
displayed value while keeping the cache entry's metadata: the
|
|
1090
|
+
cuda_march path's `cuda_view` (a CudaVolumeView over a strided view of
|
|
1091
|
+
the source) has its `.view` nulled and its key cleared, so the previous
|
|
1092
|
+
generation can die, yet draw_voxels' hold-last-frame path can still
|
|
1093
|
+
read source_shape/mapping off it to keep the slice sliders up. The FBO
|
|
1094
|
+
/ last image / GL-path textures are display-GPU objects and stay."""
|
|
1095
|
+
from meltygui.core.graphics.gl_state import GLState
|
|
1096
|
+
for state in GLState.states_under(window_ds):
|
|
1097
|
+
cv = state.peek("cuda_view")
|
|
1098
|
+
if cv is not None:
|
|
1099
|
+
try:
|
|
1100
|
+
cv.view = None
|
|
1101
|
+
cv._vol_key = None # never a cache hit again
|
|
1102
|
+
except Exception:
|
|
1103
|
+
state.drop("cuda_view")
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def release_live_value(ds, gl=True, keep_image=False):
|
|
1107
|
+
"""Drop every reference a live-value VIEW holds to its captured value, so
|
|
1108
|
+
a tensor the store no longer serves can actually die.
|
|
1109
|
+
|
|
1110
|
+
Draw_states persist (that's the framework contract — a closed window
|
|
1111
|
+
keeps its size/position/params and lazily re-renders), and the wrapper
|
|
1112
|
+
stamps the last-rendered value onto them (`_kwargs['input_value']`,
|
|
1113
|
+
`_raw_input_value`, ...). For the live lab that is the leak: each run
|
|
1114
|
+
publishes NEW tensors (the per-layer accumulators are hundreds of MB),
|
|
1115
|
+
so a marker whose key was pruned (a renamed/removed assignment — every
|
|
1116
|
+
few keystrokes while typing) or a value window the user X-closed kept
|
|
1117
|
+
its last tensor — AND its GLState volume texture — pinned for the rest
|
|
1118
|
+
of the session, one generation per orphan. VRAM climbed with every
|
|
1119
|
+
edit; torch.cuda.empty_cache() can't help while the refs live.
|
|
1120
|
+
|
|
1121
|
+
Called when a key is pruned (live_view._prune_keys, for the marker and
|
|
1122
|
+
its window) and by the marker whenever its window is closed. The marker
|
|
1123
|
+
re-supplies the value on the next show (draw_any(value, draw_state=win)),
|
|
1124
|
+
so nothing is lost: only the STALE copy goes. GL resources under the
|
|
1125
|
+
window are released through the same path a deleted window takes
|
|
1126
|
+
(GLState.on_window_deleted — queued deletes, drained on the GL thread);
|
|
1127
|
+
the window lazily re-uploads when reopened. Safe from any thread (attr
|
|
1128
|
+
writes, queued GL deletes) and idempotent. `keep_image=True` (the
|
|
1129
|
+
fresh-run release) only severs the cached torch refs (_sever_value_pins)
|
|
1130
|
+
and leaves the FBO / last image, so the window holds its last frame
|
|
1131
|
+
until the new value arrives."""
|
|
1132
|
+
if ds is None:
|
|
1133
|
+
return
|
|
1134
|
+
targets = [ds]
|
|
1135
|
+
try:
|
|
1136
|
+
targets.extend(ds.descendants(max_depth=8))
|
|
1137
|
+
except Exception:
|
|
1138
|
+
pass
|
|
1139
|
+
from meltygui.core.core_render import release_input_refs
|
|
1140
|
+
for d in targets:
|
|
1141
|
+
# The wrapper owns more refs than the obvious two: the offscreen
|
|
1142
|
+
# blit stamps `_input_value_cache` (mark_start_offscreen) and the
|
|
1143
|
+
# change detector keeps `_input_cache["external_state"]` - a closed
|
|
1144
|
+
# value window kept a 13 GB stack alive via exactly those.
|
|
1145
|
+
# release_input_refs is the wrapper's own complete list.
|
|
1146
|
+
try:
|
|
1147
|
+
release_input_refs(d)
|
|
1148
|
+
except Exception:
|
|
1149
|
+
pass
|
|
1150
|
+
for attr in _VALUE_ATTRS:
|
|
1151
|
+
if getattr(d, attr, None) is not None:
|
|
1152
|
+
try:
|
|
1153
|
+
setattr(d, attr, None)
|
|
1154
|
+
except Exception:
|
|
1155
|
+
pass
|
|
1156
|
+
kw = getattr(d, "_kwargs", None)
|
|
1157
|
+
if isinstance(kw, dict):
|
|
1158
|
+
for k in _VALUE_KWARGS:
|
|
1159
|
+
if kw.get(k) is not None:
|
|
1160
|
+
kw[k] = None
|
|
1161
|
+
if gl:
|
|
1162
|
+
try:
|
|
1163
|
+
from meltygui.core.graphics.gl_state import GLState
|
|
1164
|
+
if keep_image:
|
|
1165
|
+
_sever_value_pins(ds)
|
|
1166
|
+
else:
|
|
1167
|
+
GLState.on_window_deleted(ds)
|
|
1168
|
+
except Exception:
|
|
1169
|
+
pass
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def _drop_captured_value(store_obj, key_path):
|
|
1173
|
+
"""Forget one captured value's DATA on X-close: the loop accumulator is
|
|
1174
|
+
dropped, the store entry is swapped for the rerun hint, and every
|
|
1175
|
+
marker/window pin + GL texture is severed (live_view.park_rerun_hint).
|
|
1176
|
+
The KEY stays — the widget must keep rendering as captured so it can be
|
|
1177
|
+
reopened; the next run with it open refills it."""
|
|
1178
|
+
if store_obj is None or key_path is None:
|
|
1179
|
+
return
|
|
1180
|
+
try:
|
|
1181
|
+
from meltygui.code.live_view import park_rerun_hint
|
|
1182
|
+
park_rerun_hint(store_obj, key_path)
|
|
1183
|
+
except Exception as e:
|
|
1184
|
+
print(f"live_view: park hint for {key_path} failed: {e!r}")
|
|
1185
|
+
# The tensor is unreferenced now; hand its blocks back to the system so
|
|
1186
|
+
# the VRAM actually drops (allocator cache → empty_cache), off-thread.
|
|
1187
|
+
try:
|
|
1188
|
+
from meltygui.core.runtime.gc_manager import release_cuda_cache_soon
|
|
1189
|
+
release_cuda_cache_soon(label="live view close")
|
|
1190
|
+
except Exception:
|
|
1191
|
+
pass
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _auto_run_on_user_open(editor_ds, store_obj):
|
|
1195
|
+
"""User opened a value window: with Auto Execute on for the def, the
|
|
1196
|
+
def widget recompiles + runs so the window fills (text_editor.
|
|
1197
|
+
fnrun_auto_run_on_open — coalesced there). Never raises."""
|
|
1198
|
+
try:
|
|
1199
|
+
from meltygui.editor.text_editor import fnrun_auto_run_on_open
|
|
1200
|
+
fnrun_auto_run_on_open(editor_ds, store_obj)
|
|
1201
|
+
except Exception as e:
|
|
1202
|
+
print(f"live_view: auto-run on open failed: {e!r}")
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def set_marker_open(marker_ds, open_):
|
|
1206
|
+
"""Gutter-button entry point: latch a marker's value window open/closed
|
|
1207
|
+
from OUTSIDE the marker body (raw draw-list button, no render_func).
|
|
1208
|
+
Mirrors the double-click toggle: flipping open snaps an existing window
|
|
1209
|
+
back to the left of the editor window (it may have been dragged onto
|
|
1210
|
+
the code). The marker ds is invalidated so its body re-runs and
|
|
1211
|
+
creates/hides the window on the next editor render; the CALLER must
|
|
1212
|
+
invalidate the editor tile itself (the marker only renders inside the
|
|
1213
|
+
editor's overlay pass)."""
|
|
1214
|
+
open_ = bool(open_)
|
|
1215
|
+
if bool(getattr(marker_ds, "_lv_open", False)) == open_:
|
|
1216
|
+
return
|
|
1217
|
+
marker_ds._lv_open = open_
|
|
1218
|
+
from meltygui.core.windowing.window_visibility import marker_user_visibility
|
|
1219
|
+
marker_user_visibility(marker_ds, not open_)
|
|
1220
|
+
win_ds = getattr(marker_ds, "_lv_window_ds", None)
|
|
1221
|
+
if open_:
|
|
1222
|
+
_auto_run_on_user_open(getattr(marker_ds, "_lv_editor_ds", None),
|
|
1223
|
+
(getattr(marker_ds, "_kwargs", None) or {}).get("store_obj"))
|
|
1224
|
+
if (open_ and win_ds is not None
|
|
1225
|
+
and "window_pos" not in (win_ds._kwargs or {})):
|
|
1226
|
+
# Clear the window's stale closed flag NOW: this latch is set from
|
|
1227
|
+
# OUTSIDE the marker body (the gutter runs after a close left
|
|
1228
|
+
# closed=True), and the body's own "closed via the header X" check
|
|
1229
|
+
# (win_ds.closed and _lv_open) runs before it repaints the window -
|
|
1230
|
+
# without this reset it reads the previous close as a fresh X click
|
|
1231
|
+
# and cancels the reopen on the spot.
|
|
1232
|
+
win_ds.closed = False
|
|
1233
|
+
_ed = getattr(marker_ds, "_lv_editor_ds", None)
|
|
1234
|
+
pos = _left_of_window_pos(_ed.abs_left if _ed is not None else None,
|
|
1235
|
+
marker_ds.abs_left,
|
|
1236
|
+
marker_y=marker_ds.abs_top,
|
|
1237
|
+
win_h=win_ds.height,
|
|
1238
|
+
win_w=win_ds.width)
|
|
1239
|
+
if pos is not None:
|
|
1240
|
+
win_ds.window_pos = pos
|
|
1241
|
+
marker_ds.invalidate()
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
from meltygui.view.code_view import draw_snapshot_overlay
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def _draw_usage_labels(draw_state, fn, node, span, source_lines, snap_vals,
|
|
1248
|
+
anchor_lines, anchor_keys, origin_x, origin_y,
|
|
1249
|
+
char_w, line_px, lmap, clip, col_shift=0,
|
|
1250
|
+
binding_pills=None):
|
|
1251
|
+
"""Every later USAGE of a captured symbol reads `seq_len=384` — the
|
|
1252
|
+
value is INSERTED right after the symbol and the rest of the line
|
|
1253
|
+
shifts to make room, so name and value show together (the same
|
|
1254
|
+
grid-bending draw_text uses for inline color swatches).
|
|
1255
|
+
|
|
1256
|
+
Two halves, one frame apart: this pass STAMPS the wanted gaps on the
|
|
1257
|
+
editor ds (`_lv_trail_views`: def start → (frame, {(display line0,
|
|
1258
|
+
buffer col): cells}); a change bumps `_lv_trail_gen` + invalidates, and
|
|
1259
|
+
draw_text's _window folds them into _build_vcols as positional trails
|
|
1260
|
+
on its next layout, publishing each gap's start CELL back in
|
|
1261
|
+
`_lv_trail_cells`) — and PAINTS the value pill into every gap already
|
|
1262
|
+
laid out. Raw draw-list paint: no draw_states, no hit rects (clicks
|
|
1263
|
+
fall through; caret/click math stays exact through vcols), no captures
|
|
1264
|
+
— each pill reads the binding's single store entry at draw time (see
|
|
1265
|
+
live_usage's module docstring). The occurrence index is memoized per
|
|
1266
|
+
(source, def, store size); the per-repaint work is one cheap loop over
|
|
1267
|
+
the def's occurrences. Stale defs' stamps are pruned by draw_text after
|
|
1268
|
+
the overlay pass. No caret/selection suppression here: the code text
|
|
1269
|
+
stays fully visible and closing the gap under an active caret would
|
|
1270
|
+
shift the line mid-edit."""
|
|
1271
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
1272
|
+
usages_on = bool(Toggles.TextEditor.live_inline_usages)
|
|
1273
|
+
if not snap_vals or (not usages_on and not binding_pills):
|
|
1274
|
+
return
|
|
1275
|
+
frame = Core.melty.frame_count
|
|
1276
|
+
if not usages_on:
|
|
1277
|
+
# Binding pills only - no occurrence index needed.
|
|
1278
|
+
return _stamp_and_paint(draw_state, fn, span, (), {}, snap_vals,
|
|
1279
|
+
origin_x, origin_y, char_w, line_px, lmap,
|
|
1280
|
+
clip, col_shift, frame, binding_pills)
|
|
1281
|
+
memo = draw_state.__dict__.get("_lv_usage_memo")
|
|
1282
|
+
if memo is None or memo[0] is not source_lines:
|
|
1283
|
+
memo = (source_lines, {})
|
|
1284
|
+
object.__setattr__(draw_state, "_lv_usage_memo", memo)
|
|
1285
|
+
entry = memo[1].get(span.start_line)
|
|
1286
|
+
_fresh_owner = (entry is not None and len(entry) >= 6
|
|
1287
|
+
and entry[0]() is fn)
|
|
1288
|
+
# Key-count changes DEBOUNCE (30 frames): a run adds hundreds of
|
|
1289
|
+
# keys over frames, and rebuilding (a whole-def tokenize) per repaint
|
|
1290
|
+
# during the burst is O(def) per frame. New bindings' pills appear afte
|
|
1291
|
+
# the burst settles; a source/def change rebuilds ASAP.
|
|
1292
|
+
if not _fresh_owner or (entry[1] != len(snap_vals)
|
|
1293
|
+
and frame - entry[5] > 30):
|
|
1294
|
+
labels = vars(fn).get("__live_labels__") or {}
|
|
1295
|
+
# Binding sites extracted from the store's reverse index: name →
|
|
1296
|
+
# (all binding lines, their store keys). The store IS the
|
|
1297
|
+
# binding registry - a usage can only ever show a captured value.
|
|
1298
|
+
bindings = {}
|
|
1299
|
+
for line, key in zip(anchor_lines, anchor_keys):
|
|
1300
|
+
name = live_usage.binding_name(key, labels)
|
|
1301
|
+
if name is None:
|
|
1302
|
+
continue
|
|
1303
|
+
binding_lines, binding_keys = bindings.setdefault(name, ([], []))
|
|
1304
|
+
binding_lines.append(line)
|
|
1305
|
+
binding_keys.append(key)
|
|
1306
|
+
# Nested defs: an occurrence inside a closure's body is that
|
|
1307
|
+
# scope's own name (or a closure read at a DIFFERENT time), never a
|
|
1308
|
+
# plain read of an outer binding - keep out.
|
|
1309
|
+
exclude = []
|
|
1310
|
+
_locals = node.get("locals") if isinstance(node, dict) else None
|
|
1311
|
+
if isinstance(_locals, dict):
|
|
1312
|
+
for child in _locals.values():
|
|
1313
|
+
if isinstance(child, dict) and _is_funcdef_node(child):
|
|
1314
|
+
child_span = getattr(child, "span", None)
|
|
1315
|
+
if child_span is not None:
|
|
1316
|
+
exclude.append((child_span.start_line + 1,
|
|
1317
|
+
child_span.end_line))
|
|
1318
|
+
def_text = "\n".join(source_lines[span.start_line - 1:span.end_line])
|
|
1319
|
+
occurrences = live_usage.build_usage_index(
|
|
1320
|
+
def_text, span.start_line,
|
|
1321
|
+
{n: b[0] for n, b in bindings.items()}, tuple(exclude))
|
|
1322
|
+
entry = (weakref.ref(fn), len(snap_vals), occurrences, bindings,
|
|
1323
|
+
[o[0] for o in occurrences], frame)
|
|
1324
|
+
memo[1][span.start_line] = entry
|
|
1325
|
+
occurrences, bindings = entry[2], entry[3]
|
|
1326
|
+
# VISIBLE BAND ONLY: draw_text renders 20k+ line files and a ne
|
|
1327
|
+
# snapshot binds every local, so the full occurrence list is huge;
|
|
1328
|
+
# per-frame work must stay O(visible). Same approach as the anchor
|
|
1329
|
+
# index: occurrences are line-sorted, bisect the band (display band
|
|
1330
|
+
# ±64 lines of slack for in-flight edit shifts, matching _build_key_index)
|
|
1331
|
+
# and only those need the _lmap / resolve / stamp loop.
|
|
1332
|
+
if clip is not None and line_px:
|
|
1333
|
+
occ_lines = entry[4]
|
|
1334
|
+
band_lo, band_hi = _parse_line_band(lmap, clip, origin_y, line_px)
|
|
1335
|
+
i0 = bisect.bisect_left(occ_lines, band_lo)
|
|
1336
|
+
i1 = bisect.bisect_right(occ_lines, band_hi)
|
|
1337
|
+
occurrences = occurrences[i0:i1]
|
|
1338
|
+
return _stamp_and_paint(draw_state, fn, span, occurrences, bindings,
|
|
1339
|
+
snap_vals, origin_x, origin_y, char_w, line_px,
|
|
1340
|
+
lmap, clip, col_shift, frame, binding_pills)
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def _parse_line_band(lmap, clip, origin_y, line_px, slack=64):
|
|
1344
|
+
"""The visible band as PARSE lines (1-based, `_lv_key_index` /
|
|
1345
|
+
occurrence-list space): the clip's rows are DISPLAY lines, and with
|
|
1346
|
+
folds collapsed a display line sits at a larger buffer line — a band
|
|
1347
|
+
taken straight from the rows bisected the buffer-sorted anchor lists
|
|
1348
|
+
short of every marker past ~64 folded lines (the live views vanished
|
|
1349
|
+
towards the end of a file with its `# [` comments hidden, 09-02). The
|
|
1350
|
+
line map carries the fold layout (`_d2b`: buffer line per display
|
|
1351
|
+
line); rows past its end extend at the same rate. `slack` rows either
|
|
1352
|
+
side cover in-flight edit shifts (the bridge)."""
|
|
1353
|
+
lo = int((clip[1] - origin_y) / line_px)
|
|
1354
|
+
hi = int((clip[3] - origin_y) / line_px)
|
|
1355
|
+
d2b = getattr(lmap, "_d2b", None) if lmap is not None else None
|
|
1356
|
+
if d2b:
|
|
1357
|
+
n = len(d2b)
|
|
1358
|
+
|
|
1359
|
+
def _to_buf(dl):
|
|
1360
|
+
if dl < 0:
|
|
1361
|
+
return dl
|
|
1362
|
+
if dl < n:
|
|
1363
|
+
return d2b[dl]
|
|
1364
|
+
return d2b[-1] + (dl - n + 1)
|
|
1365
|
+
lo, hi = _to_buf(lo), _to_buf(hi)
|
|
1366
|
+
return lo - slack, hi + slack + 1
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def _stamp_and_paint(draw_state, fn, span, occurrences, bindings, snap_vals,
|
|
1370
|
+
origin_x, origin_y, char_w, line_px, lmap, clip,
|
|
1371
|
+
col_shift, frame, binding_pills):
|
|
1372
|
+
"""The trailing-gap stamp + paint shared by usage labels and binding
|
|
1373
|
+
pills (see _draw_usage_labels)."""
|
|
1374
|
+
trails = draw_state.__dict__.setdefault("_lv_trail_views", {})
|
|
1375
|
+
_fvars = vars(fn)
|
|
1376
|
+
publish_seq = _fvars.get("__live_pub_seq__") or {}
|
|
1377
|
+
# governing_key is O(bindings above the line) - a name rebound dozens
|
|
1378
|
+
# of times in a big def (`x`, `_ti`) with ~100 visible occurrences
|
|
1379
|
+
# was a per-frame scan in the thousands. Its answer only changes when
|
|
1380
|
+
# the bindings index rebuilds or a publish re-orders the sequence
|
|
1381
|
+
# (`__live_pub_gen__` bumped by add_view beside __live_pub_seq__).
|
|
1382
|
+
_pub_gen = _fvars.get("__live_pub_gen__", 0)
|
|
1383
|
+
_gov_memo = draw_state.__dict__.get("_lv_gov_memo")
|
|
1384
|
+
if (_gov_memo is None or _gov_memo[0] is not bindings
|
|
1385
|
+
or _gov_memo[1] != _pub_gen):
|
|
1386
|
+
_gov_memo = (bindings, _pub_gen, {})
|
|
1387
|
+
object.__setattr__(draw_state, "_lv_gov_memo", _gov_memo)
|
|
1388
|
+
_gov = _gov_memo[2]
|
|
1389
|
+
# Occurrence loop memo (a no-clip pane case hands EVERY occurrence of
|
|
1390
|
+
# the def here, ~5k in draw_text, every frame): its two outputs only
|
|
1391
|
+
# change with the occurrence index, a publish, the fold layout (the
|
|
1392
|
+
# fresh per-frame index keys its layout as `_d2b`, see draw_text's
|
|
1393
|
+
# _tv_fold_lm) or the geometry. An empty layout (no `_d2b`) means a
|
|
1394
|
+
# merge in flight - no memo. Watches were registered on the pass that
|
|
1395
|
+
# built the entry; they persist.
|
|
1396
|
+
_lm_key = getattr(lmap, "_lv_key", None) if lmap is not None else None
|
|
1397
|
+
_ok_key = (clip is None and (lmap is None or _lm_key is not None)
|
|
1398
|
+
and id(occurrences)) or None
|
|
1399
|
+
# The binding-pill LIST rides in the key by identity: the snapshot
|
|
1400
|
+
# overlay's full-pass memo hands the SAME list object frame after
|
|
1401
|
+
# frame while no key changes state (and pins it in its memo), so a
|
|
1402
|
+
# hit here covers the pills merge below as well.
|
|
1403
|
+
_ok = (_ok_key, _pub_gen, _lm_key, origin_y, line_px, col_shift,
|
|
1404
|
+
id(binding_pills) if binding_pills else 0)
|
|
1405
|
+
_om = draw_state.__dict__.get("_lv_stamp_memo")
|
|
1406
|
+
_hit = bool(_ok_key) and _om is not None and _om[0] == _ok
|
|
1407
|
+
if _hit:
|
|
1408
|
+
sub = _om[1] # shared: never mutated past this point
|
|
1409
|
+
paints = _om[2] # line-sorted, pills merged
|
|
1410
|
+
occurrences = ()
|
|
1411
|
+
binding_pills = None
|
|
1412
|
+
else:
|
|
1413
|
+
sub = {} # (display line0, buffer boundary col) → gap cells
|
|
1414
|
+
paints = [] # visible pills, painted after the stamp below
|
|
1415
|
+
for line, col, name, _last in occurrences:
|
|
1416
|
+
_ml = lmap(line) if lmap else line
|
|
1417
|
+
if _ml is None:
|
|
1418
|
+
continue # inside the mid-edit region - skip a wash
|
|
1419
|
+
_gk = (line, name)
|
|
1420
|
+
key = _gov.get(_gk, _NO_VALUE)
|
|
1421
|
+
if key is _NO_VALUE:
|
|
1422
|
+
binding_lines, binding_keys = bindings[name]
|
|
1423
|
+
key = live_usage.governing_key(binding_lines, binding_keys, line,
|
|
1424
|
+
publish_seq)
|
|
1425
|
+
_gov[_gk] = key
|
|
1426
|
+
if key is None or key not in snap_vals:
|
|
1427
|
+
continue
|
|
1428
|
+
_uval = snap_vals.get(key)
|
|
1429
|
+
pill_text = _inline_value_text(_uval)
|
|
1430
|
+
if pill_text is None:
|
|
1431
|
+
continue
|
|
1432
|
+
# Reads as `seq_len=384`; a color value carries the swatch hole
|
|
1433
|
+
# right after the `=`.
|
|
1434
|
+
_urgba = _inline_swatch_rgba(_uval)
|
|
1435
|
+
swatch = (_urgba, 1) if _urgba is not None else None
|
|
1436
|
+
pill_text = "=" + (_SWATCH_HOLE if _urgba is not None else "") + pill_text
|
|
1437
|
+
# A publish to the governing key must repaint usage pills even when
|
|
1438
|
+
# its binding marker sits off-viewport (culled, so its own full
|
|
1439
|
+
# watch never registered). Idempotent WeakSet add.
|
|
1440
|
+
watch(fn, key, draw_state)
|
|
1441
|
+
cells = len(pill_text) + 1 # one breathing cell around the value
|
|
1442
|
+
boundary_col = col + len(name) + col_shift
|
|
1443
|
+
sub[(_ml - 1, boundary_col)] = cells
|
|
1444
|
+
pill_y = origin_y + (_ml - 1) * line_px
|
|
1445
|
+
if clip is not None and (pill_y + line_px < clip[1]
|
|
1446
|
+
or pill_y > clip[3]):
|
|
1447
|
+
continue
|
|
1448
|
+
paints.append((_ml - 1, boundary_col, pill_text, len(name), pill_y,
|
|
1449
|
+
name, swatch or ()))
|
|
1450
|
+
# Binding pills: the captured value of an assignment/param target,
|
|
1451
|
+
# inserted right after ITS symbol exactly like a usage label -
|
|
1452
|
+
# `edited=False, new_text='...' = draw_text(...)`. A usage gap already at
|
|
1453
|
+
# the same boundary wins (same captured value).
|
|
1454
|
+
if binding_pills:
|
|
1455
|
+
for line0, bcol, pill_text, sym_len, swatch in binding_pills:
|
|
1456
|
+
if (line0, bcol) in sub:
|
|
1457
|
+
continue
|
|
1458
|
+
sub[(line0, bcol)] = len(pill_text) + 1
|
|
1459
|
+
pill_y = origin_y + line0 * line_px
|
|
1460
|
+
if clip is not None and (pill_y + line_px < clip[1]
|
|
1461
|
+
or pill_y > clip[3]):
|
|
1462
|
+
continue
|
|
1463
|
+
paints.append((line0, bcol, pill_text, sym_len, pill_y, "",
|
|
1464
|
+
swatch or ()))
|
|
1465
|
+
if _ok_key and not _hit:
|
|
1466
|
+
# `_layout`, `occurrences` and the pill list ride along so the ids
|
|
1467
|
+
# in the key stay pinned. Paints are stored line-sorted so a hit
|
|
1468
|
+
# can bisect the laid-out window instead of walking every
|
|
1469
|
+
# occurrence.
|
|
1470
|
+
paints.sort()
|
|
1471
|
+
object.__setattr__(draw_state, "_lv_stamp_memo",
|
|
1472
|
+
(_ok, sub, paints, lmap, occurrences,
|
|
1473
|
+
binding_pills))
|
|
1474
|
+
previous = trails.get(span.start_line)
|
|
1475
|
+
trails[span.start_line] = (frame, sub)
|
|
1476
|
+
if previous is None or previous[1] != sub:
|
|
1477
|
+
# New/changed gaps - relayout next frame (draw_text's _window clears
|
|
1478
|
+
# its cache on the stamp).
|
|
1479
|
+
draw_state._lv_trail_gen = getattr(draw_state, "_lv_trail_gen", 0) + 1
|
|
1480
|
+
draw_state.invalidate()
|
|
1481
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1482
|
+
request_render()
|
|
1483
|
+
# Paint into the gaps the CURRENT layout reserved (stamped back by
|
|
1484
|
+
# draw_text's _window). A gap not laid out yet - first frame after a
|
|
1485
|
+
# value appeared - skips painting; it opens on the very next layout.
|
|
1486
|
+
gap_cells = draw_state.__dict__.get("_lv_trail_cells") or {}
|
|
1487
|
+
base_x = origin_x - col_shift * char_w # buffer cell 0 in px
|
|
1488
|
+
if not gap_cells:
|
|
1489
|
+
paints = ()
|
|
1490
|
+
elif _hit and len(paints) > 256:
|
|
1491
|
+
# Memo hit: `paints` is line-sorted - only the lines draw_text has
|
|
1492
|
+
# gaps open for (the tokenized window) can paint, so bisect to them.
|
|
1493
|
+
_glo = min(k[0] for k in gap_cells)
|
|
1494
|
+
_ghi = max(k[0] for k in gap_cells)
|
|
1495
|
+
paints = paints[bisect.bisect_left(paints, (_glo,)):
|
|
1496
|
+
bisect.bisect_right(paints, (_ghi + 1,))]
|
|
1497
|
+
for line0, boundary_col, pill_text, name_len, pill_y, name, swatch in paints:
|
|
1498
|
+
gap_cell = gap_cells.get((line0, boundary_col))
|
|
1499
|
+
if gap_cell is None:
|
|
1500
|
+
continue
|
|
1501
|
+
gap_x = base_x + gap_cell * char_w
|
|
1502
|
+
# The pill wears the marker tint under its symbol (a tinted
|
|
1503
|
+
# definition of the name, else the enclosing function, else the
|
|
1504
|
+
# file) - one memoized lookup per pill.
|
|
1505
|
+
_paint_value_pill(pill_text, gap_x + 3.0, pill_y,
|
|
1506
|
+
tint=_pill_tint(draw_state, line0, name or None),
|
|
1507
|
+
swatch=swatch or None)
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
def _symbol_cols(anchor, rel_line, key_path, source_lines, labels, memo=None):
|
|
1511
|
+
"""(start_col, end_col) of the symbol a snapshot key boxes, or None to
|
|
1512
|
+
drop the key (a line-keyed anchor on a blank / out-of-range line).
|
|
1513
|
+
Resolved ONCE per source version into the anchor index: this is a
|
|
1514
|
+
regex per key, and running it per key per FRAME was 45 of the 70 ms a
|
|
1515
|
+
5k-key def (draw_text in the context menu's Code pane) cost per
|
|
1516
|
+
selection frame (09-01)."""
|
|
1517
|
+
_rl, start_col, end_col = anchor
|
|
1518
|
+
if memo is not None:
|
|
1519
|
+
# Anchor index rebuilds (every reparse while typing): the answer
|
|
1520
|
+
# depends only on the line's text + the key, and nearly every line
|
|
1521
|
+
# is unchanged, so the regex runs once per (line text, key).
|
|
1522
|
+
text = (source_lines[rel_line - 1]
|
|
1523
|
+
if 1 <= rel_line <= len(source_lines) else None)
|
|
1524
|
+
_mk = (text, key_path[-1], end_col is None, start_col)
|
|
1525
|
+
_hit = memo.get(_mk, _NO_VALUE)
|
|
1526
|
+
if _hit is not _NO_VALUE:
|
|
1527
|
+
return _hit
|
|
1528
|
+
_res = _symbol_cols(anchor, rel_line, key_path, source_lines, labels)
|
|
1529
|
+
memo[_mk] = _res
|
|
1530
|
+
return _res
|
|
1531
|
+
if end_col is None:
|
|
1532
|
+
# No span (line-only keys) - box the LABELED SYMBOL on the line when
|
|
1533
|
+
# the store's key names one (frame-snapshot params and twin
|
|
1534
|
+
# while-body keys always carry their a's name; attribute keys
|
|
1535
|
+
# box just their FINAL segment - see _label_box_span), falling
|
|
1536
|
+
# back to the whole line's text. Full-line boxes stack into an
|
|
1537
|
+
# unreadable double-washed region when several line-keyed values
|
|
1538
|
+
# land on adjacent lines (a captured signature), and their click
|
|
1539
|
+
# latches swallow the lines.
|
|
1540
|
+
if not (1 <= rel_line <= len(source_lines)):
|
|
1541
|
+
return None
|
|
1542
|
+
text = source_lines[rel_line - 1]
|
|
1543
|
+
if not text.strip():
|
|
1544
|
+
return None
|
|
1545
|
+
span_cols = _label_box_span(labels.get(key_path), text)
|
|
1546
|
+
if span_cols is not None:
|
|
1547
|
+
return span_cols
|
|
1548
|
+
return len(text) - len(text.lstrip()), len(text.rstrip())
|
|
1549
|
+
# The leaf span covers the assignment (or maybe just its target,
|
|
1550
|
+
# depending on the key) - box the target itself so the highlight
|
|
1551
|
+
# (and its click latch) doesn't swallow the line: the first
|
|
1552
|
+
# word-boundary occurrence of the target name on the line (the
|
|
1553
|
+
# assignment target precedes any RHS use of the name).
|
|
1554
|
+
name = key_path[-1].split("#", 1)[0]
|
|
1555
|
+
text = (source_lines[rel_line - 1]
|
|
1556
|
+
if 1 <= rel_line <= len(source_lines) else "")
|
|
1557
|
+
m = re.search(rf"\b{re.escape(name)}\b", text)
|
|
1558
|
+
if m is not None:
|
|
1559
|
+
return m.start(), m.end()
|
|
1560
|
+
return start_col, start_col + max(1, len(name))
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
def _node_owns_function(node, fn, span, line_offset=0):
|
|
1564
|
+
"""True when the def node at `span` IS the definition of `fn` — the name
|
|
1565
|
+
matches and fn's first line (its top decorator, so it may sit a few
|
|
1566
|
+
lines ABOVE the span's def line) lies inside the span's line range. A
|
|
1567
|
+
closure's node fails this for the enclosing function it resolved to."""
|
|
1568
|
+
from meltygui.code.libcst_conversion import parse_def_name
|
|
1569
|
+
try:
|
|
1570
|
+
inner = inspect.unwrap(fn)
|
|
1571
|
+
except Exception:
|
|
1572
|
+
inner = fn
|
|
1573
|
+
if parse_def_name(node) != getattr(inner, "__name__", None):
|
|
1574
|
+
return False
|
|
1575
|
+
code = getattr(inner, "__code__", None)
|
|
1576
|
+
first = getattr(code, "co_firstlineno", None)
|
|
1577
|
+
if first is None:
|
|
1578
|
+
return True
|
|
1579
|
+
decorator_slack = 16
|
|
1580
|
+
start = span.start_line + line_offset
|
|
1581
|
+
end = getattr(span, "end_line", span.start_line) + line_offset
|
|
1582
|
+
return start - decorator_slack <= first <= end
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def _scope_function(filename, def_line):
|
|
1586
|
+
"""The live function object for a def at an absolute file line — the same
|
|
1587
|
+
resolver capture uses, so the store read here is the store written to."""
|
|
1588
|
+
from meltygui.code.chain_converters import _enclosing_function
|
|
1589
|
+
try:
|
|
1590
|
+
return _enclosing_function(filename, def_line + 1)
|
|
1591
|
+
except Exception:
|
|
1592
|
+
return None
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _label_box_span(label, text):
|
|
1596
|
+
"""(start_col, end_col) of the symbol a line-keyed marker should box on
|
|
1597
|
+
`text`, or None (caller falls back to the whole line). A plain
|
|
1598
|
+
identifier boxes its first word-boundary occurrence. A DOTTED label
|
|
1599
|
+
(attribute keys: `draw_state.some_val`) boxes only its FINAL segment,
|
|
1600
|
+
matched right after its dotted prefix — so the base name's own marker
|
|
1601
|
+
(boxing `draw_state`) and the attribute's (boxing `some_val`) never
|
|
1602
|
+
overlap, and each toggles its own value window."""
|
|
1603
|
+
if not isinstance(label, str) or not label:
|
|
1604
|
+
return None
|
|
1605
|
+
if label.isidentifier():
|
|
1606
|
+
m = re.search(rf"\b{re.escape(label)}\b", text)
|
|
1607
|
+
return m.span() if m is not None else None
|
|
1608
|
+
if "." in label:
|
|
1609
|
+
prefix, last = label.rsplit(".", 1)
|
|
1610
|
+
if not last.isidentifier():
|
|
1611
|
+
return None
|
|
1612
|
+
m = re.search(rf"\b{re.escape(prefix)}\s*\.\s*({re.escape(last)})\b",
|
|
1613
|
+
text)
|
|
1614
|
+
return m.span(1) if m is not None else None
|
|
1615
|
+
return None
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
def _snap_line_to_text(text, rel_line, source_lines, lo, hi):
|
|
1619
|
+
"""Buffer line an exit-line wash should sit on: the stamped line if its
|
|
1620
|
+
current content still equals the stamped run-time `text` (stripped), else
|
|
1621
|
+
the NEAREST line in the def's [lo, hi] span with that content — so the
|
|
1622
|
+
return/error washes follow their statement through edits the same way
|
|
1623
|
+
line-keyed markers follow their symbols. No stamped text (old-shape
|
|
1624
|
+
stamp) or no match keeps the stamp unchanged."""
|
|
1625
|
+
want = text.strip() if isinstance(text, str) else ""
|
|
1626
|
+
if not want:
|
|
1627
|
+
return rel_line
|
|
1628
|
+
if (1 <= rel_line <= len(source_lines)
|
|
1629
|
+
and source_lines[rel_line - 1].strip() == want):
|
|
1630
|
+
return rel_line
|
|
1631
|
+
best = None
|
|
1632
|
+
for ln in range(max(1, lo), min(len(source_lines), hi) + 1):
|
|
1633
|
+
if best is not None and abs(ln - rel_line) >= abs(best - rel_line):
|
|
1634
|
+
continue
|
|
1635
|
+
if source_lines[ln - 1].strip() == want:
|
|
1636
|
+
best = ln
|
|
1637
|
+
return best if best is not None else rel_line
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
_IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
def _label_line_index(source_lines, lo, hi):
|
|
1644
|
+
"""{identifier: sorted [1-based lines]} for every identifier appearing in
|
|
1645
|
+
the def's [lo, hi] span — one linear pass, built once per source version
|
|
1646
|
+
(memoized by the caller in the _src-keyed snap memo). Turns the per-key
|
|
1647
|
+
label relocation from an O(def lines) regex scan into a lookup."""
|
|
1648
|
+
idx = {}
|
|
1649
|
+
for ln in range(max(1, lo), min(len(source_lines), hi) + 1):
|
|
1650
|
+
for m in _IDENT_RE.finditer(source_lines[ln - 1]):
|
|
1651
|
+
idx.setdefault(m.group(), []).append(ln)
|
|
1652
|
+
return idx
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def _snap_line_to_label(label, rel_line, source_lines, lo, hi, lidx=None):
|
|
1656
|
+
"""Buffer line a line-keyed marker should anchor on: the stamped line if
|
|
1657
|
+
it still contains `label` (the common case — one regex on one line), else
|
|
1658
|
+
the NEAREST line in the def's [lo, hi] span that does. Pure string ops on
|
|
1659
|
+
the already-split source — the symbol reference is recovered from the
|
|
1660
|
+
line stamp without any reparse, so line-keyed captures follow their
|
|
1661
|
+
symbols through edits instead of staying pinned where the last run left
|
|
1662
|
+
them. No match anywhere (symbol renamed/removed — the value is stale and
|
|
1663
|
+
the next run prunes it) keeps the stamp unchanged.
|
|
1664
|
+
|
|
1665
|
+
`lidx` (from _label_line_index) replaces the def-wide scan with a
|
|
1666
|
+
candidate lookup on the label's final identifier segment; candidates are
|
|
1667
|
+
tried nearest-first and verified with _label_box_span (dotted labels
|
|
1668
|
+
need their prefix checked)."""
|
|
1669
|
+
if not label:
|
|
1670
|
+
return rel_line
|
|
1671
|
+
if (1 <= rel_line <= len(source_lines)
|
|
1672
|
+
and _label_box_span(label, source_lines[rel_line - 1]) is not None):
|
|
1673
|
+
return rel_line
|
|
1674
|
+
if lidx is not None:
|
|
1675
|
+
cand = lidx.get(label.rsplit(".", 1)[-1].split("[", 1)[0])
|
|
1676
|
+
if not cand:
|
|
1677
|
+
return rel_line
|
|
1678
|
+
i = bisect.bisect_left(cand, rel_line)
|
|
1679
|
+
lo_i, hi_i = i - 1, i
|
|
1680
|
+
while lo_i >= 0 or hi_i < len(cand):
|
|
1681
|
+
_below = cand[lo_i] if lo_i >= 0 else None
|
|
1682
|
+
_above = cand[hi_i] if hi_i < len(cand) else None
|
|
1683
|
+
if _above is None or (_below is not None
|
|
1684
|
+
and rel_line - _below <= _above - rel_line):
|
|
1685
|
+
ln, lo_i = _below, lo_i - 1
|
|
1686
|
+
else:
|
|
1687
|
+
ln, hi_i = _above, hi_i + 1
|
|
1688
|
+
if _label_box_span(label, source_lines[ln - 1]) is not None:
|
|
1689
|
+
return ln
|
|
1690
|
+
return rel_line
|
|
1691
|
+
best = None
|
|
1692
|
+
for ln in range(max(1, lo), min(len(source_lines), hi) + 1):
|
|
1693
|
+
if best is not None and abs(ln - rel_line) >= abs(best - rel_line):
|
|
1694
|
+
continue
|
|
1695
|
+
if _label_box_span(label, source_lines[ln - 1]) is not None:
|
|
1696
|
+
best = ln
|
|
1697
|
+
return best if best is not None else rel_line
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def _key_anchor(scope_node, key_path, line_offset):
|
|
1701
|
+
"""(buffer-relative line, start col | None, end col | None) the symbol
|
|
1702
|
+
box for a snapshot key: descend the scope's locals by the key path to the
|
|
1703
|
+
leaf's span; a `line:N` tail IS the (absolute) anchor, with no column
|
|
1704
|
+
information (the caller boxes the whole line's text)."""
|
|
1705
|
+
tail = key_path[-1]
|
|
1706
|
+
if tail.startswith("line:"):
|
|
1707
|
+
# A '#name' suffix is the per-param qualifier frame snapshots append
|
|
1708
|
+
# so several params on one def signature line keep distinct keys.
|
|
1709
|
+
try:
|
|
1710
|
+
return int(tail[5:].split("#", 1)[0]) - line_offset, None, None
|
|
1711
|
+
except ValueError:
|
|
1712
|
+
return None
|
|
1713
|
+
node = scope_node.get("locals")
|
|
1714
|
+
if not isinstance(node, dict):
|
|
1715
|
+
node = scope_node
|
|
1716
|
+
for seg in key_path[:-1]:
|
|
1717
|
+
node = node.get(seg) if isinstance(node, dict) else None
|
|
1718
|
+
if node is None:
|
|
1719
|
+
return None
|
|
1720
|
+
spans = getattr(node, "_child_spans", None) or {}
|
|
1721
|
+
sp = spans.get(tail)
|
|
1722
|
+
if sp is None:
|
|
1723
|
+
child = node.get(tail) if isinstance(node, dict) else None
|
|
1724
|
+
sp = getattr(child, "span", None)
|
|
1725
|
+
if sp is None:
|
|
1726
|
+
return None
|
|
1727
|
+
end_col = getattr(sp, "end_col", None)
|
|
1728
|
+
return (sp.start_line,
|
|
1729
|
+
getattr(sp, "start_col", 0) if end_col is not None else None,
|
|
1730
|
+
end_col)
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
# ── Run-once snapshot view: draw_function + instrumentation + source ──
|
|
1734
|
+
|
|
1735
|
+
_proxies = weakref.WeakKeyDictionary()
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def _run_proxy(fn):
|
|
1739
|
+
"""A stable callable twin-runner for draw_function: same name/signature as
|
|
1740
|
+
`fn` (so the param UI builds identically), but the button runs the
|
|
1741
|
+
INSTRUMENTED twin. Cached per function object — identity survives hotswap,
|
|
1742
|
+
and run_instrumented re-twins per code version underneath."""
|
|
1743
|
+
proxy = _proxies.get(fn)
|
|
1744
|
+
if proxy is None:
|
|
1745
|
+
def proxy(**kw):
|
|
1746
|
+
from meltygui.code.live_instrument import run_instrumented
|
|
1747
|
+
return run_instrumented(fn, **kw)
|
|
1748
|
+
proxy.__name__ = fn.__name__
|
|
1749
|
+
proxy.__qualname__ = fn.__qualname__
|
|
1750
|
+
proxy.__signature__ = inspect.signature(fn)
|
|
1751
|
+
_proxies[fn] = proxy
|
|
1752
|
+
return proxy
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
def request_run(lab_ds):
|
|
1762
|
+
"""Ctrl+Enter = the Run button, nothing more: find the lab's
|
|
1763
|
+
draw_function runner draw_state and stamp a one-shot run request;
|
|
1764
|
+
draw_function pops it on its next render and calls the same _run() a
|
|
1765
|
+
button click does (same single-flight _run_busy guard, same twin path —
|
|
1766
|
+
the twin already compiles from the pending in-memory source, so running
|
|
1767
|
+
IS the latest code). Ctrl+Enter's DUAL dispatch calls this from both
|
|
1768
|
+
halves (the Ctrl+F rule: the behavior must live in BOTH places): the
|
|
1769
|
+
lab body's blocking on_action while the subtree renders, and draw_main's
|
|
1770
|
+
root ctrl_enter fallback when the lab is blit-cached (per-frame
|
|
1771
|
+
subscriptions lapse under a cached ancestor, so the root re-routes via
|
|
1772
|
+
BVH). At most one half fires per press — the body's blocking sub stops
|
|
1773
|
+
the chain before the root's."""
|
|
1774
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1775
|
+
for d in lab_ds.descendants(max_depth=8):
|
|
1776
|
+
if str(getattr(d, 'name', '')).endswith(" runner"):
|
|
1777
|
+
d.misc["_run_requested"] = True
|
|
1778
|
+
d.invalidate() # cached runner must re-render to consume
|
|
1779
|
+
request_render()
|
|
1780
|
+
return
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
# use_cache=False: the body must run every frame so its Ctrl+Enter on_action
|
|
1784
|
+
# re-registers - subscriptions are per-frame, and a blit-cached body would
|
|
1785
|
+
# drop the action, letting draw_main's global handler win. The two column
|
|
1786
|
+
# children keep their own tile caches, so the shell itself is all this
|
|
1787
|
+
# dispatch about.
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
def is_volume(value):
|
|
1792
|
+
"""True for values the voxel renderer should own: 3-D+ numeric tensors or
|
|
1793
|
+
ndarrays. 2-D and below keep their existing views; name-based checks so
|
|
1794
|
+
torch/numpy never import for scalar traffic."""
|
|
1795
|
+
cls = type(value).__name__
|
|
1796
|
+
try:
|
|
1797
|
+
if cls == "Tensor":
|
|
1798
|
+
return value.dim() >= 3
|
|
1799
|
+
if cls == "ndarray":
|
|
1800
|
+
return value.ndim >= 3 and value.dtype.kind in "fiu"
|
|
1801
|
+
except Exception:
|
|
1802
|
+
pass
|
|
1803
|
+
return False
|