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.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. 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
@@ -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