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 = """ + +
+