Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
f1b0dee
Introduce new plugin API
rchl Jan 27, 2026
9a749ea
public export
rchl Jan 27, 2026
cb0d6c9
opt-in flag
rchl Jan 27, 2026
9e40a7c
indent
rchl Jan 27, 2026
70096c1
can't be abstract
rchl Jan 27, 2026
d996f1b
dump old code
rchl Jan 27, 2026
af0b028
doc
rchl Jan 27, 2026
c95005e
adding new apis
rchl Jan 27, 2026
4013c91
blank
rchl Jan 27, 2026
4aa35ee
rename to LspPlugin
rchl Jan 29, 2026
d6242ee
remove on_post_start
rchl Jan 29, 2026
83e91ba
rename on_pre_server_command to on_execute_command
rchl Jan 29, 2026
16b1653
Merge branch 'main' into feat/new-plugin-api
rchl Feb 7, 2026
3173d4c
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Feb 8, 2026
ea9e9c1
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Feb 16, 2026
2227561
remove name() and configuration()
rchl Feb 16, 2026
15a011c
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Feb 16, 2026
4053880
storage_path as a property
rchl Feb 16, 2026
f35e6f3
use Path
rchl Feb 17, 2026
6f843c5
auto-detect session-name of the lsp command
rchl Feb 17, 2026
cd25ebe
remove AbstractPlugin.selector
rchl Feb 17, 2026
6637782
remove comment
rchl Feb 17, 2026
62ca15f
on_execute_command returns Promise
rchl Feb 17, 2026
a2fbdbe
update tests
rchl Feb 17, 2026
8c117c4
lint
rchl Feb 17, 2026
fc6f079
on_open_uri_async returns a Promise
rchl Feb 17, 2026
e16290b
on_open_uri_async returns a Promise
rchl Feb 17, 2026
d78037b
Add generic arguments
rchl Feb 17, 2026
53d7856
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Feb 18, 2026
1ff861c
expose plugin_storage_path instead of storage_path
rchl Feb 19, 2026
3acb880
Merge branch 'main' into feat/new-plugin-api
rchl Feb 20, 2026
b873a36
plugin_storage_path
rchl Feb 20, 2026
05df9d6
Merge branch 'main' into feat/new-plugin-api
rchl Feb 21, 2026
263524a
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Feb 22, 2026
28ddcb2
restore request_handler
rchl Feb 22, 2026
a51793a
Merge branch 'main' into feat/new-plugin-api
rchl Feb 22, 2026
1bf417a
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Feb 25, 2026
d2e334e
fixup
rchl Feb 25, 2026
3a31944
fixup
rchl Feb 25, 2026
a404bbc
fixup
rchl Feb 25, 2026
bbfb5d2
Remove AbstractPlugin.should_ignore()
rchl Feb 26, 2026
f9df53f
Merge branch 'main' into feat/new-plugin-api
rchl Feb 27, 2026
42592b3
Use ClientRequest/ClientNotification/ServerResponse/ServerNotification
rchl Feb 27, 2026
e28d939
Merge branch 'main' into feat/new-plugin-api
rchl Feb 27, 2026
868c3b1
set lsp_uri if view
rchl Feb 27, 2026
beecb8b
remove unused HandleUpdateOrInstallationParams
rchl Feb 27, 2026
7a20c5c
fixup
rchl Feb 27, 2026
48575cc
review comments
rchl Feb 28, 2026
cad2247
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Feb 28, 2026
01a4a78
use Final for plugin_storage_path
rchl Feb 28, 2026
5008c02
Merge branch 'main' into feat/new-plugin-api
rchl Mar 5, 2026
bfee983
bug
rchl Mar 5, 2026
3eca3f7
Add LspPlugin.session_name and initialize on subclass
rchl Mar 5, 2026
dc90c3e
Remove deprecated set_window_status_async (jdtls will updated)
rchl Mar 5, 2026
4b1d9c4
Replace on_pre_start with 3 separate APIs
rchl Mar 5, 2026
9aa4824
replace can_start with DontStartPluginError
rchl Mar 5, 2026
049a6ef
Merge branch 'main' into feat/new-plugin-api
rchl Mar 15, 2026
24d3fd4
Fix after merge
rchl Mar 15, 2026
723f359
change "def initialization_options" return type
rchl Mar 15, 2026
309bd9e
Rename DontStartPluginError to PluginStartError
rchl Mar 15, 2026
cf668f2
update tooling
rchl Mar 15, 2026
435a9d4
add missing super init
rchl Mar 15, 2026
eb7772d
decide workspace folder in "def working_directory"
rchl Mar 15, 2026
3a936c4
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Mar 22, 2026
d0ab4ce
formatting
rchl Mar 22, 2026
315c682
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Mar 24, 2026
4f1290e
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Mar 24, 2026
c172460
expand documentation
rchl Mar 24, 2026
bbcff02
Rename PluginContext.initiating_view to view
rchl Mar 24, 2026
4c9aaef
Update plugin/api.py
rchl Mar 24, 2026
ab945b5
add register() and unregister() on LpsPlugin
rchl Mar 24, 2026
27be495
add migration guide
rchl Mar 24, 2026
3610a4c
always return dict from additional_variables
rchl Mar 24, 2026
c12b4a4
remove on_settings_changed
rchl Mar 25, 2026
ac16cee
Merge branch 'main' into feat/new-plugin-api
rchl Mar 25, 2026
7764a62
add on_before_initialize
rchl Mar 25, 2026
c139657
compatibility with pyright
rchl Mar 25, 2026
a85aa94
add pass
rchl Mar 25, 2026
32aacdb
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Apr 9, 2026
2494693
make PackagedTask public
rchl Apr 9, 2026
1d213a9
add TransportWrapper.send_bytes()
rchl Apr 9, 2026
7fdfe9e
only import ref
rchl Apr 9, 2026
1a1784c
update migration
rchl Apr 9, 2026
be6aa2f
update migration
rchl Apr 9, 2026
68f748a
update migration
rchl Apr 9, 2026
d787d30
deprecate AbstractPlugin
rchl Apr 9, 2026
83d7d88
Merge branch 'main' into feat/new-plugin-api
rchl Apr 9, 2026
0299203
deprecate un/register_plugin
rchl Apr 9, 2026
9b1a834
nicer syntax
rchl Apr 9, 2026
f3f557d
revert docs for deprecated function
rchl Apr 9, 2026
3cea9fa
don't pass PluginContext to init
rchl Apr 10, 2026
6f5778b
Add doc to PluginStartError
rchl Apr 10, 2026
414a4a0
rename LspPlugin.session_name to name
rchl Apr 10, 2026
8e28057
store window variables in Session
rchl Apr 10, 2026
a8eef3c
fix test
rchl Apr 10, 2026
710e05c
simplify: no need to pass plugin_data to Session
rchl Apr 10, 2026
af33477
add final decorator
rchl Apr 12, 2026
569e2bf
Merge remote-tracking branch 'origin/main' into feat/new-plugin-api
rchl Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .api import AbstractPluginV2
from .api import PluginContext
from .api import HandleUpdateOrInstallationParams
from .core.collections import DottedDict
from .core.css import css
from .core.edit import apply_text_edits
Expand Down Expand Up @@ -32,6 +35,7 @@
__all__ = [
'__version__',
'AbstractPlugin',
'AbstractPluginV2',
'apply_text_edits',
'ClientConfig',
'css',
Expand All @@ -42,12 +46,14 @@
'FileWatcherEvent',
'FileWatcherEventType',
'FileWatcherProtocol',
'HandleUpdateOrInstallationParams',
'LspTextCommand',
'LspWindowCommand',
'MarkdownLangMap',
'matches_pattern',
'Notification',
'parse_uri',
'PluginContext',
'register_file_watcher_implementation',
'register_plugin',
'Request',
Expand Down
329 changes: 329 additions & 0 deletions plugin/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
from __future__ import annotations
from .core.constants import ST_STORAGE_PATH
from abc import ABCMeta
from abc import abstractmethod
from typing import Any, Callable, Literal, TypedDict, final, TYPE_CHECKING
import sublime

if TYPE_CHECKING:
from ..protocol import ConfigurationItem
from ..protocol import DocumentUri
from ..protocol import ExecuteCommandParams
from .core.collections import DottedDict
from .core.protocol import Notification
from .core.protocol import Request
from .core.protocol import Response
from .core.sessions import Session
from .core.sessions import SessionBufferProtocol
from .core.types import ClientConfig
from .core.views import MarkdownLangMap
from .core.workspace import WorkspaceFolder
from weakref import ref


class HandleUpdateOrInstallationParams(TypedDict):
set_installing_status: Callable[[], None]


@final
class PluginContext:
def __init__(
self,
configuration: ClientConfig,
initiating_view: sublime.View,
window: sublime.Window,
workspace_folders: list[WorkspaceFolder]
) -> None:
self.configuration = configuration
self.initiating_view = initiating_view
self.window = window
self.workspace_folders = workspace_folders


class AbstractPluginV2(metaclass=ABCMeta):
"""
TODO: doc
"""

API_VERSION: Literal[2] = 2

@classmethod
@abstractmethod
def name(cls) -> str:
"""
A human-friendly name. If your plugin is called "LSP-foobar", then this should return "foobar". If you also
have your settings file called "LSP-foobar.sublime-settings", then you don't even need to re-implement the
configuration method (see below).
"""
raise NotImplementedError

@classmethod
def configuration(cls) -> tuple[sublime.Settings, str]:
"""
Return the Settings object that defines the "command", "languages", and optionally the "initializationOptions",
"default_settings", "env" and "tcp_port" as the first element in the tuple, and the path to the base settings
filename as the second element in the tuple.

The second element in the tuple is used to handle "settings" overrides from users properly. For example, if your
plugin is called LSP-foobar, you would return "Packages/LSP-foobar/LSP-foobar.sublime-settings".

The "command", "initializationOptions" and "env" are subject to template string substitution. The following
template strings are recognized:

$file
$file_base_name
$file_extension
$file_name
$file_path
$platform
$project
$project_base_name
$project_extension
$project_name
$project_path

These are just the values from window.extract_variables(). Additionally,

$storage_path The path to the package storage (see AbstractPlugin.storage_path)
$cache_path sublime.cache_path()
$temp_dir tempfile.gettempdir()
$home os.path.expanduser('~')
$port A random free TCP-port on localhost in case "tcp_port" is set to 0. This string template can only
be used in the "command"

The "command" and "env" are expanded upon starting the subprocess of the Session. The "initializationOptions"
are expanded upon doing the initialize request. "initializationOptions" does not expand $port.

When you're managing your own server binary, you would typically place it in sublime.cache_path(). So your
"command" should look like this: "command": ["$cache_path/LSP-foobar/server_binary", "--stdio"]
"""
name = cls.name()
basename = f"LSP-{name}.sublime-settings"
filepath = f"Packages/LSP-{name}/{basename}"
return sublime.load_settings(basename), filepath

@classmethod
def additional_variables(cls, context: PluginContext) -> dict[str, str] | None:
"""
In addition to the above variables, add more variables here to be expanded.
"""
return None

@classmethod
def storage_path(cls) -> str:
"""
The storage path. Use this as your base directory to install server files. Its path is '$DATA/Package Storage'.
You should have an additional subdirectory preferably the same name as your plugin. For instance:

```python
from LSP.plugin import AbstractPlugin
import os


class MyPlugin(AbstractPlugin):

@classmethod
def name(cls) -> str:
return "my-plugin"

@classmethod
def basedir(cls) -> str:
# Do everything relative to this directory
return os.path.join(cls.storage_path(), cls.name())
```
"""
return ST_STORAGE_PATH

@classmethod
def handle_update_or_installation_async(
cls, context: PluginContext, params: HandleUpdateOrInstallationParams
) -> None:
"""Update or install the server binary if this plugin manages one. Called before server is started.

Make sure to call `args.set_installing_status()` before starting long-running operations to give user
a better feedback that something is happening.
"""
return

@classmethod
def can_start(cls, context: PluginContext) -> str | None:
"""
Determines ability to start. This is called after needs_update_or_installation and after install_or_update.
So you may assume that if you're managing your server binary, then it is already installed when this
classmethod is called.

:param window: The window
:param initiating_view: The initiating view
:param workspace_folders: The workspace folders
:param configuration: The configuration

:returns: A string describing the reason why we should not start a language server session, or None if we
should go ahead and start a session.
"""
return None

@classmethod
def on_pre_start(cls, context: PluginContext) -> str | None:
"""
Callback invoked just before the language server subprocess is started. This is the place to do last-minute
adjustments to your "command" or "init_options" in the passed-in "configuration" argument, or change the
order of the workspace folders. You can also choose to return a custom working directory, but consider that a
language server should not care about the working directory.

:param window: The window
:param initiating_view: The initiating view
:param workspace_folders: The workspace folders, you can modify these
:param configuration: The configuration, you can modify this one

:returns: A desired working directory, or None if you don't care
"""
return None

@classmethod
def markdown_language_id_to_st_syntax_map(cls) -> MarkdownLangMap | None:
"""
Override this method to tweak the syntax highlighting of code blocks in popups from your language server.
The returned object should be a dictionary exactly in the form of mdpopup's language_map setting.

See: https://facelessuser.github.io/sublime-markdown-popups/settings/#mdpopupssublime_user_lang_map

:returns: The markdown language map, or None
"""
return None

def __init__(self, weaksession: ref[Session], context: PluginContext) -> None:
"""
Constructs a new instance. Your instance is constructed after a response to the initialize request.

:param weaksession: A weak reference to the Session. You can grab a strong reference through
self.weaksession(), but don't hold on to that reference.
"""
self.weaksession: ref[Session] = weaksession
self.context: PluginContext = context

# ------------- OLD --------------

"""
Inherit from this class to handle non-standard requests and notifications.
Given a request/notification, replace the non-alphabetic characters with an underscore, and prepend it with "m_".
This will be the name of your method.
For instance, to implement the non-standard eslint/openDoc request, define the Python method

def m_eslint_openDoc(self, params, request_id):
session = self.weaksession()
if session:
webbrowser.open_tab(params['url'])
session.send_response(Response(request_id, None))

To handle the non-standard eslint/status notification, define the Python method

def m_eslint_status(self, params):
pass

To understand how this works, see the __getattr__ method of the Session class.
"""

def on_settings_changed(self, settings: DottedDict) -> None:
"""
Override this method to alter the settings that are returned to the server for the
workspace/didChangeConfiguration notification and the workspace/configuration requests.

:param settings: The settings that the server should receive.
"""
pass

def on_workspace_configuration(self, params: ConfigurationItem, configuration: Any) -> Any:
"""
Override to augment configuration returned for the workspace/configuration request.

:param params: A ConfigurationItem for which configuration is requested.
:param configuration: The pre-resolved configuration for given params using the settings object or None.

:returns: The resolved configuration for given params.
"""
return configuration

def on_pre_server_command(self, command: ExecuteCommandParams, done_callback: Callable[[], None]) -> bool:
"""
Intercept a command that is about to be sent to the language server.

:param command: The payload containing a "command" and optionally "arguments".
:param done_callback: The callback that you promise to invoke when you return true.

:returns: True if *YOU* will handle this command plugin-side, false otherwise. You must invoke the
passed `done_callback` when you're done.
"""
return False

def on_pre_send_request_async(self, request_id: int, request: Request) -> None:
"""
Notifies about a request that is about to be sent to the language server.
This API is triggered on async thread.

:param request_id: The request ID.
:param request: The request object. The request params can be modified by the plugin.
"""
pass

def on_pre_send_notification_async(self, notification: Notification) -> None:
"""
Notifies about a notification that is about to be sent to the language server.
This API is triggered on async thread.

:param notification: The notification object. The notification params can be modified by the plugin.
"""
pass

def on_server_response_async(self, method: str, response: Response) -> None:
"""
Notifies about a response message that has been received from the language server.
Only successful responses are passed to this method.

:param method: The method of the request.
:param response: The response object to the request. The response.result field can be modified by the
plugin, before it gets further handled by the LSP package.
"""
pass

def on_server_notification_async(self, notification: Notification) -> None:
"""
Notifies about a notification message that has been received from the language server.

:param notification: The notification object.
"""
pass

def on_open_uri_async(self, uri: DocumentUri, callback: Callable[[str, str, str], None]) -> bool:
"""
Called when a language server reports to open an URI. If you know how to handle this URI, then return True and
invoke the passed-in callback some time.

The arguments of the provided callback work as follows:

- The first argument is the title of the view that will be populated with the content of a new scratch view
- The second argument is the content of the view
- The third argument is the syntax to apply for the new view
"""
return False

def on_session_buffer_changed_async(self, session_buffer: SessionBufferProtocol) -> None:
"""
Called when the context of the session buffer has changed or a new buffer was opened.
"""
pass

def on_session_end_async(self, exit_code: int | None, exception: Exception | None) -> None:
"""
Notifies about the session ending (also if the session has crashed). Provides an opportunity to clean up
any stored state or delete references to the session or plugin instance that would otherwise prevent the
instance from being garbage-collected.

If the session hasn't crashed, a shutdown message will be send immediately
after this method returns. In this case exit_code and exception are None.
If the session has crashed, the exit_code and an optional exception are provided.

This API is triggered on async thread.
"""
pass


2 changes: 2 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,8 @@ def m_eslint_status(self, params):
To understand how this works, see the __getattr__ method of the Session class.
"""

API_VERSION: Literal[1] = 1

@classmethod
@abstractmethod
def name(cls) -> str:
Expand Down
Loading
Loading