diff --git a/boot.py b/boot.py index 395f55cab..60508e663 100644 --- a/boot.py +++ b/boot.py @@ -61,6 +61,7 @@ from .plugin.inlay_hint import LspToggleInlayHintsCommand from .plugin.inline_completion import LspCommitInlineCompletionCommand from .plugin.inline_completion import LspInlineCompletionCommand +from .plugin.inline_completion import LspNextInlineCompletionCommand from .plugin.panels import LspClearLogPanelCommand from .plugin.panels import LspClearPanelCommand from .plugin.panels import LspShowDiagnosticsPanelCommand diff --git a/plugin/inline_completion.py b/plugin/inline_completion.py index d80d131dd..6b3aa2414 100644 --- a/plugin/inline_completion.py +++ b/plugin/inline_completion.py @@ -11,6 +11,7 @@ from .core.views import range_to_region from .core.views import text_document_position_params from functools import partial +from typing import Optional import html import sublime @@ -52,21 +53,21 @@ class InlineCompletionData: def __init__(self, view: sublime.View, key: str) -> None: self.visible = False - self.region = sublime.Region(0, 0) - self.text = '' - self.command: Command | None = None - self.session_name = '' + self.index = 0 + self.position = 0 + self.items: list[tuple[str, sublime.Region, str, Optional[Command]]] = [] self._view = view self._phantom_set = sublime.PhantomSet(view, key) - def render_async(self, location: int, text: str) -> None: + def render_async(self, index: int) -> None: style = self._view.style_for_scope('comment meta.inline-completion.lsp') color = style['foreground'] font_style = 'italic' if style['italic'] else 'normal' font_weight = 'bold' if style['bold'] else 'normal' - region = sublime.Region(location) - is_at_eol = self._view.line(location).b == location - first_line, *more_lines = text.splitlines() + region = sublime.Region(self.position) + is_at_eol = self._view.line(self.position).b == self.position + item = self.items[index] + first_line, *more_lines = item[2][len(item[1]):].splitlines() suffix = '
Alt + Enter to complete
' if is_at_eol or \ more_lines else '' phantoms = [sublime.Phantom( @@ -94,10 +95,11 @@ def render_async(self, location: int, text: str) -> None: sublime.PhantomLayout.BLOCK ) ) - sublime.set_timeout(lambda: self._render(phantoms)) + sublime.set_timeout(lambda: self._render(phantoms, index)) self.visible = True - def _render(self, phantoms: list[sublime.Phantom]) -> None: + def _render(self, phantoms: list[sublime.Phantom], index: int) -> None: + self.index = index self._phantom_set.update(phantoms) def clear_async(self) -> None: @@ -153,35 +155,56 @@ def _handle_response_async( items = response['items'] if isinstance(response, dict) else response if not items: return - item = items[0] - insert_text = item['insertText'] - if not insert_text: - return - if isinstance(insert_text, dict): # StringValue - debug('Snippet completions from the 3.18 specs not yet supported') - return if view_version != self.view.change_count(): return listener = self.get_listener() if not listener: return - range_ = item.get('range') - region = range_to_region(range_, self.view) if range_ else sublime.Region(position) - region_length = len(region) - if region_length > len(insert_text): - return - listener.inline_completion.region = region - listener.inline_completion.text = insert_text - listener.inline_completion.command = item.get('command') - listener.inline_completion.session_name = session_name - listener.inline_completion.render_async(position, insert_text[region_length:]) - - # listener.inline_completion.text = lines[0] + '\n' - # listener.inline_completion.render_async(position, lines[0]) + listener.inline_completion.items.clear() + for item in items: + insert_text = item['insertText'] + if not insert_text: + continue + if isinstance(insert_text, dict): # StringValue + debug('Snippet completions from the 3.18 specs not yet supported') + continue + range_ = item.get('range') + region = range_to_region(range_, self.view) if range_ else sublime.Region(position) + region_length = len(region) + if region_length > len(insert_text): + continue + listener.inline_completion.items.append((session_name, region, insert_text, item.get('command'))) + listener.inline_completion.position = position + listener.inline_completion.render_async(0) # filter_text = item.get('filterText', insert_text) # ignored for now +class LspNextInlineCompletionCommand(LspTextCommand): + + capability = 'inlineCompletionProvider' + + def is_enabled(self, event: dict | None = None, point: int | None = None, **kwargs) -> bool: + if not super().is_enabled(event, point): + return False + listener = self.get_listener() + if not listener: + return False + return listener.inline_completion.visible + + def run( + self, edit: sublime.Edit, event: dict | None = None, point: int | None = None, forward: bool = True + ) -> None: + listener = self.get_listener() + if not listener: + return + item_count = len(listener.inline_completion.items) + if item_count < 2: + return + new_index = (listener.inline_completion.index - 1 + 2 * forward) % item_count + listener.inline_completion.render_async(new_index) + + class LspCommitInlineCompletionCommand(LspTextCommand): capability = 'inlineCompletionProvider' @@ -198,15 +221,15 @@ def run(self, edit: sublime.Edit, event: dict | None = None, point: int | None = listener = self.get_listener() if not listener: return - self.view.replace(edit, listener.inline_completion.region, listener.inline_completion.text) + session_name, region, text, command = listener.inline_completion.items[listener.inline_completion.index] + self.view.replace(edit, region, text) selection = self.view.sel() pt = selection[0].b selection.clear() selection.add(pt) - command = listener.inline_completion.command if command: self.view.run_command('lsp_execute', { "command_name": command['command'], "command_args": command.get('arguments'), - "session_name": listener.inline_completion.session_name + "session_name": session_name })