diff --git a/boot.py b/boot.py index cabd05037..f2c088e0e 100644 --- a/boot.py +++ b/boot.py @@ -5,6 +5,7 @@ # Please keep this list sorted (Edit -> Sort Lines) from .plugin.code_actions import LspCodeActionsCommand from .plugin.code_lens import LspCodeLensCommand +from .plugin.color import LspColorPresentationCommand from .plugin.completion import LspCommitCompletionWithOppositeInsertMode from .plugin.completion import LspResolveDocsCommand from .plugin.completion import LspSelectCompletionItemCommand diff --git a/plugin/color.py b/plugin/color.py new file mode 100644 index 000000000..a450df633 --- /dev/null +++ b/plugin/color.py @@ -0,0 +1,61 @@ +from .core.edit import parse_text_edit +from .core.protocol import ColorInformation +from .core.protocol import ColorPresentation +from .core.protocol import ColorPresentationParams +from .core.protocol import Request +from .core.registry import LspTextCommand +from .core.typing import List +from .core.views import range_to_region +from .core.views import text_document_identifier +import sublime + + +class LspColorPresentationCommand(LspTextCommand): + + capability = 'colorProvider' + + def run(self, edit: sublime.Edit, color_information: ColorInformation) -> None: + session = self.best_session(self.capability) + if session: + self._version = self.view.change_count() + self._range = color_information['range'] + params = { + 'textDocument': text_document_identifier(self.view), + 'color': color_information['color'], + 'range': self._range + } # type: ColorPresentationParams + session.send_request_async(Request.colorPresentation(params, self.view), self._handle_response_async) + + def want_event(self) -> bool: + return False + + def _handle_response_async(self, response: List[ColorPresentation]) -> None: + if not response: + return + window = self.view.window() + if not window: + return + if self._version != self.view.change_count(): + return + old_text = self.view.substr(range_to_region(self._range, self.view)) + self._filtered_response = [] # type: List[ColorPresentation] + for item in response: + # Filter out items that would apply no change + text_edit = item.get('textEdit') + if text_edit: + if text_edit['range'] == self._range and text_edit['newText'] == old_text: + continue + elif item['label'] == old_text: + continue + self._filtered_response.append(item) + if self._filtered_response: + window.show_quick_panel( + [sublime.QuickPanelItem(item['label']) for item in self._filtered_response], + self._on_select, + placeholder="Change color format") + + def _on_select(self, index: int) -> None: + if index > -1: + color_pres = self._filtered_response[index] + text_edit = color_pres.get('textEdit') or {'range': self._range, 'newText': color_pres['label']} + self.view.run_command('lsp_apply_document_edit', {'changes': [parse_text_edit(text_edit, self._version)]}) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index ab9995969..a744a119c 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -5880,6 +5880,10 @@ def codeAction(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': def documentColor(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': return Request('textDocument/documentColor', params, view) + @classmethod + def colorPresentation(cls, params: ColorPresentationParams, view: sublime.View) -> 'Request': + return Request('textDocument/colorPresentation', params, view) + @classmethod def willSaveWaitUntil(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': return Request("textDocument/willSaveWaitUntil", params, view) diff --git a/plugin/core/views.py b/plugin/core/views.py index 23c77c044..91f068c67 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -4,6 +4,8 @@ from .protocol import CodeActionContext from .protocol import CodeActionParams from .protocol import CodeActionTriggerKind +from .protocol import Color +from .protocol import ColorInformation from .protocol import Command from .protocol import CompletionItem from .protocol import CompletionItemKind @@ -781,28 +783,44 @@ def run(self, view_id: int, command: str, args: Optional[Dict[str, Any]] = None) COLOR_BOX_HTML = """ - + -
-
+   """ -def lsp_color_to_html(color_info: Dict[str, Any]) -> str: - color = color_info['color'] - red = color['red'] * 255 - green = color['green'] * 255 - blue = color['blue'] * 255 - alpha = color['alpha'] - return COLOR_BOX_HTML.format(red, green, blue, alpha) +def color_to_hex(color: Color) -> str: + red = round(color['red'] * 255) + green = round(color['green'] * 255) + blue = round(color['blue'] * 255) + alpha_dec = color['alpha'] + if alpha_dec < 1: + return "#{:02x}{:02x}{:02x}{:02x}".format(red, green, blue, round(alpha_dec * 255)) + return "#{:02x}{:02x}{:02x}".format(red, green, blue) -def lsp_color_to_phantom(view: sublime.View, color_info: Dict[str, Any]) -> sublime.Phantom: +def lsp_color_to_html(view: sublime.View, color_info: ColorInformation) -> str: + command = sublime.command_url('lsp_color_presentation', {'color_information': color_info}) + return COLOR_BOX_HTML.format(command=command, color=color_to_hex(color_info['color'])) + + +def lsp_color_to_phantom(view: sublime.View, color_info: ColorInformation) -> sublime.Phantom: region = range_to_region(color_info['range'], view) - return sublime.Phantom(region, lsp_color_to_html(color_info), sublime.LAYOUT_INLINE) + return sublime.Phantom(region, lsp_color_to_html(view, color_info), sublime.LAYOUT_INLINE) def document_color_params(view: sublime.View) -> Dict[str, Any]: diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 84f88057e..3b3de60d0 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -1,5 +1,6 @@ from .core.panels import is_panel_open from .core.panels import PanelName +from .core.protocol import ColorInformation from .core.protocol import Diagnostic from .core.protocol import DiagnosticSeverity from .core.protocol import DocumentLink @@ -353,9 +354,11 @@ def _do_color_boxes_async(self, view: sublime.View, version: int) -> None: self._if_view_unchanged(self._on_color_boxes_async, version) ) - def _on_color_boxes_async(self, view: sublime.View, response: Any) -> None: - color_infos = response if response else [] - self.color_phantoms.update([lsp_color_to_phantom(view, color_info) for color_info in color_infos]) + def _on_color_boxes_async(self, view: sublime.View, response: List[ColorInformation]) -> None: + if response is None: # Guard against spec violation from certain language servers + self.color_phantoms.update([]) + return + self.color_phantoms.update([lsp_color_to_phantom(view, color_info) for color_info in response]) # --- textDocument/documentLink ------------------------------------------------------------------------------------ diff --git a/tests/test_views.py b/tests/test_views.py index 0d04c8560..ca84e5081 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -329,7 +329,7 @@ def test_lsp_color_to_phantom(self) -> None: } ] phantom = lsp_color_to_phantom(self.view, response[0]) - self.assertEqual(phantom.content, lsp_color_to_html(response[0])) + self.assertEqual(phantom.content, lsp_color_to_html(self.view, response[0])) self.assertEqual(phantom.region, range_to_region(response[0]["range"], self.view)) def test_document_color_params(self) -> None: