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,152 @@
|
|
|
1
|
+
"""draw_code_line_fast — ONE line of Python painted straight onto a draw list.
|
|
2
|
+
|
|
3
|
+
The editor's look without the editor: the same Darcula palette
|
|
4
|
+
(`text_editor.COLORS` through `tokenize`), the same definition BLOCK wash
|
|
5
|
+
behind a tinted class/def body and the same occurrence washes behind every
|
|
6
|
+
symbol whose definition carries a tint (`roster_tints.collect_def_tints`,
|
|
7
|
+
adjusted through the same `_bg_adjust` knobs), the same glyph-toward-wash
|
|
8
|
+
mix. No render_func, no draw_state, no wrapper: a plain function over a
|
|
9
|
+
draw list, so a list of code rows (the Ctrl+B picker, search results) costs
|
|
10
|
+
a few `add_text` calls per row instead of a one-line editor body each.
|
|
11
|
+
|
|
12
|
+
Two halves, deliberately split:
|
|
13
|
+
• `CodeLineTints` resolves WHAT to paint behind a line (block tint +
|
|
14
|
+
span washes) — the only part that touches the roster, memoized per
|
|
15
|
+
(file, 64-line chunk) so a picker of N rows in one file runs the scan
|
|
16
|
+
once per chunk, not once per row. Resolve it when the rows are BUILT
|
|
17
|
+
(the picker open), never per frame.
|
|
18
|
+
• `draw_code_line_fast` paints — cheap enough to run every frame.
|
|
19
|
+
|
|
20
|
+
Monospace contract: every caller pushes the editor's font
|
|
21
|
+
(`push_code_font`), so glyph x = col * char_w and consecutive same-colour
|
|
22
|
+
ASCII tokens merge into one `add_text` exactly as draw_text's fast path
|
|
23
|
+
does. Font Awesome icons are NOT monospaced: each is drawn in its own cell.
|
|
24
|
+
"""
|
|
25
|
+
import bisect
|
|
26
|
+
|
|
27
|
+
import meltygui_imgui as imgui
|
|
28
|
+
from meltygui.hdr_color import pack_color
|
|
29
|
+
|
|
30
|
+
from meltygui.core.styling.fonts import Font
|
|
31
|
+
from meltygui.core.melty import Melty
|
|
32
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def push_code_font(font=Font.FONTAWESOME_MONO_19):
|
|
36
|
+
"""Push the editor's font (JetBrains Mono + Font Awesome) and measure it.
|
|
37
|
+
Returns (pushed, char_w, line_h): pass `pushed` to `pop_code_font`,
|
|
38
|
+
`char_w` / `line_h` to every `draw_code_line_fast` call in the batch."""
|
|
39
|
+
pushed = False
|
|
40
|
+
if Melty.font_mgr is not None:
|
|
41
|
+
handle = Melty.font_mgr.get(font)
|
|
42
|
+
if handle is not None:
|
|
43
|
+
imgui.push_font(handle)
|
|
44
|
+
pushed = True
|
|
45
|
+
return pushed, imgui.calc_text_size("0").x, imgui.get_text_line_height()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pop_code_font(pushed):
|
|
49
|
+
if pushed:
|
|
50
|
+
imgui.pop_font()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _u32(rgb, alpha):
|
|
54
|
+
return pack_color(rgb[0], rgb[1], rgb[2], alpha)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _wash_factors():
|
|
58
|
+
"""(block, symbol, text) factor tuples for `_bg_adjust` — the exact
|
|
59
|
+
triples draw_text builds from Toggles.TextEditor each frame."""
|
|
60
|
+
min_b = Toggles.TextEditor.bg_min_brightness
|
|
61
|
+
max_b = Toggles.TextEditor.bg_max_brightness
|
|
62
|
+
return ((Toggles.TextEditor.bg_tint_saturation,
|
|
63
|
+
Toggles.TextEditor.bg_tint_value, min_b, max_b),
|
|
64
|
+
(Toggles.TextEditor.symbol_tint_saturation,
|
|
65
|
+
Toggles.TextEditor.symbol_tint_value, min_b, max_b),
|
|
66
|
+
(Toggles.TextEditor.text_tint_saturation,
|
|
67
|
+
Toggles.TextEditor.text_tint_value, min_b, max_b))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _split_emphasis(col, token, emphasis):
|
|
71
|
+
"""Pieces of `token` (starting at column `col`) as (start_col, text,
|
|
72
|
+
bright) — cut at the boundaries of the `emphasis` column spans."""
|
|
73
|
+
end = col + len(token)
|
|
74
|
+
cuts = {col, end}
|
|
75
|
+
for c0, c1 in emphasis:
|
|
76
|
+
if col < c0 < end:
|
|
77
|
+
cuts.add(c0)
|
|
78
|
+
if col < c1 < end:
|
|
79
|
+
cuts.add(c1)
|
|
80
|
+
edges = sorted(cuts)
|
|
81
|
+
out = []
|
|
82
|
+
for a, b in zip(edges, edges[1:]):
|
|
83
|
+
bright = any(c0 <= a < c1 for c0, c1 in emphasis)
|
|
84
|
+
out.append((a, token[a - col:b - col], bright))
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CodeLineTints:
|
|
92
|
+
"""Resolves (block_tint, spans) for lines of files, memoized per
|
|
93
|
+
(file, text identity, 64-line chunk): the roster's windowed occurrence
|
|
94
|
+
scan runs once per chunk touched, and every line of that chunk reads
|
|
95
|
+
its washes from the result. Build one per row set (picker open, search
|
|
96
|
+
landing); it holds the file texts it was given."""
|
|
97
|
+
CHUNK = 64
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
self._chunks = {} # (path, id(text), chunk_lo) -> (blocks, spans_by_line)
|
|
101
|
+
self._texts = {} # id(text) -> text (holds the id's)
|
|
102
|
+
|
|
103
|
+
def tints(self, path, text, line):
|
|
104
|
+
"""`line` is 1-based. Returns (block_tint | None, [(col0, col1, rgb,
|
|
105
|
+
scale)]) — columns relative to the RAW line (indent included); use
|
|
106
|
+
`shift_spans` after stripping a display line."""
|
|
107
|
+
if not text or line < 1:
|
|
108
|
+
return None, []
|
|
109
|
+
lo = ((line - 1) // self.CHUNK) * self.CHUNK
|
|
110
|
+
key = (str(path), id(text), lo)
|
|
111
|
+
got = self._chunks.get(key)
|
|
112
|
+
if got is None:
|
|
113
|
+
got = self._chunks[key] = self._scan(path, text, lo)
|
|
114
|
+
self._texts[id(text)] = text
|
|
115
|
+
blocks, by_line = got
|
|
116
|
+
bl = line - 1
|
|
117
|
+
block_tint, best_start = None, -1
|
|
118
|
+
for start, _idx, end, tint in blocks:
|
|
119
|
+
if start <= bl <= end and start > best_start:
|
|
120
|
+
block_tint, best_start = tint, start
|
|
121
|
+
return block_tint, by_line.get(bl, [])
|
|
122
|
+
|
|
123
|
+
def _scan(self, path, text, lo):
|
|
124
|
+
from meltygui.editor.roster_tints import collect_def_tints
|
|
125
|
+
from meltygui.editor.text_editor import _line_starts
|
|
126
|
+
starts = _line_starts(text)
|
|
127
|
+
hi = min(lo + self.CHUNK - 1, len(starts) - 1)
|
|
128
|
+
try:
|
|
129
|
+
blocks, spans, _lines, _names = collect_def_tints(
|
|
130
|
+
text, 0, path, window=(lo, hi), hold_live=False)
|
|
131
|
+
except Exception:
|
|
132
|
+
return (), {}
|
|
133
|
+
by_line = {}
|
|
134
|
+
for s, e, rgb, scale in spans:
|
|
135
|
+
# bisect on the memoized line starts: the line holding `s`.
|
|
136
|
+
li = bisect.bisect_right(starts, s) - 1
|
|
137
|
+
if li < lo or li > hi:
|
|
138
|
+
continue
|
|
139
|
+
ls = starts[li]
|
|
140
|
+
by_line.setdefault(li, []).append((s - ls, e - ls, rgb, scale))
|
|
141
|
+
return tuple(blocks), by_line
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def shift_spans(spans, offset):
|
|
145
|
+
"""Line-relative spans after `offset` leading characters were stripped
|
|
146
|
+
from the display text (spans that fell inside the strip are dropped)."""
|
|
147
|
+
out = []
|
|
148
|
+
for c0, c1, rgb, scale in spans:
|
|
149
|
+
c0, c1 = c0 - offset, c1 - offset
|
|
150
|
+
if c1 > 0:
|
|
151
|
+
out.append((max(0, c0), c1, rgb, scale))
|
|
152
|
+
return out
|
meltygui/editor/diff.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Line comparison primitives shared by text inspection and integrations."""
|
|
2
|
+
import difflib
|
|
3
|
+
import bisect
|
|
4
|
+
|
|
5
|
+
def diff_opcodes(a_lines, b_lines):
|
|
6
|
+
"""Non-equal SequenceMatcher opcodes over two line lists, with the common
|
|
7
|
+
prefix/suffix trimmed BEFORE matching. SequenceMatcher walks its full
|
|
8
|
+
matching structure even for identical inputs — 108ms on an 11k-line file
|
|
9
|
+
with ZERO changes, paid on the render thread at every diff open — while
|
|
10
|
+
real compares differ in a few localized spans. Trimming equal edge lines
|
|
11
|
+
first drops the identical/localized case to ~2ms and leaves the spread
|
|
12
|
+
case unchanged. The opcodes reconstruct b exactly and change the same
|
|
13
|
+
number of lines as untrimmed difflib (pinned across repo-history pairs
|
|
14
|
+
in tests/test_diff_trim.py); when a change is bracketed by identical
|
|
15
|
+
lines (an inserted block that starts or ends with the blank line that
|
|
16
|
+
also precedes/follows it) its ANCHOR can land a line away from
|
|
17
|
+
untrimmed difflib's — equally minimal, equally valid, the same freedom
|
|
18
|
+
every trimming diff tool (git included) exercises. Bit-exact parity is
|
|
19
|
+
NOT attainable from a window: difflib resolves those ties through its
|
|
20
|
+
global longest-match recursion, so the same local shape anchors
|
|
21
|
+
differently depending on far-away content (fast_dock vs text_editor
|
|
22
|
+
pairs, 08-24). Offsets are restored before returning."""
|
|
23
|
+
len_a, len_b = len(a_lines), len(b_lines)
|
|
24
|
+
lo = 0
|
|
25
|
+
limit = min(len_a, len_b)
|
|
26
|
+
while lo < limit and a_lines[lo] == b_lines[lo]:
|
|
27
|
+
lo += 1
|
|
28
|
+
hi = 0
|
|
29
|
+
limit = min(len_a, len_b) - lo
|
|
30
|
+
while hi < limit and a_lines[len_a - 1 - hi] == b_lines[len_b - 1 - hi]:
|
|
31
|
+
hi += 1
|
|
32
|
+
sm = difflib.SequenceMatcher(a=a_lines[lo:len_a - hi],
|
|
33
|
+
b=b_lines[lo:len_b - hi], autojunk=False)
|
|
34
|
+
return [(tag, i1 + lo, i2 + lo, j1 + lo, j2 + lo)
|
|
35
|
+
for tag, i1, i2, j1, j2 in sm.get_opcodes() if tag != "equal"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _diff_blocks(base, new):
|
|
40
|
+
"""Change blocks for the side-by-side compare view: SequenceMatcher
|
|
41
|
+
opcodes over lines, as (new0, new1, base0, base1, tag) tuples with
|
|
42
|
+
0-based half-open LINE ranges — new* in the editable buffer (left
|
|
43
|
+
pane), base* in the reference (right pane, which draws the ribbons).
|
|
44
|
+
tag is 'replace' / 'delete' / 'insert' (delete = lines only in the
|
|
45
|
+
reference). Empty list = identical. diff_opcodes trims the common
|
|
46
|
+
prefix/suffix before matching — the difference between ~108ms and
|
|
47
|
+
~2ms on the render thread for a big file's diff open."""
|
|
48
|
+
from meltygui.editor.diff import diff_opcodes
|
|
49
|
+
return [(j1, j2, i1, i2, tag)
|
|
50
|
+
for tag, i1, i2, j1, j2 in diff_opcodes(base.split("\n"),
|
|
51
|
+
new.split("\n"))]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _incremental_diff_blocks(base, old_text, new_text, blocks):
|
|
55
|
+
"""Splice-update `blocks` (the diff base→old_text) into the diff
|
|
56
|
+
base→new_text for a LOCALIZED edit (typing) without re-matching the
|
|
57
|
+
whole file: line-trim the old↔new change window, widen it (+margin)
|
|
58
|
+
out of any overlapping blocks so both edges sit in verified-EQUAL
|
|
59
|
+
regions, map those edges into base space exactly (cumulative block
|
|
60
|
+
delta — exact outside blocks), SequenceMatcher only the windows, and
|
|
61
|
+
splice: blocks before stay, blocks after shift by the line delta. The
|
|
62
|
+
result is a correct (possibly non-minimal) diff. Returns None when the
|
|
63
|
+
edit isn't localized enough to pay off — the caller falls back to the
|
|
64
|
+
full (debounced) diff."""
|
|
65
|
+
old_lines = old_text.split("\n")
|
|
66
|
+
new_lines = new_text.split("\n")
|
|
67
|
+
old_count, new_count = len(old_lines), len(new_lines)
|
|
68
|
+
overlap = min(old_count, new_count)
|
|
69
|
+
common_head = 0
|
|
70
|
+
while (common_head < overlap
|
|
71
|
+
and old_lines[common_head] == new_lines[common_head]):
|
|
72
|
+
common_head += 1
|
|
73
|
+
if common_head == old_count and old_count == new_count:
|
|
74
|
+
return list(blocks) # identical diff
|
|
75
|
+
common_tail = 0
|
|
76
|
+
while (common_tail < overlap - common_head
|
|
77
|
+
and old_lines[old_count - 1 - common_tail]
|
|
78
|
+
== new_lines[new_count - 1 - common_tail]):
|
|
79
|
+
common_tail += 1
|
|
80
|
+
line_delta = new_count - old_count
|
|
81
|
+
window_start = max(0, common_head - 8)
|
|
82
|
+
window_end = min(old_count, old_count - common_tail + 8)
|
|
83
|
+
# Widen past any overlapping block (fixpoint - widening can reach the
|
|
84
|
+
# next block...).
|
|
85
|
+
grew = True
|
|
86
|
+
while grew:
|
|
87
|
+
grew = False
|
|
88
|
+
for n0, n1, _b0, _b1, _t in blocks:
|
|
89
|
+
if (n0 < window_end and n1 > window_start
|
|
90
|
+
and (n0 < window_start or n1 > window_end)):
|
|
91
|
+
window_start = min(window_start, n0)
|
|
92
|
+
window_end = max(window_end, n1)
|
|
93
|
+
grew = True
|
|
94
|
+
if (window_end - window_start) > 0.5 * max(old_count, 1):
|
|
95
|
+
return None # not localized - full diff
|
|
96
|
+
before, inside, after = [], [], []
|
|
97
|
+
for block in blocks:
|
|
98
|
+
if block[1] <= window_start:
|
|
99
|
+
before.append(block)
|
|
100
|
+
elif block[0] >= window_end:
|
|
101
|
+
after.append(block)
|
|
102
|
+
elif window_start <= block[0] and block[1] <= window_end:
|
|
103
|
+
inside.append(block)
|
|
104
|
+
else:
|
|
105
|
+
return None # straddler (shouldn't happen)
|
|
106
|
+
delta_before = sum((b1 - b0) - (n1 - n0)
|
|
107
|
+
for n0, n1, b0, b1, _t in before)
|
|
108
|
+
delta_inside = sum((b1 - b0) - (n1 - n0)
|
|
109
|
+
for n0, n1, b0, b1, _t in inside)
|
|
110
|
+
base_lines = base.split("\n")
|
|
111
|
+
base_start = window_start + delta_before
|
|
112
|
+
base_end = window_end + delta_before + delta_inside
|
|
113
|
+
if (not (0 <= base_start <= base_end <= len(base_lines))
|
|
114
|
+
or window_end + line_delta < window_start):
|
|
115
|
+
return None
|
|
116
|
+
matcher = difflib.SequenceMatcher(
|
|
117
|
+
a=base_lines[base_start:base_end],
|
|
118
|
+
b=new_lines[window_start:window_end + line_delta], autojunk=False)
|
|
119
|
+
local = [(window_start + j1, window_start + j2,
|
|
120
|
+
base_start + i1, base_start + i2, tag)
|
|
121
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes() if tag != "equal"]
|
|
122
|
+
return (before + local
|
|
123
|
+
+ [(n0 + line_delta, n1 + line_delta, b0, b1, t)
|
|
124
|
+
for n0, n1, b0, b1, t in after])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _diff_disp_span(pane, l0, l1):
|
|
128
|
+
"""Half-open buffer line range -> half-open display line range. A range
|
|
129
|
+
swallowed by a collapsed fold collapses onto the fold header's row, so
|
|
130
|
+
its wash/ribbon lands on the collapse line (IntelliJ-style)."""
|
|
131
|
+
d2b = getattr(pane, "_diff_d2b", None)
|
|
132
|
+
if d2b is None:
|
|
133
|
+
return l0, l1
|
|
134
|
+
d0 = max(bisect.bisect_right(d2b, int(l0)) - 1, 0)
|
|
135
|
+
if l1 <= l0:
|
|
136
|
+
return d0, d0
|
|
137
|
+
d1 = max(bisect.bisect_right(d2b, int(l1) - 1) - 1, 0) + 1
|
|
138
|
+
return d0, max(d1, d0)
|
|
139
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""External Changes window — the outside-in twin of draw_pending_saves.
|
|
2
|
+
|
|
3
|
+
PendingSave tracks edits made IN the studio that haven't reached disk yet;
|
|
4
|
+
ExternalChanges tracks edits made OUTSIDE the studio that have. FileWatch
|
|
5
|
+
already sees every fs event in the watched dirs — _on_event hands each one
|
|
6
|
+
here together with the pre-change code_cache text, which becomes the diff
|
|
7
|
+
baseline. Current text is never stored: the watcher pops code_cache on every
|
|
8
|
+
change, so Melty.read_code at render time is both fresh and cheap.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import difflib
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from meltygui.core.rendering.render_funcs import RenderFuncs
|
|
15
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
16
|
+
from meltygui.core.core_render import render_func
|
|
17
|
+
from meltygui.core.rendering.window_decoration import window
|
|
18
|
+
from meltygui.editor.pending_save import _diff_lines_with_numbers
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExternalChanges:
|
|
22
|
+
# resolved path (str) → file text BEFORE the first recorded change
|
|
23
|
+
# This is setdefault-only: later events on the same file keep the same
|
|
24
|
+
# baseline, so the window shows the accumulated drift since the studio
|
|
25
|
+
# last opened the file - not just the latest write.
|
|
26
|
+
originals = {}
|
|
27
|
+
_window_ds = None # draw_external_changes' draw_state - the wake target
|
|
28
|
+
# path → (id(baseline), id(disk text)) stamped when a recompile absorbed
|
|
29
|
+
# this exact drift into the pending queue (PendingSave.resolve_external).
|
|
30
|
+
# Absorbing must not hide the entry - the window keeps showing what an
|
|
31
|
+
# external program changed until the user dismisses it. The marker only
|
|
32
|
+
# stops re-processing: absorb skips the file while the ids still match,
|
|
33
|
+
# and find_conflicts won't call the absorbed drift a conflict. Both ids
|
|
34
|
+
# are content-free change signals (baseline is setdefault-once; the
|
|
35
|
+
# watcher pops code_cache on every write, so a NEW external edit yields a
|
|
36
|
+
# new disk object and the marker naturally expires).
|
|
37
|
+
absorbed = {}
|
|
38
|
+
# path → disk text the last absorb was computed against (the SYNC frame).
|
|
39
|
+
# `originals` is display-only accumulation (setdefault-once, kept until the
|
|
40
|
+
# user dismisses); merging must diff from the last text the pending queue
|
|
41
|
+
# and live co_firstlineno's were rebased to - after an absorb that is the
|
|
42
|
+
# absorbed disk, not the original baseline. Diffing new drift from the old
|
|
43
|
+
# baseline would re-apply already-absorbed hunks (false lineno shifts,
|
|
44
|
+
# phantom conflicts). Falls back to `originals` before the first absorb.
|
|
45
|
+
synced = {}
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def on_file_event(cls, src_path, old_text):
|
|
49
|
+
"""Called from FileWatch._on_event (observer thread) for every fs
|
|
50
|
+
event, with the code_cache text as it was BEFORE the event popped it.
|
|
51
|
+
old_text=None means the studio never read the file — nothing to
|
|
52
|
+
baseline against, so it isn't tracked."""
|
|
53
|
+
from meltygui.core.melty import FileWatch
|
|
54
|
+
from meltygui.core.melty import Melty
|
|
55
|
+
if FileWatch.is_self_write(src_path):
|
|
56
|
+
return # an in-process save, not an outside program
|
|
57
|
+
# An external write obsoletes any cached NO-OP pending edits for this
|
|
58
|
+
# file (data == load-time original): they'd shadow the new disk
|
|
59
|
+
# content on every span reload. Before the old_text gate - the queue
|
|
60
|
+
# must clear even for a file the studio never cached. Hopped to the
|
|
61
|
+
# render thread; frame time invalidates the queue.
|
|
62
|
+
from meltygui.editor.pending_save import PendingSave
|
|
63
|
+
Melty.post_to_render(lambda p=src_path: PendingSave.drop_noop_entries_for(p))
|
|
64
|
+
if old_text is None:
|
|
65
|
+
return
|
|
66
|
+
try:
|
|
67
|
+
resolved = str(Path(src_path).resolve())
|
|
68
|
+
except OSError:
|
|
69
|
+
return
|
|
70
|
+
cls.originals.setdefault(resolved, old_text)
|
|
71
|
+
# Same wake mechanism as FileWatch.dispatch_event_for: the flag makes
|
|
72
|
+
# core_render skip the blit cache for the window's next pass, so the
|
|
73
|
+
# new diff actually renders instead of replaying a stale tile.
|
|
74
|
+
ds = cls._window_ds
|
|
75
|
+
if ds is not None:
|
|
76
|
+
ds._external_change = True
|
|
77
|
+
# The conflict window shares the same edge: external drift may now
|
|
78
|
+
# overlap a pending span. Lazy import (merge_files imports this module).
|
|
79
|
+
try:
|
|
80
|
+
from meltygui.core.runtime.extensions import call
|
|
81
|
+
call('conflicts_changed')
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
request_render()
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def dismiss_all(cls):
|
|
88
|
+
cls.originals.clear()
|
|
89
|
+
cls.absorbed.clear()
|
|
90
|
+
cls.synced.clear()
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def untrack(cls, path):
|
|
94
|
+
"""Drop every record for `path` — dismissals and drift-back heals.
|
|
95
|
+
The next external event re-baselines from whatever the cache holds
|
|
96
|
+
then, so a stale sync frame must never outlive its originals entry."""
|
|
97
|
+
cls.originals.pop(path, None)
|
|
98
|
+
cls.absorbed.pop(path, None)
|
|
99
|
+
cls.synced.pop(path, None)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def mark_absorbed(cls, path, disk_text):
|
|
103
|
+
"""Record that the CURRENT drift of `path` (baseline → `disk_text`,
|
|
104
|
+
the held read_code object) has been folded into the pending queue."""
|
|
105
|
+
baseline = cls.originals.get(path)
|
|
106
|
+
if baseline is not None:
|
|
107
|
+
cls.absorbed[path] = (id(baseline), id(disk_text))
|
|
108
|
+
# The sync point moved with this absorb: the merge window's original
|
|
109
|
+
# world (symbol_roster.World over synced/originals) re-keys on it.
|
|
110
|
+
try:
|
|
111
|
+
from meltygui.code.symbol_roster import bump_disk_generation
|
|
112
|
+
bump_disk_generation()
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def is_absorbed(cls, path, disk_text):
|
|
118
|
+
"""True while `path`'s tracked drift is exactly the one a recompile
|
|
119
|
+
already absorbed — expires the moment either side changes object."""
|
|
120
|
+
baseline = cls.originals.get(path)
|
|
121
|
+
return (baseline is not None
|
|
122
|
+
and cls.absorbed.get(path) == (id(baseline), id(disk_text)))
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def recompile_all(cls):
|
|
126
|
+
"""External changes are no longer hotswapped directly from here (the
|
|
127
|
+
raw whole-module reload bypassed the pending machinery and caused
|
|
128
|
+
stale-tile invalidation issues). Delegate to the Pending Saves window:
|
|
129
|
+
PendingSave.recompile_all hotswaps the pending queue through the
|
|
130
|
+
established per-entry path — external drift is NOT absorbed; it is
|
|
131
|
+
merged manually via the merge window. recompile_all_ui drives
|
|
132
|
+
the Pending Saves button's own runner draw_state (busy spinner,
|
|
133
|
+
fading check mark + summary), so any caller of this alias gets the
|
|
134
|
+
exact button-click UI. Kept for backward compatibility — the MCP
|
|
135
|
+
recompile tool now calls PendingSave.recompile_all_ui directly."""
|
|
136
|
+
from meltygui.editor.pending_save import PendingSave
|
|
137
|
+
return PendingSave.recompile_all_ui()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Hotswap / dual-identity guard. A hotswap re-executes this module — under the
|
|
141
|
+
# BARE 'lsd....' name, while meltygui's _on_event hook imports the 'src.lsd....' one -
|
|
142
|
+
# so without this the window body and the event feed end up on two different
|
|
143
|
+
# ExternalChanges classes (split-brain: events land in a dict the window never
|
|
144
|
+
# reads). Alias the mutable state from whichever twin is already loaded, so
|
|
145
|
+
# every identity shares ONE originals dict and a hotswap never wipes the queue.
|
|
146
|
+
import sys as _sys
|
|
147
|
+
for _n in ("meltygui.editor.external_changes",
|
|
148
|
+
"lsd.gl_gui.view.core_views.external_changes"):
|
|
149
|
+
_twin = getattr(_sys.modules.get(_n), "ExternalChanges", None)
|
|
150
|
+
if _twin is not None and _twin is not ExternalChanges:
|
|
151
|
+
ExternalChanges.originals = _twin.originals
|
|
152
|
+
ExternalChanges._window_ds = _twin._window_ds
|
|
153
|
+
ExternalChanges.absorbed = getattr(_twin, "absorbed", ExternalChanges.absorbed)
|
|
154
|
+
ExternalChanges.synced = getattr(_twin, "synced", ExternalChanges.synced)
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
from meltygui.view.file_view import draw_external_changes
|
|
159
|
+
draw_external_changes = window(disable_scroll=False, tint=(0.16296297311782837, 0.212430558282882, 0.2611111), icon=None)(draw_external_changes)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""File labels shared by the file UI and chat change summaries."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import meltygui_imgui as imgui
|
|
4
|
+
from meltygui.core.melty import Melty
|
|
5
|
+
from meltygui.core.runtime.toggles import Toggles, Tint
|
|
6
|
+
from meltygui.hdr_color import pack_color
|
|
7
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
8
|
+
from meltygui.editor.source_ui import _tab_text_color
|
|
9
|
+
|
|
10
|
+
_ELLIPSIZE_MEMO = globals().get("_ELLIPSIZE_MEMO", {})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ellipsize_search(text, max_width):
|
|
14
|
+
low, high = 0, len(text)
|
|
15
|
+
while low < high:
|
|
16
|
+
mid = (low + high + 1) // 2
|
|
17
|
+
if imgui.calc_text_size(text[:mid] + "\u2026").x <= max_width:
|
|
18
|
+
low = mid
|
|
19
|
+
else:
|
|
20
|
+
high = mid - 1
|
|
21
|
+
return text[:low].rstrip() + "\u2026"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ellipsize(text, max_width):
|
|
25
|
+
"""`text` cut to fit `max_width` pixels in the current font with a
|
|
26
|
+
trailing ellipsis; unchanged when it already fits."""
|
|
27
|
+
# Memoized per (text, width, font scale): the commit-file column runs
|
|
28
|
+
# this for each row every frame, and each miss is a calc_text_size
|
|
29
|
+
# binary search.
|
|
30
|
+
memo_key = (text, max_width, Melty.ui_scale)
|
|
31
|
+
hit = _ELLIPSIZE_MEMO.get(memo_key)
|
|
32
|
+
if hit is not None:
|
|
33
|
+
return hit
|
|
34
|
+
if max_width <= 0 or imgui.calc_text_size(text).x <= max_width:
|
|
35
|
+
result = text
|
|
36
|
+
else:
|
|
37
|
+
result = _ellipsize_search(text, max_width)
|
|
38
|
+
if len(_ELLIPSIZE_MEMO) > 4096:
|
|
39
|
+
_ELLIPSIZE_MEMO.clear()
|
|
40
|
+
_ELLIPSIZE_MEMO[memo_key] = result
|
|
41
|
+
return result
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Display-time USAGE index for live values.
|
|
2
|
+
|
|
3
|
+
A usage of a captured symbol is never captured and never holds the value:
|
|
4
|
+
`build_usage_index` maps each occurrence of a bound name inside an
|
|
5
|
+
instrumented def to its position, and the paint pass (live_view_views.
|
|
6
|
+
_draw_usage_labels) resolves the governing BINDING at draw time and reads
|
|
7
|
+
that binding's single store entry by reference. Nothing runs in the user's
|
|
8
|
+
function for usages, and the only derived data is the formatted label.
|
|
9
|
+
|
|
10
|
+
stdlib-only (tokenize) on purpose: exact word boundaries, and string/comment
|
|
11
|
+
interiors are excluded for free. The index is rebuilt only when the parse
|
|
12
|
+
source changes or the store's key set grows (the caller memoizes) — never
|
|
13
|
+
per frame.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import bisect
|
|
17
|
+
import io
|
|
18
|
+
import keyword
|
|
19
|
+
import tokenize
|
|
20
|
+
|
|
21
|
+
# A NAME right after one of these is never a value usage: definitions,
|
|
22
|
+
# import aliases, loop/with/except binding targets, lambda params,
|
|
23
|
+
# attribute access.
|
|
24
|
+
_SKIP_AFTER = frozenset((
|
|
25
|
+
"def", "class", "import", "from", "as", "global", "nonlocal",
|
|
26
|
+
"for", "lambda", "."))
|
|
27
|
+
|
|
28
|
+
_OPEN_BRACKETS = frozenset("([{")
|
|
29
|
+
_CLOSE_BRACKETS = frozenset(")]}")
|
|
30
|
+
_SKIP_TOKEN_TYPES = frozenset((
|
|
31
|
+
tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT,
|
|
32
|
+
tokenize.COMMENT, tokenize.ENCODING, tokenize.ENDMARKER))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def binding_name(key_path, labels):
|
|
36
|
+
"""The SYMBOL a store key binds — its label when that's a plain
|
|
37
|
+
identifier, else the name encoded in the key tail ('x', 'x#1',
|
|
38
|
+
'line:N#x'). None for keys that don't name a symbol (call-site keys
|
|
39
|
+
like 'live_view()#1', expression labels, keyword tails)."""
|
|
40
|
+
label = (labels or {}).get(key_path)
|
|
41
|
+
if isinstance(label, str) and label.isidentifier() \
|
|
42
|
+
and not keyword.iskeyword(label):
|
|
43
|
+
return label
|
|
44
|
+
tail = key_path[-1] if key_path else None
|
|
45
|
+
if not isinstance(tail, str):
|
|
46
|
+
return None
|
|
47
|
+
if tail.startswith("line:"):
|
|
48
|
+
name = tail.partition("#")[2]
|
|
49
|
+
else:
|
|
50
|
+
name = tail.partition("#")[0]
|
|
51
|
+
if name.isidentifier() and not keyword.iskeyword(name):
|
|
52
|
+
return name
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _next_significant(tokens, i):
|
|
57
|
+
"""The next token past NL/comment/indent noise, else None."""
|
|
58
|
+
for j in range(i + 1, len(tokens)):
|
|
59
|
+
if tokens[j][0] not in _SKIP_TOKEN_TYPES:
|
|
60
|
+
return tokens[j]
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_usage_index(def_text, first_line, binding_lines, exclude_ranges=()):
|
|
65
|
+
"""Occurrences of the bound names inside one def's source.
|
|
66
|
+
|
|
67
|
+
`def_text` is the def's source slice (its `def` line first), sitting at
|
|
68
|
+
parse line `first_line`; `binding_lines` maps name → SORTED parse lines
|
|
69
|
+
of that name's binding sites (from the store's anchors);
|
|
70
|
+
`exclude_ranges` are (lo, hi) parse-line ranges to skip — nested def
|
|
71
|
+
bodies, where an occurrence is the closure's own scope, not a value
|
|
72
|
+
read of the outer binding.
|
|
73
|
+
|
|
74
|
+
Returns [(parse_line, col, name, last_on_line), …] sorted by position:
|
|
75
|
+
every NAME token equal to a bound name that is a genuine USE — not an
|
|
76
|
+
attribute (`.name`), not a keyword argument (`name=` inside brackets),
|
|
77
|
+
not a binding/definition target (first occurrence of the name on one
|
|
78
|
+
of its own binding lines, or a NAME right after def/for/as/…), and
|
|
79
|
+
only with at least one binding textually ABOVE it (same-line bindings
|
|
80
|
+
govern nothing on their own line: in `x = x + 1` the RHS x reads the
|
|
81
|
+
previous binding). `last_on_line` is True when no further CODE token
|
|
82
|
+
follows on the occurrence's line (comments don't count) — the pill may
|
|
83
|
+
then run past the symbol's width, since there's nothing to cover."""
|
|
84
|
+
out = []
|
|
85
|
+
# The slice keeps the module's indentation; tokenize likes column-0
|
|
86
|
+
# code, so strip the first line's indent from every line that carries it
|
|
87
|
+
# and tack it back onto the reported columns. Lines that don't start
|
|
88
|
+
# with it live inside multi-line strings - the tokenizer consumes those
|
|
89
|
+
# as part of the STRING token, so leaving them untouched is fine.
|
|
90
|
+
lines = def_text.split("\n")
|
|
91
|
+
indent = len(lines[0]) - len(lines[0].lstrip()) if lines else 0
|
|
92
|
+
if indent:
|
|
93
|
+
pad = " " * indent
|
|
94
|
+
lines = [l[indent:] if l.startswith(pad) else l for l in lines]
|
|
95
|
+
def_text = "\n".join(lines)
|
|
96
|
+
try:
|
|
97
|
+
tokens = list(tokenize.generate_tokens(io.StringIO(def_text).readline))
|
|
98
|
+
except (tokenize.TokenError, IndentationError, SyntaxError):
|
|
99
|
+
return out
|
|
100
|
+
previous = None # last significant token string
|
|
101
|
+
depth = 0 # bracket depth, for the kwarg test
|
|
102
|
+
seen_on_line = set() # (line, name) - binding-target skip
|
|
103
|
+
for i, tok in enumerate(tokens):
|
|
104
|
+
token_type, string, start = tok[0], tok[1], tok[2]
|
|
105
|
+
if token_type in _SKIP_TOKEN_TYPES:
|
|
106
|
+
continue
|
|
107
|
+
if token_type == tokenize.OP:
|
|
108
|
+
if string in _OPEN_BRACKETS:
|
|
109
|
+
depth += 1
|
|
110
|
+
elif string in _CLOSE_BRACKETS:
|
|
111
|
+
depth = max(0, depth - 1)
|
|
112
|
+
previous = string
|
|
113
|
+
continue
|
|
114
|
+
if token_type != tokenize.NAME:
|
|
115
|
+
previous = string
|
|
116
|
+
continue
|
|
117
|
+
name = string
|
|
118
|
+
line = first_line - 1 + start[0]
|
|
119
|
+
col = start[1] + indent
|
|
120
|
+
skip_after = previous in _SKIP_AFTER
|
|
121
|
+
previous = name
|
|
122
|
+
binding_list = binding_lines.get(name)
|
|
123
|
+
if not binding_list or skip_after or keyword.iskeyword(name):
|
|
124
|
+
continue
|
|
125
|
+
if any(lo <= line <= hi for lo, hi in exclude_ranges):
|
|
126
|
+
continue
|
|
127
|
+
# Keyword argument: `f(name=...)` names the call's parameter, not
|
|
128
|
+
# this local - a bare `=` (never `==`) inside brackets.
|
|
129
|
+
if depth > 0:
|
|
130
|
+
nxt = _next_significant(tokens, i)
|
|
131
|
+
if nxt is not None and nxt[0] == tokenize.OP and nxt[1] == "=":
|
|
132
|
+
continue
|
|
133
|
+
# Binding target: the FIRST occurrence of the name on one of its
|
|
134
|
+
# own binding lines is the assignment's left side; later ones on
|
|
135
|
+
# the same line (`x = x + 1`) are reads of the previous binding.
|
|
136
|
+
first_on_line = (line, name) not in seen_on_line
|
|
137
|
+
seen_on_line.add((line, name))
|
|
138
|
+
if first_on_line and _in_sorted(binding_list, line):
|
|
139
|
+
continue
|
|
140
|
+
if bisect.bisect_left(binding_list, line) == 0:
|
|
141
|
+
continue # nothing bound above - nothing to show
|
|
142
|
+
nxt = _next_significant(tokens, i)
|
|
143
|
+
last_on_line = nxt is None or nxt[2][0] > start[0]
|
|
144
|
+
out.append((line, col, name, last_on_line))
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _in_sorted(sorted_lines, line):
|
|
149
|
+
ix = bisect.bisect_left(sorted_lines, line)
|
|
150
|
+
return ix < len(sorted_lines) and sorted_lines[ix] == line
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def governing_key(binding_lines, binding_keys, line, publish_seq):
|
|
154
|
+
"""The store key a usage at parse `line` reads: among this name's
|
|
155
|
+
bindings textually ABOVE the line, the one that PUBLISHED last
|
|
156
|
+
(`publish_seq`: key → monotonic publish counter — the branch-not-taken
|
|
157
|
+
rule: the textually-later `else:` binding loses to the `if:` binding
|
|
158
|
+
that actually ran). Unpublished/unknown keys rank below every published
|
|
159
|
+
one; ties (no seq data at all) fall back to textual order — the latest
|
|
160
|
+
binding above wins. None when no binding sits above the line."""
|
|
161
|
+
ix = bisect.bisect_left(binding_lines, line)
|
|
162
|
+
best = None
|
|
163
|
+
best_rank = None
|
|
164
|
+
for j in range(ix):
|
|
165
|
+
key = binding_keys[j]
|
|
166
|
+
rank = (publish_seq.get(key, -1), j)
|
|
167
|
+
if best_rank is None or rank > best_rank:
|
|
168
|
+
best, best_rank = key, rank
|
|
169
|
+
return best
|