diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/AbstractDartAnalysisClient.java b/Dart/src/com/jetbrains/lang/dart/analyzer/AbstractDartAnalysisClient.java new file mode 100644 index 00000000000..625b353d1b9 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/AbstractDartAnalysisClient.java @@ -0,0 +1,354 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.*; +import com.google.dart.server.internal.remote.RemoteAnalysisServerImpl; +import com.google.gson.JsonObject; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import org.dartlang.analysis.server.protocol.ImportedElements; +import org.dartlang.analysis.server.protocol.RefactoringOptions; +import org.dartlang.analysis.server.protocol.RequestError; +import org.dartlang.analysis.server.protocol.RuntimeCompletionExpression; +import org.dartlang.analysis.server.protocol.RuntimeCompletionVariable; +import org.dartlang.analysis.server.protocol.SourceEdit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +abstract class AbstractDartAnalysisClient implements DartAnalysisClient { + protected final @NotNull Project myProject; + protected final @NotNull RemoteAnalysisServerImpl myServer; + private final @NotNull LspTransport myLspTransport; + + protected AbstractDartAnalysisClient(@NotNull Project project, + @NotNull AnalysisServerSocket socket, + @NotNull Function lspTransportFactory) { + myProject = project; + myServer = new DartAnalysisServerImpl(project, socket); + myLspTransport = lspTransportFactory.apply(myServer); + } + + @Override + public void start() throws Exception { + myServer.start(); + } + + @Override + public boolean isSocketOpen() { + return myServer.isSocketOpen(); + } + + @Override + public void shutdown() { + try { + myServer.server_shutdown(); + } + finally { + myLspTransport.dispose(); + } + } + + @Override + public void addAnalysisServerListener(@NotNull AnalysisServerListener listener) { + myServer.addAnalysisServerListener(listener); + } + + @Override + public void removeAnalysisServerListener(@NotNull AnalysisServerListener listener) { + myServer.removeAnalysisServerListener(listener); + } + + @Override + public void addRequestListener(@NotNull RequestListener listener) { + myServer.addRequestListener(listener); + } + + @Override + public void removeRequestListener(@NotNull RequestListener listener) { + myServer.removeRequestListener(listener); + } + + @Override + public void addResponseListener(@NotNull ResponseListener listener) { + myServer.addResponseListener(listener); + } + + @Override + public void removeResponseListener(@NotNull ResponseListener listener) { + myServer.removeResponseListener(listener); + } + + @Override + public void addStatusListener(@NotNull AnalysisServerStatusListener listener) { + myServer.addStatusListener(listener); + } + + @Override + public void completion_setSubscriptions(@NotNull List subscriptions) { + myServer.completion_setSubscriptions(subscriptions); + } + + @Override + public void analysis_updateContent(@NotNull Map files, @NotNull UpdateContentConsumer consumer) { + myLspTransport.updateContent(files, consumer); + } + + @Override + public void analysis_setAnalysisRoots(@NotNull List included, @NotNull List excluded, Map packageRoots) { + myServer.analysis_setAnalysisRoots(included, excluded, packageRoots); + } + + @Override + public void analysis_getHover(@NotNull String file, int offset, @NotNull GetHoverConsumer consumer) { + myServer.analysis_getHover(file, offset, consumer); + } + + @Override + public void analysis_getNavigation(@NotNull String file, int offset, int length, @NotNull GetNavigationConsumer consumer) { + myServer.analysis_getNavigation(file, offset, length, consumer); + } + + @Override + public void edit_getAssists(@NotNull String file, int offset, int length, @NotNull GetAssistsConsumer consumer) { + myServer.edit_getAssists(file, offset, length, consumer); + } + + @Override + public void edit_isPostfixCompletionApplicable(@NotNull String file, + @NotNull String key, + int offset, + @NotNull IsPostfixCompletionApplicableConsumer consumer) { + myServer.edit_isPostfixCompletionApplicable(file, key, offset, consumer); + } + + @Override + public void edit_listPostfixCompletionTemplates(@NotNull ListPostfixCompletionTemplatesConsumer consumer) { + myServer.edit_listPostfixCompletionTemplates(consumer); + } + + @Override + public void edit_getPostfixCompletion(@NotNull String file, + @NotNull String key, + int offset, + @NotNull GetPostfixCompletionConsumer consumer) { + myServer.edit_getPostfixCompletion(file, key, offset, consumer); + } + + @Override + public void edit_getStatementCompletion(@NotNull String file, int offset, @NotNull GetStatementCompletionConsumer consumer) { + myServer.edit_getStatementCompletion(file, offset, consumer); + } + + @Override + public void diagnostic_getServerPort(@NotNull GetServerPortConsumer consumer) { + myLspTransport.diagnosticServer(consumer); + } + + @Override + public void edit_getFixes(@NotNull String file, int offset, @NotNull GetFixesConsumer consumer) { + myServer.edit_getFixes(file, offset, consumer); + } + + @Override + public void search_findElementReferences(@NotNull String file, + int offset, + boolean includePotential, + @NotNull FindElementReferencesConsumer consumer) { + myServer.search_findElementReferences(file, offset, includePotential, consumer); + } + + @Override + public void search_getTypeHierarchy(@NotNull String file, int offset, boolean superOnly, @NotNull GetTypeHierarchyConsumer consumer) { + myServer.search_getTypeHierarchy(file, offset, superOnly, consumer); + } + + @Override + public void completion_getSuggestionDetails(@NotNull String file, + int id, + @NotNull String label, + int offset, + @NotNull GetSuggestionDetailsConsumer consumer) { + myServer.completion_getSuggestionDetails(file, id, label, offset, consumer); + } + + @Override + public void completion_getSuggestionDetails2(@NotNull String file, + int offset, + @NotNull String completion, + @NotNull String libraryUri, + @NotNull GetSuggestionDetailsConsumer2 consumer) { + myServer.completion_getSuggestionDetails2(file, offset, completion, libraryUri, consumer); + } + + @Override + public void completion_getSuggestions(@NotNull String file, int offset, @NotNull GetSuggestionsConsumer consumer) { + myServer.completion_getSuggestions(file, offset, consumer); + } + + @Override + public void completion_getSuggestions2(@NotNull String file, + int offset, + int maxResults, + @NotNull String completionCaseMatchingMode, + @NotNull String completionMode, + int invocationCount, + int timeout, + @NotNull GetSuggestionsConsumer2 consumer) { + myServer.completion_getSuggestions2(file, offset, maxResults, completionCaseMatchingMode, completionMode, invocationCount, timeout, consumer); + } + + @Override + public void edit_format(@NotNull String file, + int selectionOffset, + int selectionLength, + int lineLength, + @NotNull FormatConsumer consumer) { + String fileUri = VirtualFileManager.constructUrl("file", file); + String fileContent = readFileContent(file); + if (fileContent != null) { + myLspTransport.format(fileUri, fileContent, selectionOffset, selectionLength, lineLength, new LspTransport.FormatHandler() { + @Override + public void computedFormat(@NotNull List edits, int selOffset, int selLen) { + consumer.computedFormat(edits, selOffset, selLen); + } + + @Override + public void onError(@NotNull RequestError requestError) { + consumer.onError(requestError); + } + }); + } + else { + myServer.edit_format(file, selectionOffset, selectionLength, lineLength, consumer); + } + } + + @Override + public void analysis_getImportedElements(@NotNull String file, int offset, int length, @NotNull GetImportedElementsConsumer consumer) { + myServer.analysis_getImportedElements(file, offset, length, consumer); + } + + @Override + public void edit_importElements(@NotNull String file, + @NotNull List elements, + int offset, + @NotNull ImportElementsConsumer consumer) { + myServer.edit_importElements(file, elements, offset, consumer); + } + + @Override + public void edit_getRefactoring(@NotNull String kind, + @NotNull String file, + int offset, + int length, + boolean validateOnly, + RefactoringOptions options, + @NotNull GetRefactoringConsumer consumer) { + myServer.edit_getRefactoring(kind, file, offset, length, validateOnly, options, consumer); + } + + @Override + public void edit_organizeDirectives(@NotNull String file, @NotNull OrganizeDirectivesConsumer consumer) { + myServer.edit_organizeDirectives(file, consumer); + } + + @Override + public void edit_sortMembers(@NotNull String file, @NotNull SortMembersConsumer consumer) { + myServer.edit_sortMembers(file, consumer); + } + + @Override + public void analysis_reanalyze() { + myLspTransport.reanalyze(); + } + + @Override + public void analysis_setPriorityFiles(@NotNull List files) { + myServer.analysis_setPriorityFiles(files); + } + + @Override + public void analysis_setSubscriptions(@NotNull Map> subscriptions) { + myServer.analysis_setSubscriptions(subscriptions); + } + + @Override + public void execution_createContext(@NotNull String contextRoot, @NotNull CreateContextConsumer consumer) { + myServer.execution_createContext(contextRoot, consumer); + } + + @Override + public void execution_deleteContext(@NotNull String contextId) { + myServer.execution_deleteContext(contextId); + } + + @Override + public void execution_getSuggestions(@NotNull String code, + int offset, + @NotNull String contextFile, + int contextOffset, + @NotNull List variables, + List expressions, + @NotNull GetRuntimeCompletionConsumer consumer) { + myServer.execution_getSuggestions(code, offset, contextFile, contextOffset, variables, expressions, consumer); + } + + @Override + public void execution_mapUri(@NotNull String id, String file, String uri, @NotNull MapUriConsumer consumer) { + myServer.execution_mapUri(id, file, uri, consumer); + } + + @Override + public void server_setSubscriptions(@NotNull List subscriptions) { + myServer.server_setSubscriptions(subscriptions); + } + + @Override + public void requestTextDocumentContent(@NotNull String fileUri, + @NotNull TextDocumentContentHandler handler) { + myLspTransport.requestTextDocumentContent(fileUri, handler); + } + + @Override + public void connectToDtd(@NotNull String uri) { + myLspTransport.connectToDtd(uri); + } + + @Override + public void configureServerFeatures(boolean supportsUris, boolean supportsWorkspaceApplyEdits) { + myLspTransport.configureServerFeatures(supportsUris, supportsWorkspaceApplyEdits); + } + + @Override + public @NotNull String generateUniqueId() { + return myServer.generateUniqueId(); + } + + @Override + public void sendRequestToServer(@NotNull String id, @NotNull JsonObject request) { + myServer.sendRequestToServer(id, request); + } + + @Override + public void sendRequestToServer(@NotNull String id, @NotNull JsonObject request, @NotNull Consumer consumer) { + myServer.sendRequestToServer(id, request, consumer); + } + + private @Nullable String readFileContent(@NotNull String filePath) { + return ReadAction.compute(() -> { + VirtualFile vf = LocalFileSystem.getInstance().findFileByPath(filePath); + if (vf == null) return null; + Document doc = FileDocumentManager.getInstance().getDocument(vf); + return doc != null ? doc.getText() : null; + }); + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisClient.java b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisClient.java new file mode 100644 index 00000000000..3ead46c0126 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisClient.java @@ -0,0 +1,160 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.*; +import com.google.gson.JsonObject; +import org.dartlang.analysis.server.protocol.ImportedElements; +import org.dartlang.analysis.server.protocol.RefactoringOptions; +import org.dartlang.analysis.server.protocol.RequestError; +import org.dartlang.analysis.server.protocol.RuntimeCompletionExpression; +import org.dartlang.analysis.server.protocol.RuntimeCompletionVariable; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; + +interface DartAnalysisClient { + void start() throws Exception; + + boolean isSocketOpen(); + + void shutdown(); + + @NotNull String getProtocolName(); + + void addAnalysisServerListener(@NotNull AnalysisServerListener listener); + + void removeAnalysisServerListener(@NotNull AnalysisServerListener listener); + + void addRequestListener(@NotNull RequestListener listener); + + void removeRequestListener(@NotNull RequestListener listener); + + void addResponseListener(@NotNull ResponseListener listener); + + void removeResponseListener(@NotNull ResponseListener listener); + + void addStatusListener(@NotNull AnalysisServerStatusListener listener); + + void completion_setSubscriptions(@NotNull List subscriptions); + + void analysis_updateContent(@NotNull Map files, @NotNull UpdateContentConsumer consumer); + + void analysis_setAnalysisRoots(@NotNull List included, @NotNull List excluded, Map packageRoots); + + void analysis_getHover(@NotNull String file, int offset, @NotNull GetHoverConsumer consumer); + + void analysis_getNavigation(@NotNull String file, int offset, int length, @NotNull GetNavigationConsumer consumer); + + void edit_getAssists(@NotNull String file, int offset, int length, @NotNull GetAssistsConsumer consumer); + + void edit_isPostfixCompletionApplicable(@NotNull String file, + @NotNull String key, + int offset, + @NotNull IsPostfixCompletionApplicableConsumer consumer); + + void edit_listPostfixCompletionTemplates(@NotNull ListPostfixCompletionTemplatesConsumer consumer); + + void edit_getPostfixCompletion(@NotNull String file, + @NotNull String key, + int offset, + @NotNull GetPostfixCompletionConsumer consumer); + + void edit_getStatementCompletion(@NotNull String file, int offset, @NotNull GetStatementCompletionConsumer consumer); + + void diagnostic_getServerPort(@NotNull GetServerPortConsumer consumer); + + void edit_getFixes(@NotNull String file, int offset, @NotNull GetFixesConsumer consumer); + + void search_findElementReferences(@NotNull String file, + int offset, + boolean includePotential, + @NotNull FindElementReferencesConsumer consumer); + + void search_getTypeHierarchy(@NotNull String file, int offset, boolean superOnly, @NotNull GetTypeHierarchyConsumer consumer); + + void completion_getSuggestionDetails(@NotNull String file, + int id, + @NotNull String label, + int offset, + @NotNull GetSuggestionDetailsConsumer consumer); + + void completion_getSuggestionDetails2(@NotNull String file, + int offset, + @NotNull String completion, + @NotNull String libraryUri, + @NotNull GetSuggestionDetailsConsumer2 consumer); + + void completion_getSuggestions(@NotNull String file, int offset, @NotNull GetSuggestionsConsumer consumer); + + void completion_getSuggestions2(@NotNull String file, + int offset, + int maxResults, + @NotNull String completionCaseMatchingMode, + @NotNull String completionMode, + int invocationCount, + int timeout, + @NotNull GetSuggestionsConsumer2 consumer); + + void edit_format(@NotNull String file, + int selectionOffset, + int selectionLength, + int lineLength, + @NotNull FormatConsumer consumer); + + void analysis_getImportedElements(@NotNull String file, int offset, int length, @NotNull GetImportedElementsConsumer consumer); + + void edit_importElements(@NotNull String file, @NotNull List elements, int offset, @NotNull ImportElementsConsumer consumer); + + void edit_getRefactoring(@NotNull String kind, + @NotNull String file, + int offset, + int length, + boolean validateOnly, + RefactoringOptions options, + @NotNull GetRefactoringConsumer consumer); + + void edit_organizeDirectives(@NotNull String file, @NotNull OrganizeDirectivesConsumer consumer); + + void edit_sortMembers(@NotNull String file, @NotNull SortMembersConsumer consumer); + + void analysis_reanalyze(); + + void analysis_setPriorityFiles(@NotNull List files); + + void analysis_setSubscriptions(@NotNull Map> subscriptions); + + void execution_createContext(@NotNull String contextRoot, @NotNull CreateContextConsumer consumer); + + void execution_deleteContext(@NotNull String contextId); + + void execution_getSuggestions(@NotNull String code, + int offset, + @NotNull String contextFile, + int contextOffset, + @NotNull List variables, + List expressions, + @NotNull GetRuntimeCompletionConsumer consumer); + + void execution_mapUri(@NotNull String id, String file, String uri, @NotNull MapUriConsumer consumer); + + void server_setSubscriptions(@NotNull List subscriptions); + + void requestTextDocumentContent(@NotNull String fileUri, @NotNull TextDocumentContentHandler handler); + + void connectToDtd(@NotNull String uri); + + void configureServerFeatures(boolean supportsUris, boolean supportsWorkspaceApplyEdits); + + @NotNull String generateUniqueId(); + + void sendRequestToServer(@NotNull String id, @NotNull JsonObject request); + + void sendRequestToServer(@NotNull String id, @NotNull JsonObject request, @NotNull Consumer consumer); + + interface TextDocumentContentHandler { + void computedDocumentContents(@NotNull String contents); + + void onError(@NotNull RequestError requestError); + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisClientFactory.java b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisClientFactory.java new file mode 100644 index 00000000000..3c676ad0cee --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisClientFactory.java @@ -0,0 +1,101 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.AnalysisServerSocket; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +final class DartAnalysisClientFactory { + private static final Logger LOG = Logger.getInstance(DartAnalysisClientFactory.class); + + // Temporary opt-out while LSP client rollout is in progress. + private static final String PROTOCOL_PROPERTY = "dart.analysis.server.client.protocol"; + // Temporary opt-in for starting `dart language-server` in native LSP mode. + private static final String LSP_WIRE_PROTOCOL_PROPERTY = "dart.analysis.server.lsp.wire.protocol"; + // Explicit acknowledgement for using incomplete native-LSP wire migration. + private static final String FORCE_NATIVE_LSP_WIRE_PROTOCOL_PROPERTY = "dart.analysis.server.lsp.wire.protocol.force"; + + private DartAnalysisClientFactory() { + } + + static @NotNull DartAnalysisClient create(@NotNull Project project, @NotNull AnalysisServerSocket socket) { + Protocol protocol = Protocol.from(System.getProperty(PROTOCOL_PROPERTY)); + return switch (protocol) { + case LEGACY -> new LegacyDartAnalysisClient(project, socket); + case LSP -> { + LOG.info("Using LSP analysis client (legacy fallback paths remain enabled)."); + yield new LspDartAnalysisClient(project, socket); + } + }; + } + + static boolean isLspClientRequested() { + return Protocol.from(System.getProperty(PROTOCOL_PROPERTY)) == Protocol.LSP; + } + + static @NotNull String getLanguageServerProtocolArgument() { + if (!isLspClientRequested()) { + return "--protocol=analyzer"; + } + + WireProtocol wireProtocol = WireProtocol.from(System.getProperty(LSP_WIRE_PROTOCOL_PROPERTY)); + return switch (wireProtocol) { + case ANALYZER -> "--protocol=analyzer"; + case LSP -> { + boolean forceNativeWire = Boolean.parseBoolean(System.getProperty(FORCE_NATIVE_LSP_WIRE_PROTOCOL_PROPERTY, "false")); + if (forceNativeWire) { + LOG.warn("Using native LSP wire protocol for dart language-server in forced mode. Analyzer-protocol fallback paths are still present."); + yield "--protocol=lsp"; + } + + LOG.warn("Native LSP wire protocol is not fully migrated yet. Use '" + FORCE_NATIVE_LSP_WIRE_PROTOCOL_PROPERTY + + "=true' to force experimental mode. Falling back to analyzer wire protocol."); + yield "--protocol=analyzer"; + } + }; + } + + private enum Protocol { + LEGACY, + LSP; + + private static @NotNull Protocol from(@Nullable String value) { + if (value == null || value.isEmpty()) { + return LSP; + } + + return switch (value.toLowerCase(Locale.US)) { + case "legacy" -> LEGACY; + case "lsp" -> LSP; + default -> { + LOG.warn("Unsupported value for '" + PROTOCOL_PROPERTY + "': '" + value + "'. Using LSP client."); + yield LSP; + } + }; + } + } + + private enum WireProtocol { + ANALYZER, + LSP; + + private static @NotNull WireProtocol from(@Nullable String value) { + if (value == null || value.isEmpty()) { + return ANALYZER; + } + + return switch (value.toLowerCase(Locale.US)) { + case "analyzer" -> ANALYZER; + case "lsp" -> LSP; + default -> { + LOG.warn("Unsupported value for '" + LSP_WIRE_PROTOCOL_PROPERTY + "': '" + value + "'. Using analyzer wire protocol."); + yield ANALYZER; + } + }; + } + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java index 38100b74172..ce267e67b22 100644 --- a/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java @@ -7,7 +7,6 @@ import com.google.dart.server.AnalysisServerListener; import com.google.dart.server.AnalysisServerListenerAdapter; import com.google.dart.server.CreateContextConsumer; -import com.google.dart.server.DartLspTextDocumentContentConsumer; import com.google.dart.server.ExtendedRequestErrorCode; import com.google.dart.server.FindElementReferencesConsumer; import com.google.dart.server.FormatConsumer; @@ -34,9 +33,7 @@ import com.google.dart.server.RequestListener; import com.google.dart.server.ResponseListener; import com.google.dart.server.SortMembersConsumer; -import com.google.dart.server.generated.AnalysisServer; import com.google.dart.server.internal.remote.DebugPrintStream; -import com.google.dart.server.internal.remote.RemoteAnalysisServerImpl; import com.google.dart.server.internal.remote.StdioServerSocket; import com.google.dart.server.utilities.logging.Logging; import com.google.gson.JsonObject; @@ -107,7 +104,6 @@ import org.dartlang.analysis.server.protocol.AnalysisErrorFixes; import org.dartlang.analysis.server.protocol.AnalysisErrorSeverity; import org.dartlang.analysis.server.protocol.AnalysisErrorType; -import org.dartlang.analysis.server.protocol.AnalysisOptions; import org.dartlang.analysis.server.protocol.AnalysisService; import org.dartlang.analysis.server.protocol.AnalysisStatus; import org.dartlang.analysis.server.protocol.AvailableSuggestionSet; @@ -225,7 +221,8 @@ public final class DartAnalysisServerService implements Disposable { // Do not wait for server response under lock. Do not take read/write action under lock. private final Object myLock = new Object(); - private @Nullable RemoteAnalysisServerImpl myServer; + // Phase 1 migration seam. This currently wraps the existing RemoteAnalysisServerImpl client. + private @Nullable DartAnalysisClient myClient; private @Nullable StdioServerSocket myServerSocket; private @NotNull String myServerVersion = ""; @@ -419,8 +416,9 @@ public void serverConnected(@Nullable String version) { myServerVersion = version != null ? version : ""; // completion_setSubscriptions() are handled here instead of in startServer() as the server version isn't known until this // serverConnected() call. - if (myServer != null && !shouldUseCompletion2()) { - myServer.completion_setSubscriptions(List.of(CompletionService.AVAILABLE_SUGGESTION_SETS)); + DartAnalysisClient client = myClient; + if (client != null && !shouldUseCompletion2()) { + client.completion_setSubscriptions(List.of(CompletionService.AVAILABLE_SUGGESTION_SETS)); } } @@ -683,8 +681,8 @@ public DartAnalysisServerService(@NotNull Project project, @NotNull CoroutineSco public void addAnalysisServerListener(final @NotNull AnalysisServerListener serverListener) { if (!myAdditionalServerListeners.contains(serverListener)) { myAdditionalServerListeners.add(serverListener); - if (myServer != null && isServerProcessActive()) { - myServer.addAnalysisServerListener(serverListener); + if (myClient != null && isServerProcessActive()) { + myClient.addAnalysisServerListener(serverListener); } } } @@ -692,8 +690,8 @@ public void addAnalysisServerListener(final @NotNull AnalysisServerListener serv @SuppressWarnings("unused") // for Flutter plugin public void removeAnalysisServerListener(final @NotNull AnalysisServerListener serverListener) { myAdditionalServerListeners.remove(serverListener); - if (myServer != null) { - myServer.removeAnalysisServerListener(serverListener); + if (myClient != null) { + myClient.removeAnalysisServerListener(serverListener); } } @@ -701,8 +699,8 @@ public void removeAnalysisServerListener(final @NotNull AnalysisServerListener s public void addRequestListener(final @NotNull RequestListener requestListener) { if (!myRequestListeners.contains(requestListener)) { myRequestListeners.add(requestListener); - if (myServer != null && isServerProcessActive()) { - myServer.addRequestListener(requestListener); + if (myClient != null && isServerProcessActive()) { + myClient.addRequestListener(requestListener); } } } @@ -710,8 +708,8 @@ public void addRequestListener(final @NotNull RequestListener requestListener) { @SuppressWarnings("unused") // for Flutter plugin public void removeRequestListener(final @NotNull RequestListener requestListener) { myRequestListeners.remove(requestListener); - if (myServer != null) { - myServer.removeRequestListener(requestListener); + if (myClient != null) { + myClient.removeRequestListener(requestListener); } } @@ -719,8 +717,8 @@ public void removeRequestListener(final @NotNull RequestListener requestListener public void addResponseListener(final @NotNull ResponseListener responseListener) { if (!myResponseListeners.contains(responseListener)) { myResponseListeners.add(responseListener); - if (myServer != null && isServerProcessActive()) { - myServer.addResponseListener(responseListener); + if (myClient != null && isServerProcessActive()) { + myClient.addResponseListener(responseListener); } } } @@ -728,8 +726,8 @@ public void addResponseListener(final @NotNull ResponseListener responseListener @SuppressWarnings("unused") // for Flutter plugin public void removeResponseListener(final @NotNull ResponseListener responseListener) { myResponseListeners.remove(responseListener); - if (myServer != null) { - myServer.removeResponseListener(responseListener); + if (myClient != null) { + myClient.removeResponseListener(responseListener); } } @@ -850,7 +848,7 @@ private void registerDocumentListener() { final DocumentListener documentListener = new DocumentListener() { @Override public void beforeDocumentChange(@NotNull DocumentEvent e) { - if (myServer == null) return; + if (myClient == null) return; myServerData.onDocumentChanged(e); @@ -1015,7 +1013,7 @@ public static boolean isFileNameRespectedByAnalysisServer(@NotNull String _fileN } public void updateFilesContent() { - if (myServer != null) { + if (myClient != null) { ApplicationManager.getApplication().runReadAction(this::doUpdateFilesContent); } } @@ -1023,8 +1021,8 @@ public void updateFilesContent() { private void doUpdateFilesContent() { // may be use DocumentListener to collect deltas instead of sending the whole Document.getText() each time? - AnalysisServer server = myServer; - if (server == null) { + DartAnalysisClient client = myClient; + if (client == null) { return; } @@ -1083,7 +1081,7 @@ private void doUpdateFilesContent() { } if (!fileUriToContentOverlay.isEmpty()) { - server.analysis_updateContent(fileUriToContentOverlay, () -> { + client.analysis_updateContent(fileUriToContentOverlay, () -> { synchronized (myFilePathWithOverlaidContentToTimestamp) { filePathsToRemoveContentOverlay.forEach(myFilePathWithOverlaidContentToTimestamp::remove); } @@ -1097,8 +1095,8 @@ public void ensureAnalysisRootsUpToDate() { } boolean setAnalysisRoots(@NotNull List includedRootPaths, @NotNull List excludedRootPaths) { - AnalysisServer server = myServer; - if (server == null) { + DartAnalysisClient client = myClient; + if (client == null) { return false; } @@ -1109,7 +1107,7 @@ boolean setAnalysisRoots(@NotNull List includedRootPaths, @NotNull List< List includedRootUris = ContainerUtil.map(includedRootPaths, this::getLocalFileUri); List excludedRootUris = ContainerUtil.map(excludedRootPaths, this::getLocalFileUri); - server.analysis_setAnalysisRoots(includedRootUris, excludedRootUris, null); + client.analysis_setAnalysisRoots(includedRootUris, excludedRootUris, null); return true; } @@ -1170,8 +1168,8 @@ private void clearAllErrors() { } public @NotNull List analysis_getHover(final @NotNull VirtualFile file, final int _offset) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return HoverInformation.EMPTY_LIST; } @@ -1180,7 +1178,7 @@ private void clearAllErrors() { final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.analysis_getHover(fileUri, offset, new GetHoverConsumer() { + client.analysis_getHover(fileUri, offset, new GetHoverConsumer() { @Override public void computedHovers(HoverInformation[] hovers) { Collections.addAll(result, hovers); @@ -1194,7 +1192,7 @@ public void onError(RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_HOVER_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_HOVER_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("analysis_getHover", GET_HOVER_TIMEOUT, fileUri); @@ -1205,8 +1203,8 @@ public void onError(RequestError error) { public @Nullable List analysis_getNavigation(final @NotNull VirtualFile file, final int _offset, final int length) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1217,7 +1215,7 @@ public void onError(RequestError error) { LOG.debug("analysis_getNavigation(" + fileUri + ")"); final int offset = getOriginalOffset(file, _offset); - server.analysis_getNavigation(fileUri, offset, length, new GetNavigationConsumer() { + client.analysis_getNavigation(fileUri, offset, length, new GetNavigationConsumer() { @Override public void computedNavigation(final List regions) { final List dartRegions = new ArrayList<>(regions.size()); @@ -1244,7 +1242,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_NAVIGATION_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_NAVIGATION_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("analysis_getNavigation", GET_NAVIGATION_TIMEOUT, fileUri); @@ -1256,8 +1254,8 @@ public void onError(final RequestError error) { public @NotNull List edit_getAssists(final @NotNull VirtualFile file, final int _offset, final int _length) { if (!file.isInLocalFileSystem()) return Collections.emptyList(); - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return Collections.emptyList(); } @@ -1266,7 +1264,7 @@ public void onError(final RequestError error) { final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); final int length = getOriginalOffset(file, _offset + _length) - offset; - server.edit_getAssists(fileUri, offset, length, new GetAssistsConsumer() { + client.edit_getAssists(fileUri, offset, length, new GetAssistsConsumer() { @Override public void computedSourceChanges(List sourceChanges) { results.addAll(sourceChanges); @@ -1282,7 +1280,7 @@ public void onError(final RequestError error) { final long timeout = ApplicationManager.getApplication().isDispatchThread() ? GET_ASSISTS_TIMEOUT_EDT : GET_ASSISTS_TIMEOUT; - awaitForLatchCheckingCanceled(server, latch, timeout); + awaitForLatchCheckingCanceled(client, latch, timeout); if (latch.getCount() > 0) { logTookTooLongMessage("edit_getAssists", timeout, fileUri); @@ -1293,8 +1291,8 @@ public void onError(final RequestError error) { public boolean edit_isPostfixCompletionApplicable(VirtualFile file, int _offset, String key) { if (!file.isInLocalFileSystem()) return false; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return false; } @@ -1302,7 +1300,7 @@ public boolean edit_isPostfixCompletionApplicable(VirtualFile file, int _offset, final Ref resultRef = Ref.create(false); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.edit_isPostfixCompletionApplicable(fileUri, key, offset, new IsPostfixCompletionApplicableConsumer() { + client.edit_isPostfixCompletionApplicable(fileUri, key, offset, new IsPostfixCompletionApplicableConsumer() { @Override public void isPostfixCompletionApplicable(Boolean value) { resultRef.set(value); @@ -1310,7 +1308,7 @@ public void isPostfixCompletionApplicable(Boolean value) { } }); - awaitForLatchCheckingCanceled(server, latch, POSTFIX_COMPLETION_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, POSTFIX_COMPLETION_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_isPostfixCompletionApplicable", POSTFIX_COMPLETION_TIMEOUT, fileUri); @@ -1319,8 +1317,8 @@ public void isPostfixCompletionApplicable(Boolean value) { } public PostfixTemplateDescriptor @Nullable [] edit_listPostfixCompletionTemplates() { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1330,7 +1328,7 @@ public void isPostfixCompletionApplicable(Boolean value) { final Ref resultRef = Ref.create(); final CountDownLatch latch = new CountDownLatch(1); - server.edit_listPostfixCompletionTemplates(new ListPostfixCompletionTemplatesConsumer() { + client.edit_listPostfixCompletionTemplates(new ListPostfixCompletionTemplatesConsumer() { @Override public void postfixCompletionTemplates(PostfixTemplateDescriptor[] templates) { resultRef.set(templates); @@ -1346,7 +1344,7 @@ public void onError(RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, POSTFIX_INITIALIZATION_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, POSTFIX_INITIALIZATION_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_listPostfixCompletionTemplates", POSTFIX_INITIALIZATION_TIMEOUT, null); @@ -1358,8 +1356,8 @@ public void onError(RequestError error) { public @Nullable SourceChange edit_getPostfixCompletion(final @NotNull VirtualFile file, final int _offset, final String key) { if (!file.isInLocalFileSystem()) return null; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1367,7 +1365,7 @@ public void onError(RequestError error) { final Ref resultRef = Ref.create(); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.edit_getPostfixCompletion(fileUri, key, offset, new GetPostfixCompletionConsumer() { + client.edit_getPostfixCompletion(fileUri, key, offset, new GetPostfixCompletionConsumer() { @Override public void computedSourceChange(SourceChange sourceChange) { resultRef.set(sourceChange); @@ -1381,7 +1379,7 @@ public void onError(RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, POSTFIX_COMPLETION_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, POSTFIX_COMPLETION_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_getPostfixCompletion", POSTFIX_COMPLETION_TIMEOUT, fileUri); @@ -1392,8 +1390,8 @@ public void onError(RequestError error) { public @Nullable SourceChange edit_getStatementCompletion(final @NotNull VirtualFile file, final int _offset) { if (!file.isInLocalFileSystem()) return null; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1401,7 +1399,7 @@ public void onError(RequestError error) { final Ref resultRef = Ref.create(); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.edit_getStatementCompletion(fileUri, offset, new GetStatementCompletionConsumer() { + client.edit_getStatementCompletion(fileUri, offset, new GetStatementCompletionConsumer() { @Override public void computedSourceChange(SourceChange sourceChange) { resultRef.set(sourceChange); @@ -1415,7 +1413,7 @@ public void onError(RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, STATEMENT_COMPLETION_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, STATEMENT_COMPLETION_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_getStatementCompletion", STATEMENT_COMPLETION_TIMEOUT, fileUri); @@ -1424,13 +1422,13 @@ public void onError(RequestError error) { } public void diagnostic_getServerPort(GetServerPortConsumer consumer) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { consumer.onError(new RequestError(ExtendedRequestErrorCode.INVALID_SERVER_RESPONSE, DartBundle.message("analysis.server.not.running"), null)); } else { - server.diagnostic_getServerPort(consumer); + client.diagnostic_getServerPort(consumer); } } @@ -1446,8 +1444,8 @@ public void askForFixesAndWaitABitIfReceivedQuickly(final @NotNull VirtualFile f return; } - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { consumer.consume(Collections.emptyList()); return; } @@ -1455,7 +1453,7 @@ public void askForFixesAndWaitABitIfReceivedQuickly(final @NotNull VirtualFile f final String fileUri = getLocalFileUri(file.getPath()); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.edit_getFixes(fileUri, offset, new GetFixesConsumer() { + client.edit_getFixes(fileUri, offset, new GetFixesConsumer() { @Override public void computedFixes(final List fixes) { consumer.consume(fixes); @@ -1471,7 +1469,7 @@ public void onError(final RequestError error) { final long timeout = ApplicationManager.getApplication().isDispatchThread() ? GET_FIXES_TIMEOUT_EDT : GET_FIXES_TIMEOUT; - awaitForLatchCheckingCanceled(server, latch, timeout); + awaitForLatchCheckingCanceled(client, latch, timeout); if (latch.getCount() > 0) { logTookTooLongMessage("edit_getFixes", timeout, fileUri); @@ -1481,8 +1479,8 @@ public void onError(final RequestError error) { public void search_findElementReferences(final @NotNull VirtualFile file, final int _offset, final @NotNull Consumer consumer) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return; } @@ -1491,7 +1489,7 @@ public void search_findElementReferences(final @NotNull VirtualFile file, final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.search_findElementReferences(fileUri, offset, true, new FindElementReferencesConsumer() { + client.search_findElementReferences(fileUri, offset, true, new FindElementReferencesConsumer() { @Override public void computedElementReferences(String searchId, Element element) { searchIdRef.set(searchId); @@ -1505,7 +1503,7 @@ public void onError(RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, FIND_ELEMENT_REFERENCES_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, FIND_ELEMENT_REFERENCES_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("search_findElementReferences", FIND_ELEMENT_REFERENCES_TIMEOUT, fileUri + "@" + offset); @@ -1544,15 +1542,15 @@ public void onError(RequestError error) { final int _offset, final boolean superOnly) { final List results = new ArrayList<>(); - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return results; } final String fileUri = getFileUri(file); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.search_getTypeHierarchy(fileUri, offset, superOnly, new GetTypeHierarchyConsumer() { + client.search_getTypeHierarchy(fileUri, offset, superOnly, new GetTypeHierarchyConsumer() { @Override public void computedHierarchy(List hierarchyItems) { results.addAll(hierarchyItems); @@ -1566,7 +1564,7 @@ public void onError(RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_TYPE_HIERARCHY_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_TYPE_HIERARCHY_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("search_getTypeHierarchy", GET_TYPE_HIERARCHY_TIMEOUT, fileUri); @@ -1580,8 +1578,8 @@ public void onError(RequestError error) { int _offset) { if (!file.isInLocalFileSystem()) return null; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1589,7 +1587,7 @@ public void onError(RequestError error) { final Ref> resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.completion_getSuggestionDetails(fileUri, id, label, offset, new GetSuggestionDetailsConsumer() { + client.completion_getSuggestionDetails(fileUri, id, label, offset, new GetSuggestionDetailsConsumer() { @Override public void computedDetails(String completion, SourceChange change) { resultRef.set(new Pair<>(completion, change)); @@ -1602,7 +1600,7 @@ public void onError(RequestError requestError) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTION_DETAILS_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_SUGGESTION_DETAILS_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("completion_getSuggestionDetails", GET_SUGGESTION_DETAILS_TIMEOUT, fileUri); @@ -1616,8 +1614,8 @@ public void onError(RequestError requestError) { @NotNull String libraryUri) { if (!file.isInLocalFileSystem()) return null; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1625,7 +1623,7 @@ public void onError(RequestError requestError) { final Ref> resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.completion_getSuggestionDetails2(fileUri, offset, completion, libraryUri, new GetSuggestionDetailsConsumer2() { + client.completion_getSuggestionDetails2(fileUri, offset, completion, libraryUri, new GetSuggestionDetailsConsumer2() { @Override public void computedDetails(String completion, SourceChange change) { resultRef.set(new Pair<>(completion, change)); @@ -1638,7 +1636,7 @@ public void onError(RequestError requestError) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTION_DETAILS2_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_SUGGESTION_DETAILS2_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("completion_getSuggestionDetails2", GET_SUGGESTION_DETAILS2_TIMEOUT, fileUri); @@ -1650,8 +1648,8 @@ public void onError(RequestError requestError) { public @Nullable String completion_getSuggestions(final @NotNull VirtualFile file, final int _offset) { if (!file.isInLocalFileSystem()) return null; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1663,7 +1661,7 @@ public void onError(RequestError requestError) { final Ref resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.completion_getSuggestions(fileUri, offset, new GetSuggestionsConsumer() { + client.completion_getSuggestions(fileUri, offset, new GetSuggestionsConsumer() { @Override public void computedCompletionId(final @NotNull String completionId) { resultRef.set(completionId); @@ -1681,7 +1679,7 @@ public void onError(final @NotNull RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTIONS_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_SUGGESTIONS_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("completion_getSuggestions", GET_SUGGESTIONS_TIMEOUT, fileUri); @@ -1697,8 +1695,8 @@ public void onError(final @NotNull RequestError error) { final int invocationCount) { if (!file.isInLocalFileSystem()) return null; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1723,7 +1721,7 @@ else if (caseSensitiveSetting == CodeInsightSettings.NONE) { completionCaseMatchingMode = CompletionCaseMatchingMode.ALL_CHARS; } - server.completion_getSuggestions2(fileUri, offset, maxResults, completionCaseMatchingMode, completionMode, invocationCount, -1, + client.completion_getSuggestions2(fileUri, offset, maxResults, completionCaseMatchingMode, completionMode, invocationCount, -1, new GetSuggestionsConsumer2() { @Override public void computedSuggestions(int replacementOffset, @@ -1752,7 +1750,7 @@ public void onError(final @NotNull RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTIONS_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_SUGGESTIONS_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("completion_getSuggestions2", GET_SUGGESTIONS_TIMEOUT, fileUri); @@ -1771,8 +1769,8 @@ public void onError(final @NotNull RequestError error) { final int lineLength) { if (!file.isInLocalFileSystem()) return null; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1782,7 +1780,7 @@ public void onError(final @NotNull RequestError error) { final CountDownLatch latch = new CountDownLatch(1); final int selectionOffset = getOriginalOffset(file, _selectionOffset); final int selectionLength = getOriginalOffset(file, _selectionOffset + _selectionLength) - selectionOffset; - server.edit_format(fileUri, selectionOffset, selectionLength, lineLength, new FormatConsumer() { + client.edit_format(fileUri, selectionOffset, selectionLength, lineLength, new FormatConsumer() { @Override public void computedFormat(final List edits, final int selectionOffset, final int selectionLength) { resultRef.set(new FormatResult(edits, selectionOffset, selectionLength)); @@ -1802,7 +1800,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, EDIT_FORMAT_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, EDIT_FORMAT_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_format", EDIT_FORMAT_TIMEOUT, fileUri); @@ -1814,8 +1812,8 @@ public void onError(final RequestError error) { public @Nullable List analysis_getImportedElements(final @NotNull VirtualFile file, final int _selectionOffset, final int _selectionLength) { - final AnalysisServer server = myServer; - if (server == null || StringUtil.compareVersionNumbers(mySdkVersion, "1.25") < 0) { + final DartAnalysisClient client = myClient; + if (client == null || StringUtil.compareVersionNumbers(mySdkVersion, "1.25") < 0) { return null; } @@ -1824,7 +1822,7 @@ public void onError(final RequestError error) { final CountDownLatch latch = new CountDownLatch(1); final int selectionOffset = getOriginalOffset(file, _selectionOffset); final int selectionLength = getOriginalOffset(file, _selectionOffset + _selectionLength) - selectionOffset; - server.analysis_getImportedElements(fileUri, selectionOffset, selectionLength, new GetImportedElementsConsumer() { + client.analysis_getImportedElements(fileUri, selectionOffset, selectionLength, new GetImportedElementsConsumer() { @Override public void computedImportedElements(final List importedElements) { resultRef.set(importedElements); @@ -1841,7 +1839,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, IMPORTED_ELEMENTS_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, IMPORTED_ELEMENTS_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("analysis_getImportedElements", IMPORTED_ELEMENTS_TIMEOUT, fileUri); @@ -1853,8 +1851,8 @@ public void onError(final RequestError error) { public @Nullable SourceFileEdit edit_importElements(final @NotNull VirtualFile file, final @NotNull List importedElements, final int _offset) { - final AnalysisServer server = myServer; - if (server == null || StringUtil.compareVersionNumbers(mySdkVersion, "1.25") < 0) { + final DartAnalysisClient client = myClient; + if (client == null || StringUtil.compareVersionNumbers(mySdkVersion, "1.25") < 0) { return null; } @@ -1862,7 +1860,7 @@ public void onError(final RequestError error) { final Ref resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); final int offset = getOriginalOffset(file, _offset); - server.edit_importElements(fileUri, importedElements, offset, new ImportElementsConsumer() { + client.edit_importElements(fileUri, importedElements, offset, new ImportElementsConsumer() { @Override public void computedImportedElements(final SourceFileEdit edit) { resultRef.set(edit); @@ -1879,7 +1877,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, IMPORTED_ELEMENTS_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, IMPORTED_ELEMENTS_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_importElements", IMPORTED_ELEMENTS_TIMEOUT, fileUri); @@ -1897,28 +1895,28 @@ public boolean edit_getRefactoring(String kind, GetRefactoringConsumer consumer) { if (!file.isInLocalFileSystem()) return false; - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return false; } final String fileUri = getLocalFileUri(file.getPath()); final int offset = getOriginalOffset(file, _offset); final int length = getOriginalOffset(file, _offset + _length) - offset; - server.edit_getRefactoring(kind, fileUri, offset, length, validateOnly, options, consumer); + client.edit_getRefactoring(kind, fileUri, offset, length, validateOnly, options, consumer); return true; } public @Nullable SourceFileEdit edit_organizeDirectives(@NotNull String filePath) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } final String fileUri = getLocalFileUri(filePath); final Ref resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); - server.edit_organizeDirectives(fileUri, new OrganizeDirectivesConsumer() { + client.edit_organizeDirectives(fileUri, new OrganizeDirectivesConsumer() { @Override public void computedEdit(final SourceFileEdit edit) { resultRef.set(edit); @@ -1938,7 +1936,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, EDIT_ORGANIZE_DIRECTIVES_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, EDIT_ORGANIZE_DIRECTIVES_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_organizeDirectives", EDIT_ORGANIZE_DIRECTIVES_TIMEOUT, fileUri); @@ -1948,8 +1946,8 @@ public void onError(final RequestError error) { } public @Nullable SourceFileEdit edit_sortMembers(@NotNull String filePath) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -1957,7 +1955,7 @@ public void onError(final RequestError error) { final Ref resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); - server.edit_sortMembers(fileUri, new SortMembersConsumer() { + client.edit_sortMembers(fileUri, new SortMembersConsumer() { @Override public void computedEdit(final SourceFileEdit edit) { resultRef.set(edit); @@ -1978,7 +1976,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, EDIT_SORT_MEMBERS_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, EDIT_SORT_MEMBERS_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("edit_sortMembers", EDIT_SORT_MEMBERS_TIMEOUT, fileUri); @@ -1988,31 +1986,33 @@ public void onError(final RequestError error) { } public void analysis_reanalyze() { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return; } - server.analysis_reanalyze(); + client.analysis_reanalyze(); ApplicationManager.getApplication().invokeLater(this::clearAllErrors, ModalityState.nonModal(), myDisposedCondition); } private void analysis_setPriorityFiles() { synchronized (myLock) { - if (myServer == null) return; + DartAnalysisClient client = myClient; + if (client == null) return; if (LOG.isDebugEnabled()) { LOG.debug("analysis_setPriorityFiles, files:\n" + StringUtil.join(myVisibleFileUris, ",\n")); } - myServer.analysis_setPriorityFiles(myVisibleFileUris); + client.analysis_setPriorityFiles(myVisibleFileUris); } } private void analysis_setSubscriptions() { synchronized (myLock) { - if (myServer == null) return; + DartAnalysisClient client = myClient; + if (client == null) return; final Map> subscriptions = new HashMap<>(); subscriptions.put(AnalysisService.HIGHLIGHTS, myVisibleFileUris); @@ -2030,7 +2030,7 @@ private void analysis_setSubscriptions() { LOG.debug("analysis_setSubscriptions, subscriptions:\n" + subscriptions); } - myServer.analysis_setSubscriptions(subscriptions); + client.analysis_setSubscriptions(subscriptions); } } @@ -2039,8 +2039,8 @@ private void analysis_setSubscriptions() { } public @Nullable String execution_createContext(@NotNull String filePath, @NotNull CoverageLoadErrorReporter reporter) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { String message = "Dart Analysis Server is not available."; LOG.warn(message); reporter.reportWarning(message, null); @@ -2050,7 +2050,7 @@ private void analysis_setSubscriptions() { final String fileUri = getLocalFileUri(filePath); final Ref resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); - server.execution_createContext(fileUri, new CreateContextConsumer() { + client.execution_createContext(fileUri, new CreateContextConsumer() { @Override public void computedExecutionContext(final String contextId) { resultRef.set(contextId); @@ -2066,7 +2066,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, EXECUTION_CREATE_CONTEXT_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, EXECUTION_CREATE_CONTEXT_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("execution_createContext", EXECUTION_CREATE_CONTEXT_TIMEOUT, fileUri); @@ -2077,9 +2077,9 @@ public void onError(final RequestError error) { } public void execution_deleteContext(final @NotNull String contextId) { - final AnalysisServer server = myServer; - if (server != null) { - server.execution_deleteContext(contextId); + final DartAnalysisClient client = myClient; + if (client != null) { + client.execution_deleteContext(contextId); } } @@ -2089,15 +2089,15 @@ public void execution_deleteContext(final @NotNull String contextId) { int contextOffset, @NotNull List variables, @NotNull List expressions) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return new Pair<>(new ArrayList<>(), new ArrayList<>()); } final String contextFileUri = getFileUri(contextFile); final CountDownLatch latch = new CountDownLatch(1); final Ref, List>> refResult = Ref.create(); - server.execution_getSuggestions( + client.execution_getSuggestions( code, offset, contextFileUri, contextOffset, variables, expressions, @@ -2117,7 +2117,7 @@ public void onError(RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTIONS_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, GET_SUGGESTIONS_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("execution_getSuggestions", GET_SUGGESTIONS_TIMEOUT, contextFileUri); @@ -2138,8 +2138,8 @@ public void onError(RequestError error) { */ @Deprecated public @Nullable String execution_mapUri(@NotNull String _id, @Nullable String _filePathOrUri, @Nullable String _executionContextUri) { - final AnalysisServer server = myServer; - if (server == null) { + final DartAnalysisClient client = myClient; + if (client == null) { return null; } @@ -2158,7 +2158,7 @@ public void onError(RequestError error) { final Ref resultRef = new Ref<>(); final CountDownLatch latch = new CountDownLatch(1); - server.execution_mapUri(_id, _filePathOrUri, _executionContextUri, new MapUriConsumer() { + client.execution_mapUri(_id, _filePathOrUri, _executionContextUri, new MapUriConsumer() { @Override public void computedFileOrUri(final String file, final String uri) { if (uri != null) { @@ -2176,7 +2176,7 @@ public void onError(final RequestError error) { } }); - awaitForLatchCheckingCanceled(server, latch, EXECUTION_MAP_URI_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, EXECUTION_MAP_URI_TIMEOUT); if (latch.getCount() > 0) { logTookTooLongMessage("execution_mapUri", EXECUTION_MAP_URI_TIMEOUT, _filePathOrUri != null ? _filePathOrUri : _executionContextUri); @@ -2190,16 +2190,15 @@ public void onError(final RequestError error) { return resultRef.get(); } - // LSP over Legacy Dart Analysis Server protocols - public @Nullable String lspMessage_dart_textDocumentContent(@NotNull String fileUri) { - RemoteAnalysisServerImpl server = myServer; - if (server == null) { + public @Nullable String requestTextDocumentContent(@NotNull String fileUri) { + DartAnalysisClient client = myClient; + if (client == null) { return null; } Ref resultRef = new Ref<>(); CountDownLatch latch = new CountDownLatch(1); - server.lspMessage_dart_textDocumentContent(fileUri, new DartLspTextDocumentContentConsumer() { + client.requestTextDocumentContent(fileUri, new DartAnalysisClient.TextDocumentContentHandler() { @Override public void computedDocumentContents(@NotNull String contents) { resultRef.set(StringUtil.convertLineSeparators(contents)); @@ -2208,19 +2207,27 @@ public void computedDocumentContents(@NotNull String contents) { @Override public void onError(RequestError error) { - logError("lspMessage_dart_textDocumentContent()", fileUri, error); + logError("requestTextDocumentContent()", fileUri, error); latch.countDown(); } }); - awaitForLatchCheckingCanceled(server, latch, LSP_MESSAGE_TEXT_DOCUMENT_CONTENT_TIMEOUT); + awaitForLatchCheckingCanceled(client, latch, LSP_MESSAGE_TEXT_DOCUMENT_CONTENT_TIMEOUT); if (latch.getCount() > 0) { - logTookTooLongMessage("lspMessage_dart_textDocumentContent", LSP_MESSAGE_TEXT_DOCUMENT_CONTENT_TIMEOUT, fileUri); + logTookTooLongMessage("requestTextDocumentContent", LSP_MESSAGE_TEXT_DOCUMENT_CONTENT_TIMEOUT, fileUri); } return resultRef.get(); } + /** + * @deprecated use {@link #requestTextDocumentContent(String)} + */ + @Deprecated + public @Nullable String lspMessage_dart_textDocumentContent(@NotNull String fileUri) { + return requestTextDocumentContent(fileUri); + } + private void startServer(final @NotNull DartSdk sdk) { if (DartPubActionBase.isInProgress()) return; // DartPubActionBase will start the server itself when finished @@ -2282,7 +2289,7 @@ else if (!useDartLangServerCall && !dasSnapshotFile.canRead()) { @NonNls String serverArgsRaw; if (useDartLangServerCall) { - serverArgsRaw = "--protocol=analyzer"; + serverArgsRaw = DartAnalysisClientFactory.getLanguageServerProtocolArgument(); } else { // Note that as of Dart 2.12.0 the '--useAnalysisHighlight2' flag is ignored (and is the @@ -2304,11 +2311,14 @@ else if (!useDartLangServerCall && !dasSnapshotFile.canRead()) { myServerSocket.setClientId(getClientId()); myServerSocket.setClientVersion(getClientVersion()); - final RemoteAnalysisServerImpl startedServer = new DartAnalysisServerImpl(myProject, myServerSocket); + final DartAnalysisClient startedClient = DartAnalysisClientFactory.create(myProject, myServerSocket); + LOG.info("Starting Dart analysis client protocol=" + startedClient.getProtocolName() + + ", sdk=" + sdk.getVersion() + + ", launchMode=" + (useDartLangServerCall ? "dart language-server" : "analysis_server.dart.snapshot")); try { - startedServer.start(); - server_setSubscriptions(startedServer); + startedClient.start(); + server_setSubscriptions(startedClient); if (!myInitializationOnServerStartupDone) { myInitializationOnServerStartupDone = true; @@ -2318,22 +2328,22 @@ else if (!useDartLangServerCall && !dasSnapshotFile.canRead()) { setDasLogger(); } - startedServer.addAnalysisServerListener(myAnalysisServerListener); + startedClient.addAnalysisServerListener(myAnalysisServerListener); for (AnalysisServerListener listener : myAdditionalServerListeners) { - startedServer.addAnalysisServerListener(listener); + startedClient.addAnalysisServerListener(listener); } for (RequestListener listener : myRequestListeners) { - startedServer.addRequestListener(listener); + startedClient.addRequestListener(listener); } for (ResponseListener listener : myResponseListeners) { - startedServer.addResponseListener(listener); + startedClient.addResponseListener(listener); } myHaveShownInitialProgress = false; - startedServer.addStatusListener(isAlive -> { + startedClient.addStatusListener(isAlive -> { if (!isAlive) { synchronized (myLock) { - if (startedServer == myServer) { + if (startedClient == myClient) { // Show a notification on the dart analysis tool window. ApplicationManager.getApplication().invokeLater( () -> { @@ -2352,14 +2362,11 @@ else if (!useDartLangServerCall && !dasSnapshotFile.canRead()) { mySdkVersion = sdk.getVersion(); - startedServer.analysis_updateOptions(new AnalysisOptions(true, true, true, true, false, true, false)); boolean supportsUris = isDartSdkVersionSufficientForFileUri(mySdkVersion); boolean supportsWorkspaceApplyEdits = isDartSdkVersionSufficientForWorkspaceApplyEdits(mySdkVersion); - startedServer.server_setClientCapabilities(List.of("openUrlRequest", "showMessageRequest"), - supportsUris, - supportsWorkspaceApplyEdits); + startedClient.configureServerFeatures(supportsUris, supportsWorkspaceApplyEdits); - myServer = startedServer; + myClient = startedClient; // Clear any dart view notifications. ApplicationManager.getApplication().invokeLater( @@ -2371,7 +2378,7 @@ else if (!useDartLangServerCall && !dasSnapshotFile.canRead()) { myDisposedCondition ); - // This must be done after myServer is set, and should be done each time the server starts. + // This must be done after myClient is set, and should be done each time the server starts. registerPostfixCompletionTemplates(); myDtdUri = null; @@ -2390,7 +2397,7 @@ else if (!useDartLangServerCall && !dasSnapshotFile.canRead()) { public boolean isServerProcessActive() { synchronized (myLock) { - return myServer != null && myServer.isSocketOpen(); + return myClient != null && myClient.isSocketOpen(); } } @@ -2403,20 +2410,20 @@ public boolean serverReadyForRequest() { ApplicationManager.getApplication().assertReadAccessAllowed(); synchronized (myLock) { - if (myServer == null || + if (myClient == null || !sdk.getHomePath().equals(mySdkHome) || !sdk.getVersion().equals(mySdkVersion) || - !myServer.isSocketOpen()) { + !myClient.isSocketOpen()) { stopServer(); DartProblemsView.getInstance(myProject).setInitialCurrentFileBeforeServerStart(getCurrentOpenFile()); startServer(sdk); - if (myServer != null) { + if (myClient != null) { myRootsHandler.onServerStarted(); } } - return myServer != null; + return myClient != null; } } @@ -2427,36 +2434,38 @@ public void restartServer() { void stopServer() { synchronized (myLock) { - if (myServer != null) { + DartAnalysisClient client = myClient; + if (client != null || myServerSocket != null) { LOG.debug("stopping server"); - myServer.removeAnalysisServerListener(myAnalysisServerListener); + } + if (client != null) { + client.removeAnalysisServerListener(myAnalysisServerListener); for (AnalysisServerListener listener : myAdditionalServerListeners) { - myServer.removeAnalysisServerListener(listener); + client.removeAnalysisServerListener(listener); } for (RequestListener listener : myRequestListeners) { - myServer.removeRequestListener(listener); + client.removeRequestListener(listener); } for (ResponseListener listener : myResponseListeners) { - myServer.removeResponseListener(listener); + client.removeResponseListener(listener); } + client.shutdown(); + } - myServer.server_shutdown(); - - long startTime = System.currentTimeMillis(); - while (myServerSocket != null && myServerSocket.isOpen()) { - if (System.currentTimeMillis() - startTime > SEND_REQUEST_TIMEOUT) { - myServerSocket.stop(); - break; - } - Uninterruptibles.sleepUninterruptibly(CHECK_CANCELLED_PERIOD, TimeUnit.MILLISECONDS); + long startTime = System.currentTimeMillis(); + while (myServerSocket != null && myServerSocket.isOpen()) { + if (System.currentTimeMillis() - startTime > SEND_REQUEST_TIMEOUT) { + myServerSocket.stop(); + break; } + Uninterruptibles.sleepUninterruptibly(CHECK_CANCELLED_PERIOD, TimeUnit.MILLISECONDS); } stopShowingServerProgress(); myUpdateFilesAlarm.cancelAllRequests(); myServerSocket = null; - myServer = null; + myClient = null; mySdkHome = null; mySdkVersion = ""; myServerVersion = ""; @@ -2473,8 +2482,8 @@ void stopServer() { } public void connectToDtd(@NotNull String uri) { - AnalysisServer server = myServer; - if (server == null) { + DartAnalysisClient client = myClient; + if (client == null) { return; } @@ -2482,7 +2491,7 @@ public void connectToDtd(@NotNull String uri) { // Connection to DTD is used for server-initiated edits (workspace.applyEdits) if (supportsWorkspaceApplyEdits && !uri.equals(myDtdUri)) { myDtdUri = uri; - server.lsp_connectToDtd(uri); + client.connectToDtd(uri); } } @@ -2566,7 +2575,7 @@ private void logTookTooLongMessage(final @NonNls @NotNull String methodName, fin LOG.info(builder.toString()); } - private static boolean awaitForLatchCheckingCanceled(final @NotNull AnalysisServer server, + private static boolean awaitForLatchCheckingCanceled(final @NotNull DartAnalysisClient client, final @NotNull CountDownLatch latch, long timeoutInMillis) { if (ApplicationManager.getApplication().isUnitTestMode()) { @@ -2576,7 +2585,7 @@ private static boolean awaitForLatchCheckingCanceled(final @NotNull AnalysisServ long startTime = System.currentTimeMillis(); while (true) { ProgressManager.checkCanceled(); - if (!server.isSocketOpen()) { + if (!client.isSocketOpen()) { return false; } if (timeoutInMillis != -1 && System.currentTimeMillis() > startTime + timeoutInMillis) { @@ -2685,11 +2694,11 @@ public void removeOutlineListener(final @NotNull DartServerData.OutlineListener */ @SuppressWarnings("unused") // for Flutter plugin public String generateUniqueId() { - final RemoteAnalysisServerImpl server = myServer; - if (server == null) { + DartAnalysisClient client = myClient; + if (client == null) { return null; } - return server.generateUniqueId(); + return client.generateUniqueId(); } /** @@ -2697,9 +2706,9 @@ public String generateUniqueId() { */ @SuppressWarnings("unused") // for Flutter plugin public void sendRequest(String id, JsonObject request) { - final RemoteAnalysisServerImpl server = myServer; - if (server != null) { - server.sendRequestToServer(id, request); + DartAnalysisClient client = myClient; + if (client != null) { + client.sendRequestToServer(id, request); } } @@ -2708,9 +2717,9 @@ public void sendRequest(String id, JsonObject request) { */ @SuppressWarnings("unused") // for Flutter plugin public void sendRequestToServer(String id, JsonObject request, com.google.dart.server.Consumer consumer) { - final RemoteAnalysisServerImpl server = myServer; - if (server != null) { - server.sendRequestToServer(id, request, consumer); + DartAnalysisClient client = myClient; + if (client != null) { + client.sendRequestToServer(id, request, consumer); } } @@ -2721,13 +2730,13 @@ public void sendRequestToServer(String id, JsonObject request, com.google.dart.s public void setServerLogSubscription(boolean subscribeToLog) { if (mySubscribeToServerLog != subscribeToLog) { mySubscribeToServerLog = subscribeToLog; - server_setSubscriptions(myServer); + server_setSubscriptions(myClient); } } - private void server_setSubscriptions(@Nullable AnalysisServer server) { - if (server != null) { - server.server_setSubscriptions(mySubscribeToServerLog ? Arrays.asList(ServerService.STATUS, ServerService.LOG) + private void server_setSubscriptions(@Nullable DartAnalysisClient client) { + if (client != null) { + client.server_setSubscriptions(mySubscribeToServerLog ? Arrays.asList(ServerService.STATUS, ServerService.LOG) : Collections.singletonList(ServerService.STATUS)); } } diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/DartServerData.java b/Dart/src/com/jetbrains/lang/dart/analyzer/DartServerData.java index 2edbbc5b5aa..b01b72ec0a3 100644 --- a/Dart/src/com/jetbrains/lang/dart/analyzer/DartServerData.java +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/DartServerData.java @@ -274,7 +274,7 @@ void textDocumentContentDidChange(@NotNull String fileUri) { } ApplicationManager.getApplication().executeOnPooledThread(() -> { - String contents = myService.lspMessage_dart_textDocumentContent(fileUri); + String contents = myService.requestTextDocumentContent(fileUri); if (contents == null || contents.isEmpty()) { myNotLocalFileUriToVirtualFileMap.remove(fileUri); return; diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/DirectLspRequestSender.java b/Dart/src/com/jetbrains/lang/dart/analyzer/DirectLspRequestSender.java new file mode 100644 index 00000000000..a3cc7548d70 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/DirectLspRequestSender.java @@ -0,0 +1,249 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.ExtendedRequestErrorCode; +import com.google.dart.server.JsonConsumer; +import com.google.dart.server.ResponseListener; +import com.google.dart.server.internal.remote.RemoteAnalysisServerImpl; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.concurrency.AppExecutorUtil; +import org.dartlang.analysis.server.protocol.RequestError; +import org.dartlang.analysis.server.protocol.RequestErrorCode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +final class DirectLspRequestSender implements LspRequestSender { + private static final Logger LOG = Logger.getInstance(DirectLspRequestSender.class); + private static final String REQUEST_TIMEOUT_PROPERTY = "dart.analysis.server.lsp.direct.request.timeout.ms"; + private static final long DEFAULT_REQUEST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30); + + private final @NotNull RemoteAnalysisServerImpl myServer; + private final @NotNull Map myPendingRequests = new ConcurrentHashMap<>(); + private final @NotNull ResponseListener myResponseListener = this::onResponse; + private final long myRequestTimeoutMs; + private final @NotNull AtomicBoolean myDisposed = new AtomicBoolean(); + + DirectLspRequestSender(@NotNull RemoteAnalysisServerImpl server) { + myServer = server; + myRequestTimeoutMs = parseRequestTimeoutMs(); + myServer.addResponseListener(myResponseListener); + } + + @Override + public void sendRequest(@NotNull String id, @NotNull JsonObject request, @NotNull JsonConsumer consumer) { + if (myDisposed.get()) { + consumer.onResponse(null, buildInternalRequestError("LSP request sender is already disposed.")); + return; + } + + PendingRequest pendingRequest = new PendingRequest(consumer); + PendingRequest previousRequest = myPendingRequests.putIfAbsent(id, pendingRequest); + if (previousRequest != null) { + consumer.onResponse(null, new RequestError(RequestErrorCode.INVALID_REQUEST, "Duplicate request id: " + id, null)); + return; + } + + if (myDisposed.get()) { + if (myPendingRequests.remove(id, pendingRequest)) { + pendingRequest.onResponse(null, buildInternalRequestError("LSP request sender is already disposed.")); + } + return; + } + + try { + pendingRequest.setTimeoutFuture(AppExecutorUtil.getAppScheduledExecutorService().schedule( + () -> onRequestTimeout(id, pendingRequest), + myRequestTimeoutMs, + TimeUnit.MILLISECONDS + )); + } + catch (RuntimeException e) { + myPendingRequests.remove(id, pendingRequest); + pendingRequest.onResponse(null, buildInternalRequestError("Failed to schedule direct LSP request timeout.", e)); + return; + } + + try { + myServer.sendRequestToServer(id, request); + } + catch (RuntimeException e) { + if (myPendingRequests.remove(id, pendingRequest)) { + pendingRequest.cancelTimeout(); + pendingRequest.onResponse(null, buildInternalRequestError("Failed to send direct LSP request.", e)); + } + } + } + + @Override + public void sendNotification(@NotNull JsonObject notification) { + if (myDisposed.get()) return; + try { + myServer.sendNotification(notification); + } + catch (RuntimeException e) { + LOG.warn("Failed to send LSP notification", e); + } + } + + @Override + public void dispose() { + if (!myDisposed.compareAndSet(false, true)) return; + + myServer.removeResponseListener(myResponseListener); + + RequestError disposeError = buildInternalRequestError("Direct LSP request sender was disposed."); + myPendingRequests.forEach((id, pendingRequest) -> { + if (myPendingRequests.remove(id, pendingRequest)) { + pendingRequest.cancelTimeout(); + pendingRequest.onResponse(null, disposeError); + } + }); + } + + private void onResponse(@NotNull String responseJson) { + JsonObject responseObject = parseResponseObject(responseJson); + if (responseObject == null) return; + + String requestId = parseResponseId(responseObject); + if (requestId == null) return; + + PendingRequest pendingRequest = myPendingRequests.remove(requestId); + if (pendingRequest == null) return; + pendingRequest.cancelTimeout(); + + JsonObject resultObject = parseResultObject(responseObject); + RequestError requestError = parseRequestError(responseObject); + pendingRequest.onResponse(resultObject, requestError); + } + + private static @Nullable JsonObject parseResponseObject(@NotNull String responseJson) { + JsonElement jsonElement; + try { + jsonElement = JsonParser.parseString(responseJson); + } + catch (JsonSyntaxException ignored) { + return null; + } + + if (!jsonElement.isJsonObject()) { + return null; + } + + return jsonElement.getAsJsonObject(); + } + + private static @Nullable String parseResponseId(@NotNull JsonObject responseObject) { + JsonElement idElement = responseObject.get("id"); + if (!(idElement instanceof JsonPrimitive)) { + return null; + } + return idElement.getAsString(); + } + + static final @NotNull String LSP_ARRAY_RESULT_KEY = "__lsp_array_result"; + + private static @Nullable JsonObject parseResultObject(@NotNull JsonObject responseObject) { + JsonElement resultElement = responseObject.get("result"); + if (resultElement instanceof JsonObject) { + return resultElement.getAsJsonObject(); + } + if (resultElement instanceof JsonArray) { + JsonObject wrapper = new JsonObject(); + wrapper.add(LSP_ARRAY_RESULT_KEY, resultElement); + return wrapper; + } + return null; + } + + private static @Nullable RequestError parseRequestError(@NotNull JsonObject responseObject) { + JsonElement errorElement = responseObject.get("error"); + if (!(errorElement instanceof JsonObject)) { + return null; + } + + try { + return RequestError.fromJson(errorElement.getAsJsonObject()); + } + catch (RuntimeException e) { + return buildInternalRequestError("Failed to parse response error payload.", e); + } + } + + private static @NotNull RequestError buildInternalRequestError(@NotNull String message) { + return buildInternalRequestError(message, null); + } + + private static @NotNull RequestError buildInternalRequestError(@NotNull String message, @Nullable Throwable throwable) { + if (throwable != null) { + LOG.info(message, throwable); + } + return new RequestError(ExtendedRequestErrorCode.INVALID_SERVER_RESPONSE, message, throwable != null ? throwable.toString() : null); + } + + private void onRequestTimeout(@NotNull String requestId, @NotNull PendingRequest pendingRequest) { + if (!myPendingRequests.remove(requestId, pendingRequest)) return; + + pendingRequest.onResponse( + null, + buildInternalRequestError("Direct LSP request timed out after " + myRequestTimeoutMs + " ms: id=" + requestId) + ); + } + + private static long parseRequestTimeoutMs() { + String timeoutPropertyValue = System.getProperty(REQUEST_TIMEOUT_PROPERTY); + if (timeoutPropertyValue == null || timeoutPropertyValue.isBlank()) { + return DEFAULT_REQUEST_TIMEOUT_MS; + } + + try { + long parsedTimeoutMs = Long.parseLong(timeoutPropertyValue); + if (parsedTimeoutMs > 0) { + return parsedTimeoutMs; + } + + LOG.warn("Property '" + REQUEST_TIMEOUT_PROPERTY + "' must be > 0, but was '" + timeoutPropertyValue + + "'. Using default " + DEFAULT_REQUEST_TIMEOUT_MS + " ms."); + } + catch (NumberFormatException e) { + LOG.warn("Property '" + REQUEST_TIMEOUT_PROPERTY + "' is not a number: '" + timeoutPropertyValue + + "'. Using default " + DEFAULT_REQUEST_TIMEOUT_MS + " ms."); + } + return DEFAULT_REQUEST_TIMEOUT_MS; + } + + private static final class PendingRequest { + private final @NotNull JsonConsumer myConsumer; + private volatile @Nullable ScheduledFuture myTimeoutFuture; + + private PendingRequest(@NotNull JsonConsumer consumer) { + myConsumer = consumer; + } + + private void setTimeoutFuture(@NotNull ScheduledFuture timeoutFuture) { + myTimeoutFuture = timeoutFuture; + } + + private void cancelTimeout() { + ScheduledFuture timeoutFuture = myTimeoutFuture; + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + } + } + + private void onResponse(@Nullable JsonObject resultObject, @Nullable RequestError requestError) { + myConsumer.onResponse(resultObject, requestError); + } + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/DirectLspTransport.java b/Dart/src/com/jetbrains/lang/dart/analyzer/DirectLspTransport.java new file mode 100644 index 00000000000..2bed40425ba --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/DirectLspTransport.java @@ -0,0 +1,414 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.GetServerPortConsumer; +import com.google.dart.server.JsonConsumer; +import com.google.dart.server.UpdateContentConsumer; +import com.google.dart.server.internal.remote.utilities.RequestUtilities; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.intellij.openapi.diagnostic.Logger; +import org.dartlang.analysis.server.protocol.AddContentOverlay; +import org.dartlang.analysis.server.protocol.AnalysisOptions; +import org.dartlang.analysis.server.protocol.RemoveContentOverlay; +import org.dartlang.analysis.server.protocol.RequestError; +import org.dartlang.analysis.server.protocol.SourceEdit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +final class DirectLspTransport implements LspTransport { + private static final Logger LOG = Logger.getInstance(DirectLspTransport.class); + + private static final AtomicLong NEXT_ID = new AtomicLong(1); + private static final String REQUEST_ID_PREFIX = "dart-lsp-direct-"; + + private static final String DART_TEXT_DOCUMENT_CONTENT_METHOD = "dart/textDocumentContent"; + private static final String DART_CONNECT_TO_DTD_METHOD = "dart/connectToDtd"; + private static final String DART_REANALYZE_METHOD = "dart/reanalyze"; + private static final String DART_DIAGNOSTIC_SERVER_METHOD = "dart/diagnosticServer"; + private static final String TEXT_DOCUMENT_FORMATTING_METHOD = "textDocument/formatting"; + + private final @NotNull LspRequestSender myRequestSender; + private final @NotNull LspTransport myFallbackTransport; + private final @NotNull Map myOpenFileVersions = new ConcurrentHashMap<>(); + private final @NotNull Set myDisabledDirectMethods = ConcurrentHashMap.newKeySet(); + private final @NotNull Set myBridgeRoutingLoggedMethods = ConcurrentHashMap.newKeySet(); + private final @NotNull AtomicLong myDirectAttempts = new AtomicLong(); + private final @NotNull AtomicLong myDirectSuccess = new AtomicLong(); + private final @NotNull AtomicLong myFallbackByError = new AtomicLong(); + private final @NotNull AtomicLong myFallbackByShape = new AtomicLong(); + private final @NotNull AtomicBoolean myDisposed = new AtomicBoolean(); + + DirectLspTransport(@NotNull LspRequestSender requestSender, @NotNull LspTransport fallbackTransport) { + myRequestSender = requestSender; + myFallbackTransport = fallbackTransport; + } + + @Override + public void requestTextDocumentContent(@NotNull String fileUri, @NotNull DartAnalysisClient.TextDocumentContentHandler handler) { + if (routeToBridgeIfDirectModeDisabled(DART_TEXT_DOCUMENT_CONTENT_METHOD, + () -> myFallbackTransport.requestTextDocumentContent(fileUri, handler))) { + return; + } + + myDirectAttempts.incrementAndGet(); + String id = nextRequestId(); + LOG.info("[direct-lsp] Sending dart/textDocumentContent request: id=" + id + ", uri=" + fileUri); + myRequestSender.sendRequest(id, LspRequestBuilders.buildTextDocumentContentRequest(id, fileUri), new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + myFallbackByError.incrementAndGet(); + // If direct request mode isn't supported by the server setup, fallback to bridge mode. + LOG.info("[direct-lsp] Falling back to bridge transport due to direct request error: id=" + id + + ", method=" + DART_TEXT_DOCUMENT_CONTENT_METHOD + + ", code=" + requestError.getCode() + ", message=" + requestError.getMessage()); + disableDirectModeForMethod(DART_TEXT_DOCUMENT_CONTENT_METHOD, "request error"); + myFallbackTransport.requestTextDocumentContent(fileUri, handler); + return; + } + + if (resultObject == null || !resultObject.has("content")) { + myFallbackByShape.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to unexpected direct response shape: id=" + id + + ", method=" + DART_TEXT_DOCUMENT_CONTENT_METHOD + + ", hasResult=" + (resultObject != null)); + disableDirectModeForMethod(DART_TEXT_DOCUMENT_CONTENT_METHOD, "unexpected response shape"); + myFallbackTransport.requestTextDocumentContent(fileUri, handler); + return; + } + + String content; + try { + content = resultObject.get("content").getAsString(); + } + catch (RuntimeException contentParseError) { + myFallbackByShape.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to invalid direct response content type: id=" + id + + ", method=" + DART_TEXT_DOCUMENT_CONTENT_METHOD, contentParseError); + disableDirectModeForMethod(DART_TEXT_DOCUMENT_CONTENT_METHOD, "invalid response content type"); + myFallbackTransport.requestTextDocumentContent(fileUri, handler); + return; + } + + myDirectSuccess.incrementAndGet(); + LOG.info("[direct-lsp] Direct request succeeded: id=" + id + ", method=" + DART_TEXT_DOCUMENT_CONTENT_METHOD); + handler.computedDocumentContents(content); + } + }); + } + + @Override + public void connectToDtd(@NotNull String uri) { + if (routeToBridgeIfDirectModeDisabled(DART_CONNECT_TO_DTD_METHOD, () -> myFallbackTransport.connectToDtd(uri))) return; + + String id = nextRequestId(); + sendDirectVoidRequest(DART_CONNECT_TO_DTD_METHOD, + id, + LspRequestBuilders.buildConnectToDtdRequest(id, uri), + ", uri=" + uri, + () -> myFallbackTransport.connectToDtd(uri), + () -> { + }); + } + + @Override + public void configureServerFeatures(boolean supportsUris, boolean supportsWorkspaceApplyEdits) { + if (routeToBridgeIfDirectModeDisabled(ANALYSIS_UPDATE_OPTIONS_METHOD, + () -> myFallbackTransport.configureServerFeatures(supportsUris, supportsWorkspaceApplyEdits))) { + return; + } + if (routeToBridgeIfDirectModeDisabled(SERVER_SET_CLIENT_CAPABILITIES_METHOD, + () -> myFallbackTransport.configureServerFeatures(supportsUris, supportsWorkspaceApplyEdits))) { + return; + } + + String optionsRequestId = nextRequestId(); + sendDirectVoidRequest(ANALYSIS_UPDATE_OPTIONS_METHOD, + optionsRequestId, + RequestUtilities.generateAnalysisUpdateOptions(optionsRequestId, new AnalysisOptions(true, true, true, true, false, true, false)), + "", + () -> myFallbackTransport.configureServerFeatures(supportsUris, supportsWorkspaceApplyEdits), + () -> { + String capabilitiesRequestId = nextRequestId(); + sendDirectVoidRequest(SERVER_SET_CLIENT_CAPABILITIES_METHOD, + capabilitiesRequestId, + RequestUtilities.generateClientCapabilities(capabilitiesRequestId, CLIENT_CAPABILITIES, supportsUris, supportsWorkspaceApplyEdits), + ", supportsUris=" + supportsUris + ", supportsWorkspaceApplyEdits=" + supportsWorkspaceApplyEdits, + () -> myFallbackTransport.configureServerFeatures(supportsUris, supportsWorkspaceApplyEdits), + () -> { + }); + }); + } + + @Override + public void reanalyze() { + if (routeToBridgeIfDirectModeDisabled(DART_REANALYZE_METHOD, () -> myFallbackTransport.reanalyze())) return; + + String id = nextRequestId(); + sendDirectVoidRequest(DART_REANALYZE_METHOD, + id, + LspRequestBuilders.buildReanalyzeRequest(id), + "", + () -> myFallbackTransport.reanalyze(), + () -> { + }); + } + + @Override + public void diagnosticServer(@NotNull GetServerPortConsumer consumer) { + if (routeToBridgeIfDirectModeDisabled(DART_DIAGNOSTIC_SERVER_METHOD, () -> myFallbackTransport.diagnosticServer(consumer))) return; + + myDirectAttempts.incrementAndGet(); + String id = nextRequestId(); + LOG.info("[direct-lsp] Sending dart/diagnosticServer request: id=" + id); + myRequestSender.sendRequest(id, LspRequestBuilders.buildDiagnosticServerRequest(id), new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + myFallbackByError.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to direct request error: id=" + id + + ", method=" + DART_DIAGNOSTIC_SERVER_METHOD + + ", code=" + requestError.getCode() + ", message=" + requestError.getMessage()); + disableDirectModeForMethod(DART_DIAGNOSTIC_SERVER_METHOD, "request error"); + myFallbackTransport.diagnosticServer(consumer); + return; + } + + if (resultObject == null || !resultObject.has("port")) { + myFallbackByShape.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to unexpected direct response shape: id=" + id + + ", method=" + DART_DIAGNOSTIC_SERVER_METHOD + + ", hasResult=" + (resultObject != null)); + disableDirectModeForMethod(DART_DIAGNOSTIC_SERVER_METHOD, "unexpected response shape"); + myFallbackTransport.diagnosticServer(consumer); + return; + } + + try { + int port = resultObject.get("port").getAsInt(); + myDirectSuccess.incrementAndGet(); + LOG.info("[direct-lsp] Direct request succeeded: id=" + id + ", method=" + DART_DIAGNOSTIC_SERVER_METHOD); + consumer.computedServerPort(port); + } + catch (RuntimeException e) { + myFallbackByShape.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to invalid response content: id=" + id + + ", method=" + DART_DIAGNOSTIC_SERVER_METHOD, e); + disableDirectModeForMethod(DART_DIAGNOSTIC_SERVER_METHOD, "invalid response content"); + myFallbackTransport.diagnosticServer(consumer); + } + } + }); + } + + @Override + public void updateContent(@NotNull Map files, @NotNull UpdateContentConsumer consumer) { + for (Map.Entry entry : files.entrySet()) { + String fileUri = entry.getKey(); + Object overlay = entry.getValue(); + + if (overlay instanceof AddContentOverlay addOverlay) { + String content = addOverlay.getContent(); + String languageId = getLanguageId(fileUri); + Integer existingVersion = myOpenFileVersions.get(fileUri); + if (existingVersion == null) { + myOpenFileVersions.put(fileUri, 1); + myRequestSender.sendNotification(LspRequestBuilders.buildDidOpenNotification(fileUri, languageId, 1, content)); + } + else { + int newVersion = existingVersion + 1; + myOpenFileVersions.put(fileUri, newVersion); + myRequestSender.sendNotification(LspRequestBuilders.buildDidChangeNotification(fileUri, newVersion, content)); + } + } + else if (overlay instanceof RemoveContentOverlay) { + myOpenFileVersions.remove(fileUri); + myRequestSender.sendNotification(LspRequestBuilders.buildDidCloseNotification(fileUri)); + } + } + consumer.onResponse(); + } + + @Override + public void format(@NotNull String fileUri, + @NotNull String fileContent, + int selectionOffset, + int selectionLength, + int lineLength, + @NotNull FormatHandler handler) { + if (routeToBridgeIfDirectModeDisabled(TEXT_DOCUMENT_FORMATTING_METHOD, + () -> myFallbackTransport.format(fileUri, fileContent, selectionOffset, selectionLength, lineLength, handler))) { + return; + } + + myDirectAttempts.incrementAndGet(); + String id = nextRequestId(); + LOG.info("[direct-lsp] Sending textDocument/formatting request: id=" + id + ", uri=" + fileUri); + myRequestSender.sendRequest(id, LspRequestBuilders.buildFormattingRequest(id, fileUri), new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + myFallbackByError.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to direct request error: id=" + id + + ", method=" + TEXT_DOCUMENT_FORMATTING_METHOD + + ", code=" + requestError.getCode() + ", message=" + requestError.getMessage()); + disableDirectModeForMethod(TEXT_DOCUMENT_FORMATTING_METHOD, "request error"); + myFallbackTransport.format(fileUri, fileContent, selectionOffset, selectionLength, lineLength, handler); + return; + } + + if (resultObject == null || !resultObject.has(DirectLspRequestSender.LSP_ARRAY_RESULT_KEY)) { + myFallbackByShape.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to unexpected direct response shape: id=" + id + + ", method=" + TEXT_DOCUMENT_FORMATTING_METHOD + + ", hasResult=" + (resultObject != null)); + disableDirectModeForMethod(TEXT_DOCUMENT_FORMATTING_METHOD, "unexpected response shape"); + myFallbackTransport.format(fileUri, fileContent, selectionOffset, selectionLength, lineLength, handler); + return; + } + + try { + JsonArray textEdits = resultObject.getAsJsonArray(DirectLspRequestSender.LSP_ARRAY_RESULT_KEY); + List sourceEdits = convertTextEditsToSourceEdits(textEdits, fileContent); + myDirectSuccess.incrementAndGet(); + LOG.info("[direct-lsp] Direct request succeeded: id=" + id + ", method=" + TEXT_DOCUMENT_FORMATTING_METHOD); + handler.computedFormat(sourceEdits, selectionOffset, selectionLength); + } + catch (RuntimeException e) { + myFallbackByShape.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to invalid response content: id=" + id + + ", method=" + TEXT_DOCUMENT_FORMATTING_METHOD, e); + disableDirectModeForMethod(TEXT_DOCUMENT_FORMATTING_METHOD, "invalid response content"); + myFallbackTransport.format(fileUri, fileContent, selectionOffset, selectionLength, lineLength, handler); + } + } + }); + } + + @Override + public void dispose() { + if (!myDisposed.compareAndSet(false, true)) return; + + myOpenFileVersions.clear(); + LOG.info("[direct-lsp] session summary: attempts=" + myDirectAttempts.get() + + ", success=" + myDirectSuccess.get() + + ", fallbackError=" + myFallbackByError.get() + + ", fallbackShape=" + myFallbackByShape.get() + + ", disabledMethods=" + myDisabledDirectMethods); + try { + myRequestSender.dispose(); + } + finally { + myFallbackTransport.dispose(); + } + } + + private boolean routeToBridgeIfDirectModeDisabled(@NotNull String method, @NotNull Runnable fallbackAction) { + if (!myDisabledDirectMethods.contains(method)) { + return false; + } + + if (myBridgeRoutingLoggedMethods.add(method)) { + LOG.info("[direct-lsp] Direct mode is disabled for method '" + method + "'. Routing requests to bridge transport."); + } + fallbackAction.run(); + return true; + } + + private void sendDirectVoidRequest(@NotNull String method, + @NotNull String id, + @NotNull JsonObject request, + @NotNull String requestSuffix, + @NotNull Runnable fallbackAction, + @NotNull Runnable onSuccess) { + if (routeToBridgeIfDirectModeDisabled(method, fallbackAction)) return; + + myDirectAttempts.incrementAndGet(); + LOG.info("[direct-lsp] Sending " + method + " request: id=" + id + requestSuffix); + myRequestSender.sendRequest(id, request, new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + myFallbackByError.incrementAndGet(); + LOG.info("[direct-lsp] Falling back to bridge transport due to direct request error: id=" + id + + ", method=" + method + + ", code=" + requestError.getCode() + ", message=" + requestError.getMessage()); + disableDirectModeForMethod(method, "request error"); + fallbackAction.run(); + return; + } + + myDirectSuccess.incrementAndGet(); + LOG.info("[direct-lsp] Direct request succeeded: id=" + id + ", method=" + method); + onSuccess.run(); + } + }); + } + + private static @NotNull String nextRequestId() { + return REQUEST_ID_PREFIX + NEXT_ID.getAndIncrement(); + } + + private void disableDirectModeForMethod(@NotNull String method, @NotNull String reason) { + if (!myDisabledDirectMethods.add(method)) return; + + LOG.warn("[direct-lsp] Disabling direct mode for method '" + method + "' after " + reason + + ". attempts=" + myDirectAttempts.get() + + ", success=" + myDirectSuccess.get() + + ", fallbackError=" + myFallbackByError.get() + + ", fallbackShape=" + myFallbackByShape.get()); + } + + private static @NotNull List convertTextEditsToSourceEdits(@NotNull JsonArray textEdits, + @NotNull String fileContent) { + int[] lineOffsets = computeLineOffsets(fileContent); + List result = new ArrayList<>(textEdits.size()); + for (JsonElement element : textEdits) { + JsonObject textEdit = element.getAsJsonObject(); + JsonObject range = textEdit.getAsJsonObject("range"); + JsonObject start = range.getAsJsonObject("start"); + JsonObject end = range.getAsJsonObject("end"); + + int startOffset = lineCharToOffset(lineOffsets, start.get("line").getAsInt(), start.get("character").getAsInt()); + int endOffset = lineCharToOffset(lineOffsets, end.get("line").getAsInt(), end.get("character").getAsInt()); + String newText = textEdit.get("newText").getAsString(); + + result.add(new SourceEdit(startOffset, endOffset - startOffset, newText, null, null)); + } + return result; + } + + private static int[] computeLineOffsets(@NotNull String content) { + List offsets = new ArrayList<>(); + offsets.add(0); + for (int i = 0; i < content.length(); i++) { + if (content.charAt(i) == '\n') { + offsets.add(i + 1); + } + } + return offsets.stream().mapToInt(Integer::intValue).toArray(); + } + + private static int lineCharToOffset(int[] lineOffsets, int line, int character) { + if (line >= lineOffsets.length) { + return lineOffsets[lineOffsets.length - 1]; + } + return lineOffsets[line] + character; + } + + private static @NotNull String getLanguageId(@NotNull String fileUri) { + if (fileUri.endsWith(".dart")) return "dart"; + if (fileUri.endsWith(".yaml") || fileUri.endsWith(".yml")) return "yaml"; + return "plaintext"; + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyBridgeLspRequestSender.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyBridgeLspRequestSender.java new file mode 100644 index 00000000000..fdab0241c14 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyBridgeLspRequestSender.java @@ -0,0 +1,105 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.ExtendedRequestErrorCode; +import com.google.dart.server.JsonConsumer; +import com.google.dart.server.internal.remote.RemoteAnalysisServerImpl; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.dartlang.analysis.server.protocol.RequestError; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class LegacyBridgeLspRequestSender implements LspRequestSender { + private static final String METHOD_PROPERTY = "method"; + private static final String LSP_HANDLE_METHOD = "lsp.handle"; + + private final @NotNull RemoteAnalysisServerImpl myLegacyBridgeServer; + + LegacyBridgeLspRequestSender(@NotNull RemoteAnalysisServerImpl legacyBridgeServer) { + myLegacyBridgeServer = legacyBridgeServer; + } + + @Override + public void sendRequest(@NotNull String id, @NotNull JsonObject request, @NotNull JsonConsumer consumer) { + JsonObject wrappedRequest = isAlreadyWrappedWithLspHandle(request) ? request : LspRequestBuilders.wrapWithLspHandle(id, request); + myLegacyBridgeServer.sendRequestToServer(id, wrappedRequest, new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + consumer.onResponse(null, requestError); + return; + } + + JsonObject lspResponse = extractLspResponse(resultObject); + if (lspResponse == null) { + consumer.onResponse(null, buildInvalidServerResponseError("Missing lspResponse in lsp.handle response.")); + return; + } + + RequestError lspError = parseLspError(lspResponse); + if (lspError != null) { + consumer.onResponse(null, lspError); + return; + } + + JsonObject lspResult = parseLspResult(lspResponse); + consumer.onResponse(lspResult, null); + } + }); + } + + @Override + public void sendNotification(@NotNull JsonObject notification) { + // Legacy bridge transport does not support LSP notifications directly. + } + + @Override + public void dispose() { + } + + private static @Nullable JsonObject extractLspResponse(@Nullable JsonObject resultObject) { + if (resultObject == null) { + return null; + } + + JsonObject wrappedResponse = resultObject.getAsJsonObject("lspResponse"); + if (wrappedResponse != null) { + return wrappedResponse; + } + + // Defensive fallback: some senders may already provide the unwrapped LSP response object. + return resultObject.has("jsonrpc") && (resultObject.has("result") || resultObject.has("error")) ? resultObject : null; + } + + private static @Nullable JsonObject parseLspResult(@NotNull JsonObject lspResponse) { + JsonElement resultElement = lspResponse.get("result"); + return resultElement instanceof JsonObject ? resultElement.getAsJsonObject() : null; + } + + private static @Nullable RequestError parseLspError(@NotNull JsonObject lspResponse) { + JsonElement errorElement = lspResponse.get("error"); + if (!(errorElement instanceof JsonObject)) { + return null; + } + try { + return RequestError.fromJson(errorElement.getAsJsonObject()); + } + catch (RuntimeException e) { + return buildInvalidServerResponseError("Failed to parse LSP error payload.", e); + } + } + + private static @NotNull RequestError buildInvalidServerResponseError(@NotNull String message) { + return buildInvalidServerResponseError(message, null); + } + + private static @NotNull RequestError buildInvalidServerResponseError(@NotNull String message, @Nullable Throwable throwable) { + return new RequestError(ExtendedRequestErrorCode.INVALID_SERVER_RESPONSE, message, throwable != null ? throwable.toString() : null); + } + + private static boolean isAlreadyWrappedWithLspHandle(@NotNull JsonObject request) { + JsonElement methodElement = request.get(METHOD_PROPERTY); + return methodElement != null && methodElement.isJsonPrimitive() && LSP_HANDLE_METHOD.equals(methodElement.getAsString()); + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyBridgeLspTransport.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyBridgeLspTransport.java new file mode 100644 index 00000000000..829e28daff7 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyBridgeLspTransport.java @@ -0,0 +1,171 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.ExtendedRequestErrorCode; +import com.google.dart.server.FormatConsumer; +import com.google.dart.server.GetServerPortConsumer; +import com.google.dart.server.JsonConsumer; +import com.google.dart.server.UpdateContentConsumer; +import com.google.dart.server.internal.remote.RemoteAnalysisServerImpl; +import com.google.dart.server.internal.remote.utilities.RequestUtilities; +import com.google.gson.JsonObject; +import com.intellij.openapi.diagnostic.Logger; +import org.dartlang.analysis.server.protocol.AnalysisOptions; +import org.dartlang.analysis.server.protocol.RequestError; +import org.dartlang.analysis.server.protocol.SourceEdit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +final class LegacyBridgeLspTransport implements LspTransport { + private static final Logger LOG = Logger.getInstance(LegacyBridgeLspTransport.class); + + private final @NotNull RemoteAnalysisServerImpl myLegacyBridgeServer; + private final @NotNull LspRequestSender myRequestSender; + + LegacyBridgeLspTransport(@NotNull RemoteAnalysisServerImpl legacyBridgeServer) { + myLegacyBridgeServer = legacyBridgeServer; + myRequestSender = new LegacyBridgeLspRequestSender(legacyBridgeServer); + } + + @Override + public void requestTextDocumentContent(@NotNull String fileUri, @NotNull DartAnalysisClient.TextDocumentContentHandler handler) { + String id = myLegacyBridgeServer.generateUniqueId(); + JsonObject request = LspRequestBuilders.buildTextDocumentContentRequest(id, fileUri); + myRequestSender.sendRequest(id, request, new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + handler.onError(requestError); + return; + } + + if (resultObject == null || !resultObject.has("content")) { + handler.onError(buildInvalidServerResponseError("Missing 'content' in response for dart/textDocumentContent.")); + return; + } + + try { + handler.computedDocumentContents(resultObject.get("content").getAsString()); + } + catch (RuntimeException e) { + handler.onError(buildInvalidServerResponseError("Invalid 'content' in response for dart/textDocumentContent.", e)); + } + } + }); + } + + @Override + public void connectToDtd(@NotNull String uri) { + String id = myLegacyBridgeServer.generateUniqueId(); + JsonObject request = LspRequestBuilders.buildConnectToDtdRequest(id, uri); + myRequestSender.sendRequest(id, request, new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + LOG.info("Failed to connect to DTD via legacy bridge sender: code=" + requestError.getCode() + ", message=" + requestError.getMessage()); + } + } + }); + } + + @Override + public void configureServerFeatures(boolean supportsUris, boolean supportsWorkspaceApplyEdits) { + String optionsRequestId = myLegacyBridgeServer.generateUniqueId(); + JsonObject optionsRequest = + RequestUtilities.generateAnalysisUpdateOptions(optionsRequestId, new AnalysisOptions(true, true, true, true, false, true, false)); + + sendVoidRequest(ANALYSIS_UPDATE_OPTIONS_METHOD, optionsRequestId, optionsRequest, () -> { + String capabilitiesRequestId = myLegacyBridgeServer.generateUniqueId(); + JsonObject capabilitiesRequest = RequestUtilities.generateClientCapabilities(capabilitiesRequestId, + CLIENT_CAPABILITIES, + supportsUris, + supportsWorkspaceApplyEdits); + sendVoidRequest(SERVER_SET_CLIENT_CAPABILITIES_METHOD, capabilitiesRequestId, capabilitiesRequest, null); + }); + } + + @Override + public void reanalyze() { + myLegacyBridgeServer.analysis_reanalyze(); + } + + @Override + public void diagnosticServer(@NotNull GetServerPortConsumer consumer) { + myLegacyBridgeServer.diagnostic_getServerPort(consumer); + } + + @Override + public void updateContent(@NotNull Map files, @NotNull UpdateContentConsumer consumer) { + myLegacyBridgeServer.analysis_updateContent(files, consumer); + } + + @Override + public void format(@NotNull String fileUri, + @NotNull String fileContent, + int selectionOffset, + int selectionLength, + int lineLength, + @NotNull FormatHandler handler) { + // The legacy bridge operates on file paths, not URIs. Convert the URI to a path for the call. + String filePath = uriToPath(fileUri); + myLegacyBridgeServer.edit_format(filePath, selectionOffset, selectionLength, lineLength, new FormatConsumer() { + @Override + public void computedFormat(List edits, int selOffset, int selLen) { + handler.computedFormat(edits, selOffset, selLen); + } + + @Override + public void onError(RequestError requestError) { + handler.onError(requestError); + } + }); + } + + @Override + public void dispose() { + myRequestSender.dispose(); + } + + private static @NotNull String uriToPath(@NotNull String fileUri) { + if (fileUri.startsWith("file://")) { + try { + return new java.net.URI(fileUri).getPath(); + } + catch (java.net.URISyntaxException ignored) { + } + } + return fileUri; + } + + private static @NotNull RequestError buildInvalidServerResponseError(@NotNull String message) { + return buildInvalidServerResponseError(message, null); + } + + private static @NotNull RequestError buildInvalidServerResponseError(@NotNull String message, @Nullable Throwable throwable) { + return new RequestError(ExtendedRequestErrorCode.INVALID_SERVER_RESPONSE, message, throwable != null ? throwable.toString() : null); + } + + private void sendVoidRequest(@NotNull String method, + @NotNull String id, + @NotNull JsonObject request, + @Nullable Runnable onSuccess) { + myLegacyBridgeServer.sendRequestToServer(id, request, new JsonConsumer() { + @Override + public void onResponse(JsonObject resultObject, RequestError requestError) { + if (requestError != null) { + LOG.info("Failed request via legacy bridge transport: method=" + method + + ", code=" + requestError.getCode() + + ", message=" + requestError.getMessage()); + return; + } + + if (onSuccess != null) { + onSuccess.run(); + } + } + }); + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyDartAnalysisClient.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyDartAnalysisClient.java new file mode 100644 index 00000000000..02fca444f1f --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LegacyDartAnalysisClient.java @@ -0,0 +1,17 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.AnalysisServerSocket; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +final class LegacyDartAnalysisClient extends AbstractDartAnalysisClient { + LegacyDartAnalysisClient(@NotNull Project project, @NotNull AnalysisServerSocket socket) { + super(project, socket, LegacyBridgeLspTransport::new); + } + + @Override + public @NotNull String getProtocolName() { + return "legacy"; + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LspDartAnalysisClient.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LspDartAnalysisClient.java new file mode 100644 index 00000000000..635122611ca --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LspDartAnalysisClient.java @@ -0,0 +1,17 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.AnalysisServerSocket; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +final class LspDartAnalysisClient extends AbstractDartAnalysisClient { + LspDartAnalysisClient(@NotNull Project project, @NotNull AnalysisServerSocket socket) { + super(project, socket, LspTransportFactory::create); + } + + @Override + public @NotNull String getProtocolName() { + return "lsp"; + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LspRequestBuilders.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LspRequestBuilders.java new file mode 100644 index 00000000000..a56ea731440 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LspRequestBuilders.java @@ -0,0 +1,120 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; + +final class LspRequestBuilders { + private static final String LSP_JSONRPC_VERSION = "2.0"; + private static final String LSP_HANDLE_METHOD = "lsp.handle"; + + private LspRequestBuilders() { + } + + static @NotNull JsonObject buildTextDocumentContentRequest(@NotNull String id, @NotNull String fileUri) { + JsonObject params = new JsonObject(); + params.addProperty("uri", fileUri); + return buildLspRequest(id, "dart/textDocumentContent", params); + } + + static @NotNull JsonObject buildConnectToDtdRequest(@NotNull String id, @NotNull String uri) { + JsonObject params = new JsonObject(); + params.addProperty("uri", uri); + return buildLspRequest(id, "dart/connectToDtd", params); + } + + static @NotNull JsonObject buildReanalyzeRequest(@NotNull String id) { + return buildLspRequest(id, "dart/reanalyze", new JsonObject()); + } + + static @NotNull JsonObject buildDiagnosticServerRequest(@NotNull String id) { + return buildLspRequest(id, "dart/diagnosticServer", new JsonObject()); + } + + static @NotNull JsonObject buildDidOpenNotification(@NotNull String fileUri, + @NotNull String languageId, + int version, + @NotNull String text) { + JsonObject textDocument = new JsonObject(); + textDocument.addProperty("uri", fileUri); + textDocument.addProperty("languageId", languageId); + textDocument.addProperty("version", version); + textDocument.addProperty("text", text); + + JsonObject params = new JsonObject(); + params.add("textDocument", textDocument); + + return buildLspNotification("textDocument/didOpen", params); + } + + static @NotNull JsonObject buildDidChangeNotification(@NotNull String fileUri, int version, @NotNull String text) { + JsonObject textDocument = new JsonObject(); + textDocument.addProperty("uri", fileUri); + textDocument.addProperty("version", version); + + JsonObject change = new JsonObject(); + change.addProperty("text", text); + JsonArray contentChanges = new JsonArray(); + contentChanges.add(change); + + JsonObject params = new JsonObject(); + params.add("textDocument", textDocument); + params.add("contentChanges", contentChanges); + + return buildLspNotification("textDocument/didChange", params); + } + + static @NotNull JsonObject buildDidCloseNotification(@NotNull String fileUri) { + JsonObject textDocument = new JsonObject(); + textDocument.addProperty("uri", fileUri); + + JsonObject params = new JsonObject(); + params.add("textDocument", textDocument); + + return buildLspNotification("textDocument/didClose", params); + } + + static @NotNull JsonObject buildFormattingRequest(@NotNull String id, @NotNull String fileUri) { + JsonObject textDocument = new JsonObject(); + textDocument.addProperty("uri", fileUri); + + JsonObject options = new JsonObject(); + options.addProperty("tabSize", 2); + options.addProperty("insertSpaces", true); + + JsonObject params = new JsonObject(); + params.add("textDocument", textDocument); + params.add("options", options); + + return buildLspRequest(id, "textDocument/formatting", params); + } + + static @NotNull JsonObject wrapWithLspHandle(@NotNull String id, @NotNull JsonObject lspRequest) { + JsonObject requestParams = new JsonObject(); + requestParams.add("lspMessage", lspRequest); + + JsonObject request = new JsonObject(); + request.addProperty("id", id); + request.addProperty("method", LSP_HANDLE_METHOD); + request.add("params", requestParams); + return request; + } + + private static @NotNull JsonObject buildLspRequest(@NotNull String id, @NotNull String method, @NotNull JsonObject params) { + JsonObject request = new JsonObject(); + request.addProperty("id", id); + request.addProperty("jsonrpc", LSP_JSONRPC_VERSION); + request.addProperty("method", method); + request.add("params", params); + return request; + } + + private static @NotNull JsonObject buildLspNotification(@NotNull String method, @NotNull JsonObject params) { + JsonObject notification = new JsonObject(); + notification.addProperty("jsonrpc", LSP_JSONRPC_VERSION); + notification.addProperty("method", method); + notification.add("params", params); + return notification; + } +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LspRequestSender.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LspRequestSender.java new file mode 100644 index 00000000000..7351596e501 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LspRequestSender.java @@ -0,0 +1,14 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.JsonConsumer; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; + +interface LspRequestSender { + void sendRequest(@NotNull String id, @NotNull JsonObject request, @NotNull JsonConsumer consumer); + + void sendNotification(@NotNull JsonObject notification); + + void dispose(); +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LspTransport.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LspTransport.java new file mode 100644 index 00000000000..e79b845e8f2 --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LspTransport.java @@ -0,0 +1,45 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.FormatConsumer; +import com.google.dart.server.GetServerPortConsumer; +import com.google.dart.server.UpdateContentConsumer; +import org.dartlang.analysis.server.protocol.RequestError; +import org.dartlang.analysis.server.protocol.SourceEdit; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; + +interface LspTransport { + @NotNull List CLIENT_CAPABILITIES = List.of("openUrlRequest", "showMessageRequest"); + @NotNull String ANALYSIS_UPDATE_OPTIONS_METHOD = "analysis.updateOptions"; + @NotNull String SERVER_SET_CLIENT_CAPABILITIES_METHOD = "server.setClientCapabilities"; + + interface FormatHandler { + void computedFormat(@NotNull List edits, int selectionOffset, int selectionLength); + + void onError(@NotNull RequestError requestError); + } + + void requestTextDocumentContent(@NotNull String fileUri, @NotNull DartAnalysisClient.TextDocumentContentHandler handler); + + void connectToDtd(@NotNull String uri); + + void configureServerFeatures(boolean supportsUris, boolean supportsWorkspaceApplyEdits); + + void reanalyze(); + + void diagnosticServer(@NotNull GetServerPortConsumer consumer); + + void updateContent(@NotNull Map files, @NotNull UpdateContentConsumer consumer); + + void format(@NotNull String fileUri, + @NotNull String fileContent, + int selectionOffset, + int selectionLength, + int lineLength, + @NotNull FormatHandler handler); + + void dispose(); +} diff --git a/Dart/src/com/jetbrains/lang/dart/analyzer/LspTransportFactory.java b/Dart/src/com/jetbrains/lang/dart/analyzer/LspTransportFactory.java new file mode 100644 index 00000000000..7e30ace6a3c --- /dev/null +++ b/Dart/src/com/jetbrains/lang/dart/analyzer/LspTransportFactory.java @@ -0,0 +1,86 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.internal.remote.RemoteAnalysisServerImpl; +import com.intellij.openapi.diagnostic.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +final class LspTransportFactory { + private static final Logger LOG = Logger.getInstance(LspTransportFactory.class); + + // Temporary opt-out while direct transport rollout is in progress. + private static final String TRANSPORT_PROPERTY = "dart.analysis.server.lsp.transport"; + // Temporary opt-out while direct request sender rollout is in progress. + private static final String DIRECT_REQUEST_SENDER_PROPERTY = "dart.analysis.server.lsp.direct.request.sender"; + + private LspTransportFactory() { + } + + static @NotNull LspTransport create(@NotNull RemoteAnalysisServerImpl legacyBridgeServer) { + LspTransport bridgeTransport = new LegacyBridgeLspTransport(legacyBridgeServer); + Transport transport = Transport.from(System.getProperty(TRANSPORT_PROPERTY)); + return switch (transport) { + case BRIDGE -> bridgeTransport; + case DIRECT -> { + LspRequestSender directRequestSender = createDirectRequestSender(legacyBridgeServer); + LOG.info("Using direct LSP transport (legacy bridge fallback remains enabled)."); + yield new DirectLspTransport(directRequestSender, bridgeTransport); + } + }; + } + + private static @NotNull LspRequestSender createDirectRequestSender(@NotNull RemoteAnalysisServerImpl legacyBridgeServer) { + RequestSender requestSender = RequestSender.from(System.getProperty(DIRECT_REQUEST_SENDER_PROPERTY)); + return switch (requestSender) { + case LISTENER -> new DirectLspRequestSender(legacyBridgeServer); + case LEGACY -> { + LOG.info("Using legacy-bridge request sender (lsp.handle wrapper). Listener-based sender can be enabled via '" + + DIRECT_REQUEST_SENDER_PROPERTY + "=listener'."); + yield new LegacyBridgeLspRequestSender(legacyBridgeServer); + } + }; + } + + private enum Transport { + BRIDGE, + DIRECT; + + private static @NotNull Transport from(@Nullable String value) { + if (value == null || value.isEmpty()) { + return DIRECT; + } + + return switch (value.toLowerCase(Locale.US)) { + case "bridge" -> BRIDGE; + case "direct" -> DIRECT; + default -> { + LOG.warn("Unsupported value for '" + TRANSPORT_PROPERTY + "': '" + value + "'. Using direct transport."); + yield DIRECT; + } + }; + } + } + + private enum RequestSender { + LISTENER, + LEGACY; + + private static @NotNull RequestSender from(@Nullable String value) { + if (value == null || value.isEmpty()) { + return LISTENER; + } + + return switch (value.toLowerCase(Locale.US)) { + case "listener" -> LISTENER; + case "legacy" -> LEGACY; + default -> { + LOG.warn("Unsupported value for '" + DIRECT_REQUEST_SENDER_PROPERTY + "': '" + value + "'. Using listener sender."); + yield LISTENER; + } + }; + } + } +} diff --git a/Dart/testSrc/com/jetbrains/lang/dart/analyzer/DartAnalysisClientFactoryTest.java b/Dart/testSrc/com/jetbrains/lang/dart/analyzer/DartAnalysisClientFactoryTest.java new file mode 100644 index 00000000000..057fb100db1 --- /dev/null +++ b/Dart/testSrc/com/jetbrains/lang/dart/analyzer/DartAnalysisClientFactoryTest.java @@ -0,0 +1,76 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import junit.framework.TestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class DartAnalysisClientFactoryTest extends TestCase { + private static final String PROTOCOL_PROPERTY = "dart.analysis.server.client.protocol"; + private static final String WIRE_PROTOCOL_PROPERTY = "dart.analysis.server.lsp.wire.protocol"; + private static final String FORCE_WIRE_PROTOCOL_PROPERTY = "dart.analysis.server.lsp.wire.protocol.force"; + + public void testNativeWireLspRequiresForceFlag() { + PropertiesSnapshot snapshot = PropertiesSnapshot.capture(); + try { + System.setProperty(PROTOCOL_PROPERTY, "lsp"); + System.setProperty(WIRE_PROTOCOL_PROPERTY, "lsp"); + System.clearProperty(FORCE_WIRE_PROTOCOL_PROPERTY); + assertEquals("--protocol=analyzer", DartAnalysisClientFactory.getLanguageServerProtocolArgument()); + + System.setProperty(FORCE_WIRE_PROTOCOL_PROPERTY, "true"); + assertEquals("--protocol=lsp", DartAnalysisClientFactory.getLanguageServerProtocolArgument()); + } + finally { + snapshot.restore(); + } + } + + public void testWireProtocolIgnoredWhenLegacyClientRequested() { + PropertiesSnapshot snapshot = PropertiesSnapshot.capture(); + try { + System.setProperty(PROTOCOL_PROPERTY, "legacy"); + System.setProperty(WIRE_PROTOCOL_PROPERTY, "lsp"); + System.setProperty(FORCE_WIRE_PROTOCOL_PROPERTY, "true"); + assertEquals("--protocol=analyzer", DartAnalysisClientFactory.getLanguageServerProtocolArgument()); + } + finally { + snapshot.restore(); + } + } + + private static final class PropertiesSnapshot { + private final @Nullable String protocol; + private final @Nullable String wireProtocol; + private final @Nullable String forceWireProtocol; + + private PropertiesSnapshot(@Nullable String protocol, @Nullable String wireProtocol, @Nullable String forceWireProtocol) { + this.protocol = protocol; + this.wireProtocol = wireProtocol; + this.forceWireProtocol = forceWireProtocol; + } + + private static @NotNull PropertiesSnapshot capture() { + return new PropertiesSnapshot( + System.getProperty(PROTOCOL_PROPERTY), + System.getProperty(WIRE_PROTOCOL_PROPERTY), + System.getProperty(FORCE_WIRE_PROTOCOL_PROPERTY) + ); + } + + private void restore() { + restoreProperty(PROTOCOL_PROPERTY, protocol); + restoreProperty(WIRE_PROTOCOL_PROPERTY, wireProtocol); + restoreProperty(FORCE_WIRE_PROTOCOL_PROPERTY, forceWireProtocol); + } + + private static void restoreProperty(@NotNull String propertyName, @Nullable String value) { + if (value == null) { + System.clearProperty(propertyName); + } + else { + System.setProperty(propertyName, value); + } + } + } +} diff --git a/Dart/testSrc/com/jetbrains/lang/dart/analyzer/DirectLspTransportTest.java b/Dart/testSrc/com/jetbrains/lang/dart/analyzer/DirectLspTransportTest.java new file mode 100644 index 00000000000..0698740e03e --- /dev/null +++ b/Dart/testSrc/com/jetbrains/lang/dart/analyzer/DirectLspTransportTest.java @@ -0,0 +1,195 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.dart.server.JsonConsumer; +import com.google.gson.JsonObject; +import junit.framework.TestCase; +import org.dartlang.analysis.server.protocol.RequestError; +import org.dartlang.analysis.server.protocol.RequestErrorCode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class DirectLspTransportTest extends TestCase { + + public void testDisablesOnlyFailingMethod() { + RecordingRequestSender sender = new RecordingRequestSender(); + sender.whenMethod("dart/connectToDtd", null, new RequestError(RequestErrorCode.INVALID_REQUEST, "connect failed", null)); + + JsonObject textResult = new JsonObject(); + textResult.addProperty("content", "direct-content"); + sender.whenMethod("dart/textDocumentContent", textResult, null); + + RecordingFallbackTransport fallback = new RecordingFallbackTransport(); + DirectLspTransport transport = new DirectLspTransport(sender, fallback); + + transport.connectToDtd("ws://localhost:1234"); + assertEquals(1, fallback.connectToDtdCalls); + assertEquals(1, sender.sentCount("dart/connectToDtd")); + + Holder directContent = new Holder<>(); + transport.requestTextDocumentContent("dart-macro+file:///sample.dart", new DartAnalysisClient.TextDocumentContentHandler() { + @Override + public void computedDocumentContents(@NotNull String contents) { + directContent.value = contents; + } + + @Override + public void onError(@NotNull RequestError requestError) { + fail("Did not expect error: " + requestError.getMessage()); + } + }); + + assertEquals("direct-content", directContent.value); + assertEquals(0, fallback.requestTextDocumentContentCalls); + assertEquals(1, sender.sentCount("dart/textDocumentContent")); + + transport.connectToDtd("ws://localhost:1234"); + assertEquals(2, fallback.connectToDtdCalls); + assertEquals(1, sender.sentCount("dart/connectToDtd")); + } + + public void testMalformedDirectTextResponseFallsBackAndDisablesTextMethod() { + RecordingRequestSender sender = new RecordingRequestSender(); + sender.whenMethod("dart/textDocumentContent", new JsonObject(), null); + + RecordingFallbackTransport fallback = new RecordingFallbackTransport(); + DirectLspTransport transport = new DirectLspTransport(sender, fallback); + + Holder firstContent = new Holder<>(); + transport.requestTextDocumentContent("dart-macro+file:///sample.dart", new DartAnalysisClient.TextDocumentContentHandler() { + @Override + public void computedDocumentContents(@NotNull String contents) { + firstContent.value = contents; + } + + @Override + public void onError(@NotNull RequestError requestError) { + fail("Did not expect error: " + requestError.getMessage()); + } + }); + assertEquals("fallback-content", firstContent.value); + assertEquals(1, fallback.requestTextDocumentContentCalls); + assertEquals(1, sender.sentCount("dart/textDocumentContent")); + + Holder secondContent = new Holder<>(); + transport.requestTextDocumentContent("dart-macro+file:///sample.dart", new DartAnalysisClient.TextDocumentContentHandler() { + @Override + public void computedDocumentContents(@NotNull String contents) { + secondContent.value = contents; + } + + @Override + public void onError(@NotNull RequestError requestError) { + fail("Did not expect error: " + requestError.getMessage()); + } + }); + + assertEquals("fallback-content", secondContent.value); + assertEquals(2, fallback.requestTextDocumentContentCalls); + assertEquals(1, sender.sentCount("dart/textDocumentContent")); + } + + public void testDisposeDisposesSenderAndFallbackTransport() { + RecordingRequestSender sender = new RecordingRequestSender(); + RecordingFallbackTransport fallback = new RecordingFallbackTransport(); + DirectLspTransport transport = new DirectLspTransport(sender, fallback); + + transport.dispose(); + + assertTrue(sender.disposed); + assertTrue(fallback.disposed); + } + + public void testConfigureFeaturesFallsBackImmediatelyAfterCapabilitiesMethodDisabled() { + RecordingRequestSender sender = new RecordingRequestSender(); + sender.whenMethod("analysis.updateOptions", new JsonObject(), null); + sender.whenMethod("server.setClientCapabilities", null, new RequestError(RequestErrorCode.INVALID_REQUEST, "capabilities failed", null)); + + RecordingFallbackTransport fallback = new RecordingFallbackTransport(); + DirectLspTransport transport = new DirectLspTransport(sender, fallback); + + transport.configureServerFeatures(true, true); + assertEquals(1, sender.sentCount("analysis.updateOptions")); + assertEquals(1, sender.sentCount("server.setClientCapabilities")); + assertEquals(1, fallback.configureServerFeaturesCalls); + + transport.configureServerFeatures(true, true); + assertEquals(1, sender.sentCount("analysis.updateOptions")); + assertEquals(1, sender.sentCount("server.setClientCapabilities")); + assertEquals(2, fallback.configureServerFeaturesCalls); + } + + private static final class RecordingRequestSender implements LspRequestSender { + private final Map methodToCount = new HashMap<>(); + private final Map responses = new HashMap<>(); + private boolean disposed; + + private void whenMethod(@NotNull String method, @Nullable JsonObject result, @Nullable RequestError error) { + responses.put(method, new Response(result, error)); + } + + private int sentCount(@NotNull String method) { + return methodToCount.getOrDefault(method, 0); + } + + @Override + public void sendRequest(@NotNull String id, @NotNull JsonObject request, @NotNull JsonConsumer consumer) { + String method = request.get("method").getAsString(); + methodToCount.put(method, sentCount(method) + 1); + Response response = responses.getOrDefault(method, Response.SUCCESS); + consumer.onResponse(response.result, response.error); + } + + @Override + public void dispose() { + disposed = true; + } + } + + private static final class RecordingFallbackTransport implements LspTransport { + private int requestTextDocumentContentCalls; + private int connectToDtdCalls; + private int configureServerFeaturesCalls; + private boolean disposed; + + @Override + public void requestTextDocumentContent(@NotNull String fileUri, @NotNull DartAnalysisClient.TextDocumentContentHandler handler) { + requestTextDocumentContentCalls++; + handler.computedDocumentContents("fallback-content"); + } + + @Override + public void connectToDtd(@NotNull String uri) { + connectToDtdCalls++; + } + + @Override + public void configureServerFeatures(boolean supportsUris, boolean supportsWorkspaceApplyEdits) { + configureServerFeaturesCalls++; + } + + @Override + public void dispose() { + disposed = true; + } + } + + private static final class Response { + private static final Response SUCCESS = new Response(new JsonObject(), null); + + private final @Nullable JsonObject result; + private final @Nullable RequestError error; + + private Response(@Nullable JsonObject result, @Nullable RequestError error) { + this.result = result; + this.error = error; + } + } + + private static final class Holder { + private @Nullable T value; + } +} diff --git a/Dart/testSrc/com/jetbrains/lang/dart/analyzer/LspRequestBuildersTest.java b/Dart/testSrc/com/jetbrains/lang/dart/analyzer/LspRequestBuildersTest.java new file mode 100644 index 00000000000..b3b30a9436a --- /dev/null +++ b/Dart/testSrc/com/jetbrains/lang/dart/analyzer/LspRequestBuildersTest.java @@ -0,0 +1,28 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.lang.dart.analyzer; + +import com.google.gson.JsonObject; +import junit.framework.TestCase; + +public class LspRequestBuildersTest extends TestCase { + + public void testBuildTextDocumentContentRequest() { + JsonObject request = LspRequestBuilders.buildTextDocumentContentRequest("id-1", "dart-macro+file:///sample.dart"); + + assertEquals("id-1", request.get("id").getAsString()); + assertEquals("2.0", request.get("jsonrpc").getAsString()); + assertEquals("dart/textDocumentContent", request.get("method").getAsString()); + assertEquals("dart-macro+file:///sample.dart", request.getAsJsonObject("params").get("uri").getAsString()); + } + + public void testWrapWithLspHandle() { + JsonObject lspRequest = LspRequestBuilders.buildConnectToDtdRequest("inner-id", "ws://localhost:1234"); + JsonObject wrappedRequest = LspRequestBuilders.wrapWithLspHandle("outer-id", lspRequest); + + assertEquals("outer-id", wrappedRequest.get("id").getAsString()); + assertEquals("lsp.handle", wrappedRequest.get("method").getAsString()); + JsonObject wrappedLspMessage = wrappedRequest.getAsJsonObject("params").getAsJsonObject("lspMessage"); + assertEquals("inner-id", wrappedLspMessage.get("id").getAsString()); + assertEquals("dart/connectToDtd", wrappedLspMessage.get("method").getAsString()); + } +} diff --git a/Dart/thirdPartySrc/analysisServer/com/google/dart/server/internal/remote/RemoteAnalysisServerImpl.java b/Dart/thirdPartySrc/analysisServer/com/google/dart/server/internal/remote/RemoteAnalysisServerImpl.java index c7e4f0b56a1..7652365996d 100644 --- a/Dart/thirdPartySrc/analysisServer/com/google/dart/server/internal/remote/RemoteAnalysisServerImpl.java +++ b/Dart/thirdPartySrc/analysisServer/com/google/dart/server/internal/remote/RemoteAnalysisServerImpl.java @@ -773,7 +773,7 @@ private boolean processNotification(JsonObject response) throws Exception { // prepare notification kind JsonElement eventElement = response.get("event"); if (eventElement == null || !eventElement.isJsonPrimitive()) { - return false; + return processDirectLspNotification(response); } String event = eventElement.getAsString(); // handle each supported notification kind @@ -856,6 +856,32 @@ else if (event.equals(LSP_NOTIFICATION)) { return true; } + private boolean processDirectLspNotification(JsonObject response) { + if (response.get("id") != null) { + return false; + } + + JsonElement methodElement = response.get("method"); + if (!(methodElement instanceof JsonPrimitive)) { + return false; + } + if (!"dart/textDocumentContentDidChange".equals(methodElement.getAsString())) { + return false; + } + + JsonObject paramsObject = response.getAsJsonObject("params"); + if (paramsObject == null) { + return false; + } + JsonElement uriElement = paramsObject.get("uri"); + if (!(uriElement instanceof JsonPrimitive)) { + return false; + } + + listener.lspTextDocumentContentDidChange(uriElement.getAsString()); + return true; + } + /** * Attempts to handle the given {@link JsonObject} as a request from the server. * Return {@code true} if it was handled, otherwise {@code false} is returned. @@ -900,18 +926,26 @@ public void onError(RequestError requestError) { server_showMessageRequest(type, message, messageActions, consumer); } else if (method.equals("lsp.handle")) { - processLspRequestFromServer(idString, response.get("params").getAsJsonObject().get("lspMessage").getAsJsonObject()); + processLspRequestFromServer(idString, + response.get("params").getAsJsonObject().get("lspMessage").getAsJsonObject(), + true); + } + else if (method.equals("workspace/applyEdit")) { + processLspRequestFromServer(idString, response, false); } // it is a request from the server, even if we did not handle it return true; } - private void processLspRequestFromServer(String dasRequestId, JsonObject lspMessage) { - String lspRequestId = lspMessage.get("id").getAsString(); + private void processLspRequestFromServer(String requestId, JsonObject lspMessage, boolean wrappedByLegacyBridge) { + JsonElement lspRequestId = lspMessage.get("id"); + if (lspRequestId == null || !lspRequestId.isJsonPrimitive()) { + return; + } String lspMethod = lspMessage.get("method").getAsString(); if (lspMethod.equals("workspace/applyEdit")) { - processWorspaceApplyEditRequestFromServer(dasRequestId, lspRequestId, lspMessage.get("params")); + processWorkspaceApplyEditRequestFromServer(requestId, lspRequestId, lspMessage.get("params"), wrappedByLegacyBridge); } } @@ -950,7 +984,10 @@ private void processLspRequestFromServer(String dasRequestId, JsonObject lspMess } } */ - private void processWorspaceApplyEditRequestFromServer(String dasRequestId, String lspRequestId, JsonElement paramsElement) { + private void processWorkspaceApplyEditRequestFromServer(String requestId, + JsonElement lspRequestId, + JsonElement paramsElement, + boolean wrappedByLegacyBridge) { DartLspApplyWorkspaceEditParams workspaceEditParams = getAsWorkspaceEditParams(paramsElement); if (workspaceEditParams == null) return; @@ -960,8 +997,17 @@ public void workspaceEditApplied(DartLspApplyWorkspaceEditResult result) { JsonObject lspResultElement = new JsonObject(); lspResultElement.addProperty("applied", result.getApplied()); + if (!wrappedByLegacyBridge) { + JsonObject directLspResponse = new JsonObject(); + directLspResponse.add("id", lspRequestId); + directLspResponse.addProperty("jsonrpc", "2.0"); + directLspResponse.add("result", lspResultElement); + sendResponseToServer(directLspResponse); + return; + } + JsonObject lspResponseElement = new JsonObject(); - lspResponseElement.addProperty("id", lspRequestId); + lspResponseElement.add("id", lspRequestId); lspResponseElement.addProperty("jsonrpc", "2.0"); lspResponseElement.add("result", lspResultElement); @@ -969,7 +1015,7 @@ public void workspaceEditApplied(DartLspApplyWorkspaceEditResult result) { resultJsonElement.add("lspResponse", lspResponseElement); JsonObject responseElement = new JsonObject(); - responseElement.addProperty("id", dasRequestId); + responseElement.addProperty("id", requestId); responseElement.add("result", resultJsonElement); sendResponseToServer(responseElement); @@ -1246,6 +1292,18 @@ public void sendResponseToServer(JsonObject response) { } } + /** + * Sends an LSP notification (fire-and-forget, no consumer registered). + * Unlike {@link #sendRequestToServer}, this does not put anything in the consumer map. + */ + public void sendNotification(JsonObject notification) { + notifyRequestListeners(notification); + lastRequestTime.set(System.currentTimeMillis()); + synchronized (requestSinkLock) { + requestSink.add(notification); + } + } + private void startServer() throws Exception { socket.start(); consumerMap.clear();