Skip to content

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

Merged
jwortmann merged 13 commits into
sublimelsp:mainfrom
jwortmann:textDocumentContent
May 23, 2026
Merged

jwortmann merged 13 commits into
sublimelsp:mainfrom
jwortmann:textDocumentContent

Conversation

@jwortmann

@jwortmann jwortmann commented Apr 30, 2026 •

Copy link
Copy Markdown
Member

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 = ''

@jwortmann jwortmann May 20, 2026 •

Copy link
Copy Markdown
Member Author

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
Comment thread plugin/core/sessions.py Resolved
Comment thread plugin/core/sessions.py Resolved
@netlify

netlify Bot commented May 23, 2026

Copy link
Copy Markdown

βœ… Deploy Preview for sublime-lsp ready!

Name Link
πŸ”¨ Latest commit cb1f1f3
πŸ” Latest deploy log https://app.netlify.com/projects/sublime-lsp/deploys/6a11ef418b913d00081a4aee
😎 Deploy Preview https://deploy-preview-2890--sublime-lsp.netlify.app
πŸ“± Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@rchl rchl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not testing this (I can't get dotnet to work on mac) but looks good to me.

@jwortmann

jwortmann commented May 23, 2026 •

Copy link
Copy Markdown
Member Author

I have tried to test this using the repo from the link in neovim/neovim#39592 (comment), but I have no idea how to make the server create URIs with that roslyn-source-generated scheme.

But I think if anything from this PR doesn't work, we will find out sooner or later.


As mentioned above we might want to also add a config option (or some other strategy) to let LSP know the appropriate syntax for given URI schemes, but we can probably add that at some later time, when we have a practical use case for it or when more servers add support for this feature.

@jwortmann
jwortmann merged commit 7981a13 into sublimelsp:main May 23, 2026
11 of 12 checks passed
@jwortmann
jwortmann deleted the textDocumentContent branch May 23, 2026 20:44
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.

2 participants