From 57f6a304d22bba3d4fc211829237157a155883e0 Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Sun, 13 Oct 2024 05:25:41 +0200 Subject: [PATCH 1/8] Add support for inline completions --- ColorSchemes/Breakers.sublime-color-scheme | 7 +- ColorSchemes/Celeste.sublime-color-scheme | 7 +- ColorSchemes/Mariana.sublime-color-scheme | 7 +- ColorSchemes/Monokai.sublime-color-scheme | 7 +- ColorSchemes/Sixteen.sublime-color-scheme | 7 +- Default.sublime-keymap | 16 +- boot.py | 4 + docs/src/customization.md | 6 + docs/src/features.md | 6 + docs/src/language_servers.md | 63 ++++++ plugin/core/sessions.py | 10 + plugin/documents.py | 8 + plugin/inline_completion.py | 212 +++++++++++++++++++++ 13 files changed, 350 insertions(+), 10 deletions(-) create mode 100644 plugin/inline_completion.py diff --git a/ColorSchemes/Breakers.sublime-color-scheme b/ColorSchemes/Breakers.sublime-color-scheme index 2205fda4b..82018c25b 100644 --- a/ColorSchemes/Breakers.sublime-color-scheme +++ b/ColorSchemes/Breakers.sublime-color-scheme @@ -11,6 +11,11 @@ { "scope": "meta.semantic-token", "background": "#00000001" - } + }, + { + "scope": "meta.inline-completion", + "foreground": "color(var(grey3) alpha(0.6))", + "font_style": "italic" + }, ] } diff --git a/ColorSchemes/Celeste.sublime-color-scheme b/ColorSchemes/Celeste.sublime-color-scheme index 169e486b9..ded575b63 100644 --- a/ColorSchemes/Celeste.sublime-color-scheme +++ b/ColorSchemes/Celeste.sublime-color-scheme @@ -11,6 +11,11 @@ { "scope": "meta.semantic-token", "background": "#00000001" - } + }, + { + "scope": "meta.inline-completion", + "foreground": "color(var(black) alpha(0.6))", + "font_style": "italic" + }, ] } diff --git a/ColorSchemes/Mariana.sublime-color-scheme b/ColorSchemes/Mariana.sublime-color-scheme index 8aed2ae52..034fa9b1d 100644 --- a/ColorSchemes/Mariana.sublime-color-scheme +++ b/ColorSchemes/Mariana.sublime-color-scheme @@ -11,6 +11,11 @@ { "scope": "meta.semantic-token", "background": "#00000001" - } + }, + { + "scope": "meta.inline-completion", + "foreground": "color(var(white3) alpha(0.6))", + "font_style": "italic" + }, ] } diff --git a/ColorSchemes/Monokai.sublime-color-scheme b/ColorSchemes/Monokai.sublime-color-scheme index 995afb972..77e08e7da 100644 --- a/ColorSchemes/Monokai.sublime-color-scheme +++ b/ColorSchemes/Monokai.sublime-color-scheme @@ -11,6 +11,11 @@ { "scope": "meta.semantic-token", "background": "#00000001" - } + }, + { + "scope": "meta.inline-completion", + "foreground": "color(var(white3) alpha(0.6))", + "font_style": "italic" + }, ] } diff --git a/ColorSchemes/Sixteen.sublime-color-scheme b/ColorSchemes/Sixteen.sublime-color-scheme index 2205fda4b..77a6fe9d5 100644 --- a/ColorSchemes/Sixteen.sublime-color-scheme +++ b/ColorSchemes/Sixteen.sublime-color-scheme @@ -11,6 +11,11 @@ { "scope": "meta.semantic-token", "background": "#00000001" - } + }, + { + "scope": "meta.inline-completion", + "foreground": "color(var(grey5) alpha(0.6))", + "font_style": "italic" + }, ] } diff --git a/Default.sublime-keymap b/Default.sublime-keymap index 325077470..24ffcc36e 100644 --- a/Default.sublime-keymap +++ b/Default.sublime-keymap @@ -9,13 +9,19 @@ // "args": {"overlay": "command_palette", "text": "LSP: "} // }, // Insert/Replace Completions + // { + // "keys": ["UNBOUND"], + // "command": "lsp_commit_completion_with_opposite_insert_mode", + // "context": [ + // {"key": "lsp.session_with_capability", "operand": "completionProvider"}, + // {"key": "auto_complete_visible"} + // ] + // }, + // Insert Inline Completion { "keys": ["alt+enter"], - "command": "lsp_commit_completion_with_opposite_insert_mode", - "context": [ - {"key": "lsp.session_with_capability", "operand": "completionProvider"}, - {"key": "auto_complete_visible"} - ] + "command": "lsp_commit_inline_completion", + "context": [{"key": "lsp.inline_completion_visible"}] }, // Save all open files that have a language server attached with lsp_save // { diff --git a/boot.py b/boot.py index 56ae078db..395f55cab 100644 --- a/boot.py +++ b/boot.py @@ -59,6 +59,8 @@ from .plugin.hover import LspToggleHoverPopupsCommand from .plugin.inlay_hint import LspInlayHintClickCommand from .plugin.inlay_hint import LspToggleInlayHintsCommand +from .plugin.inline_completion import LspCommitInlineCompletionCommand +from .plugin.inline_completion import LspInlineCompletionCommand from .plugin.panels import LspClearLogPanelCommand from .plugin.panels import LspClearPanelCommand from .plugin.panels import LspShowDiagnosticsPanelCommand @@ -99,6 +101,7 @@ "LspCollapseTreeItemCommand", "LspColorPresentationCommand", "LspCommitCompletionWithOppositeInsertMode", + "LspCommitInlineCompletionCommand", "LspCopyToClipboardFromBase64Command", "LspDisableLanguageServerGloballyCommand", "LspDisableLanguageServerInProjectCommand", @@ -120,6 +123,7 @@ "LspHierarchyToggleCommand", "LspHoverCommand", "LspInlayHintClickCommand", + "LspInlineCompletionCommand", "LspNextDiagnosticCommand", "LspOnDoubleClickCommand", "LspOpenLinkCommand", diff --git a/docs/src/customization.md b/docs/src/customization.md index ba133ea5c..c10738889 100644 --- a/docs/src/customization.md +++ b/docs/src/customization.md @@ -227,3 +227,9 @@ The color scheme rule only works if the "background" color is (marginally) diffe | ----- | ----------- | | `markup.accent.codelens.lsp` | Accent color for code lens annotations | | `markup.accent.codeaction.lsp` | Accent color for code action annotations | + +### Inline Completions + +| scope | description | +| ----- | ----------- | +| `meta.inline-completion.lsp` | Style for inline completions | diff --git a/docs/src/features.md b/docs/src/features.md index 67d156aca..b7f556173 100644 --- a/docs/src/features.md +++ b/docs/src/features.md @@ -170,6 +170,12 @@ Inlay hints are disabled by default and can be enabled with the `"show_inlay_hin !!! info "Some servers require additional settings to be enabled in order to show inlay hints." +## Inline Completions + +Inline completions are typically provided by an AI code assistant. +They can span multiple lines and are rendered directly in the source code as grayed out text ("ghost text"). +Currently inline completions are only requested when you manually trigger auto-completions (Ctrl + Space). + ## Server Commands In Sublime Text you can bind any runnable command to a key or add it to various UI elements. Commands in Sublime Text are normally supplied by plugins or packages written in Python. A language server may provide a runnable command as well. These kinds of commands are wrapped in an `lsp_execute` Sublime command that you can bind to a key. diff --git a/docs/src/language_servers.md b/docs/src/language_servers.md index 4026f2151..564909eb8 100644 --- a/docs/src/language_servers.md +++ b/docs/src/language_servers.md @@ -12,6 +12,69 @@ If there are no setup steps for a language server on this page, but a [language !!! info "For legacy ST3 docs, see [lsp.readthedocs.io](https://lsp.readthedocs.io)." +## Universal + +### Tabby + +[Tabby](https://tabby.tabbyml.com/) is a self-hosted AI coding assistant which can provide inline completions for [various programming languages](https://tabby.tabbyml.com/docs/references/programming-languages/). + +In order to use Tabby you need a sufficiently fast GPU; the CPU version which can also be downloaded from the GitHub releases page is much too slow and it will result in timeouts for the completion requests. +Alternatively, Tabby can be setup on a separate server with capable hardware; see the [Configuration docs](https://tabby.tabbyml.com/docs/extensions/configurations/) for the required configuration details. +The following steps describe a local installation on a PC with compatible Nvidia GPU on Windows. More installation methods and the steps for other operation systems are listed in the [Tabby docs](https://tabby.tabbyml.com/docs/quick-start/installation/docker/). + +1. Download and install the CUDA Toolkit from https://developer.nvidia.com/cuda-downloads + +2. Download and extract a CUDA version of Tabby from the [GitHub releases page](https://github.com/TabbyML/tabby/releases) (click on "Assets"); e.g. `tabby_x86_64-windows-msvc-cuda122.zip` + +3. Install the `tabby-agent` language server via npm (requires NodeJS): + + ```sh + npm install -g tabby-agent + ``` + +4. Open `Preferences > Package Settings > LSP > Settings` and add the `"tabby"` client configuration to the `"clients"`: + + ```jsonc + { + "clients": { + "tabby": { + "enabled": true, + "command": ["tabby-agent", "--stdio"], + "selector": "source.js | source.python | source.rust", // replace with your relevant filetype(s) + "disabled_capabilities": { + "completionProvider": true + } + }, + } + } + ``` + +5. Download a completion model (see https://tabby.tabbyml.com/docs/models/ for available model files and GPU requirements): + + ```sh + tabby download --model StarCoder-1B + ``` + +6. If necessary, edit the configuration file under `~/.tabby-client/agent/config.toml`, which is generated automatically on the first start of tabby-agent. + For example, to disable anonymous usage tracking add + + ```toml + [anonymousUsageTracking] + disable = true + ``` + +7. Manually start the Tabby backend: + + ```sh + tabby serve --model StarCoder-1B --no-webserver + ``` + + The language server communicates with this backend, i.e. it needs to be running in order for `tabby-agent` to work. + +8. Now you can open a file in Sublime Text and start coding. + Inline completions are requested when you manually trigger auto-complete via Ctrl + Space. + + ## Angular Follow installation instructions on [LSP-angular](https://github.com/sublimelsp/LSP-angular). diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index b4392348d..0c15447de 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -114,6 +114,7 @@ from enum import IntEnum, IntFlag from typing import Any, Callable, Generator, List, Protocol, TypeVar from typing import cast +from typing import TYPE_CHECKING from typing_extensions import TypeAlias, TypeGuard from weakref import WeakSet import functools @@ -122,6 +123,11 @@ import sublime import weakref + +if TYPE_CHECKING: + from ..inline_completion import InlineCompletionData + + InitCallback: TypeAlias = Callable[['Session', bool], None] T = TypeVar('T') @@ -325,6 +331,9 @@ def get_initialize_params(variables: dict[str, str], workspace_folders: list[Wor "itemDefaults": ["editRange", "insertTextFormat", "data"] } }, + "inlineCompletion": { + "dynamicRegistration": True + }, "signatureHelp": { "dynamicRegistration": True, "contextSupport": True, @@ -701,6 +710,7 @@ class AbstractViewListener(metaclass=ABCMeta): view = cast(sublime.View, None) hover_provider_count = 0 + inline_completion = cast('InlineCompletionData', None) @abstractmethod def session_async(self, capability: str, point: int | None = None) -> Session | None: diff --git a/plugin/documents.py b/plugin/documents.py index 9dbf5d366..65eb64e2d 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -52,6 +52,7 @@ from .core.windows import WindowManager from .folding_range import folding_range_to_range from .hover import code_actions_content +from .inline_completion import InlineCompletionData from .session_buffer import SessionBuffer from .session_view import SessionView from functools import partial @@ -197,6 +198,7 @@ def on_change() -> None: self._stored_selection: list[sublime.Region] = [] self._should_format_on_paste = False self.hover_provider_count = 0 + self.inline_completion = InlineCompletionData(self.view, 'lsp_inline_completion') self._setup() def __del__(self) -> None: @@ -226,6 +228,7 @@ def _cleanup(self) -> None: self._stored_selection = [] self.view.erase_status(AbstractViewListener.TOTAL_ERRORS_AND_WARNINGS_STATUS_KEY) self._clear_highlight_regions() + self.inline_completion.clear_async() self._clear_session_views_async() def _reset(self) -> None: @@ -418,6 +421,7 @@ def on_selection_modified_async(self) -> None: if not self._is_in_higlighted_region(first_region.b): self._clear_highlight_regions() self._clear_code_actions_annotation() + self.inline_completion.clear_async() if userprefs().document_highlight_style or userprefs().show_code_actions: self._when_selection_remains_stable_async( self._on_selection_modified_debounced_async, first_region, after_ms=self.debounce_time) @@ -511,6 +515,8 @@ def on_query_context(self, key: str, operator: int, operand: Any, match_all: boo if not session_view: return not operand return operand == bool(session_view.session_buffer.get_document_link_at_point(self.view, position)) + elif key == 'lsp.inline_completion_visible' and operator == sublime.QueryOperator.EQUAL: + return operand == self.inline_completion.visible return None @requires_session @@ -560,6 +566,7 @@ def _on_hover_gutter_async(self, point: int) -> None: def on_text_command(self, command_name: str, args: dict | None) -> tuple[str, dict] | None: if command_name == "auto_complete": self._auto_complete_triggered_manually = True + self.view.run_command('lsp_inline_completion') elif command_name == "show_scope_name" and userprefs().semantic_highlighting: session = self.session_async("semanticTokensProvider") if session: @@ -990,6 +997,7 @@ def _on_view_updated_async(self) -> None: if first_region is None: return self._clear_highlight_regions() + self.inline_completion.clear_async() if userprefs().document_highlight_style: self._when_selection_remains_stable_async( self._do_highlights_async, first_region, after_ms=self.debounce_time) diff --git a/plugin/inline_completion.py b/plugin/inline_completion.py new file mode 100644 index 000000000..d80d131dd --- /dev/null +++ b/plugin/inline_completion.py @@ -0,0 +1,212 @@ +from __future__ import annotations +from .core.logging import debug +from .core.protocol import Command +from .core.protocol import InlineCompletionItem +from .core.protocol import InlineCompletionList +from .core.protocol import InlineCompletionParams +from .core.protocol import InlineCompletionTriggerKind +from .core.protocol import Request +from .core.registry import get_position +from .core.registry import LspTextCommand +from .core.views import range_to_region +from .core.views import text_document_position_params +from functools import partial +import html +import sublime + + +PHANTOM_HTML = """ + + +
{content}
{suffix} +""" + + +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._view = view + self._phantom_set = sublime.PhantomSet(view, key) + + def render_async(self, location: int, text: str) -> 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() + suffix = '
Alt + Enter to complete
' if is_at_eol or \ + more_lines else '' + phantoms = [sublime.Phantom( + region, + PHANTOM_HTML.format( + color=color, + font_style=font_style, + font_weight=font_weight, + content=self._normalize_html(first_line), + suffix=suffix + ), + sublime.PhantomLayout.INLINE + )] + if more_lines: + phantoms.append( + sublime.Phantom( + region, + PHANTOM_HTML.format( + color=color, + font_style=font_style, + font_weight=font_weight, + content='
'.join(self._normalize_html(line) for line in more_lines), + suffix='' + ), + sublime.PhantomLayout.BLOCK + ) + ) + sublime.set_timeout(lambda: self._render(phantoms)) + self.visible = True + + def _render(self, phantoms: list[sublime.Phantom]) -> None: + self._phantom_set.update(phantoms) + + def clear_async(self) -> None: + if self.visible: + sublime.set_timeout(self._clear) + self.visible = False + + def _clear(self) -> None: + self._phantom_set.update([]) + + def _normalize_html(self, content: str) -> str: + return html.escape(content).replace(' ', ' ') + + +class LspInlineCompletionCommand(LspTextCommand): + + capability = 'inlineCompletionProvider' + + def run(self, edit: sublime.Edit, event: dict | None = None, point: int | None = None) -> None: + sublime.set_timeout_async(partial(self._run_async, event, point)) + + def _run_async(self, event: dict | None = None, point: int | None = None) -> None: + position = get_position(self.view, event, point) + if position is None: + return + session = self.best_session(self.capability, point) + if not session: + return + if self.view.settings().get('mini_auto_complete', False): + return + position_params = text_document_position_params(self.view, position) + params: InlineCompletionParams = { + 'textDocument': position_params['textDocument'], + 'position': position_params['position'], + 'context': { + 'triggerKind': InlineCompletionTriggerKind.Invoked + } + } + session.send_request_async( + Request('textDocument/inlineCompletion', params), + partial(self._handle_response_async, session.config.name, self.view.change_count(), position) + ) + + def _handle_response_async( + self, + session_name: str, + view_version: int, + position: int, + response: list[InlineCompletionItem] | InlineCompletionList | None + ) -> None: + if response is None: + return + 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]) + + # filter_text = item.get('filterText', insert_text) # ignored for now + + +class LspCommitInlineCompletionCommand(LspTextCommand): + + capability = 'inlineCompletionProvider' + + def is_enabled(self, event: dict | None = None, point: int | None = None) -> 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) -> None: + listener = self.get_listener() + if not listener: + return + self.view.replace(edit, listener.inline_completion.region, listener.inline_completion.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 + }) From 2d6aed56e21c599466a84bd2e54894c0a8a9db0f Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Thu, 14 Nov 2024 18:34:17 +0100 Subject: [PATCH 2/8] Fix formatting in docs --- docs/src/language_servers.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/language_servers.md b/docs/src/language_servers.md index 564909eb8..275a648e7 100644 --- a/docs/src/language_servers.md +++ b/docs/src/language_servers.md @@ -20,9 +20,9 @@ If there are no setup steps for a language server on this page, but a [language In order to use Tabby you need a sufficiently fast GPU; the CPU version which can also be downloaded from the GitHub releases page is much too slow and it will result in timeouts for the completion requests. Alternatively, Tabby can be setup on a separate server with capable hardware; see the [Configuration docs](https://tabby.tabbyml.com/docs/extensions/configurations/) for the required configuration details. -The following steps describe a local installation on a PC with compatible Nvidia GPU on Windows. More installation methods and the steps for other operation systems are listed in the [Tabby docs](https://tabby.tabbyml.com/docs/quick-start/installation/docker/). +The following steps describe a local installation on a Windows PC with compatible Nvidia GPU. More installation methods and the steps for other operation systems are listed in the [Tabby docs](https://tabby.tabbyml.com/docs/quick-start/installation/docker/). -1. Download and install the CUDA Toolkit from https://developer.nvidia.com/cuda-downloads +1. Download and install the CUDA Toolkit from 2. Download and extract a CUDA version of Tabby from the [GitHub releases page](https://github.com/TabbyML/tabby/releases) (click on "Assets"); e.g. `tabby_x86_64-windows-msvc-cuda122.zip` @@ -49,7 +49,7 @@ The following steps describe a local installation on a PC with compatible Nvidia } ``` -5. Download a completion model (see https://tabby.tabbyml.com/docs/models/ for available model files and GPU requirements): +5. Download a completion model (see for available model files and GPU requirements): ```sh tabby download --model StarCoder-1B @@ -58,10 +58,10 @@ The following steps describe a local installation on a PC with compatible Nvidia 6. If necessary, edit the configuration file under `~/.tabby-client/agent/config.toml`, which is generated automatically on the first start of tabby-agent. For example, to disable anonymous usage tracking add - ```toml - [anonymousUsageTracking] - disable = true - ``` + ```toml + [anonymousUsageTracking] + disable = true + ``` 7. Manually start the Tabby backend: From 5c773e02732c10afde98793fd2413462adaf7e14 Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Fri, 15 Nov 2024 15:11:11 +0100 Subject: [PATCH 3/8] Add command to cycle through multiple completions --- boot.py | 1 + plugin/inline_completion.py | 89 +++++++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 33 deletions(-) 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 }) From 2080f75fba3a4ac3c21cbadebdc5e9bd099ef2dd Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Fri, 15 Nov 2024 15:15:30 +0100 Subject: [PATCH 4/8] Remove key hint --- plugin/inline_completion.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/plugin/inline_completion.py b/plugin/inline_completion.py index 6b3aa2414..df361db14 100644 --- a/plugin/inline_completion.py +++ b/plugin/inline_completion.py @@ -28,24 +28,9 @@ font-style: {font_style}; font-weight: {font_weight}; }} - .key-hint {{ - display: inline; - padding-left: 2em; - font-size: 0.9rem; - color: color(var(--foreground) alpha(0.8)); - }} - kbd {{ - font-family: monospace; - font-size: 0.75rem; - color: color(var(--foreground) alpha(0.8)); - background-color: color(var(--foreground) alpha(0.08)); - border: 1px solid color(var(--foreground) alpha(0.5)); - border-radius: 4px; - padding: 0px 3px; - }} -
{content}
{suffix} +
{content}
""" @@ -65,19 +50,15 @@ def render_async(self, index: int) -> None: font_style = 'italic' if style['italic'] else 'normal' font_weight = 'bold' if style['bold'] else 'normal' 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( region, PHANTOM_HTML.format( color=color, font_style=font_style, font_weight=font_weight, - content=self._normalize_html(first_line), - suffix=suffix + content=self._normalize_html(first_line) ), sublime.PhantomLayout.INLINE )] @@ -89,8 +70,7 @@ def render_async(self, index: int) -> None: color=color, font_style=font_style, font_weight=font_weight, - content='
'.join(self._normalize_html(line) for line in more_lines), - suffix='' + content='
'.join(self._normalize_html(line) for line in more_lines) ), sublime.PhantomLayout.BLOCK ) From 6dc8dc25d4bbfa73460623bd7afaac375b547f99 Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Fri, 15 Nov 2024 15:20:52 +0100 Subject: [PATCH 5/8] Add missed command --- Default.sublime-keymap | 6 ++++++ boot.py | 1 + 2 files changed, 7 insertions(+) diff --git a/Default.sublime-keymap b/Default.sublime-keymap index 24ffcc36e..74c689493 100644 --- a/Default.sublime-keymap +++ b/Default.sublime-keymap @@ -23,6 +23,12 @@ "command": "lsp_commit_inline_completion", "context": [{"key": "lsp.inline_completion_visible"}] }, + // Show next Inline Completion + // { + // "keys": ["UNBOUND"], + // "command": "lsp_next_inline_completion", + // "context": [{"key": "lsp.inline_completion_visible"}] + // }, // Save all open files that have a language server attached with lsp_save // { // "keys": ["UNBOUND"], diff --git a/boot.py b/boot.py index 60508e663..774abd9cd 100644 --- a/boot.py +++ b/boot.py @@ -126,6 +126,7 @@ "LspInlayHintClickCommand", "LspInlineCompletionCommand", "LspNextDiagnosticCommand", + "LspNextInlineCompletionCommand", "LspOnDoubleClickCommand", "LspOpenLinkCommand", "LspOpenLocationCommand", From 3063b628d698339d3641d029caf64fbb2bd6d5dd Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Sat, 16 Nov 2024 18:05:20 +0100 Subject: [PATCH 6/8] Update docs --- docs/src/features.md | 5 +++- docs/src/keyboard_shortcuts.md | 3 +++ docs/src/language_servers.md | 46 +++++++++++++++++++++------------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/docs/src/features.md b/docs/src/features.md index b7f556173..a73da87a3 100644 --- a/docs/src/features.md +++ b/docs/src/features.md @@ -174,7 +174,10 @@ Inlay hints are disabled by default and can be enabled with the `"show_inlay_hin Inline completions are typically provided by an AI code assistant. They can span multiple lines and are rendered directly in the source code as grayed out text ("ghost text"). -Currently inline completions are only requested when you manually trigger auto-completions (Ctrl + Space). + +!!! note + Currently inline completions are only requested when you manually trigger auto-completions (Ctrl + Space). + Inline completions are disabled if you have enabled `"mini_auto_complete"`. ## Server Commands diff --git a/docs/src/keyboard_shortcuts.md b/docs/src/keyboard_shortcuts.md index d5ee364ae..e3ebf4ba1 100644 --- a/docs/src/keyboard_shortcuts.md +++ b/docs/src/keyboard_shortcuts.md @@ -8,6 +8,7 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin | Feature | Shortcut | Command | | ------- | -------- | ------- | | Auto Complete | ctrl space (also on macOS) | `auto_complete` +| Commit Inline Completion | alt enter | `lsp_commit_inline_completion` | Expand Selection | unbound | `lsp_expand_selection` | Find References | shift f12 | `lsp_symbol_references`
Supports optional args: `{"include_declaration": true | false, "output_mode": "output_panel" | "quick_panel"}`.
Triggering from context menus while holding ctrl opens in "side by side" mode. Holding shift triggers opposite behavior relative to what `show_references_in_quick_panel` is set to. | Fold | unbound | `lsp_fold`
Supports optional args: `{"strict": true/false}` - to configure whether to fold only when the caret is contained within the folded region (`true`), or even when it is anywhere on the starting line (`false`). @@ -28,6 +29,7 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin | Next Diagnostic | unbound | `lsp_next_diagnostic` | Previous Diagnostic | unbound | `lsp_prev_diagnostic` | Rename | unbound | `lsp_symbol_rename` +| Request Inline Completions | unbound | `lsp_inline_completion` | Restart Server | unbound | `lsp_restart_server` | Run Code Action | unbound | `lsp_code_actions` | Run Code Lens | unbound | `lsp_code_lens` @@ -35,6 +37,7 @@ Refer to the [Customization section](customization.md#keyboard-shortcuts-key-bin | Run Source Action | unbound | `lsp_code_actions`
With args: `{"only_kinds": ["source"]}`. | Save All | unbound | `lsp_save_all`
Supports optional args `{"only_files": true | false}` - whether to ignore buffers which have no associated file on disk. | Show Call Hierarchy | unbound | `lsp_call_hierarchy` +| Show next Inline Completion | unbound | `lsp_next_inline_completion` | Show Type Hierarchy | unbound | `lsp_type_hierarchy` | Signature Help | ctrl alt space | `lsp_signature_help_show` | Toggle Diagnostics Panel | ctrl alt m | `lsp_show_diagnostics_panel` diff --git a/docs/src/language_servers.md b/docs/src/language_servers.md index 275a648e7..be8d956db 100644 --- a/docs/src/language_servers.md +++ b/docs/src/language_servers.md @@ -26,13 +26,37 @@ The following steps describe a local installation on a Windows PC with compatibl 2. Download and extract a CUDA version of Tabby from the [GitHub releases page](https://github.com/TabbyML/tabby/releases) (click on "Assets"); e.g. `tabby_x86_64-windows-msvc-cuda122.zip` -3. Install the `tabby-agent` language server via npm (requires NodeJS): +3. On macOS and Linux it might be necessary to change the access permissions of `lama-server` and `tabby` to be executable: + + ```sh + $ tabby_aarch64-apple-darwin chmod +x llama-server + $ tabby_aarch64-apple-darwin chmod +x tabby + ``` + + !!! note "On macOS you might get an error that “tabby” cannot be opened because it is from an unidentified developer." + After changing the permission to executable, right click on `tabby` and select "Open", that will get rid of the error. + +4. Download a completion model (see for available model files and GPU requirements): + + ```sh + tabby download --model StarCoder-1B + ``` + +5. If necessary, edit the configuration file under `~/.tabby-client/agent/config.toml`, which is generated automatically on the first start of tabby-agent. + For example, to disable anonymous usage tracking add + + ```toml + [anonymousUsageTracking] + disable = true + ``` + +6. Install the `tabby-agent` language server via npm (requires NodeJS): ```sh npm install -g tabby-agent ``` -4. Open `Preferences > Package Settings > LSP > Settings` and add the `"tabby"` client configuration to the `"clients"`: +7. Open `Preferences > Package Settings > LSP > Settings` and add the `"tabby"` client configuration to the `"clients"`: ```jsonc { @@ -49,21 +73,7 @@ The following steps describe a local installation on a Windows PC with compatibl } ``` -5. Download a completion model (see for available model files and GPU requirements): - - ```sh - tabby download --model StarCoder-1B - ``` - -6. If necessary, edit the configuration file under `~/.tabby-client/agent/config.toml`, which is generated automatically on the first start of tabby-agent. - For example, to disable anonymous usage tracking add - - ```toml - [anonymousUsageTracking] - disable = true - ``` - -7. Manually start the Tabby backend: +8. Manually start the Tabby backend: ```sh tabby serve --model StarCoder-1B --no-webserver @@ -71,7 +81,7 @@ The following steps describe a local installation on a Windows PC with compatibl The language server communicates with this backend, i.e. it needs to be running in order for `tabby-agent` to work. -8. Now you can open a file in Sublime Text and start coding. +9. Now you can open a file in Sublime Text and start coding. Inline completions are requested when you manually trigger auto-complete via Ctrl + Space. From 1ef090c056daf850be3cd5d4492722dbb0a57697 Mon Sep 17 00:00:00 2001 From: jwortmann Date: Mon, 18 Nov 2024 14:54:20 +0100 Subject: [PATCH 7/8] Update docs/src/language_servers.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Предраг Николић --- docs/src/language_servers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/language_servers.md b/docs/src/language_servers.md index be8d956db..cb432b47b 100644 --- a/docs/src/language_servers.md +++ b/docs/src/language_servers.md @@ -29,8 +29,8 @@ The following steps describe a local installation on a Windows PC with compatibl 3. On macOS and Linux it might be necessary to change the access permissions of `lama-server` and `tabby` to be executable: ```sh - $ tabby_aarch64-apple-darwin chmod +x llama-server - $ tabby_aarch64-apple-darwin chmod +x tabby + $ chmod +x llama-server + $ chmod +x tabby ``` !!! note "On macOS you might get an error that “tabby” cannot be opened because it is from an unidentified developer." From ef73daa6ab7ffe733b6b3b9971fedec7bc2f68ab Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Mon, 18 Nov 2024 14:56:44 +0100 Subject: [PATCH 8/8] Update docs --- docs/src/language_servers.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/language_servers.md b/docs/src/language_servers.md index cb432b47b..3cc843b0d 100644 --- a/docs/src/language_servers.md +++ b/docs/src/language_servers.md @@ -42,7 +42,13 @@ The following steps describe a local installation on a Windows PC with compatibl tabby download --model StarCoder-1B ``` -5. If necessary, edit the configuration file under `~/.tabby-client/agent/config.toml`, which is generated automatically on the first start of tabby-agent. +5. Install the `tabby-agent` language server via npm (requires NodeJS): + + ```sh + npm install -g tabby-agent + ``` + +6. If necessary, edit the configuration file under `~/.tabby-client/agent/config.toml`, which is generated automatically on the first start of tabby-agent. For example, to disable anonymous usage tracking add ```toml @@ -50,12 +56,6 @@ The following steps describe a local installation on a Windows PC with compatibl disable = true ``` -6. Install the `tabby-agent` language server via npm (requires NodeJS): - - ```sh - npm install -g tabby-agent - ``` - 7. Open `Preferences > Package Settings > LSP > Settings` and add the `"tabby"` client configuration to the `"clients"`: ```jsonc