flutter/migrate-das-to-lsp
Guide for converting legacy Dart Analysis Server (DAS) feature implementations to JetBrains LSP in the Dart IntelliJ plugin.
npx skills add https://github.com/flutter/dart-intellij-third-party --skill migrate-das-to-lsp
This skill provides comprehensive architectural context, guidelines, and step-by-step instructions for converting legacy Dart Analysis Server (DAS) features (such as hover, navigation, diagnostics, code completion, etc.) over to the JetBrains LSP integration.
The Dart IntelliJ plugin operates under a hybrid architecture during the transition from custom DAS protocols to the Language Server Protocol (LSP):
graph TD
Client[IntelliJ IDEA / IDE Frontend]
Legacy[Legacy Dart Providers e.g. DartDocumentationProvider]
LspClient[JetBrains LSP Framework com.intellij.platform.dartlsp]
BridgeMgr[DartBridgeLspServerManager]
Bridge[DartBridgeLspServer In-Process TCP Socket]
DAS[Dart Analysis Server External Process]
Client -->|Experimental LSP Off| Legacy
Legacy -->|Custom DAS API| DAS
Client -->|Experimental LSP On| LspClient
LspClient -->|TCP Socket| BridgeMgr
BridgeMgr -->|Routes Requests| Bridge
Bridge -->|lsp.handle JSON-RPC| DAS
JetBrains' native LSP client expects to connect to a socket or standard I/O stream speaking JSON-RPC 2.0. Because the Dart Analysis Server (DAS) is already running as a standalone process managed by DartAnalysisServerService, we do not launch a second LSP server process.
Instead, DartBridgeLspServerManager hosts a lightweight in-process TCP socket bridge (DartBridgeLspServer). When the JetBrains LSP framework sends an LSP request (e.g., textDocument/hover), DartBridgeLspServer wraps the LSP payload into a custom DAS request named lsp.handle ({ "lspMessage": <jsonrpc payload> }), forwards it to DAS, and unpacks the returned lspResponse back to the JetBrains LSP framework.
> [!IMPORTANT]
> Copied JetBrains LSP Client Sources (com.intellij.platform.dartlsp)
> To support IntelliJ Platform versions 2025.3 and 2026.1 before JetBrains' official Ultimate LSP client becomes open source in Community Edition, the platform LSP framework was copied into third_party/thirdPartySrc/platform-lsp/ under the namespace com.intellij.platform.dartlsp.
When working with or modifying LSP features, adhere to these rules regarding copied platform code:
third_party/thirdPartySrc/platform-lsp/. We anticipate dropping this directory entirely and migrating to JetBrains' official open-source platform code in the future.ProjectFileIndex.isInContent check in LspServerImpl.kt so external library files in .pub-cache or dart:io receive LSP hovers), you must codify the fix inside .agents/skills/patch-copied-lsp-sources/scripts/patch.py. This ensures the change persists automatically whenever upstream LSP sources are re-synchronized.intellij-community so the future transition is seamless.Standard LSP servers rely on textDocument/didOpen, didChange, and didClose notifications to maintain file state. However, in our architecture, document synchronization is already handled globally and synchronously by the legacy DartAnalysisServerService (das.updateFilesContent()).
Therefore, DartBridgeLspServer intentionally ignores LSP didOpen/didChange payloads. However, the JetBrains frontend LSP client must still register files as "opened" internally (via LspOpenedFilesService and openForOpenedOrUnsavedFiles()) so UI features like quick documentation target providers know an active server exists for the file.
Switching from a custom legacy DAS UI provider to standard JetBrains LSP features can sometimes result in subtle behavioral changes or minor loss of custom functionality (for example, custom interactive buttons or specialized text formatting present in legacy tooltips that aren't natively supported by standard LSP Markdown renderings).
bin/main.dart), whereas legacy DAS hover worked on external files in .pub-cache and Dart SDK libraries (dart:io). This occurred because upstream JetBrains hardcoded an isInContent check. Always actively test edge cases—such as external library files, injected code fragments, and scratch files—when converting a feature.Before writing any code to replace an analysis feature with LSP, perform a thorough baseline evaluation:
com.jetbrains.lang.dart.*, which specific DartAnalysisServerService methods are invoked, how the DAS response is parsed, and how the UI element is constructed.When migrating a legacy DAS feature (e.g., Go To Definition, Rename, Diagnostics) to LSP, follow this systematic workflow:
Locate the existing IntelliJ provider implementing the feature using DAS (e.g., DartDocumentationProvider.java or DartRenameHandler.java).
Gate the legacy logic behind the experimental LSP feature flag so that when experimental LSP is enabled, the legacy provider yields control (returns null or false):
if (DartConfigurable.isExperimentalLspFeaturesEnabled(element.getProject())) {
return null; // Let JetBrains native LSP client handle this request
}
Open DartLspServerDescriptor.kt and update lspCustomization to enable the feature when the setting is toggled on:
override val hoverCustomizer: LspHoverCustomizer
get() = if (DartConfigurable.isExperimentalLspFeaturesEnabled(project)) {
LspHoverSupport()
} else {
LspHoverDisabled
}
*(Note: Replace hoverCustomizer / LspHoverSupport with the appropriate customizer property for your feature, such as goToDefinitionCustomizer, renameCustomizer, etc.)*
Open DartBridgeLspServer.kt and update the initialize() method to declare support for the feature in ServerCapabilities:
override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> {
val capabilities = ServerCapabilities().apply {
setHoverProvider(true)
// Set your feature capability here (e.g., setDefinitionProvider(true))
}
return CompletableFuture.completedFuture(InitializeResult(capabilities))
}
DartBridgeLspServerImplement or override the corresponding LSP4J service method in DartBridgeLspServer.kt and forward it via forwardRequest():
override fun hover(params: HoverParams): CompletableFuture<Hover> {
return forwardRequest("textDocument/hover", params, Hover::class.java)
}
forwardRequest() automatically packages the parameters into an lsp.handle JSON-RPC request and registers a pending CompletableFuture. When DAS responds asynchronously, handleDasResponse() resolves the future.
Always verify changes in a clean sandbox IDE instance:
./gradlew clean prepareSandbox --no-build-cache
Test the feature both on files that are open during IDE initial startup, and on files opened dynamically after startup (including external dependencies in .pub-cache or dart:io).
Verify that toggling Settings | Languages & Frameworks | Dart | Enable Experimental LSP features cleanly stops and restarts the bridge server without leaving hanging sockets or corrupted state.
Run ./gradlew verifyPlugin. If verification baselines change, run third_party/tool/update_baselines.sh to ensure copied LSP client issues remain filtered out.
Take flutter/migrate-das-to-lsp from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.