diff --git a/plugin/helpers.py b/plugin/helpers.py index 036c73b..e3de76f 100644 --- a/plugin/helpers.py +++ b/plugin/helpers.py @@ -10,6 +10,8 @@ from typing import Any, Callable, Literal, Sequence, cast import sublime +from LSP.plugin.core.protocol import Position as LspPosition +from LSP.plugin.core.protocol import Range as LspRange from LSP.plugin.core.url import filename_to_uri from more_itertools import duplicates_everseen, first_true from wcmatch import glob @@ -22,6 +24,7 @@ CopilotPayloadCompletion, CopilotPayloadPanelSolution, CopilotRequestConversationTurn, + CopilotRequestConversationTurnReference, CopilotUserDefinedPromptTemplates, ) from .utils import ( @@ -166,14 +169,34 @@ def trigger(self, view: sublime.View) -> bool: return False +def st_point_to_lsp_position(point: int, view: sublime.View) -> LspPosition: + row, col = view.rowcol_utf16(point) + return {"line": row, "character": col} + + +def lsp_position_to_st_point(position: LspPosition, view: sublime.View) -> int: + return view.text_point_utf16(position["line"], position["character"]) + + +def st_region_to_lsp_range(region: sublime.Region, view: sublime.View) -> LspRange: + return { + "start": st_point_to_lsp_position(region.begin(), view), + "end": st_point_to_lsp_position(region.end(), view), + } + + +def lsp_range_to_st_region(range_: LspRange, view: sublime.View) -> sublime.Region: + return sublime.Region( + lsp_position_to_st_point(range_["start"], view), + lsp_position_to_st_point(range_["end"], view), + ) + + def prepare_completion_request_doc(view: sublime.View, max_selections: int = 1) -> CopilotDocType | None: - if not view: - return None - if len(sel := view.sel()) > max_selections or len(sel) == 0: + if not view or len(sel := view.sel()) > max_selections or len(sel) == 0: return None file_path = view.file_name() or f"buffer:{view.buffer().id()}" - row, col = view.rowcol_utf16(sel[0].begin()) return { "source": view.substr(sublime.Region(0, view.size())), "tabSize": cast(int, view.settings().get("tab_size")), @@ -183,7 +206,7 @@ def prepare_completion_request_doc(view: sublime.View, max_selections: int = 1) "uri": file_path if file_path.startswith("buffer:") else filename_to_uri(file_path), "relativePath": get_project_relative_path(file_path), "languageId": get_view_language_id(view), - "position": {"line": row, "character": col}, + "position": st_point_to_lsp_position(sel[0].begin(), view), # Buffer Version. Generally this is handled by LSP, but we need to handle it here # Will need to test getting the version from LSP "version": view.change_count(), @@ -199,43 +222,32 @@ def prepare_conversation_turn_request( ) -> CopilotRequestConversationTurn | None: if not (doc := prepare_completion_request_doc(view, max_selections=5)): return None - turn: CopilotRequestConversationTurn = { - "conversationId": conversation_id, - "message": message, - "workDoneToken": f"copilot_chat://{window_id}", - "doc": doc, - "computeSuggestions": True, - "references": [], - "source": source, - } - visible_region = view.visible_region() - visible_start = view.rowcol_utf16(visible_region.begin()) - visible_end = view.rowcol_utf16(visible_region.end()) + visible_range = st_region_to_lsp_range(view.visible_region(), view) # References can technicaly be across multiple files # TODO: Support references across multiple files + references: list[CopilotRequestConversationTurnReference] = [] for selection in view.sel(): - if selection.empty() or view.substr(selection).strip() == "": + if selection.empty() or view.substr(selection).isspace(): continue - file_path = view.file_name() or f"buffer:{view.buffer().id()}" - selection_start = view.rowcol_utf16(selection.begin()) - selection_end = view.rowcol_utf16(selection.end()) - turn["references"].append({ + references.append({ "type": "file", "status": "included", - "uri": file_path if file_path.startswith("buffer:") else filename_to_uri(file_path), + "uri": filename_to_uri(file_path) if (file_path := view.file_name()) else f"buffer:{view.buffer().id()}", "range": doc["position"], - "visibleRange": { - "start": {"line": visible_start[0], "character": visible_start[1]}, - "end": {"line": visible_end[0], "character": visible_end[1]}, - }, - "selection": { - "start": {"line": selection_start[0], "character": selection_start[1]}, - "end": {"line": selection_end[0], "character": selection_end[1]}, - }, + "visibleRange": visible_range, + "selection": st_region_to_lsp_range(selection, view), }) - return turn + return { + "conversationId": conversation_id, + "message": message, + "workDoneToken": f"copilot_chat://{window_id}", + "doc": doc, + "computeSuggestions": True, + "references": references, + "source": source, + } def preprocess_message_for_html(message: str) -> str: @@ -305,33 +317,14 @@ def preprocess_completions(view: sublime.View, completions: list[CopilotPayloadC # inject extra information for convenience for completion in completions: - completion["point"] = view.text_point_utf16( - completion["position"]["line"], - completion["position"]["character"], - ) - _generate_completion_region(view, completion) + completion["point"] = lsp_position_to_st_point(completion["position"], view) + completion["region"] = lsp_range_to_st_region(completion["range"], view).to_tuple() def preprocess_panel_completions(view: sublime.View, completions: Sequence[CopilotPayloadPanelSolution]) -> None: """Preprocess the `completions` from "getCompletionsCycling" request.""" for completion in completions: - _generate_completion_region(view, completion) - - -def _generate_completion_region( - view: sublime.View, - completion: CopilotPayloadCompletion | CopilotPayloadPanelSolution, -) -> None: - completion["region"] = ( - view.text_point_utf16( - completion["range"]["start"]["line"], - completion["range"]["start"]["character"], - ), - view.text_point_utf16( - completion["range"]["end"]["line"], - completion["range"]["end"]["character"], - ), - ) + completion["region"] = lsp_range_to_st_region(completion["range"], view).to_tuple() def is_debug_mode() -> bool: diff --git a/plugin/types.py b/plugin/types.py index da88f79..5ba6ddc 100644 --- a/plugin/types.py +++ b/plugin/types.py @@ -3,6 +3,8 @@ from dataclasses import dataclass from typing import Any, Callable, Literal, Tuple, TypedDict, TypeVar +from LSP.plugin.core.protocol import Position as LspPosition +from LSP.plugin.core.protocol import Range as LspRange from LSP.plugin.core.typing import StrEnum T_Callable = TypeVar("T_Callable", bound=Callable[..., Any]) @@ -42,16 +44,6 @@ class NetworkProxy(TypedDict, total=True): # ------------------- # -class CopilotPositionType(TypedDict, total=True): - character: int - line: int - - -class CopilotRangeType(TypedDict, total=True): - start: CopilotPositionType - end: CopilotPositionType - - class CopilotDocType(TypedDict, total=True): source: str tabSize: int @@ -61,7 +53,7 @@ class CopilotDocType(TypedDict, total=True): uri: str relativePath: str languageId: str - position: CopilotPositionType + position: LspPosition version: int @@ -76,9 +68,9 @@ class CopilotPayloadFileStatus(TypedDict, total=True): class CopilotPayloadCompletion(TypedDict, total=True): text: str - position: CopilotPositionType + position: LspPosition uuid: str - range: CopilotRangeType + range: LspRange displayText: str point: StPoint region: StRegion @@ -151,7 +143,7 @@ class CopilotPayloadPanelSolution(TypedDict, total=True): score: int panelId: str completionText: str - range: CopilotRangeType + range: LspRange region: StRegion @@ -212,9 +204,9 @@ class CopilotRequestConversationTurnReference(TypedDict, total=True): type: str status: str uri: str - range: CopilotPositionType - visibleRange: CopilotRangeType - selection: CopilotRangeType + range: LspPosition + visibleRange: LspRange + selection: LspRange class CopilotRequestConversationAgent(TypedDict, total=True):