Skip to content

Add support to open URI schemes with server-provided content#2890

Open
jwortmann wants to merge 11 commits into
sublimelsp:mainfrom
jwortmann:textDocumentContent
Open

Add support to open URI schemes with server-provided content#2890
jwortmann wants to merge 11 commits into
sublimelsp:mainfrom
jwortmann:textDocumentContent

Conversation

@jwortmann
Copy link
Copy Markdown
Member

@jwortmann jwortmann commented Apr 30, 2026

LSP specs: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#workspace_textDocumentContent

It looks like support for this was recently added into roslyn-language-server for the roslyn-source-generated URI scheme, but as far as I can tell it hasn't landed in a release yet. Or at least the language server installed with

dotnet tool install --global roslyn-language-server --prerelease

doesn't seem to register anything for that method. Therefore untested and draft PR for now.

Edit: According to https://www.nuget.org/packages/roslyn-language-server/#versions-body-tab the last release was 3 months ago, but the functionality was added only 3 weeks ago. So we'll have to wait for a new release first, if we want to test it with that server.


Sidenote:

Not sure whether this URI scheme (or in general server-provided content for custom URI schemes) is also relevant for links in hover popups, but perhaps we should refactor the code from

LSP/plugin/hover.py

Lines 314 to 345 in 17b2e67

def _on_navigate(self, uri: str) -> None:
scheme = parse_uri(uri)[0]
if scheme == 'subl':
pass
elif scheme == 'file':
if window := self.view.window():
open_file_uri(window, uri)
elif scheme == CODE_ACTION_SCHEME:
session_name, version, action = decode_code_action_uri(uri)
if version == self.view.change_count() and (session := self.session_by_name(session_name)):
sublime.set_timeout_async(lambda: session.run_code_action_async(action, progress=True, view=self.view))
self.view.hide_popup()
elif uri == "quick-panel:DocumentLink":
if window := self.view.window():
targets = [link["target"] for link in self._document_links] # pyright: ignore
def on_select(targets: list[str], idx: int) -> None:
if idx > -1:
self._on_navigate(targets[idx])
window.show_quick_panel(
[parse_uri(target)[1] for target in targets], partial(on_select, targets), placeholder="Open Link")
elif is_location_href(uri):
session_name, uri, row, col_utf16 = unpack_href_location(uri)
if session := self.session_by_name(session_name):
position: Position = {"line": row, "character": col_utf16}
r: Range = {"start": position, "end": position}
sublime.set_timeout_async(partial(session.open_uri_async, uri, r))
elif scheme.lower() in {"http", "https"} or (not scheme and uri.startswith('www.')):
open_in_browser(uri)
elif scheme:
sublime.set_timeout_async(partial(self.try_open_custom_uri_async, uri))

for links in hover popups to call Session.open_uri_async instead, in order to reduce the code duplication. Then some parts which are currently specific to links in hover popups would need to be moved into Session.open_uri_async, and it would also need to pass the view as an argument. Although there could be multiple sessions running for the view, with the hover content merged from the different hoverProviders, so it's not clear which session to pick, or how to delegate the links to the correct session. Maybe the URI opening could alternatively refactored into a free function, possibly with the session(s) as another argument.
But we could look into that later and do it in a separate PR. Maybe we could also encode the target for DocumentLinks directly into the URI (instead of using quick-panel:DocumentLink URI), similar to the encoded code actions in

LSP/plugin/core/url.py

Lines 113 to 114 in 17b2e67

def encode_code_action_uri(session_name: str, version: int, action: Command | CodeAction) -> URI:
return f'{CODE_ACTION_SCHEME}:{session_name}/{version}/{urlsafe_b64encode(json.dumps(action).encode()).decode()}'

or even handle that differently (maybe just take the first valid response instead of listing the links in a quick panel if there are links from multiple sessions).

@jwortmann
Copy link
Copy Markdown
Member Author

So it looks like a new version of the roslyn server was published, and I can see in the initialize response that it registers the roslyn-source-generated scheme for that method.

Unfortunately I have no idea how to make the server emit URIs with that scheme. Perhaps anyone who knows more about C# could help me with a simple example how to trigger this?

Comment thread plugin/core/sessions.py
return Promise.resolve(None)
title = urlparse(uri).path.split('/')[-1]
content = response['text'].replace('\r', '')
syntax = ''
Copy link
Copy Markdown
Member Author

@jwortmann jwortmann May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to microsoft/language-server-protocol#1994 (comment) the client is responsible to determine the language/syntax for the buffer. The only information that we have are the URI and the content, but I see no generic way to determine it just from the URI if it is for example something like generated-source:aca9a9c1-a993-4ae4-b5c0-1864392b4630.

Another comment at microsoft/language-server-protocol#1994 (comment) suggests that the editor extension (in VSCode) sets the language ID for a given URI scheme. So we could add this as a new config key that maps URI schemes to syntax names (Window.new_file takes the syntax name as a string). It seems that using scope:source.xxx also works, but is somewhat buggy, because the syntax name in the bottom right corner is bugged then (sublimehq/sublime_text#4449). But perhaps we could use sublime.find_syntax_by_scope API for that. So maybe it should be a URI scheme -> scope mapping in the config instead.

I still think that it is a mistake in the spec design to do it that way, because the language server should easily know the proper language ID for the document content that itself has generated. So it doesn't make much sense to me why it was designed how it is, but frankly we can't do anything about that.

@jwortmann jwortmann marked this pull request as ready for review May 20, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant