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,617 @@
|
|
|
1
|
+
"""GitHub Copilot FIM provider — drives the official Copilot Language
|
|
2
|
+
Server (`@github/copilot-language-server`, LSP over stdio) the way
|
|
3
|
+
copilot.vim / the JetBrains plugin do. One `CopilotSession` = one LS
|
|
4
|
+
process = one GitHub account: the LS keeps its token under
|
|
5
|
+
`$XDG_CONFIG_HOME/github-copilot/`, so a profile with a different
|
|
6
|
+
`config_dir` is a different login (work vs home). The default (no
|
|
7
|
+
config_dir) shares `~/.config/github-copilot` with the IDE plugins.
|
|
8
|
+
|
|
9
|
+
Install (one-off, also offered by the Internet Accounts window):
|
|
10
|
+
npm install --prefix ~/.lsd/copilot-ls @github/copilot-language-server
|
|
11
|
+
|
|
12
|
+
Process spawn uses a full node path, `cwd=None`, `close_fds=False` so
|
|
13
|
+
CPython takes the posix_spawn path (a fork of the CUDA/GL address space
|
|
14
|
+
stalls the render thread — project_subprocess_fork_stall).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from meltygui.completion.fim import FimRequest
|
|
27
|
+
from meltygui.completion.fim import FimResult
|
|
28
|
+
from meltygui.completion.fim import FimSession
|
|
29
|
+
from meltygui.completion.fim import fim_provider
|
|
30
|
+
|
|
31
|
+
LS_ROOT = Path.home() / ".lsd" / "copilot-ls"
|
|
32
|
+
LS_ENTRY = LS_ROOT / "node_modules" / "@github" / "copilot-language-server" / "dist" / "language-server.js"
|
|
33
|
+
EDITOR_INFO = {"name": "LatentDescent", "version": "1.0"}
|
|
34
|
+
PLUGIN_INFO = {"name": "meltygui-fim", "version": "1.0"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_node() -> str | None:
|
|
38
|
+
"""Full path to a node ≥ 20.8 (PATH, then nvm installs, newest first)."""
|
|
39
|
+
p = shutil.which("node")
|
|
40
|
+
if p:
|
|
41
|
+
return p
|
|
42
|
+
nvm = Path.home() / ".nvm" / "versions" / "node"
|
|
43
|
+
if nvm.is_dir():
|
|
44
|
+
vers = sorted(nvm.iterdir(), key=lambda d: d.name, reverse=True)
|
|
45
|
+
for d in vers:
|
|
46
|
+
cand = d / "bin" / "node"
|
|
47
|
+
if cand.exists():
|
|
48
|
+
return str(cand)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def server_installed() -> bool:
|
|
53
|
+
return LS_ENTRY.exists()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cached_login_user(config_dir=None):
|
|
57
|
+
"""The signed-in GitHub user from the Copilot token file on disk, or
|
|
58
|
+
None — WITHOUT spawning the language server. Lets the accounts window
|
|
59
|
+
show sign-in state at startup with no process spawn and no web request.
|
|
60
|
+
`config_dir` is the account's XDG_CONFIG_HOME (None = the user default,
|
|
61
|
+
shared with the IDE plugins)."""
|
|
62
|
+
base = Path(os.path.expanduser(config_dir)) if config_dir else (Path.home() / ".config")
|
|
63
|
+
for name in ("apps.json", "hosts.json"):
|
|
64
|
+
f = base / "github-copilot" / name
|
|
65
|
+
try:
|
|
66
|
+
if not f.exists():
|
|
67
|
+
continue
|
|
68
|
+
data = json.loads(f.read_text())
|
|
69
|
+
except Exception:
|
|
70
|
+
continue
|
|
71
|
+
for entry in (data.values() if isinstance(data, dict) else []):
|
|
72
|
+
if isinstance(entry, dict):
|
|
73
|
+
if entry.get("oauth_token") or entry.get("user"):
|
|
74
|
+
return entry.get("user") or "signed in"
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def install_server(log=print) -> bool:
|
|
79
|
+
"""`npm install` the language server under LS_ROOT (network). Blocking —
|
|
80
|
+
call from a worker thread."""
|
|
81
|
+
node = find_node()
|
|
82
|
+
if node is None:
|
|
83
|
+
log("copilot: node not found")
|
|
84
|
+
return False
|
|
85
|
+
npm = str(Path(node).parent / "npm")
|
|
86
|
+
if not os.path.exists(npm):
|
|
87
|
+
npm = shutil.which("npm") or npm
|
|
88
|
+
LS_ROOT.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
env = dict(os.environ)
|
|
90
|
+
env["PATH"] = str(Path(node).parent) + os.pathsep + env.get("PATH", "")
|
|
91
|
+
try:
|
|
92
|
+
r = subprocess.run([npm, "install", "--prefix", str(LS_ROOT), "--no-fund", "--no-audit",
|
|
93
|
+
"@github/copilot-language-server"],
|
|
94
|
+
capture_output=True, text=True, env=env, close_fds=False, timeout=600)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
log(f"copilot: npm install failed: {e}")
|
|
97
|
+
return False
|
|
98
|
+
if r.returncode != 0:
|
|
99
|
+
log(f"copilot: npm install failed: {r.stderr[-400:]}")
|
|
100
|
+
return False
|
|
101
|
+
return server_installed()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
105
|
+
# JSON-RPC over stdio
|
|
106
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
class _RpcError(RuntimeError):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _Rpc:
|
|
113
|
+
"""Minimal LSP transport: Content-Length framing, id-correlated
|
|
114
|
+
requests from any thread, notification handlers, and replies to the
|
|
115
|
+
few server→client requests the LS makes."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, proc, on_notification):
|
|
118
|
+
self.proc = proc
|
|
119
|
+
self._on_notification = on_notification
|
|
120
|
+
self._wlock = threading.Lock()
|
|
121
|
+
self._plock = threading.Lock()
|
|
122
|
+
self._pending = {} # id -> [Event, result, error]
|
|
123
|
+
self._next_id = 1
|
|
124
|
+
self.alive = True
|
|
125
|
+
self._reader = threading.Thread(target=self._read_loop, daemon=True, name="copilot-ls-reader")
|
|
126
|
+
self._reader.start()
|
|
127
|
+
|
|
128
|
+
def _send(self, msg):
|
|
129
|
+
data = json.dumps(msg).encode("utf-8")
|
|
130
|
+
head = f"Content-Length: {len(data)}\r\n\r\n".encode("ascii")
|
|
131
|
+
with self._wlock:
|
|
132
|
+
try:
|
|
133
|
+
self.proc.stdin.write(head + data)
|
|
134
|
+
self.proc.stdin.flush()
|
|
135
|
+
except (BrokenPipeError, OSError, ValueError) as e:
|
|
136
|
+
self.alive = False
|
|
137
|
+
raise _RpcError(f"language server pipe closed: {e}")
|
|
138
|
+
|
|
139
|
+
def notify(self, method, params=None):
|
|
140
|
+
self._send({"jsonrpc": "2.0", "method": method, "params": params or {}})
|
|
141
|
+
|
|
142
|
+
def request(self, method, params=None, timeout=60.0, cancel_event=None):
|
|
143
|
+
with self._plock:
|
|
144
|
+
rid = self._next_id
|
|
145
|
+
self._next_id += 1
|
|
146
|
+
slot = [threading.Event(), None, None]
|
|
147
|
+
self._pending[rid] = slot
|
|
148
|
+
self._send({"jsonrpc": "2.0", "id": rid, "method": method, "params": params or {}})
|
|
149
|
+
deadline = time.monotonic() + timeout if timeout else None
|
|
150
|
+
while not slot[0].wait(0.05):
|
|
151
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
152
|
+
with self._plock:
|
|
153
|
+
self._pending.pop(rid, None)
|
|
154
|
+
try:
|
|
155
|
+
self.notify("$/cancelRequest", {"id": rid})
|
|
156
|
+
except _RpcError:
|
|
157
|
+
pass
|
|
158
|
+
return None
|
|
159
|
+
if not self.alive:
|
|
160
|
+
raise _RpcError("language server exited")
|
|
161
|
+
if deadline is not None and time.monotonic() > deadline:
|
|
162
|
+
with self._plock:
|
|
163
|
+
self._pending.pop(rid, None)
|
|
164
|
+
raise _RpcError(f"{method}: timed out")
|
|
165
|
+
if slot[2] is not None:
|
|
166
|
+
err = slot[2]
|
|
167
|
+
raise _RpcError(f"{method}: {err.get('message') if isinstance(err, dict) else err}")
|
|
168
|
+
return slot[1]
|
|
169
|
+
|
|
170
|
+
def _reply(self, rid, result=None, error=None):
|
|
171
|
+
msg = {"jsonrpc": "2.0", "id": rid}
|
|
172
|
+
if error is not None:
|
|
173
|
+
msg["error"] = error
|
|
174
|
+
else:
|
|
175
|
+
msg["result"] = result
|
|
176
|
+
try:
|
|
177
|
+
self._send(msg)
|
|
178
|
+
except _RpcError:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
def _read_loop(self):
|
|
182
|
+
out = self.proc.stdout
|
|
183
|
+
try:
|
|
184
|
+
while True:
|
|
185
|
+
length = None
|
|
186
|
+
while True:
|
|
187
|
+
line = out.readline()
|
|
188
|
+
if not line:
|
|
189
|
+
raise EOFError
|
|
190
|
+
if line in (b"\r\n", b"\n"):
|
|
191
|
+
break
|
|
192
|
+
k, _, v = line.decode("ascii", "replace").partition(":")
|
|
193
|
+
if k.strip().lower() == "content-length":
|
|
194
|
+
length = int(v.strip())
|
|
195
|
+
if length is None:
|
|
196
|
+
continue
|
|
197
|
+
body = out.read(length)
|
|
198
|
+
if len(body) < length:
|
|
199
|
+
raise EOFError
|
|
200
|
+
try:
|
|
201
|
+
msg = json.loads(body.decode("utf-8", "replace"))
|
|
202
|
+
except ValueError:
|
|
203
|
+
continue
|
|
204
|
+
self._dispatch(msg)
|
|
205
|
+
except (EOFError, OSError, ValueError):
|
|
206
|
+
pass
|
|
207
|
+
finally:
|
|
208
|
+
self.alive = False
|
|
209
|
+
with self._plock:
|
|
210
|
+
for slot in self._pending.values():
|
|
211
|
+
slot[2] = {"message": "language server exited"}
|
|
212
|
+
slot[0].set()
|
|
213
|
+
self._pending.clear()
|
|
214
|
+
|
|
215
|
+
def _dispatch(self, msg):
|
|
216
|
+
if "id" in msg and "method" in msg:
|
|
217
|
+
# server → client request: answer what we can, null otherwise
|
|
218
|
+
method = msg["method"]
|
|
219
|
+
params = msg.get("params") or {}
|
|
220
|
+
if method == "workspace/configuration":
|
|
221
|
+
items = params.get("items") or []
|
|
222
|
+
self._reply(msg["id"], [{} for _ in items])
|
|
223
|
+
elif method == "window/showMessageRequest":
|
|
224
|
+
self._reply(msg["id"], None)
|
|
225
|
+
elif method == "window/showDocument":
|
|
226
|
+
uri = params.get("uri")
|
|
227
|
+
if uri:
|
|
228
|
+
open_url(uri)
|
|
229
|
+
self._reply(msg["id"], {"success": bool(uri)})
|
|
230
|
+
else:
|
|
231
|
+
self._reply(msg["id"], None)
|
|
232
|
+
return
|
|
233
|
+
if "id" in msg:
|
|
234
|
+
with self._plock:
|
|
235
|
+
slot = self._pending.pop(msg["id"], None)
|
|
236
|
+
if slot is not None:
|
|
237
|
+
slot[1] = msg.get("result")
|
|
238
|
+
slot[2] = msg.get("error")
|
|
239
|
+
slot[0].set()
|
|
240
|
+
return
|
|
241
|
+
method = msg.get("method")
|
|
242
|
+
if method:
|
|
243
|
+
try:
|
|
244
|
+
self._on_notification(method, msg.get("params") or {})
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def open_url(url: str):
|
|
250
|
+
"""Open a URL in the user's browser without fork()ing the studio:
|
|
251
|
+
full-path xdg-open via posix_spawn (see module docstring)."""
|
|
252
|
+
for cmd in ("xdg-open", "gio", "open"):
|
|
253
|
+
exe = shutil.which(cmd)
|
|
254
|
+
if exe:
|
|
255
|
+
args = [exe, url] if cmd != "gio" else [exe, "open", url]
|
|
256
|
+
try:
|
|
257
|
+
subprocess.Popen(args, close_fds=False, stdin=subprocess.DEVNULL,
|
|
258
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
259
|
+
return True
|
|
260
|
+
except OSError:
|
|
261
|
+
continue
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
266
|
+
# Session
|
|
267
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
class CopilotSession(FimSession):
|
|
270
|
+
"""One language-server process / one GitHub login. `account` names an
|
|
271
|
+
Internet Accounts entry of kind "copilot" whose `config_dir` is the
|
|
272
|
+
login's XDG_CONFIG_HOME (empty = the user's default, shared with the
|
|
273
|
+
IDE plugins); an explicit `config_dir` overrides it. `workspace` is
|
|
274
|
+
the folder reported to the LS."""
|
|
275
|
+
KIND = "copilot"
|
|
276
|
+
|
|
277
|
+
def __init__(self, account="default", config_dir=None, workspace=None, node=None):
|
|
278
|
+
self.account = account
|
|
279
|
+
if config_dir is None:
|
|
280
|
+
from meltygui.accounts.internet_accounts import account_field
|
|
281
|
+
config_dir = account_field("copilot", account, "config_dir")
|
|
282
|
+
self.config_dir = os.path.expanduser(config_dir) if config_dir else None
|
|
283
|
+
from meltygui.core.runtime.paths import application_root
|
|
284
|
+
self.workspace = workspace or str(application_root())
|
|
285
|
+
self.node = node or find_node()
|
|
286
|
+
self._lock = threading.RLock()
|
|
287
|
+
self._docs = {} # uri -> (version, text)
|
|
288
|
+
self._status = ("starting",)
|
|
289
|
+
self._kind = "Unknown"
|
|
290
|
+
self._message = ""
|
|
291
|
+
self._busy = False
|
|
292
|
+
self.user = None
|
|
293
|
+
self.login = None # (user_code, verification_uri) while a login flow is open
|
|
294
|
+
self.proc = None
|
|
295
|
+
self.rpc = None
|
|
296
|
+
self._start()
|
|
297
|
+
|
|
298
|
+
# ── lifecycle ───────────────────────────────────────────────────────
|
|
299
|
+
def _start(self):
|
|
300
|
+
if self.node is None:
|
|
301
|
+
raise RuntimeError("node not found (need Node ≥ 20.8 for the Copilot language server)")
|
|
302
|
+
if not server_installed():
|
|
303
|
+
raise RuntimeError(f"Copilot language server not installed under {LS_ROOT} "
|
|
304
|
+
f"(Internet Accounts → Copilot → Install)")
|
|
305
|
+
env = dict(os.environ)
|
|
306
|
+
if self.config_dir:
|
|
307
|
+
env["XDG_CONFIG_HOME"] = self.config_dir
|
|
308
|
+
Path(self.config_dir).mkdir(parents=True, exist_ok=True)
|
|
309
|
+
# --dns-result-order=ipv4first: Node ≥ 17 tries AAAA records first;
|
|
310
|
+
# on a host whose IPv6 route is dead every GitHub access inside the LS
|
|
311
|
+
# ends in an undici timeout (~30 s) before its fallback fetcher kicks
|
|
312
|
+
# in - the first completion took 30 s here. IPv4-first makes it fast.
|
|
313
|
+
self.proc = subprocess.Popen(
|
|
314
|
+
[self.node, "--dns-result-order=ipv4first", str(LS_ENTRY), "--stdio"],
|
|
315
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
316
|
+
env=env, close_fds=False)
|
|
317
|
+
self.rpc = _Rpc(self.proc, self._on_notification)
|
|
318
|
+
init = {
|
|
319
|
+
"processId": os.getpid(),
|
|
320
|
+
"rootUri": Path(self.workspace).as_uri(),
|
|
321
|
+
"workspaceFolders": [{"uri": Path(self.workspace).as_uri(), "name": Path(self.workspace).name}],
|
|
322
|
+
"capabilities": {"workspace": {"workspaceFolders": True, "configuration": True},
|
|
323
|
+
"window": {"showDocument": {"support": True}}},
|
|
324
|
+
"initializationOptions": {"editorInfo": EDITOR_INFO, "editorPluginInfo": PLUGIN_INFO},
|
|
325
|
+
}
|
|
326
|
+
self.rpc.request("initialize", init, timeout=60.0)
|
|
327
|
+
self.rpc.notify("initialized", {})
|
|
328
|
+
self.rpc.notify("workspace/didChangeConfiguration",
|
|
329
|
+
{"settings": {"telemetry": {"telemetryLevel": "off"}}})
|
|
330
|
+
self._status = ("ready",)
|
|
331
|
+
# Warm-up: the LS's FIRST completion pays for auth + fallback fetcher
|
|
332
|
+
# negotiation (~30 s on a host whose primary fetcher can't reach
|
|
333
|
+
# GitHub); do it on a throwaway document now so the user's first
|
|
334
|
+
# real request is the ~0.3 s warm-up.
|
|
335
|
+
threading.Thread(target=self._warm, daemon=True, name="copilot-warm").start()
|
|
336
|
+
|
|
337
|
+
def _warm(self):
|
|
338
|
+
try:
|
|
339
|
+
self.check_status()
|
|
340
|
+
if self._kind == "Normal":
|
|
341
|
+
text = "def add(a, b):\n return "
|
|
342
|
+
self.complete("untitled:warmup.py", text, len(text), threading.Event())
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
_notify_account_change()
|
|
346
|
+
|
|
347
|
+
def close(self):
|
|
348
|
+
rpc, proc = self.rpc, self.proc
|
|
349
|
+
self.rpc = None
|
|
350
|
+
self.proc = None
|
|
351
|
+
if rpc is not None and rpc.alive:
|
|
352
|
+
try:
|
|
353
|
+
rpc.request("shutdown", {}, timeout=5.0)
|
|
354
|
+
rpc.notify("exit", {})
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
if proc is not None:
|
|
358
|
+
try:
|
|
359
|
+
proc.wait(timeout=3.0)
|
|
360
|
+
except Exception:
|
|
361
|
+
try:
|
|
362
|
+
proc.kill()
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
def restart(self):
|
|
367
|
+
self.close()
|
|
368
|
+
self._docs.clear()
|
|
369
|
+
self._start()
|
|
370
|
+
|
|
371
|
+
# ── status & auth ───────────────────────────────────────────────────
|
|
372
|
+
def _on_notification(self, method, params):
|
|
373
|
+
if method == "didChangeStatus":
|
|
374
|
+
self._kind = params.get("kind") or "Normal"
|
|
375
|
+
self._message = params.get("message") or ""
|
|
376
|
+
self._busy = bool(params.get("busy"))
|
|
377
|
+
if self._kind == "Normal" and self.login is not None and not self._busy:
|
|
378
|
+
self.login = None
|
|
379
|
+
_notify_account_change()
|
|
380
|
+
elif method == "window/logMessage":
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
def alive(self):
|
|
384
|
+
return self.rpc is not None and self.rpc.alive
|
|
385
|
+
|
|
386
|
+
def status(self):
|
|
387
|
+
if self.rpc is None or not self.rpc.alive:
|
|
388
|
+
return ("error", "language server exited")
|
|
389
|
+
if self.login is not None:
|
|
390
|
+
code, uri = self.login
|
|
391
|
+
return ("needs_login", code, uri)
|
|
392
|
+
if self._kind == "Error":
|
|
393
|
+
return ("error", self._message or "not signed in")
|
|
394
|
+
if self._kind == "Warning":
|
|
395
|
+
return ("warning", self._message)
|
|
396
|
+
return ("ready",)
|
|
397
|
+
|
|
398
|
+
def check_status(self):
|
|
399
|
+
"""Ask the LS who is signed in (`signIn` answers AlreadySignedIn
|
|
400
|
+
without starting a flow when a token exists). Returns the user
|
|
401
|
+
login or None."""
|
|
402
|
+
try:
|
|
403
|
+
res = self.rpc.request("signIn", {}, timeout=30.0) or {}
|
|
404
|
+
except _RpcError as e:
|
|
405
|
+
self._kind, self._message = "Error", str(e)
|
|
406
|
+
return None
|
|
407
|
+
st = res.get("status")
|
|
408
|
+
if st == "AlreadySignedIn" or res.get("user"):
|
|
409
|
+
self.user = res.get("user") or self.user
|
|
410
|
+
self._kind, self._message = "Normal", ""
|
|
411
|
+
return self.user
|
|
412
|
+
# A device flow was started that we did not ask for: save its
|
|
413
|
+
# details so the UI can show them, but don't block on it.
|
|
414
|
+
if res.get("userCode"):
|
|
415
|
+
self.login = (res.get("userCode"), res.get("verificationUri") or "https://github.com/login/device")
|
|
416
|
+
self._pending_login_cmd = res.get("command")
|
|
417
|
+
self._kind, self._message = "Error", "not signed in"
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
_pending_login_cmd = None
|
|
421
|
+
|
|
422
|
+
def sign_in(self, open_browser=True):
|
|
423
|
+
"""Start (or resume) the device flow. Returns (user_code, uri) or
|
|
424
|
+
the signed-in user. The completion runs on a worker: the LS's
|
|
425
|
+
finish command blocks until the user approves in the browser."""
|
|
426
|
+
if self.login is None:
|
|
427
|
+
res = self.rpc.request("signIn", {}, timeout=30.0) or {}
|
|
428
|
+
if res.get("status") == "AlreadySignedIn" or (res.get("user") and not res.get("userCode")):
|
|
429
|
+
self.user = res.get("user") or self.user
|
|
430
|
+
self._kind, self._message = "Normal", ""
|
|
431
|
+
return self.user
|
|
432
|
+
if not res.get("userCode"):
|
|
433
|
+
raise RuntimeError(f"unexpected signIn reply: {res}")
|
|
434
|
+
self.login = (res["userCode"], res.get("verificationUri") or "https://github.com/login/device")
|
|
435
|
+
self._pending_login_cmd = res.get("command")
|
|
436
|
+
code, uri = self.login
|
|
437
|
+
if open_browser:
|
|
438
|
+
open_url(uri)
|
|
439
|
+
cmd = self._pending_login_cmd
|
|
440
|
+
if cmd:
|
|
441
|
+
self._pending_login_cmd = None
|
|
442
|
+
|
|
443
|
+
def finish():
|
|
444
|
+
try:
|
|
445
|
+
res = self.rpc.request("workspace/executeCommand",
|
|
446
|
+
{"command": cmd.get("command"),
|
|
447
|
+
"arguments": cmd.get("arguments") or []},
|
|
448
|
+
timeout=600.0) or {}
|
|
449
|
+
if isinstance(res, dict):
|
|
450
|
+
self.user = res.get("user") or self.user
|
|
451
|
+
if res.get("status") in ("OK", "AlreadySignedIn") or res.get("user"):
|
|
452
|
+
self._kind, self._message = "Normal", ""
|
|
453
|
+
except Exception as e:
|
|
454
|
+
self._kind, self._message = "Error", str(e)
|
|
455
|
+
self.login = None
|
|
456
|
+
_notify_account_change()
|
|
457
|
+
|
|
458
|
+
threading.Thread(target=finish, daemon=True, name="copilot-signin").start()
|
|
459
|
+
return (code, uri)
|
|
460
|
+
|
|
461
|
+
def sign_out(self):
|
|
462
|
+
try:
|
|
463
|
+
self.rpc.request("signOut", {}, timeout=30.0)
|
|
464
|
+
except _RpcError as e:
|
|
465
|
+
self._kind, self._message = "Error", str(e)
|
|
466
|
+
self.user = None
|
|
467
|
+
self.login = None
|
|
468
|
+
self._kind, self._message = "Error", "not signed in"
|
|
469
|
+
_notify_account_change()
|
|
470
|
+
|
|
471
|
+
# ── documents / completion ──────────────────────────────────────────
|
|
472
|
+
@staticmethod
|
|
473
|
+
def _uri(path):
|
|
474
|
+
try:
|
|
475
|
+
return Path(path).resolve().as_uri()
|
|
476
|
+
except Exception:
|
|
477
|
+
return "untitled:" + str(path).replace(" ", "_")
|
|
478
|
+
|
|
479
|
+
def sync_document(self, path, text, version=None, language="python"):
|
|
480
|
+
uri = self._uri(path)
|
|
481
|
+
with self._lock:
|
|
482
|
+
prev = self._docs.get(uri)
|
|
483
|
+
if prev is not None and prev[1] == text:
|
|
484
|
+
return uri, prev[0]
|
|
485
|
+
v = (prev[0] + 1) if prev is not None else 1
|
|
486
|
+
if prev is None:
|
|
487
|
+
self.rpc.notify("textDocument/didOpen",
|
|
488
|
+
{"textDocument": {"uri": uri, "languageId": language,
|
|
489
|
+
"version": v, "text": text}})
|
|
490
|
+
else:
|
|
491
|
+
self.rpc.notify("textDocument/didChange",
|
|
492
|
+
{"textDocument": {"uri": uri, "version": v},
|
|
493
|
+
"contentChanges": [{"text": text}]})
|
|
494
|
+
self._docs[uri] = (v, text)
|
|
495
|
+
return uri, v
|
|
496
|
+
|
|
497
|
+
def complete(self, path, text, cursor, cancelled, language="python") -> FimResult:
|
|
498
|
+
uri, v = self.sync_document(path, text, language=language)
|
|
499
|
+
line = text.count("\n", 0, cursor)
|
|
500
|
+
ls = text.rfind("\n", 0, cursor) + 1
|
|
501
|
+
char = _utf16_len(text[ls:cursor])
|
|
502
|
+
res = self.rpc.request("textDocument/inlineCompletion",
|
|
503
|
+
{"textDocument": {"uri": uri, "version": v},
|
|
504
|
+
"position": {"line": line, "character": char},
|
|
505
|
+
"context": {"triggerKind": 2},
|
|
506
|
+
"formattingOptions": {"tabSize": 4, "insertSpaces": True}},
|
|
507
|
+
timeout=30.0, cancel_event=cancelled)
|
|
508
|
+
items = (res or {}).get("items") or []
|
|
509
|
+
if not items:
|
|
510
|
+
return FimResult("", provider="copilot")
|
|
511
|
+
texts = []
|
|
512
|
+
for it in items:
|
|
513
|
+
ins = it.get("insertText") or ""
|
|
514
|
+
rng = it.get("range") or {}
|
|
515
|
+
start = rng.get("start") or {}
|
|
516
|
+
end = rng.get("end") or {}
|
|
517
|
+
# The item's range usually starts at the line start and
|
|
518
|
+
# re-states what's already typed: trim what precedes the cursor
|
|
519
|
+
# and count what follows it as a suffix.
|
|
520
|
+
if start.get("line") == line and start.get("character", char) < char:
|
|
521
|
+
already = text[ls + _utf16_to_index(text[ls:], start["character"]):cursor]
|
|
522
|
+
if ins.startswith(already):
|
|
523
|
+
ins = ins[len(already):]
|
|
524
|
+
replace_to = 0
|
|
525
|
+
if end.get("line") == line and end.get("character", char) > char:
|
|
526
|
+
replace_to = _utf16_to_index(text[cursor:], end["character"] - char)
|
|
527
|
+
texts.append((ins, replace_to, it))
|
|
528
|
+
first_ins, first_rep, first_item = texts[0]
|
|
529
|
+
return FimResult(first_ins, replace_to=first_rep,
|
|
530
|
+
alternatives=tuple(t for t, _r, _i in texts[1:] if t),
|
|
531
|
+
token=first_item, provider="copilot")
|
|
532
|
+
|
|
533
|
+
def shown(self, item):
|
|
534
|
+
try:
|
|
535
|
+
self.rpc.notify("textDocument/didShowCompletion", {"item": item})
|
|
536
|
+
except _RpcError:
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
def accepted(self, item, accepted_length):
|
|
540
|
+
try:
|
|
541
|
+
full = len(item.get("insertText") or "")
|
|
542
|
+
if accepted_length >= full:
|
|
543
|
+
cmd = item.get("command") or {}
|
|
544
|
+
if cmd.get("command"):
|
|
545
|
+
self.rpc.request("workspace/executeCommand",
|
|
546
|
+
{"command": cmd["command"], "arguments": cmd.get("arguments") or []},
|
|
547
|
+
timeout=10.0)
|
|
548
|
+
else:
|
|
549
|
+
self.rpc.notify("textDocument/didPartiallyAcceptCompletion",
|
|
550
|
+
{"item": item, "acceptedLength": accepted_length})
|
|
551
|
+
except _RpcError:
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _utf16_len(s: str) -> int:
|
|
556
|
+
return sum(2 if ord(c) > 0xFFFF else 1 for c in s)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _utf16_to_index(s: str, units: int) -> int:
|
|
560
|
+
n = 0
|
|
561
|
+
for i, c in enumerate(s):
|
|
562
|
+
if n >= units:
|
|
563
|
+
return i
|
|
564
|
+
n += 2 if ord(c) > 0xFFFF else 1
|
|
565
|
+
return len(s)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _notify_account_change():
|
|
569
|
+
"""Repaint whoever shows account status (the Internet Accounts window)."""
|
|
570
|
+
try:
|
|
571
|
+
from meltygui.accounts.internet_accounts import accounts_changed
|
|
572
|
+
accounts_changed()
|
|
573
|
+
except Exception:
|
|
574
|
+
pass
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
578
|
+
# Provider
|
|
579
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
@fim_provider(name="copilot", session=CopilotSession)
|
|
582
|
+
def copilot_fim(req: FimRequest, session: CopilotSession, neighbors=4) -> FimResult:
|
|
583
|
+
"""Copilot completion for the request's file. The LS builds its own
|
|
584
|
+
prompt from the documents it has seen, so the editor's context reaches
|
|
585
|
+
it as neighbor documents: the files of up to `neighbors` referenced
|
|
586
|
+
definitions are opened alongside."""
|
|
587
|
+
st = session.status()
|
|
588
|
+
if st[0] == "needs_login":
|
|
589
|
+
raise RuntimeError(f"Copilot: sign in with code {st[1]} at {st[2]}")
|
|
590
|
+
if st[0] == "error":
|
|
591
|
+
raise RuntimeError(f"Copilot: {st[1]}")
|
|
592
|
+
if req.context is not None and neighbors:
|
|
593
|
+
seen = 0
|
|
594
|
+
for it in req.context.items:
|
|
595
|
+
if it.kind in ("definition", "signature") and it.path and it.path != req.path:
|
|
596
|
+
try:
|
|
597
|
+
from meltygui.code.symbol_roster import file_text
|
|
598
|
+
session.sync_document(it.path, file_text(it.path))
|
|
599
|
+
seen += 1
|
|
600
|
+
except Exception:
|
|
601
|
+
pass
|
|
602
|
+
if seen >= neighbors:
|
|
603
|
+
break
|
|
604
|
+
return session.complete(req.path or "untitled", req.text, req.cursor, req.cancelled,
|
|
605
|
+
language=req.language or "python")
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _copilot_shown(req, result):
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _copilot_accept(result, accepted_chars):
|
|
613
|
+
pass
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
copilot_fim._on_shown = _copilot_shown
|
|
617
|
+
copilot_fim._on_accept = _copilot_accept
|