41 KiB
F# LSP
F# LSP support design proposal. To be expanded as we learn more / settle on things.
Table of contents
- F# LSP
Goals / Requirements
- Fully featured F# LSP server implementation that can be used by any editor
- Can be used as a library (with optional customization) or executable
- Independent of any particular project system - this needs to be handled by the client
- Target LSP protocol version 3.17 (at least initially, we can upgrade as we go)
- Support all useful LSP APIs
- Support for analyzers
- Support for code fixes and refactoring
- Support for mixed F# / C# solutions (being able to delegate to C# LSP server)
- Good performance
- Support for multiple workspaces
- Support for LSIF
- Visual Studio extension that will use the LSP server
Overview
graph TD
VS(VS) -->|VS LSP client| NewVsix("NewVsix (Out-of-process)")
VsCode(VS Code) --> Ionide(Ionide/FSAC)
Vim(Vim) --> Ionide(Ionide/FSAC)
FsLSPServer(FsLSP Server)
NewVsix --> ProjectQuery(Project Query API)
NewVsix --> FsLSP
FsLSPServer --> FsLSP
subgraph FsLSP
ProjectModel(Project Model)
WorkspaceStateManagement(Workspace State Management)
LSPLibrary(LSP Library)
end
FsLSP --> FCS
Ionide --> FsLSPServer
Ionide --> ProjInfo(ProjInfo)
style VS stroke-dasharray: 2 4
style Vim stroke-dasharray: 2 4
style VsCode stroke-dasharray: 2 4
style Ionide stroke-dasharray: 2 4
style ProjInfo stroke-dasharray: 2 4
style LSPLibrary stroke-dasharray: 2 4
style ProjectQuery stroke-dasharray: 2 4
Component | Description | Release Process |
---|---|---|
FCS | F# Compiler Service | Nuget |
FsLSP | The F# LSP library. This can be used to create an LSP server. | Nuget |
- FsLSPServer | The F# LSP server. An executable that can be started and will process LSP calls. Thin wrapper around FsLSP. | with FsLSP |
- Workspace State Management | A system for keeping a "current snapshot" of projects the user is working with | |
- Project Model | A model for a project system we will use since we can't be tied to any particular one | |
- LSP Library | A ready made library we will hopefully find to simplify LSP server development | |
NewVsix | The new VS Extension which will be powered by LSP | with VS |
Components
FsLSP
The library containing the LSP functionality. Can be used to create an LSP server by adding the transport layer.
FsLSPServer
The LSP server executable using STD IN/OUT for transport.
Will be packaged with FsLSP and available as a dotnet tool (?)
It should always target the latest .NET runtime.
Workspace State Management
This part of the library is responsible for keeping track of the current workspace state. The internal workspace model is used to create requests for FCS. It needs to make sure the internal state doesn't get out of sync with what's in the editor and on disk.
Things to keep track of:
- Incoming document changes
- Documents' open/closed status
- Source files on-disk changes for documents that aren't open
- Any on-disk references that could have been rebuilt/updated
Project model
We need some internal model for a workspace/solution with projects. We don't want to use any project system library because we don't need most of the functionality which will be instead handled by the client.
The model needs to be able to quickly absorb incoming changes and easily convert to (or already contain) inputs for FCS. We could consider a reactive/adaptive approach for this.
We should keep track of versions of each project and source file including all dependencies with regards to parsing and type checking.
Example sequence diagram
sequenceDiagram
participant Client
participant FsLSP
participant WorkspaceStateManagement
participant FCS
Client->>FsLSP: textDocument/didChange
FsLSP->>WorkspaceStateManagement: Update workspace state
Client->>+FsLSP: textDocument/diagnostic
FsLSP->>+WorkspaceStateManagement: Get current workspace snapshot
WorkspaceStateManagement-->>-FsLSP: Current workspace snapshot
FsLSP->>+FCS: ParseAndCheckFileInProject
FCS-->>-FsLSP: FSharpCheckFileResults
FsLSP-->>-Client: DocumentDiagnosticReport
Concurrency and cancellation
We need to make sure when a project snapshot is needed to process a request, it will always contain all change notifications that were received before. There shouldn't be need for a global request queue, possibly only for change notifications inside WorkspaceStateManagement. And any language requests will wait until WorkspaceStateManagement has absorbed all previous changes.
For cancellation we should decide, potentially on a case-by-case basis what to do when an incoming change notification invalidates an ongoing request.
- Do nothing and let the request run either until it completes or is cancelled by the client. This is the easiest so we will probably start with this.
- Cancel the request and return "cancelled" result to the client. The client then needs to deal with it.
- Internally restart the work updated inputs and complete the original request until cancelled. This mode might save a bunch of back-and-forth with the client when user is typing. But it requires the client to expect it and not send cancellations a re-requests for the same thing.
Request update example
sequenceDiagram
participant Client
participant FsLSP
participant WorkspaceStateManagement
participant FCS
Client->>+FsLSP: textDocument/diagnostic
FsLSP->>+WorkspaceStateManagement: Get current workspace snapshot
WorkspaceStateManagement-->>-FsLSP: Current workspace snapshot
FsLSP->>FCS: ParseAndCheckFileInProject
activate FCS
Client->>FsLSP: textDocument/didChange
FsLSP->>FCS: Cancel ParseAndCheckFileInProject
deactivate FCS
FsLSP->>WorkspaceStateManagement: Update workspace state
FsLSP->>+WorkspaceStateManagement: Get current workspace snapshot
WorkspaceStateManagement-->>-FsLSP: Current workspace snapshot
FsLSP->>+FCS: ParseAndCheckFileInProject
FCS-->>-FsLSP: FSharpCheckFileResults
FsLSP-->>-Client: DocumentDiagnosticReport
Request tracking
This will hopefully be handled by the LSP library. Although we might need to be able to keep track of requests in progress for the purpose of cancellation.
Configuration updates
We should be able to apply configuration changes that require re-creating the FCS checker. We should be able to keep the caches since the results shouldn't depend on checker settings (rather only the input Project Snapshots).
Extensibility
The FsLSP library should be extensible by adding custom endpoints or overriding existing ones.
It should also allow to hook into background processes or events to customize behavior.
The code for extending the behavior should look the same as the code for implementing the core functionality. That way it's easy to test out and then potentially contribute it directly to the library.
Release process
We should use a similar release process as we have now for FCS. Potentially also provide a pre-release package with the latest version from main
.
NewVsix
New VS extension that will be backed by the LSP server. It should mostly contain plumbing to connect LSP to the editor and CPS.
We should build on new VisualStudio.Extensibility model to run the extension out-of-process. It can also directly connect native VS LSP directly to our server which we can host directly in the extension.
We might need a wrapper C# project because the new extensibility model relies on source generation.
We should be able to load project information via ProjectQuery API to which we can also subscribe for changes.
The new extension should be able to run side by side with the current and extension gradually take over the functionality we will add to LSP.
If there is any missing functionality in any of these, we might need to supplement it from our current MEF extension and connect from it to the new one.
FCS
The only strict requirement on FCS is to be able to process open files from memory and non-open files from filesystem. But probably best to abstract that within FsLSP and just provide ISourceText
objects for both cases.
At the moment this is possible via the experimental DocumentSource.Custom
parameter. But hopefully we will be able to use a new FSharpProjectSnapshot
model (from #15179) which combines all the necessary inputs into a single object.
LSP library
The library needs to support all the features we want to implement including the ability to add custom F# ones. It also shouldn't incur an unnecessary performance overhead.
Candidates
Library | Pros | Cons |
---|---|---|
We write our own | + Complete control over everything | - A lot of extra work |
Ionide LanguageServerProtocol | + Battle tested with Ionide and FSAC + Natural F# API | - Might not be able to fully customize it to our need |
Roslyn CLaSP | + Should be performant + Future support | - Not stable API - Unnatural C# API |
OmniSharp C# LSP | - Unnatural C# API |
It probably makes sense to start with an existing library which should be able to get us to a working product and only then consider writing our own in case it's the only way to enable more functionality or performance.
LSP client (?)
- Is there any code that can be shared by various clients?
- Most likely not, since each client will be specific to its platform and project system
Diagnostic mode
Initially we will be offering the Pull Diagnostics mode where the client will request diagnostics for a particular document. It will be up to the client to ask for diagnostics for documents at appropriate times and cancel requests that are no longer needed. The client can also use this to implement background checking of all documents in the workspace.
Later we can also add an option to have the server control diagnostics and publish them via Publish Diagnostics. Then it will be pushing diagnostics as they are discovered either for active document, open documents or all documents.
LSIF
We should eventually add support for LSIF. At the moment it looks like we would have to construct it manually (as in, no helpful libraries are available).
This is probably lower priority than anything else.
It might also be a good opportunity to re-think how we store symbols for purposes of finding references, go to definition, or code search (go to symbol in workspace by name/query).
Performance
Areas to pay attention to with regards to performance
- Serialization. Hopefully this will be handled reasonably by a library.
- Spamming too many requests. Though this will be mainly up to the client, we could think about throttling or debouncing.
- Caching. This will be mostly up to FCS but we can send hints about what can be cleaned up.
Analyzers & Refactoring support
These will hopefully not require any special treatment and can just use existing LSP APIs.
Mixed F# / C# solutions
We need to figure out how to talk to C# LSP server for:
- Go to definition
- Find references / rename
- (Make sure these work both ways)
Multiple workspace support
We should be able to use a single instance of LSP server even for multiple workspaces. We can keep a separate instance of WorkspaceStateManagement
for each workspace but send language requests to a single instance of FCS checker. This way it's possible to reuse some results if the workspaces share the same projects (with the same options).
Action plan
A rough draft of how to plan the work and which items can be done in parallel.
Phase 0
We need to investigate which system we'll use for the New Vsix. Ideally we're able to go with new VisualStudio.Extensibility but if that has any blockers we might need to stick to in-process MEF.
We can prototype the New Vsix with some sample LSP server or maybe with an existing one like FSAC. Just to figure out how to connect it to F# projects in VS.
For FsLSP we need to choose a library and implement a dummy LSP server with it that we can connect to the New Vsix and get some sort of basic setup that can then be improved upon.
gantt
axisFormat .
section FsLSP
Choose LSP library :fs1, 1900-01-01, 1d
Add projects for FsLSP and FsLSPServer :fs2, after fs1, 1d
Implement dummy LSP server using the library :fs3, after fs2, 1d
Connect to NewVsix :after vsix2 fs3, 1d
section Vsix
Investigate options for NewVsix :vsix1, 1900-01-01, 1d
Implement NewVsix prototype with dummy or some existing LSP server :vsix2, after vsix1, 2d
Phase 1
Get some sort of working prototype that can do semantic highlighting and show diagnostics.
Figure out how to run side-by-side and gradually take over functionality from the current Vsix, if possible.
gantt
axisFormat .
section FsLSP
Implement WorkspaceStateManagement :fs1, 1900-01-01, 3d
Implement Phase 1 endpoints :fs2, after fs1, 2d
section Vsix
Investigate running side-by-side with old Vsix :vsix1, 1900-01-01, 1d
Implement configuration of which features are handled by which Vsix :vsix2, after vsix1, 2d
Phase 2 - 5
Implement endpoints based on their assigned priority. See next section.
Independently we can start on setting up the build & release process for FsLSP and insertions of NewVsix into VS.
Another potentially parallel track can be figuring out interop with C# LSP server for mixed solutions.
gantt
axisFormat .
section FsLSP
Implement Phase 2 endpoints :fs1, 1900-01-01, 1d
Implement Phase 3 endpoints :fs2, after fs1, 1d
Implement Phase 4 endpoints :fs3, after fs2, 1d
Implement Phase 5 endpoints :fs4, after fs3, 1d
section Distribution
Settle on release process :d1, 1900-01-01, 1d
Add / update build pipelines :d2, after d1, 1d
Set up VS insertions :d3, after d2, 1d
section Cs interop
Figure out how to talk to CSharp LSP server :c1, 1900-01-01, 4d
LSP Endpoints
This is a preliminary list of LSP APIs we want (or don't want) to implement, roughly ordered by priority. We will probably re-classify some of them as we go.
Name | Type | Direction | Category | Priority / Dev phase | Supported by FSAC | Note |
---|---|---|---|---|---|---|
initialize |
Request | ↩️ | Lifecycle | 0 | ✅ | |
initialized |
Notification | ➡️ | Lifecycle | 0 | ||
textDocument/didChange |
Notification | ➡️ | Document synchronization | 1 | ✅ | |
textDocument/didClose |
Notification | ➡️ | Document synchronization | 1 | ||
textDocument/didOpen |
Notification | ➡️ | Document synchronization | 1 | ✅ | |
textDocument/diagnostic |
Request | ↩️ | Language features | 1 | ||
textDocument/semanticTokens/full |
Request | ↩️ | Language features | 1 | ||
$/logTrace |
Notification | ⬅️ | Lifecycle | 1 | ||
$/setTrace |
Notification | ➡️ | Lifecycle | 1 | ||
exit |
Notification | ➡️ | Lifecycle | 1 | ||
shutdown |
Request | ↩️ | Lifecycle | 1 | ||
completionItem/resolve |
Request | ↩️ | Language features | 2 | ✅ | |
textDocument/completion |
Request | ↩️ | Language features | 2 | ✅ | |
textDocument/declaration |
Request | ↩️ | Language features | 2 | ||
textDocument/definition |
Request | ↩️ | Language features | 2 | ✅ | |
textDocument/hover |
Request | ↩️ | Language features | 2 | ✅ | |
textDocument/implementation |
Request | ↩️ | Language features | 2 | ✅ | |
textDocument/references |
Request | ↩️ | Language features | 2 | ✅ | |
textDocument/semanticTokens/full/delta |
Request | ↩️ | Language features | 2 | ||
textDocument/semanticTokens/range |
Request | ↩️ | Language features | 2 | ||
textDocument/typeDefinition |
Request | ↩️ | Language features | 2 | ✅ | |
workspace/semanticTokens/refresh |
Request | ↩️ | Language features | 2 | ✅ | |
fsharp/compilerLocation |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/documentAnalyzed |
Notification | ⬅️ | F# Custom | 3 | ✅ | |
fsharp/documentation |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/documentationGenerator |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/documentationSymbol |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/dotnetaddproject |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/dotnetaddsln |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/dotnetnewlist |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/dotnetnewrun |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/dotnetremoveproject |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/fileParsed |
Notification | ⬅️ | F# Custom | 3 | ✅ | |
fsharp/lineLens |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/notifyCancel |
Notification | ⬅️ | F# Custom | 3 | ✅ | |
fsharp/notifyWorkspace |
Notification | ⬅️ | F# Custom | 3 | ✅ | |
fsharp/notifyWorkspacePeek |
Notification | ⬅️ | F# Custom | 3 | ✅ | |
fsharp/pipelineHint |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/project |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/signature |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/signatureData |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/testDetected |
Notification | ⬅️ | F# Custom | 3 | ✅ | |
fsharp/workspaceLoad |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsharp/workspacePeek |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsproj/addExistingFile |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsproj/addFile |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsproj/addFileAbove |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsproj/addFileBelow |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsproj/moveFileDown |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsproj/moveFileUp |
Request | ↩️ | F# Custom | 3 | ✅ | |
fsproj/removeFile |
Request | ↩️ | F# Custom | 3 | ✅ | |
textDocument/documentHighlight |
Request | ↩️ | Language features | 3 | ✅ | |
textDocument/documentSymbol |
Request | ↩️ | Language features | 3 | ✅ | |
textDocument/formatting |
Request | ↩️ | Language features | 3 | ✅ | |
textDocument/rename |
Request | ↩️ | Language features | 3 | ✅ | |
textDocument/signatureHelp |
Request | ↩️ | Language features | 3 | ✅ | |
$/progress |
Notification | ⬅️ | Window Features | 3 | ✅ | Would be nice to have for long running operations |
workspace/didChangeWatchedFiles |
Notification | ➡️ | Workspace Features | 3 | ✅ | |
textDocument/willSave |
Notification | ➡️ | Document synchronization | 4 | ||
textDocument/willSaveWaitUntil |
Request | ↩️ | Document synchronization | 4 | ||
codeAction/resolve |
Request | ↩️ | Language features | 4 | ||
codeLens/resolve |
Request | ↩️ | Language features | 4 | ✅ | |
inlayHint/resolve |
Request | ↩️ | Language features | 4 | ||
textDocument/codeAction |
Request | ↩️ | Language features | 4 | ✅ | |
textDocument/codeLens |
Request | ↩️ | Language features | 4 | ✅ | |
textDocument/prepareRename |
Request | ↩️ | Language features | 4 | ||
textDocument/publishDiagnostics |
Notification | ⬅️ | Language features | 4 | ✅ | |
workspace/codeLens/refresh |
Request | ↪️ | Language Features | 4 | ✅ | |
window/showMessage |
Notification | ⬅️ | Window Features | 4 | ✅ | |
workspace/configuration |
Request | ↪️ | Workspace Features | 4 | ||
workspace/diagnostic |
Request | ↩️ | Workspace Features | 4 | ||
workspace/didChangeConfiguration |
Notification | ➡️ | Workspace Features | 4 | ✅ | |
workspace/symbol |
Request | ↩️ | Workspace Features | 4 | ✅ | |
workspaceSymbol/resolve |
Request | ↩️ | Workspace Features | 4 | ||
notebookDocument/didChange |
Notification | ➡️ | Notebook Document synchronization | 5 | ||
notebookDocument/didClose |
Notification | ➡️ | Notebook Document synchronization | 5 | ||
notebookDocument/didOpen |
Notification | ➡️ | Notebook Document synchronization | 5 | ||
notebookDocument/didSave |
Notification | ➡️ | Notebook Document synchronization | 5 | ||
fsharp/f1Help |
Request | ↩️ | F# Custom | ❔ | ✅ | |
fsharp/fsdn |
Request | ↩️ | F# Custom | ❔ | ✅ | |
fsharp/loadAnalyzers |
Request | ↩️ | F# Custom | ❔ | ✅ | |
callHierarchy/incomingCalls |
Request | ↩️ | Language features | ❔ | ||
callHierarchy/outgoingCalls |
Request | ↩️ | Language features | ❔ | ||
documentLink/resolve |
Request | ↩️ | Language features | ❔ | ||
textDocument/documentLink |
Request | ↩️ | Language features | ❔ | Is this useful for us? | |
textDocument/inlayHint |
Request | ↩️ | Language features | ❔ | ||
textDocument/inlineValue |
Request | ↩️ | Language features | ❔ | ||
textDocument/moniker |
Request | ↩️ | Language features | ❔ | ||
textDocument/prepareCallHierarchy |
Request | ↩️ | Language features | ❔ | ||
textDocument/prepareTypeHierarchy |
Request | ↩️ | Language features | ❔ | ||
textDocument/rangeFormatting |
Request | ↩️ | Language features | ❔ | ||
textDocument/selectionRange |
Request | ↩️ | Language features | ❔ | ||
typeHierarchy/subtypes |
Request | ↩️ | Language features | ❔ | ||
typeHierarchy/supertypes |
Request | ↩️ | Language features | ❔ | ||
client/registerCapability |
Request | ↩️ | Lifecycle | ❔ | ||
client/unregisterCapability |
Request | ↩️ | Lifecycle | ❔ | ||
telemetry/event |
Notification | ⬅️ | Window Features | ❔ | ✅ | |
window/logMessage |
Notification | ⬅️ | Window Features | ❔ | ✅ | |
window/workDoneProgress/cancel |
Notification | ⬅️ | Window Features | ❔ | ||
window/workDoneProgress/create |
Request | ↩️ | Window Features | ❔ | ||
workDoneProgress/cancel |
Notification | ➡️ | Window Features | ❔ | ||
workDoneProgress/create |
Request | ⬅️ | Window Features | ❔ | ||
workspace/applyEdit |
Request | ↪️ | Workspace Features | ❔ | ✅ | Could be used for refactoring? |
workspace/diagnostic/refresh |
Request | ↪️ | Workspace Features | ❔ | ||
workspace/executeCommand |
Request | ↩️ | Workspace Features | ❔ | Could be used for refactoring? | |
textDocument/didSave |
Notification | ➡️ | Document synchronization | ❌ | ✅ | Probably don't need this? |
textDocument/colorPresentation |
Request | ↩️ | Language features | ❌ | ||
textDocument/documentColor |
Request | ↩️ | Language features | ❌ | ||
textDocument/linkedEditingRange |
Request | ↩️ | Language features | ❌ | ||
textDocument/onTypeFormatting |
Request | ↩️ | Language features | ❌ | ||
window/showDocument |
Request | ↪️ | Window Features | ❌ | ||
window/showMessageRequest |
Request | ↪️ | Window Features | ❌ | ||
workspace/didChangeWorkspaceFolders |
Notification | ➡️ | Workspace Features | ❌ | ||
workspace/didCreateFiles |
Notification | ➡️ | Workspace Features | ❌ | ||
workspace/didDeleteFiles |
Notification | ➡️ | Workspace Features | ❌ | ||
workspace/didRenameFiles |
Notification | ➡️ | Workspace Features | ❌ | ||
workspace/willCreateFiles |
Notification | ➡️ | Workspace Features | ❌ | ||
workspace/willDeleteFiles |
Notification | ➡️ | Workspace Features | ❌ | ||
workspace/willRenameFiles |
Notification | ➡️ | Workspace Features | ❌ | ||
workspace/workspaceFolders |
Request | ⬅️ | Workspace Features | ❌ | ||