Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java (2,159 lines of code) (raw):
// 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.common.collect.EvictingQueue;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.dart.server.*;
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;
import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.coverage.CoverageLoadErrorReporter;
import com.intellij.coverage.DummyCoverageLoadErrorReporter;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.fileEditor.impl.FileOffsetsManager;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.SearchScope;
import com.intellij.util.*;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.URLUtil;
import com.jetbrains.lang.dart.DartBundle;
import com.jetbrains.lang.dart.DartFileType;
import com.jetbrains.lang.dart.assists.DartQuickAssistIntention;
import com.jetbrains.lang.dart.assists.DartQuickAssistIntentionListener;
import com.jetbrains.lang.dart.fixes.DartQuickFix;
import com.jetbrains.lang.dart.fixes.DartQuickFixListener;
import com.jetbrains.lang.dart.ide.actions.DartPubActionBase;
import com.jetbrains.lang.dart.ide.completion.DartCompletionTimerExtension;
import com.jetbrains.lang.dart.ide.errorTreeView.DartProblemsView;
import com.jetbrains.lang.dart.ide.template.postfix.DartPostfixTemplateProvider;
import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService;
import com.jetbrains.lang.dart.sdk.DartSdk;
import com.jetbrains.lang.dart.sdk.DartSdkUpdateChecker;
import com.jetbrains.lang.dart.sdk.DartSdkUtil;
import com.jetbrains.lang.dart.util.PubspecYamlUtil;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import kotlinx.coroutines.CoroutineScope;
import org.dartlang.analysis.server.protocol.*;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public final class DartAnalysisServerService implements Disposable {
public static final String MIN_SDK_VERSION = "1.12";
private static final String MIN_MOVE_FILE_SDK_VERSION = "2.3.2";
private static final String COMPLETION_2_SERVER_VERSION = "1.33";
// Webdev works going back to 2.6.0, future minimum version listed in the pubspec.yaml, link below, won't mean that 2.6.0 aren't
// supported.
// https://github.com/dart-lang/webdev/blob/master/webdev/pubspec.yaml#L11
public static final String MIN_WEBDEV_SDK_VERSION = "2.6.0";
// As of the Dart SDK version 2.8.0, the file .dart_tool/package_config.json is preferred over the .packages file.
// https://github.com/dart-lang/sdk/issues/48272
public static final String MIN_PACKAGE_CONFIG_JSON_SDK_VERSION = "2.8.0";
// The dart cli command provides a language server command, `dart language-server`, which
// should be used going forward instead of `dart .../analysis_server.dart.snapshot`.
public static final String MIN_DART_LANG_SERVER_SDK_VERSION = "2.16.0";
// See "supportsUris"
// https://htmlpreview.github.io/?https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/doc/api.html#request_server.setClientCapabilities
public static final String MIN_FILE_URI_SDK_VERSION = "3.4.0";
private static final String MIN_WORKSPACE_APPLY_EDITS_SDK_VERSION = "3.8";
private static final long UPDATE_FILES_TIMEOUT = 300;
private static final long CHECK_CANCELLED_PERIOD = 10;
private static final long SEND_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
private static final long EDIT_FORMAT_TIMEOUT = TimeUnit.SECONDS.toMillis(3);
private static final long EDIT_ORGANIZE_DIRECTIVES_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(300);
private static final long EDIT_SORT_MEMBERS_TIMEOUT = TimeUnit.SECONDS.toMillis(3);
private static final long GET_HOVER_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
private static final long GET_NAVIGATION_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
private static final long GET_ASSISTS_TIMEOUT_EDT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long GET_ASSISTS_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(1000);
private static final long GET_FIXES_TIMEOUT_EDT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long GET_FIXES_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(1000);
private static final long IMPORTED_ELEMENTS_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long POSTFIX_COMPLETION_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long POSTFIX_INITIALIZATION_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(5000);
private static final long STATEMENT_COMPLETION_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long GET_SUGGESTIONS_TIMEOUT = TimeUnit.SECONDS.toMillis(5);
private static final long GET_SUGGESTION_DETAILS_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long GET_SUGGESTION_DETAILS2_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long FIND_ELEMENT_REFERENCES_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
private static final long GET_TYPE_HIERARCHY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final long EXECUTION_CREATE_CONTEXT_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
private static final long EXECUTION_MAP_URI_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
private static final long ANALYSIS_IN_TESTS_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final long LSP_MESSAGE_TEXT_DOCUMENT_CONTENT_TIMEOUT = TimeUnit.SECONDS.toMillis(50);
private static final long TESTS_TIMEOUT_COEFF = 10;
private static final Logger LOG = Logger.getInstance(DartAnalysisServerService.class);
private static final int DEBUG_LOG_CAPACITY = 30;
private static final int MAX_DEBUG_LOG_LINE_LENGTH = 200; // Saw one line while testing that was > 50k
private final @NotNull Project myProject;
private final @NotNull CoroutineScope myServiceScope;
private boolean myInitializationOnServerStartupDone;
private boolean mySubscribeToServerLog;
// 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;
private @Nullable StdioServerSocket myServerSocket;
private @NotNull String myServerVersion = "";
private @NotNull String mySdkVersion = "";
private @Nullable String mySdkHome;
private final DartServerRootsHandler myRootsHandler;
private final Map<String, Long> myFilePathWithOverlaidContentToTimestamp = Collections.synchronizedMap(new HashMap<>());
private final List<String> myVisibleFileUris = new ArrayList<>();
private final Set<Document> myChangedDocuments = new HashSet<>();
private final Alarm myUpdateFilesAlarm;
private final @NotNull Queue<CompletionInfo> myCompletionInfos = new LinkedList<>();
private final @NotNull Queue<SearchResultsSet> mySearchResultSets = new LinkedList<>();
private final @NotNull DartServerData myServerData;
private volatile boolean myAnalysisInProgress;
private volatile boolean myPubListInProgress;
private final @NotNull Alarm myShowServerProgressAlarm;
private final @NotNull DartAnalysisServerErrorHandler myServerErrorHandler;
private @Nullable ProgressIndicator myProgressIndicator;
private final Object myProgressLock = new Object();
private boolean myHaveShownInitialProgress;
private boolean mySentAnalysisBusy;
private @Nullable String myDtdUri;
// files with red squiggles in Project View. This field is also used as a lock to access these 3 collections
private final @NotNull Set<String> myFilePathsWithErrors = new HashSet<>();
// how many files with errors are in this folder (recursively)
private final @NotNull Object2IntMap<String> myFolderPathsWithErrors = new Object2IntOpenHashMap<>();
// errors hash is tracked to optimize error notification listener: do not handle equal notifications more than once
private final @NotNull Object2IntMap<String> myFilePathToErrorsHash = new Object2IntOpenHashMap<>();
private final @NotNull EvictingQueue<String> myDebugLog = EvictingQueue.create(DEBUG_LOG_CAPACITY);
private boolean myDisposed;
private final @NotNull Condition<?> myDisposedCondition = o -> myDisposed;
public static String getClientId() {
return ApplicationNamesInfo.getInstance().getFullProductName().replace(' ', '-');
}
private static String getClientVersion() {
return ApplicationInfo.getInstance().getApiVersion();
}
private final @NotNull List<AnalysisServerListener> myAdditionalServerListeners = new SmartList<>();
private final @NotNull List<RequestListener> myRequestListeners = new SmartList<>();
private final @NotNull List<ResponseListener> myResponseListeners = new SmartList<>();
private final @NotNull List<DartQuickAssistIntentionListener> myQuickAssistIntentionListeners = new SmartList<>();
private final @NotNull List<DartQuickFixListener> myQuickFixListeners = new SmartList<>();
private final AnalysisServerListener myAnalysisServerListener = new AnalysisServerListenerAdapter() {
@Override
public void computedAvailableSuggestions(@NotNull List<AvailableSuggestionSet> changed, int @NotNull [] removed) {
myServerData.computedAvailableSuggestions(changed, removed);
}
@Override
public void computedExistingImports(@NotNull String filePathSD, @NotNull Map<String, Map<String, Set<String>>> existingImports) {
myServerData.computedExistingImports(filePathSD, existingImports);
}
@Override
public void computedErrors(@NotNull String filePathOrUri, @NotNull List<AnalysisError> errors) {
DartFileInfo fileInfo = DartFileInfoKt.getDartFileInfo(myProject, filePathOrUri);
final ProgressIndicator indicator = myProgressIndicator;
if (indicator != null && fileInfo instanceof DartLocalFileInfo localFileInfo) {
String fileName = PathUtil.getFileName(localFileInfo.getFilePath());
indicator.setText(DartBundle.message("dart.analysis.progress.with.file", fileName));
}
final List<AnalysisError> errorsWithoutTodo = errors.isEmpty() ? Collections.emptyList() : new ArrayList<>(errors.size());
boolean hasSevereProblems = false;
for (AnalysisError error : errors) {
if (AnalysisErrorSeverity.ERROR.equals(error.getSeverity())) {
hasSevereProblems = true;
}
if (!AnalysisErrorType.TODO.equals(error.getType())) {
errorsWithoutTodo.add(error);
}
}
int newHash = errorsWithoutTodo.isEmpty() ? 0 : ensureNotZero(errorsWithoutTodo.hashCode());
if (fileInfo instanceof DartLocalFileInfo localFileInfo) {
int oldHash;
synchronized (myFilePathsWithErrors) {
// TObjectIntHashMap returns 0 if there's no such entry, it's equivalent to empty error set for this file
oldHash = myFilePathToErrorsHash.getInt(localFileInfo.getFilePath());
}
// do nothing if errors are the same as were already handled previously
if (oldHash == newHash && myServerData.isErrorInfoUpToDate(localFileInfo)) return;
}
boolean restartHighlighting =
fileInfo instanceof DartLocalFileInfo localFileInfo && myVisibleFileUris.contains(getLocalFileUri(localFileInfo.getFilePath()))
||
fileInfo instanceof DartNotLocalFileInfo notLocalFileInfo && myVisibleFileUris.contains(notLocalFileInfo.getFileUri());
if (myServerData.computedErrors(fileInfo, errorsWithoutTodo, restartHighlighting)) {
if (fileInfo instanceof DartLocalFileInfo localFileInfo) {
onErrorsUpdated(localFileInfo, errorsWithoutTodo, hasSevereProblems, newHash);
}
}
}
@Override
public void computedHighlights(@NotNull String filePathOrUri, @NotNull List<HighlightRegion> regions) {
DartFileInfo fileInfo = DartFileInfoKt.getDartFileInfo(myProject, filePathOrUri);
myServerData.computedHighlights(fileInfo, regions);
}
@Override
public void computedClosingLabels(@NotNull String filePathOrUri, @NotNull List<ClosingLabel> labels) {
DartFileInfo fileInfo = DartFileInfoKt.getDartFileInfo(myProject, filePathOrUri);
myServerData.computedClosingLabels(fileInfo, labels);
}
@Override
public void computedImplemented(@NotNull String filePathOrUri,
@NotNull List<ImplementedClass> implementedClasses,
@NotNull List<ImplementedMember> implementedMembers) {
DartFileInfo fileInfo = DartFileInfoKt.getDartFileInfo(myProject, filePathOrUri);
myServerData.computedImplemented(fileInfo, implementedClasses, implementedMembers);
}
@Override
public void computedNavigation(@NotNull String filePathOrUri, @NotNull List<NavigationRegion> regions) {
DartFileInfo fileInfo = DartFileInfoKt.getDartFileInfo(myProject, filePathOrUri);
myServerData.computedNavigation(fileInfo, regions);
}
@Override
public void computedOverrides(@NotNull String filePathOrUri, @NotNull List<OverrideMember> overrides) {
DartFileInfo fileInfo = DartFileInfoKt.getDartFileInfo(myProject, filePathOrUri);
myServerData.computedOverrides(fileInfo, overrides);
}
@Override
public void computedOutline(@NotNull String filePathOrUri, @NotNull Outline outline) {
DartFileInfo fileInfo = DartFileInfoKt.getDartFileInfo(myProject, filePathOrUri);
myServerData.computedOutline(fileInfo, outline);
}
@Override
public void flushedResults(@NotNull List<String> filePathsOrUris) {
List<DartFileInfo> fileInfos = ContainerUtil.map(filePathsOrUris, pathOrUri -> DartFileInfoKt.getDartFileInfo(myProject, pathOrUri));
myServerData.onFlushedResults(fileInfos);
for (DartFileInfo fileInfo : fileInfos) {
if (fileInfo instanceof DartLocalFileInfo localFileInfo) {
onErrorsUpdated(localFileInfo, AnalysisError.EMPTY_LIST, false, 0);
}
}
}
@Override
public void computedCompletion(final @NotNull String completionId,
final int replacementOffset,
final int replacementLength,
final @NotNull List<CompletionSuggestion> completions,
final @NotNull List<IncludedSuggestionSet> includedSuggestionSets,
final @NotNull List<String> includedElementKinds,
final @NotNull List<IncludedSuggestionRelevanceTag> includedSuggestionRelevanceTags,
final boolean isLast,
final @Nullable String libraryFilePathSD) {
synchronized (myCompletionInfos) {
myCompletionInfos.add(new CompletionInfo(completionId, replacementOffset, replacementLength, completions, includedSuggestionSets,
includedElementKinds, includedSuggestionRelevanceTags, isLast, libraryFilePathSD));
myCompletionInfos.notifyAll();
}
}
@Override
public void computedSearchResults(String searchId, List<SearchResult> results, boolean last) {
synchronized (mySearchResultSets) {
mySearchResultSets.add(new SearchResultsSet(searchId, results, last));
mySearchResultSets.notifyAll();
}
}
@Override
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));
}
}
@Override
public void requestError(RequestError error) {
if (RequestErrorCode.SERVER_ERROR.equals(error.getCode())) {
serverError(false, error.getMessage(), error.getStackTrace());
}
else {
LOG.info(getShortErrorMessage("unknown", null, error));
}
}
@Override
public void serverError(boolean isFatal, @Nullable String message, @NonNls @Nullable String stackTrace) {
if (message == null) {
message = DartBundle.message("issue.occurred.with.analysis.server");
}
if (!isFatal &&
stackTrace != null &&
stackTrace.startsWith("#0 checkValidPackageUri (package:package_config/src/util.dart:72)")) {
return;
}
String sdkVersion = mySdkVersion.isEmpty() ? null : mySdkVersion;
StringBuilder debugLog = new StringBuilder();
synchronized (myDebugLog) {
for (String s : myDebugLog) {
debugLog.append(s).append('\n');
}
}
myServerErrorHandler.handleError(message, stackTrace, isFatal, sdkVersion, debugLog.isEmpty() ? null : debugLog.toString());
}
@Override
public void serverStatus(final @Nullable AnalysisStatus analysisStatus, final @Nullable PubStatus pubStatus) {
final boolean wasBusy = myAnalysisInProgress || myPubListInProgress;
if (analysisStatus != null) myAnalysisInProgress = analysisStatus.isAnalyzing();
if (pubStatus != null) myPubListInProgress = pubStatus.isListingPackageDirs();
if (!wasBusy && (myAnalysisInProgress || myPubListInProgress)) {
final Runnable delayedRunnable = () -> {
if (myAnalysisInProgress || myPubListInProgress) {
startShowingServerProgress();
}
};
// 50ms delay to minimize blinking in case of consequent start-stop-start-stop-... events that happen with pubStatus events
// 300ms delay to avoid showing progress for very fast analysis start-stop cycle that happens with analysisStatus events
final int delay = pubStatus != null && pubStatus.isListingPackageDirs() ? 50 : 300;
myShowServerProgressAlarm.addRequest(delayedRunnable, delay, ModalityState.any());
}
if (!myAnalysisInProgress && !myPubListInProgress) {
stopShowingServerProgress();
}
}
@Override
public void lspTextDocumentContentDidChange(@NotNull String fileUri) {
flushedResults(List.of(fileUri));
myServerData.textDocumentContentDidChange(fileUri);
}
};
private static int ensureNotZero(int i) {
return i == 0 ? Integer.MAX_VALUE : i;
}
private void startShowingServerProgress() {
if (!myHaveShownInitialProgress) {
myHaveShownInitialProgress = true;
final Task.Backgroundable task = new Task.Backgroundable(myProject, DartBundle.message("dart.analysis.progress.title"), false) {
@Override
public void run(final @NotNull ProgressIndicator indicator) {
if (DartAnalysisServerService.this.myProject.isDisposed()) return;
if (!myAnalysisInProgress && !myPubListInProgress) return;
indicator.setText(DartBundle.message("dart.analysis.progress.title"));
if (ApplicationManager.getApplication().isDispatchThread()) {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
LOG.error("wait() in EDT");
}
}
else {
try {
myProgressIndicator = indicator;
waitWhileServerBusy();
}
finally {
myProgressIndicator = null;
}
}
}
};
ProgressManager.getInstance().run(task);
}
DartAnalysisServerMessages.sendAnalysisStarted(myProject, true);
mySentAnalysisBusy = true;
}
/**
* Must use it each time right after reading any offset or length from any class from org.dartlang.analysis.server.protocol package
*/
public int getConvertedOffset(final @Nullable VirtualFile file, final int originalOffset) {
if (originalOffset <= 0 || file == null || !file.isInLocalFileSystem()) return originalOffset;
return myFilePathWithOverlaidContentToTimestamp.containsKey(file.getPath())
? originalOffset
: FileOffsetsManager.getInstance().getConvertedOffset(file, originalOffset);
}
/**
* Must use it right before sending any offsets and lengths to the AnalysisServer
*/
public int getOriginalOffset(final @Nullable VirtualFile file, final int convertedOffset) {
if (file == null || !file.isInLocalFileSystem()) return convertedOffset;
return myFilePathWithOverlaidContentToTimestamp.containsKey(file.getPath())
? convertedOffset
: FileOffsetsManager.getInstance().getOriginalOffset(file, convertedOffset);
}
public int[] getConvertedOffsets(final @NotNull VirtualFile file, final int[] _offsets) {
final int[] offsets = new int[_offsets.length];
for (int i = 0; i < _offsets.length; i++) {
offsets[i] = getConvertedOffset(file, _offsets[i]);
}
return offsets;
}
public int[] getConvertedLengths(final @NotNull VirtualFile file, final int[] _offsets, final int[] _lengths) {
final int[] offsets = getConvertedOffsets(file, _offsets);
final int[] lengths = new int[_lengths.length];
for (int i = 0; i < _lengths.length; i++) {
lengths[i] = getConvertedOffset(file, _offsets[i] + _lengths[i]) - offsets[i];
}
return lengths;
}
public static boolean isDartSdkVersionSufficient(final @NotNull DartSdk sdk) {
return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_SDK_VERSION) >= 0;
}
public static boolean isDartSdkVersionSufficientForMoveFileRefactoring(final @NotNull DartSdk sdk) {
return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_MOVE_FILE_SDK_VERSION) >= 0;
}
public static boolean isDartSdkVersionSufficientForWebdev(final @NotNull DartSdk sdk) {
return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_WEBDEV_SDK_VERSION) >= 0;
}
public static boolean isDartSdkVersionSufficientForPackageConfigJson(final @NotNull DartSdk sdk) {
return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_PACKAGE_CONFIG_JSON_SDK_VERSION) >= 0;
}
public static boolean isDartSdkVersionSufficientForDartLangServer(final @NotNull DartSdk sdk) {
return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_DART_LANG_SERVER_SDK_VERSION) >= 0;
}
public static boolean isDartSdkVersionSufficientForFileUri(@NotNull String sdkVersion) {
return Registry.is("dart.macros.support", false) && StringUtil.compareVersionNumbers(sdkVersion, MIN_FILE_URI_SDK_VERSION) >= 0;
}
public static boolean isDartSdkVersionSufficientForWorkspaceApplyEdits(@NotNull String sdkVersion) {
return StringUtil.compareVersionNumbers(sdkVersion, MIN_WORKSPACE_APPLY_EDITS_SDK_VERSION) >= 0;
}
public boolean shouldUseCompletion2() {
return StringUtil.compareVersionNumbers(getServerVersion(), COMPLETION_2_SERVER_VERSION) >= 0;
}
public void addCompletions(final @NotNull VirtualFile file,
final @NotNull String completionId,
final @NotNull CompletionSuggestionConsumer consumer,
final @NotNull CompletionLibraryRefConsumer libraryRefConsumer) {
while (true) {
ProgressManager.checkCanceled();
synchronized (myCompletionInfos) {
CompletionInfo completionInfo;
while ((completionInfo = myCompletionInfos.poll()) != null) {
if (!completionInfo.myCompletionId.equals(completionId)) continue;
if (!completionInfo.isLast) continue;
for (final CompletionSuggestion completion : completionInfo.myCompletions) {
final int convertedReplacementOffset = getConvertedOffset(file, completionInfo.myOriginalReplacementOffset);
consumer.consumeCompletionSuggestion(convertedReplacementOffset, completionInfo.myReplacementLength, completion);
}
final Set<String> includedKinds = Sets.newHashSet(completionInfo.myIncludedElementKinds);
final Map<String, IncludedSuggestionRelevanceTag> includedRelevanceTags = new HashMap<>();
for (IncludedSuggestionRelevanceTag includedRelevanceTag : completionInfo.myIncludedSuggestionRelevanceTags) {
includedRelevanceTags.put(includedRelevanceTag.getTag(), includedRelevanceTag);
}
for (final IncludedSuggestionSet includedSet : completionInfo.myIncludedSuggestionSets) {
libraryRefConsumer.consumeLibraryRef(includedSet, includedKinds, includedRelevanceTags, completionInfo.myLibraryFilePathSD);
}
for (DartCompletionTimerExtension extension : DartCompletionTimerExtension.getExtensions()) {
extension.dartCompletionEnd();
}
return;
}
try {
myCompletionInfos.wait(CHECK_CANCELLED_PERIOD);
}
catch (InterruptedException e) {
return;
}
}
}
}
public static class FormatResult {
private final @Nullable List<SourceEdit> myEdits;
private final int myOffset;
private final int myLength;
public FormatResult(final @Nullable List<SourceEdit> edits, final int selectionOffset, final int selectionLength) {
myEdits = edits;
myOffset = selectionOffset;
myLength = selectionLength;
}
public int getLength() {
return myLength;
}
public int getOffset() {
return myOffset;
}
public @Nullable List<SourceEdit> getEdits() {
return myEdits;
}
}
public DartAnalysisServerService(@NotNull Project project, @NotNull CoroutineScope serviceScope) {
myProject = project;
myServiceScope = serviceScope;
myRootsHandler = new DartServerRootsHandler(project);
myServerData = new DartServerData(this);
myUpdateFilesAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
myShowServerProgressAlarm = new Alarm(this);
myServerErrorHandler = new DartAnalysisServerErrorHandler(project);
}
public @NotNull CoroutineScope getServiceScope() {
return myServiceScope;
}
@SuppressWarnings("unused") // for Flutter plugin
public void addAnalysisServerListener(final @NotNull AnalysisServerListener serverListener) {
if (!myAdditionalServerListeners.contains(serverListener)) {
myAdditionalServerListeners.add(serverListener);
if (myServer != null && isServerProcessActive()) {
myServer.addAnalysisServerListener(serverListener);
}
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void removeAnalysisServerListener(final @NotNull AnalysisServerListener serverListener) {
myAdditionalServerListeners.remove(serverListener);
if (myServer != null) {
myServer.removeAnalysisServerListener(serverListener);
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void addRequestListener(final @NotNull RequestListener requestListener) {
if (!myRequestListeners.contains(requestListener)) {
myRequestListeners.add(requestListener);
if (myServer != null && isServerProcessActive()) {
myServer.addRequestListener(requestListener);
}
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void removeRequestListener(final @NotNull RequestListener requestListener) {
myRequestListeners.remove(requestListener);
if (myServer != null) {
myServer.removeRequestListener(requestListener);
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void addResponseListener(final @NotNull ResponseListener responseListener) {
if (!myResponseListeners.contains(responseListener)) {
myResponseListeners.add(responseListener);
if (myServer != null && isServerProcessActive()) {
myServer.addResponseListener(responseListener);
}
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void removeResponseListener(final @NotNull ResponseListener responseListener) {
myResponseListeners.remove(responseListener);
if (myServer != null) {
myServer.removeResponseListener(responseListener);
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void addQuickAssistIntentionListener(@NotNull DartQuickAssistIntentionListener listener) {
if (!myQuickAssistIntentionListeners.contains(listener)) {
myQuickAssistIntentionListeners.add(listener);
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void removeQuickAssistIntentionListener(@NotNull DartQuickAssistIntentionListener listener) {
myQuickAssistIntentionListeners.remove(listener);
}
public void fireBeforeQuickAssistIntentionInvoked(@NotNull DartQuickAssistIntention intention,
@NotNull Editor editor,
@NotNull PsiFile file) {
try {
myQuickAssistIntentionListeners.forEach(listener -> listener.beforeQuickAssistIntentionInvoked(intention, editor, file));
}
catch (Throwable t) {
LOG.error(t);
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void addQuickFixListener(@NotNull DartQuickFixListener listener) {
if (!myQuickFixListeners.contains(listener)) {
myQuickFixListeners.add(listener);
}
}
@SuppressWarnings("unused") // for Flutter plugin
public void removeQuickFixListener(@NotNull DartQuickFixListener listener) {
myQuickFixListeners.remove(listener);
}
public void fireBeforeQuickFixInvoked(@NotNull DartQuickFix fix, @NotNull Editor editor, @NotNull PsiFile file) {
try {
myQuickFixListeners.forEach(listener -> listener.beforeQuickFixInvoked(fix, editor, file));
}
catch (Throwable t) {
LOG.error(t);
}
}
private static void setDasLogger() {
if (Logging.getLogger() != com.google.dart.server.utilities.logging.Logger.NULL) {
return; // already registered
}
Logging.setLogger(new com.google.dart.server.utilities.logging.Logger() {
@Override
public void logError(String message) {
LOG.error(message);
}
@Override
public void logError(String message, Throwable exception) {
LOG.error(message, exception);
}
@Override
public void logInformation(String message) {
LOG.debug(message);
}
@Override
public void logInformation(String message, Throwable exception) {
LOG.debug(message, exception);
}
});
}
private void registerFileEditorManagerListener() {
myProject.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerListener() {
@Override
public void fileOpened(final @NotNull FileEditorManager source, final @NotNull VirtualFile file) {
if (PubspecYamlUtil.PUBSPEC_YAML.equals(file.getName()) ||
FileTypeRegistry.getInstance().isFileOfType(file, DartFileType.INSTANCE)) {
DartSdkUpdateChecker.mayBeCheckForSdkUpdate(source.getProject());
}
updateCurrentFile();
if (isLocalAnalyzableFile(file)) {
updateVisibleFiles();
}
}
@Override
public void selectionChanged(@NotNull FileEditorManagerEvent event) {
updateCurrentFile();
if (isLocalAnalyzableFile(event.getOldFile()) || isLocalAnalyzableFile(event.getNewFile())) {
updateVisibleFiles();
}
}
@Override
public void fileClosed(final @NotNull FileEditorManager source, final @NotNull VirtualFile file) {
updateCurrentFile();
if (isLocalAnalyzableFile(file)) {
// file could be opened in more than one editor, so this check is needed
if (FileEditorManager.getInstance(myProject).getSelectedEditor(file) == null) {
myServerData.onFileClosed(file);
}
updateVisibleFiles();
}
}
});
}
private void registerDocumentListener() {
final DocumentListener documentListener = new DocumentListener() {
@Override
public void beforeDocumentChange(@NotNull DocumentEvent e) {
if (myServer == null) return;
myServerData.onDocumentChanged(e);
final VirtualFile file = FileDocumentManager.getInstance().getFile(e.getDocument());
if (isLocalAnalyzableFile(file)) {
for (VirtualFile fileInEditor : FileEditorManager.getInstance(myProject).getOpenFiles()) {
if (fileInEditor.equals(file)) {
synchronized (myLock) {
myChangedDocuments.add(e.getDocument());
}
break;
}
}
}
myUpdateFilesAlarm.cancelAllRequests();
myUpdateFilesAlarm.addRequest(DartAnalysisServerService.this::updateFilesContent, UPDATE_FILES_TIMEOUT);
}
};
EditorFactory.getInstance().getEventMulticaster().addDocumentListener(documentListener, this);
}
public static @NotNull DartAnalysisServerService getInstance(final @NotNull Project project) {
return project.getService(DartAnalysisServerService.class);
}
public @NotNull String getSdkVersion() {
return mySdkVersion;
}
public @NotNull String getServerVersion() {
return myServerVersion;
}
public @NotNull Project getProject() {
return myProject;
}
@Override
public void dispose() {
myDisposed = true;
stopServer();
}
public @NotNull Condition<?> getDisposedCondition() {
return myDisposedCondition;
}
public @Nullable AvailableSuggestionSet getAvailableSuggestionSet(int id) {
return myServerData.getAvailableSuggestionSet(id);
}
public @Nullable Map<String, Map<String, Set<String>>> getExistingImports(@Nullable String filePathSD) {
return myServerData.getExistingImports(filePathSD);
}
public @NotNull List<DartServerData.DartError> getErrors(final @NotNull VirtualFile file) {
return myServerData.getErrors(file);
}
public List<DartServerData.DartError> getErrors(final @NotNull SearchScope scope) {
return myServerData.getErrors(scope);
}
public @NotNull List<DartServerData.DartHighlightRegion> getHighlight(final @NotNull VirtualFile file) {
return myServerData.getHighlight(file);
}
public @NotNull List<DartServerData.DartNavigationRegion> getNavigation(final @NotNull VirtualFile file) {
return myServerData.getNavigation(file);
}
public @NotNull List<DartServerData.DartOverrideMember> getOverrideMembers(final @NotNull VirtualFile file) {
return myServerData.getOverrideMembers(file);
}
public @NotNull List<DartServerData.DartRegion> getImplementedClasses(final @NotNull VirtualFile file) {
return myServerData.getImplementedClasses(file);
}
public @NotNull List<DartServerData.DartRegion> getImplementedMembers(final @NotNull VirtualFile file) {
return myServerData.getImplementedMembers(file);
}
public @NotNull List<DartServerData.DartClosingLabel> getClosingLabels(final @NotNull VirtualFile file) {
return myServerData.getClosingLabels(file);
}
@Contract("null -> null")
public @Nullable Outline getOutline(final @Nullable VirtualFile file) {
if (file == null) return null;
return myServerData.getOutline(file);
}
@Nullable
VirtualFile getNotLocalVirtualFile(@NotNull String fileUri) {
return myServerData.getNotLocalVirtualFile(fileUri);
}
void updateCurrentFile() {
ModalityUiUtil.invokeLaterIfNeeded(ModalityState.nonModal(), myDisposedCondition,
() -> DartProblemsView.getInstance(myProject).setCurrentFile(getCurrentOpenFile())
);
}
public boolean isInIncludedRoots(final @Nullable VirtualFile vFile) {
return myRootsHandler.isInIncludedRoots(vFile);
}
private @Nullable VirtualFile getCurrentOpenFile() {
final VirtualFile[] files = FileEditorManager.getInstance(myProject).getSelectedFiles();
if (files.length > 0 && files[0].isInLocalFileSystem()) {
return files[0];
}
return null;
}
public void updateVisibleFiles() {
ApplicationManager.getApplication().assertReadAccessAllowed();
synchronized (myLock) {
final List<String> newVisibleFileUris = new ArrayList<>();
for (VirtualFile file : FileEditorManager.getInstance(myProject).getSelectedFiles()) {
if (isLocalAnalyzableFile(file)) {
newVisibleFileUris.add(getFileUri(file));
}
}
if (!Comparing.haveEqualElements(myVisibleFileUris, newVisibleFileUris)) {
myVisibleFileUris.clear();
myVisibleFileUris.addAll(newVisibleFileUris);
analysis_setPriorityFiles();
analysis_setSubscriptions();
}
}
}
/**
* Return true if the given file can be analyzed by Dart Analysis Server.
*/
@Contract("null->false")
public static boolean isLocalAnalyzableFile(final @Nullable VirtualFile file) {
if (file == null) return false;
return file.getUserData(DartFileInfoKt.DART_NOT_LOCAL_FILE_URI_KEY) != null ||
file.isInLocalFileSystem() && isFileNameRespectedByAnalysisServer(file.getName());
}
public static boolean isFileNameRespectedByAnalysisServer(@NotNull String _fileName) {
// see https://github.com/dart-lang/sdk/blob/master/pkg/analyzer/lib/src/generated/engine.dart (class AnalysisEngine)
// and AbstractAnalysisServer.analyzableFilePatterns
@NonNls String fileName = _fileName.toLowerCase(Locale.US);
return fileName.endsWith(".dart") ||
fileName.endsWith(".htm") ||
fileName.endsWith(".html") ||
fileName.equals("analysis_options.yaml") ||
fileName.equals("pubspec.yaml") ||
fileName.equals("fix_data.yaml") ||
fileName.equals("androidmanifest.xml");
}
public void updateFilesContent() {
if (myServer != null) {
ApplicationManager.getApplication().runReadAction(this::doUpdateFilesContent);
}
}
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) {
return;
}
myUpdateFilesAlarm.cancelAllRequests();
final Map<String, Object> fileUriToContentOverlay = new HashMap<>();
final Set<String> filePathsToRemoveContentOverlay;
ApplicationManager.getApplication().assertReadAccessAllowed();
synchronized (myLock) {
final Set<String> oldTrackedFilePaths = new HashSet<>(myFilePathWithOverlaidContentToTimestamp.keySet());
final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
// some documents in myChangedDocuments may be updated by external change, such as switch branch, that's why we track them,
// getUnsavedDocuments() is not enough, we must make sure that overlaid content is sent for myChangedDocuments as well (to trigger DAS notifications)
final Set<Document> documents = new HashSet<>(myChangedDocuments);
myChangedDocuments.clear();
ContainerUtil.addAll(documents, fileDocumentManager.getUnsavedDocuments());
for (Document document : documents) {
final VirtualFile file = fileDocumentManager.getFile(document);
if (isLocalAnalyzableFile(file) && file.isInLocalFileSystem()) {
oldTrackedFilePaths.remove(file.getPath());
final Long oldTimestamp = myFilePathWithOverlaidContentToTimestamp.get(file.getPath());
if (oldTimestamp == null || document.getModificationStamp() != oldTimestamp) {
fileUriToContentOverlay.put(getLocalFileUri(file.getPath()), new AddContentOverlay(document.getText()));
myFilePathWithOverlaidContentToTimestamp.put(file.getPath(), document.getModificationStamp());
}
}
}
// oldTrackedFilePaths at this point contains only those files that are not in FileDocumentManager.getUnsavedDocuments() anymore
filePathsToRemoveContentOverlay = Collections.unmodifiableSet(oldTrackedFilePaths);
for (String oldPath : filePathsToRemoveContentOverlay) {
if (myFilePathWithOverlaidContentToTimestamp.get(oldPath) != null) {
fileUriToContentOverlay.put(getLocalFileUri(oldPath), new RemoveContentOverlay());
}
}
if (LOG.isDebugEnabled()) {
final Set<String> overlaidFileUris = new HashSet<>(fileUriToContentOverlay.keySet());
for (String filePathToRemoveContentOverlay : filePathsToRemoveContentOverlay) {
overlaidFileUris.remove(getLocalFileUri(filePathToRemoveContentOverlay));
}
if (!overlaidFileUris.isEmpty()) {
LOG.debug("Sending overlaid content: " + StringUtil.join(overlaidFileUris, ",\n"));
}
if (!filePathsToRemoveContentOverlay.isEmpty()) {
LOG.debug("Removing overlaid content: " + StringUtil.join(filePathsToRemoveContentOverlay, ",\n"));
}
}
}
if (!fileUriToContentOverlay.isEmpty()) {
server.analysis_updateContent(fileUriToContentOverlay, () -> {
synchronized (myFilePathWithOverlaidContentToTimestamp) {
filePathsToRemoveContentOverlay.forEach(myFilePathWithOverlaidContentToTimestamp::remove);
}
myServerData.onFilesContentUpdated();
});
}
}
public void ensureAnalysisRootsUpToDate() {
myRootsHandler.scheduleDartRootsUpdate(null);
}
boolean setAnalysisRoots(@NotNull List<String> includedRootPaths, @NotNull List<String> excludedRootPaths) {
AnalysisServer server = myServer;
if (server == null) {
return false;
}
if (LOG.isDebugEnabled()) {
LOG.debug("analysis_setAnalysisRoots, included:\n" + StringUtil.join(includedRootPaths, ",\n") +
"\nexcluded:\n" + StringUtil.join(excludedRootPaths, ",\n"));
}
List<String> includedRootUris = ContainerUtil.map(includedRootPaths, this::getLocalFileUri);
List<String> excludedRootUris = ContainerUtil.map(excludedRootPaths, this::getLocalFileUri);
server.analysis_setAnalysisRoots(includedRootUris, excludedRootUris, null);
return true;
}
private void onErrorsUpdated(@NotNull DartLocalFileInfo localFileInfo,
@NotNull List<? extends AnalysisError> errors,
boolean hasSevereProblems,
int errorsHash) {
String filePath = localFileInfo.getFilePath();
updateFilesWithErrorsSet(filePath, hasSevereProblems, errorsHash);
DartProblemsView.getInstance(myProject).updateErrorsForFile(filePath, errors);
}
private void updateFilesWithErrorsSet(final @NotNull String filePath, final boolean hasSevereProblems, final int errorsHash) {
synchronized (myFilePathsWithErrors) {
if (errorsHash == 0) {
// no errors
myFilePathToErrorsHash.removeInt(filePath);
}
else {
myFilePathToErrorsHash.put(filePath, errorsHash);
}
if (hasSevereProblems) {
if (myFilePathsWithErrors.add(filePath)) {
String parentPath = PathUtil.getParentPath(filePath);
while (!parentPath.isEmpty()) {
final int count = myFolderPathsWithErrors.getInt(parentPath); // returns zero if there were no path in the map
myFolderPathsWithErrors.put(parentPath, count + 1);
parentPath = PathUtil.getParentPath(parentPath);
}
}
}
else {
if (myFilePathsWithErrors.remove(filePath)) {
String parentPath = PathUtil.getParentPath(filePath);
while (!parentPath.isEmpty()) {
final int count = myFolderPathsWithErrors.removeInt(parentPath); // returns zero if there was no path in the map
if (count > 1) {
myFolderPathsWithErrors.put(parentPath, count - 1);
}
parentPath = PathUtil.getParentPath(parentPath);
}
}
}
}
}
private void clearAllErrors() {
synchronized (myFilePathsWithErrors) {
myFilePathsWithErrors.clear();
myFilePathToErrorsHash.clear();
myFolderPathsWithErrors.clear();
}
if (myInitializationOnServerStartupDone) {
DartProblemsView.getInstance(myProject).clearAll();
}
}
public @NotNull List<HoverInformation> analysis_getHover(final @NotNull VirtualFile file, final int _offset) {
final AnalysisServer server = myServer;
if (server == null) {
return HoverInformation.EMPTY_LIST;
}
final String fileUri = getFileUri(file);
final List<HoverInformation> result = new ArrayList<>();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.analysis_getHover(fileUri, offset, new GetHoverConsumer() {
@Override
public void computedHovers(HoverInformation[] hovers) {
Collections.addAll(result, hovers);
latch.countDown();
}
@Override
public void onError(RequestError error) {
logError("analysis_getHover()", fileUri, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_HOVER_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("analysis_getHover", GET_HOVER_TIMEOUT, fileUri);
}
return result;
}
public @Nullable List<DartServerData.DartNavigationRegion> analysis_getNavigation(final @NotNull VirtualFile file,
final int _offset,
final int length) {
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final String fileUri = getFileUri(file);
final Ref<List<DartServerData.DartNavigationRegion>> resultRef = Ref.create();
final CountDownLatch latch = new CountDownLatch(1);
LOG.debug("analysis_getNavigation(" + fileUri + ")");
final int offset = getOriginalOffset(file, _offset);
server.analysis_getNavigation(fileUri, offset, length, new GetNavigationConsumer() {
@Override
public void computedNavigation(final List<NavigationRegion> regions) {
final List<DartServerData.DartNavigationRegion> dartRegions = new ArrayList<>(regions.size());
for (NavigationRegion region : regions) {
if (region.getLength() > 0) {
dartRegions.add(DartServerData.createDartNavigationRegion(DartAnalysisServerService.this, file, region));
}
}
resultRef.set(dartRegions);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
if (RequestErrorCode.GET_NAVIGATION_INVALID_FILE.equals(error.getCode())) {
LOG.info(getShortErrorMessage("analysis_getNavigation()", fileUri, error));
}
else {
logError("analysis_getNavigation()", fileUri, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_NAVIGATION_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("analysis_getNavigation", GET_NAVIGATION_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @NotNull List<SourceChange> 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) {
return Collections.emptyList();
}
final String fileUri = getLocalFileUri(file.getPath());
final List<SourceChange> results = new ArrayList<>();
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() {
@Override
public void computedSourceChanges(List<SourceChange> sourceChanges) {
results.addAll(sourceChanges);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
logError("edit_getAssists()", fileUri, error);
latch.countDown();
}
});
final long timeout = ApplicationManager.getApplication().isDispatchThread() ? GET_ASSISTS_TIMEOUT_EDT : GET_ASSISTS_TIMEOUT;
awaitForLatchCheckingCanceled(server, latch, timeout);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_getAssists", timeout, fileUri);
}
return results;
}
public boolean edit_isPostfixCompletionApplicable(VirtualFile file, int _offset, String key) {
if (!file.isInLocalFileSystem()) return false;
final AnalysisServer server = myServer;
if (server == null) {
return false;
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<Boolean> resultRef = Ref.create(false);
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.edit_isPostfixCompletionApplicable(fileUri, key, offset, new IsPostfixCompletionApplicableConsumer() {
@Override
public void isPostfixCompletionApplicable(Boolean value) {
resultRef.set(value);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, POSTFIX_COMPLETION_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_isPostfixCompletionApplicable", POSTFIX_COMPLETION_TIMEOUT, fileUri);
}
return resultRef.get();
}
public PostfixTemplateDescriptor @Nullable [] edit_listPostfixCompletionTemplates() {
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
if (StringUtil.compareVersionNumbers(mySdkVersion, "1.25") < 0) {
return PostfixTemplateDescriptor.EMPTY_ARRAY;
}
final Ref<PostfixTemplateDescriptor[]> resultRef = Ref.create();
final CountDownLatch latch = new CountDownLatch(1);
server.edit_listPostfixCompletionTemplates(new ListPostfixCompletionTemplatesConsumer() {
@Override
public void postfixCompletionTemplates(PostfixTemplateDescriptor[] templates) {
resultRef.set(templates);
latch.countDown();
}
@Override
public void onError(RequestError error) {
if (!RequestErrorCode.UNKNOWN_REQUEST.equals(error.getCode())) {
logError("edit_listPostfixCompletionTemplates()", null, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, POSTFIX_INITIALIZATION_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_listPostfixCompletionTemplates", POSTFIX_INITIALIZATION_TIMEOUT, null);
}
return resultRef.get();
}
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) {
return null;
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<SourceChange> resultRef = Ref.create();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.edit_getPostfixCompletion(fileUri, key, offset, new GetPostfixCompletionConsumer() {
@Override
public void computedSourceChange(SourceChange sourceChange) {
resultRef.set(sourceChange);
latch.countDown();
}
@Override
public void onError(RequestError error) {
logError("edit_getPostfixCompletion()", fileUri, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, POSTFIX_COMPLETION_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_getPostfixCompletion", POSTFIX_COMPLETION_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @Nullable SourceChange edit_getStatementCompletion(final @NotNull VirtualFile file, final int _offset) {
if (!file.isInLocalFileSystem()) return null;
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<SourceChange> resultRef = Ref.create();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.edit_getStatementCompletion(fileUri, offset, new GetStatementCompletionConsumer() {
@Override
public void computedSourceChange(SourceChange sourceChange) {
resultRef.set(sourceChange);
latch.countDown();
}
@Override
public void onError(RequestError error) {
latch.countDown();
logError("edit_getStatementCompletion()", fileUri, error);
}
});
awaitForLatchCheckingCanceled(server, latch, STATEMENT_COMPLETION_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_getStatementCompletion", STATEMENT_COMPLETION_TIMEOUT, fileUri);
}
return resultRef.get();
}
public void diagnostic_getServerPort(GetServerPortConsumer consumer) {
final AnalysisServer server = myServer;
if (server == null) {
consumer.onError(new RequestError(ExtendedRequestErrorCode.INVALID_SERVER_RESPONSE,
DartBundle.message("analysis.server.not.running"), null));
}
else {
server.diagnostic_getServerPort(consumer);
}
}
/**
* If server responds in less than {@code GET_FIXES_TIMEOUT_EDT} / {@code GET_FIXES_TIMEOUT} then this method can be considered synchronous: when exiting this method
* {@code consumer} is already notified. Otherwise, this method is async.
*/
public void askForFixesAndWaitABitIfReceivedQuickly(final @NotNull VirtualFile file,
final int _offset,
final @NotNull Consumer<? super List<AnalysisErrorFixes>> consumer) {
if (!file.isInLocalFileSystem()) {
consumer.consume(Collections.emptyList());
return;
}
final AnalysisServer server = myServer;
if (server == null) {
consumer.consume(Collections.emptyList());
return;
}
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() {
@Override
public void computedFixes(final List<AnalysisErrorFixes> fixes) {
consumer.consume(fixes);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
logError("edit_getFixes()", fileUri, error);
latch.countDown();
}
});
final long timeout = ApplicationManager.getApplication().isDispatchThread() ? GET_FIXES_TIMEOUT_EDT : GET_FIXES_TIMEOUT;
awaitForLatchCheckingCanceled(server, latch, timeout);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_getFixes", timeout, fileUri);
}
}
public void search_findElementReferences(final @NotNull VirtualFile file,
final int _offset,
final @NotNull Consumer<? super SearchResult> consumer) {
final AnalysisServer server = myServer;
if (server == null) {
return;
}
final String fileUri = getFileUri(file);
final Ref<String> searchIdRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.search_findElementReferences(fileUri, offset, true, new FindElementReferencesConsumer() {
@Override
public void computedElementReferences(String searchId, Element element) {
searchIdRef.set(searchId);
latch.countDown();
}
@Override
public void onError(RequestError error) {
LOG.info(getShortErrorMessage("search_findElementReferences()", fileUri, error));
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, FIND_ELEMENT_REFERENCES_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("search_findElementReferences", FIND_ELEMENT_REFERENCES_TIMEOUT, fileUri + "@" + offset);
return;
}
final String searchId = searchIdRef.get();
if (searchId == null) {
return;
}
while (true) {
ProgressManager.checkCanceled();
synchronized (mySearchResultSets) {
SearchResultsSet resultSet;
// process already received results
while ((resultSet = mySearchResultSets.poll()) != null) {
if (!resultSet.id.equals(searchId)) continue;
for (final SearchResult searchResult : resultSet.results) {
consumer.consume(searchResult);
}
if (resultSet.isLast) return;
}
// wait for more results
try {
mySearchResultSets.wait(CHECK_CANCELLED_PERIOD);
}
catch (InterruptedException e) {
return;
}
}
}
}
public @NotNull List<TypeHierarchyItem> search_getTypeHierarchy(final @NotNull VirtualFile file,
final int _offset,
final boolean superOnly) {
final List<TypeHierarchyItem> results = new ArrayList<>();
final AnalysisServer server = myServer;
if (server == 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() {
@Override
public void computedHierarchy(List<TypeHierarchyItem> hierarchyItems) {
results.addAll(hierarchyItems);
latch.countDown();
}
@Override
public void onError(RequestError error) {
logError("search_getTypeHierarchy()", fileUri, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_TYPE_HIERARCHY_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("search_getTypeHierarchy", GET_TYPE_HIERARCHY_TIMEOUT, fileUri);
}
return results;
}
public @Nullable Pair<String, SourceChange> completion_getSuggestionDetails(@NotNull VirtualFile file,
int id,
String label,
int _offset) {
if (!file.isInLocalFileSystem()) return null;
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<Pair<String, SourceChange>> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.completion_getSuggestionDetails(fileUri, id, label, offset, new GetSuggestionDetailsConsumer() {
@Override
public void computedDetails(String completion, SourceChange change) {
resultRef.set(new Pair<>(completion, change));
latch.countDown();
}
@Override
public void onError(RequestError requestError) {
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTION_DETAILS_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("completion_getSuggestionDetails", GET_SUGGESTION_DETAILS_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @Nullable Pair<String, SourceChange> completion_getSuggestionDetails2(@NotNull VirtualFile file,
int _offset,
@NotNull String completion,
@NotNull String libraryUri) {
if (!file.isInLocalFileSystem()) return null;
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<Pair<String, SourceChange>> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.completion_getSuggestionDetails2(fileUri, offset, completion, libraryUri, new GetSuggestionDetailsConsumer2() {
@Override
public void computedDetails(String completion, SourceChange change) {
resultRef.set(new Pair<>(completion, change));
latch.countDown();
}
@Override
public void onError(RequestError requestError) {
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTION_DETAILS2_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("completion_getSuggestionDetails2", GET_SUGGESTION_DETAILS2_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @Nullable String completion_getSuggestions(final @NotNull VirtualFile file, final int _offset) {
if (!file.isInLocalFileSystem()) return null;
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
for (DartCompletionTimerExtension extension : DartCompletionTimerExtension.getExtensions()) {
extension.dartCompletionStart();
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<String> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.completion_getSuggestions(fileUri, offset, new GetSuggestionsConsumer() {
@Override
public void computedCompletionId(final @NotNull String completionId) {
resultRef.set(completionId);
latch.countDown();
}
@Override
public void onError(final @NotNull RequestError error) {
for (DartCompletionTimerExtension extension : DartCompletionTimerExtension.getExtensions()) {
extension.dartCompletionError(StringUtil.notNullize(error.getCode()), StringUtil.notNullize(error.getMessage()),
StringUtil.notNullize(error.getStackTrace()));
}
// Not a problem. Happens if a file is outside the project, or server is just not ready yet.
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTIONS_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("completion_getSuggestions", GET_SUGGESTIONS_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @Nullable CompletionInfo2 completion_getSuggestions2(final @NotNull VirtualFile file,
final int _offset,
final int maxResults,
final String completionMode,
final int invocationCount) {
if (!file.isInLocalFileSystem()) return null;
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
for (DartCompletionTimerExtension extension : DartCompletionTimerExtension.getExtensions()) {
extension.dartCompletionStart();
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<CompletionInfo2> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
final String completionCaseMatchingMode;
final int caseSensitiveSetting = CodeInsightSettings.getInstance().getCompletionCaseSensitive();
if (caseSensitiveSetting == CodeInsightSettings.FIRST_LETTER) {
completionCaseMatchingMode = CompletionCaseMatchingMode.FIRST_CHAR;
}
else if (caseSensitiveSetting == CodeInsightSettings.NONE) {
completionCaseMatchingMode = CompletionCaseMatchingMode.NONE;
}
else {
completionCaseMatchingMode = CompletionCaseMatchingMode.ALL_CHARS;
}
server.completion_getSuggestions2(fileUri, offset, maxResults, completionCaseMatchingMode, completionMode, invocationCount, -1,
new GetSuggestionsConsumer2() {
@Override
public void computedSuggestions(int replacementOffset,
int replacementLength,
List<CompletionSuggestion> suggestions,
boolean isIncomplete) {
resultRef.set(
new CompletionInfo2(replacementOffset, replacementLength, suggestions, isIncomplete));
latch.countDown();
for (DartCompletionTimerExtension extension : DartCompletionTimerExtension.getExtensions()) {
extension.dartCompletionEnd();
}
}
@Override
public void onError(final @NotNull RequestError error) {
// Not a problem. Happens if a file is outside the project, or server is just not ready yet.
latch.countDown();
for (DartCompletionTimerExtension extension : DartCompletionTimerExtension.getExtensions()) {
extension.dartCompletionError(StringUtil.notNullize(error.getCode()),
StringUtil.notNullize(error.getMessage()),
StringUtil.notNullize(error.getStackTrace()));
}
}
});
awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTIONS_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("completion_getSuggestions2", GET_SUGGESTIONS_TIMEOUT, fileUri);
for (DartCompletionTimerExtension extension : DartCompletionTimerExtension.getExtensions()) {
extension.dartCompletionEnd();
}
}
return resultRef.get();
}
public @Nullable FormatResult edit_format(final @NotNull VirtualFile file,
final int _selectionOffset,
final int _selectionLength,
final int lineLength) {
if (!file.isInLocalFileSystem()) return null;
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final String fileUri = getLocalFileUri(file.getPath());
final Ref<FormatResult> resultRef = new Ref<>();
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() {
@Override
public void computedFormat(final List<SourceEdit> edits, final int selectionOffset, final int selectionLength) {
resultRef.set(new FormatResult(edits, selectionOffset, selectionLength));
latch.countDown();
}
@Override
public void onError(final RequestError error) {
if (RequestErrorCode.FORMAT_WITH_ERRORS.equals(error.getCode()) || RequestErrorCode.FORMAT_INVALID_FILE.equals(error.getCode())) {
LOG.info(getShortErrorMessage("edit_format()", fileUri, error));
}
else {
logError("edit_format()", fileUri, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EDIT_FORMAT_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_format", EDIT_FORMAT_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @Nullable List<ImportedElements> 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) {
return null;
}
final String fileUri = getFileUri(file);
final Ref<List<ImportedElements>> resultRef = new Ref<>();
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() {
@Override
public void computedImportedElements(final List<ImportedElements> importedElements) {
resultRef.set(importedElements);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
if (!RequestErrorCode.GET_IMPORTED_ELEMENTS_INVALID_FILE.equals(error.getCode())) {
logError("analysis_getImportedElements()", fileUri, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, IMPORTED_ELEMENTS_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("analysis_getImportedElements", IMPORTED_ELEMENTS_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @Nullable SourceFileEdit edit_importElements(final @NotNull VirtualFile file,
final @NotNull List<ImportedElements> importedElements,
final int _offset) {
final AnalysisServer server = myServer;
if (server == null || StringUtil.compareVersionNumbers(mySdkVersion, "1.25") < 0) {
return null;
}
final String fileUri = getFileUri(file);
final Ref<SourceFileEdit> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.edit_importElements(fileUri, importedElements, offset, new ImportElementsConsumer() {
@Override
public void computedImportedElements(final SourceFileEdit edit) {
resultRef.set(edit);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
if (!RequestErrorCode.IMPORT_ELEMENTS_INVALID_FILE.equals(error.getCode())) {
logError("edit_importElements()", fileUri, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, IMPORTED_ELEMENTS_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_importElements", IMPORTED_ELEMENTS_TIMEOUT, fileUri);
}
return resultRef.get();
}
public boolean edit_getRefactoring(String kind,
VirtualFile file,
int _offset,
int _length,
boolean validateOnly,
RefactoringOptions options,
GetRefactoringConsumer consumer) {
if (!file.isInLocalFileSystem()) return false;
final AnalysisServer server = myServer;
if (server == 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);
return true;
}
public @Nullable SourceFileEdit edit_organizeDirectives(@NotNull String filePath) {
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final String fileUri = getLocalFileUri(filePath);
final Ref<SourceFileEdit> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
server.edit_organizeDirectives(fileUri, new OrganizeDirectivesConsumer() {
@Override
public void computedEdit(final SourceFileEdit edit) {
resultRef.set(edit);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
if (RequestErrorCode.ORGANIZE_DIRECTIVES_ERROR.equals(error.getCode())) {
LOG.info(getShortErrorMessage("edit_organizeDirectives()", fileUri, error));
}
else {
logError("edit_organizeDirectives()", fileUri, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EDIT_ORGANIZE_DIRECTIVES_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_organizeDirectives", EDIT_ORGANIZE_DIRECTIVES_TIMEOUT, fileUri);
}
return resultRef.get();
}
public @Nullable SourceFileEdit edit_sortMembers(@NotNull String filePath) {
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final String fileUri = getLocalFileUri(filePath);
final Ref<SourceFileEdit> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
server.edit_sortMembers(fileUri, new SortMembersConsumer() {
@Override
public void computedEdit(final SourceFileEdit edit) {
resultRef.set(edit);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
if (RequestErrorCode.SORT_MEMBERS_PARSE_ERRORS.equals(error.getCode()) ||
RequestErrorCode.SORT_MEMBERS_INVALID_FILE.equals(error.getCode())) {
LOG.info(getShortErrorMessage("edit_sortMembers()", fileUri, error));
}
else {
logError("edit_sortMembers()", fileUri, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EDIT_SORT_MEMBERS_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("edit_sortMembers", EDIT_SORT_MEMBERS_TIMEOUT, fileUri);
}
return resultRef.get();
}
public void analysis_reanalyze() {
final AnalysisServer server = myServer;
if (server == null) {
return;
}
server.analysis_reanalyze();
ApplicationManager.getApplication().invokeLater(this::clearAllErrors, ModalityState.nonModal(), myDisposedCondition);
}
private void analysis_setPriorityFiles() {
synchronized (myLock) {
if (myServer == null) return;
if (LOG.isDebugEnabled()) {
LOG.debug("analysis_setPriorityFiles, files:\n" + StringUtil.join(myVisibleFileUris, ",\n"));
}
myServer.analysis_setPriorityFiles(myVisibleFileUris);
}
}
private void analysis_setSubscriptions() {
synchronized (myLock) {
if (myServer == null) return;
final Map<String, List<String>> subscriptions = new HashMap<>();
subscriptions.put(AnalysisService.HIGHLIGHTS, myVisibleFileUris);
subscriptions.put(AnalysisService.NAVIGATION, myVisibleFileUris);
subscriptions.put(AnalysisService.OVERRIDES, myVisibleFileUris);
subscriptions.put(AnalysisService.OUTLINE, myVisibleFileUris);
if (StringUtil.compareVersionNumbers(mySdkVersion, "1.13") >= 0) {
subscriptions.put(AnalysisService.IMPLEMENTED, myVisibleFileUris);
}
if (StringUtil.compareVersionNumbers(mySdkVersion, "1.25.0") >= 0) {
subscriptions.put(AnalysisService.CLOSING_LABELS, myVisibleFileUris);
}
if (LOG.isDebugEnabled()) {
LOG.debug("analysis_setSubscriptions, subscriptions:\n" + subscriptions);
}
myServer.analysis_setSubscriptions(subscriptions);
}
}
public @Nullable String execution_createContext(@NotNull String filePath) {
return execution_createContext(filePath, new DummyCoverageLoadErrorReporter());
}
public @Nullable String execution_createContext(@NotNull String filePath, @NotNull CoverageLoadErrorReporter reporter) {
final AnalysisServer server = myServer;
if (server == null) {
String message = "Dart Analysis Server is not available.";
LOG.warn(message);
reporter.reportWarning(message, null);
return null;
}
final String fileUri = getLocalFileUri(filePath);
final Ref<String> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
server.execution_createContext(fileUri, new CreateContextConsumer() {
@Override
public void computedExecutionContext(final String contextId) {
resultRef.set(contextId);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
logError("execution_createContext()", fileUri, error);
String message = getShortErrorMessage("execution_createContext()", fileUri, error);
reporter.reportError("Execution context creation failed: " + message);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EXECUTION_CREATE_CONTEXT_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("execution_createContext", EXECUTION_CREATE_CONTEXT_TIMEOUT, fileUri);
String message = "Execution context creation timed out.";
reporter.reportWarning(message, null);
}
return resultRef.get();
}
public void execution_deleteContext(final @NotNull String contextId) {
final AnalysisServer server = myServer;
if (server != null) {
server.execution_deleteContext(contextId);
}
}
public @Nullable Pair<List<CompletionSuggestion>, List<RuntimeCompletionExpression>> execution_getSuggestions(@NotNull String code,
int offset,
@NotNull VirtualFile contextFile,
int contextOffset,
@NotNull List<RuntimeCompletionVariable> variables,
@NotNull List<RuntimeCompletionExpression> expressions) {
final AnalysisServer server = myServer;
if (server == null) {
return new Pair<>(new ArrayList<>(), new ArrayList<>());
}
final String contextFileUri = getFileUri(contextFile);
final CountDownLatch latch = new CountDownLatch(1);
final Ref<Pair<List<CompletionSuggestion>, List<RuntimeCompletionExpression>>> refResult = Ref.create();
server.execution_getSuggestions(
code, offset,
contextFileUri, contextOffset,
variables, expressions,
new GetRuntimeCompletionConsumer() {
@Override
public void computedResult(List<CompletionSuggestion> suggestions, List<RuntimeCompletionExpression> expressions) {
refResult.set(new Pair<>(suggestions, expressions));
latch.countDown();
}
@Override
public void onError(RequestError error) {
latch.countDown();
if (!RequestErrorCode.UNKNOWN_REQUEST.equals(error.getCode())) {
logError("execution_getSuggestions()", contextFileUri, error);
}
}
});
awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTIONS_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("execution_getSuggestions", GET_SUGGESTIONS_TIMEOUT, contextFileUri);
}
return refResult.get();
}
public @Nullable String execution_mapUri(@NotNull String _id, @NotNull VirtualFile _file) {
return execution_mapUri(_id, getFileUri(_file), null);
}
public @Nullable String execution_mapUri(@NotNull String _id, @NotNull String _executionContextUri) {
return execution_mapUri(_id, null, _executionContextUri);
}
/**
* @deprecated use {@link #execution_mapUri(String, VirtualFile)} or {@link #execution_mapUri(String, String)}
*/
@Deprecated
public @Nullable String execution_mapUri(@NotNull String _id, @Nullable String _filePathOrUri, @Nullable String _executionContextUri) {
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
// From the Dart Analysis Server Spec:
// Exactly one of the file and uri fields must be provided. If both fields are provided, then an error of type INVALID_PARAMETER will
// be generated. Similarly, if neither field is provided, then an error of type INVALID_PARAMETER will be generated.
if ((_filePathOrUri == null && _executionContextUri == null) || (_filePathOrUri != null && _executionContextUri != null)) {
LOG.error("execution_mapUri - one of _filePathOrUri and _executionContextUri must be non-null.");
return null;
}
if (_filePathOrUri != null && !_filePathOrUri.contains("://")) {
_filePathOrUri = FileUtil.toSystemDependentName(_filePathOrUri);
}
final Ref<String> resultRef = new Ref<>();
final CountDownLatch latch = new CountDownLatch(1);
server.execution_mapUri(_id, _filePathOrUri, _executionContextUri, new MapUriConsumer() {
@Override
public void computedFileOrUri(final String file, final String uri) {
if (uri != null) {
resultRef.set(uri);
}
else {
resultRef.set(file);
}
latch.countDown();
}
@Override
public void onError(final RequestError error) {
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EXECUTION_MAP_URI_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("execution_mapUri", EXECUTION_MAP_URI_TIMEOUT, _filePathOrUri != null ? _filePathOrUri : _executionContextUri);
return null;
}
if (_executionContextUri != null && !resultRef.isNull()) {
return FileUtil.toSystemIndependentName(resultRef.get());
}
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) {
return null;
}
Ref<String> resultRef = new Ref<>();
CountDownLatch latch = new CountDownLatch(1);
server.lspMessage_dart_textDocumentContent(fileUri, new DartLspTextDocumentContentConsumer() {
@Override
public void computedDocumentContents(@NotNull String contents) {
resultRef.set(StringUtil.convertLineSeparators(contents));
latch.countDown();
}
@Override
public void onError(RequestError error) {
logError("lspMessage_dart_textDocumentContent()", fileUri, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, LSP_MESSAGE_TEXT_DOCUMENT_CONTENT_TIMEOUT);
if (latch.getCount() > 0) {
logTookTooLongMessage("lspMessage_dart_textDocumentContent", LSP_MESSAGE_TEXT_DOCUMENT_CONTENT_TIMEOUT, fileUri);
}
return resultRef.get();
}
private void startServer(final @NotNull DartSdk sdk) {
if (DartPubActionBase.isInProgress()) return; // DartPubActionBase will start the server itself when finished
synchronized (myLock) {
mySdkHome = sdk.getHomePath();
final String runtimePath = FileUtil.toSystemDependentName(DartSdkUtil.getDartExePath(sdk));
// If true, then the DAS will be started via `dart language-server`, instead of `dart .../analysis_server.dart.snapshot`
final boolean useDartLangServerCall = isDartSdkVersionSufficientForDartLangServer(sdk);
String analysisServerPath = FileUtil.toSystemDependentName(mySdkHome + "/bin/snapshots/analysis_server.dart.snapshot");
analysisServerPath = System.getProperty("dart.server.path", analysisServerPath);
String dasStartupErrorMessage = "";
final File runtimePathFile = new File(runtimePath);
final File dasSnapshotFile = new File(analysisServerPath);
if (!runtimePathFile.exists()) {
dasStartupErrorMessage = DartBundle.message("dart.vm.file.does.not.exist.at.0", runtimePath);
}
else if (!useDartLangServerCall && !dasSnapshotFile.exists()) {
dasStartupErrorMessage = DartBundle.message("analysis.server.snapshot.file.does.not.exist.at.0", analysisServerPath);
}
else if (!runtimePathFile.canExecute()) {
dasStartupErrorMessage = DartBundle.message("dart.vm.file.is.not.executable.at.0", runtimePath);
}
else if (!useDartLangServerCall && !dasSnapshotFile.canRead()) {
dasStartupErrorMessage = DartBundle.message("analysis.server.snapshot.file.is.not.readable.at.0", analysisServerPath);
}
if (!dasStartupErrorMessage.isEmpty()) {
LOG.warn("Failed to start Dart analysis server: " + dasStartupErrorMessage);
stopServer();
return;
}
// To use a Dart Analysis Server locally, uncomment the line below and replace the `localDartSdkPath` value with your path on disk.
// When configuring the Dart Plugin, set the Dart SDK to your locally built Dart SDK.
// Directions here on getting the Dart SDK sources: https://github.com/dart-lang/sdk/wiki/Building
//
//final String localDartSdkPath = ".../dart-sdk/sdk/";
//analysisServerPath =
// FileUtil.toSystemDependentName(localDartSdkPath + "pkg/analysis_server/bin/server.dart");
final DebugPrintStream debugStream = str -> {
str = StringUtil.first(str, MAX_DEBUG_LOG_LINE_LENGTH, true);
synchronized (myDebugLog) {
myDebugLog.add(str);
}
};
String vmArgsRaw;
try {
vmArgsRaw = Registry.stringValue("dart.server.vm.options");
}
catch (MissingResourceException e) {
vmArgsRaw = "";
}
@NonNls String serverArgsRaw;
if (useDartLangServerCall) {
serverArgsRaw = "--protocol=analyzer";
}
else {
// Note that as of Dart 2.12.0 the '--useAnalysisHighlight2' flag is ignored (and is the
// default highlighting mode). We still want to pass it in for earlier SDKs.
serverArgsRaw = "--useAnalysisHighlight2";
}
try {
serverArgsRaw += " " + Registry.stringValue("dart.server.additional.arguments");
}
catch (MissingResourceException e) {
// NOP
}
String firstArgument = useDartLangServerCall ? "language-server" : analysisServerPath;
myServerSocket =
new StdioServerSocket(runtimePath, StringUtil.split(vmArgsRaw, " "), firstArgument, StringUtil.split(serverArgsRaw, " "),
debugStream);
myServerSocket.setClientId(getClientId());
myServerSocket.setClientVersion(getClientVersion());
final RemoteAnalysisServerImpl startedServer = new DartAnalysisServerImpl(myProject, myServerSocket);
try {
startedServer.start();
server_setSubscriptions(startedServer);
if (!myInitializationOnServerStartupDone) {
myInitializationOnServerStartupDone = true;
registerFileEditorManagerListener();
registerDocumentListener();
setDasLogger();
}
startedServer.addAnalysisServerListener(myAnalysisServerListener);
for (AnalysisServerListener listener : myAdditionalServerListeners) {
startedServer.addAnalysisServerListener(listener);
}
for (RequestListener listener : myRequestListeners) {
startedServer.addRequestListener(listener);
}
for (ResponseListener listener : myResponseListeners) {
startedServer.addResponseListener(listener);
}
myHaveShownInitialProgress = false;
startedServer.addStatusListener(isAlive -> {
if (!isAlive) {
synchronized (myLock) {
if (startedServer == myServer) {
// Show a notification on the dart analysis tool window.
ApplicationManager.getApplication().invokeLater(
() -> {
final DartProblemsView problemsView = DartProblemsView.getInstance(myProject);
problemsView.showErrorNotificationTerse(DartBundle.message("analysis.server.terminated"));
},
ModalityState.nonModal(),
myDisposedCondition
);
stopServer();
}
}
}
});
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);
myServer = startedServer;
// Clear any dart view notifications.
ApplicationManager.getApplication().invokeLater(
() -> {
final DartProblemsView problemsView = DartProblemsView.getInstance(myProject);
problemsView.clearNotifications();
},
ModalityState.nonModal(),
myDisposedCondition
);
// This must be done after myServer is set, and should be done each time the server starts.
registerPostfixCompletionTemplates();
myDtdUri = null;
DartToolingDaemonService dtdService = myProject.getServiceIfCreated(DartToolingDaemonService.class);
String dtdUri = dtdService != null ? dtdService.getUri() : null;
if (dtdUri != null) {
connectToDtd(dtdUri);
}
}
catch (Exception e) {
LOG.warn("Failed to start Dart analysis server", e);
stopServer();
}
}
}
public boolean isServerProcessActive() {
synchronized (myLock) {
return myServer != null && myServer.isSocketOpen();
}
}
public boolean serverReadyForRequest() {
final DartSdk sdk = DartSdk.getDartSdk(myProject);
if (sdk == null || !isDartSdkVersionSufficient(sdk)) {
stopServer();
return false;
}
ApplicationManager.getApplication().assertReadAccessAllowed();
synchronized (myLock) {
if (myServer == null ||
!sdk.getHomePath().equals(mySdkHome) ||
!sdk.getVersion().equals(mySdkVersion) ||
!myServer.isSocketOpen()) {
stopServer();
DartProblemsView.getInstance(myProject).setInitialCurrentFileBeforeServerStart(getCurrentOpenFile());
startServer(sdk);
if (myServer != null) {
myRootsHandler.onServerStarted();
}
}
return myServer != null;
}
}
public void restartServer() {
stopServer();
serverReadyForRequest();
}
void stopServer() {
synchronized (myLock) {
if (myServer != null) {
LOG.debug("stopping server");
myServer.removeAnalysisServerListener(myAnalysisServerListener);
for (AnalysisServerListener listener : myAdditionalServerListeners) {
myServer.removeAnalysisServerListener(listener);
}
for (RequestListener listener : myRequestListeners) {
myServer.removeRequestListener(listener);
}
for (ResponseListener listener : myResponseListeners) {
myServer.removeResponseListener(listener);
}
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);
}
}
stopShowingServerProgress();
myUpdateFilesAlarm.cancelAllRequests();
myServerSocket = null;
myServer = null;
mySdkHome = null;
mySdkVersion = "";
myServerVersion = "";
myFilePathWithOverlaidContentToTimestamp.clear();
myVisibleFileUris.clear();
myChangedDocuments.clear();
myServerData.clearData();
myRootsHandler.onServerStopped();
if (myProject.isOpen() && !myProject.isDisposed()) {
ApplicationManager.getApplication().invokeLater(this::clearAllErrors, ModalityState.nonModal(), myDisposedCondition);
}
}
}
public void connectToDtd(@NotNull String uri) {
AnalysisServer server = myServer;
if (server == null) {
return;
}
boolean supportsWorkspaceApplyEdits = isDartSdkVersionSufficientForWorkspaceApplyEdits(mySdkVersion);
// Connection to DTD is used for server-initiated edits (workspace.applyEdits)
if (supportsWorkspaceApplyEdits && !uri.equals(myDtdUri)) {
myDtdUri = uri;
server.lsp_connectToDtd(uri);
}
}
public void waitForAnalysisToComplete_TESTS_ONLY(final @NotNull VirtualFile file) {
assert ApplicationManager.getApplication().isUnitTestMode();
long startTime = System.currentTimeMillis();
while (isServerProcessActive() && !myServerData.hasAllData_TESTS_ONLY(file)) {
if (System.currentTimeMillis() > startTime + ANALYSIS_IN_TESTS_TIMEOUT) return;
TimeoutUtil.sleep(100);
}
}
private void waitWhileServerBusy() {
try {
synchronized (myProgressLock) {
while (myAnalysisInProgress || myPubListInProgress) {
myProgressLock.wait();
}
}
}
catch (InterruptedException e) {/* unlucky */}
}
private void stopShowingServerProgress() {
myShowServerProgressAlarm.cancelAllRequests();
synchronized (myProgressLock) {
myAnalysisInProgress = false;
myPubListInProgress = false;
myProgressLock.notifyAll();
if (mySentAnalysisBusy) {
mySentAnalysisBusy = false;
DartAnalysisServerMessages.sendAnalysisStarted(myProject, false);
}
}
}
public boolean isFileWithErrors(final @NotNull VirtualFile file) {
synchronized (myFilePathsWithErrors) {
return file.isDirectory() ? myFolderPathsWithErrors.getInt(file.getPath()) > 0 : myFilePathsWithErrors.contains(file.getPath());
}
}
public int getFilePathsWithErrorsHash() {
synchronized (myFilePathsWithErrors) {
return myFilePathsWithErrors.hashCode();
}
}
private void logError(final @NonNls @NotNull String methodName, final @Nullable String filePath, final @NotNull RequestError error) {
if (RequestErrorCode.FILE_NOT_ANALYZED.equals(error.getCode())) {
LOG.info(getShortErrorMessage(methodName, filePath, error));
return;
}
final String trace = error.getStackTrace();
final String partialTrace = trace == null || trace.isEmpty() ? "" : trace.substring(0, Math.min(trace.length(), 1000));
final String message = getShortErrorMessage(methodName, filePath, error) + "\n" + partialTrace + "...";
LOG.error(message);
}
private @NonNls @NotNull String getShortErrorMessage(@NonNls @NotNull String methodName,
@Nullable String filePath,
@NotNull RequestError error) {
return "Error from " + methodName +
(filePath == null ? "" : (", file = " + filePath)) +
", SDK version = " + mySdkVersion +
", server version = " + myServerVersion +
", error code = " + error.getCode() + ": " + error.getMessage();
}
private void logTookTooLongMessage(final @NonNls @NotNull String methodName, final long timeout, @Nullable String filePath) {
@NonNls StringBuilder builder = new StringBuilder();
builder.append(methodName).append("() took longer than ").append(timeout).append("ms");
if (filePath != null) {
builder.append(", for file ").append(filePath);
}
builder.append(", Dart SDK version: ").append(mySdkVersion);
LOG.info(builder.toString());
}
private static boolean awaitForLatchCheckingCanceled(final @NotNull AnalysisServer server,
final @NotNull CountDownLatch latch,
long timeoutInMillis) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
timeoutInMillis *= TESTS_TIMEOUT_COEFF;
}
long startTime = System.currentTimeMillis();
while (true) {
ProgressManager.checkCanceled();
if (!server.isSocketOpen()) {
return false;
}
if (timeoutInMillis != -1 && System.currentTimeMillis() > startTime + timeoutInMillis) {
return false;
}
if (Uninterruptibles.awaitUninterruptibly(latch, CHECK_CANCELLED_PERIOD, TimeUnit.MILLISECONDS)) {
return true;
}
}
}
private void registerPostfixCompletionTemplates() {
ApplicationManager.getApplication().executeOnPooledThread(() -> DartPostfixTemplateProvider.initializeTemplates(this));
}
public interface CompletionSuggestionConsumer {
void consumeCompletionSuggestion(final int replacementOffset,
final int replacementLength,
final @NotNull CompletionSuggestion completionSuggestion);
}
public interface CompletionLibraryRefConsumer {
void consumeLibraryRef(@NotNull IncludedSuggestionSet includedSet,
@NotNull Set<String> includedKinds,
@NotNull Map<String, IncludedSuggestionRelevanceTag> includedRelevanceTags,
@Nullable String libraryFilePathSD);
}
private static class CompletionInfo {
private final @NotNull String myCompletionId;
/**
* must be converted before any usage
*/
private final int myOriginalReplacementOffset;
private final int myReplacementLength;
private final @NotNull List<CompletionSuggestion> myCompletions;
private final @NotNull List<IncludedSuggestionSet> myIncludedSuggestionSets;
private final @NotNull List<String> myIncludedElementKinds;
private final @NotNull List<IncludedSuggestionRelevanceTag> myIncludedSuggestionRelevanceTags;
private final boolean isLast;
private final @Nullable String myLibraryFilePathSD;
CompletionInfo(final @NotNull String completionId,
int replacementOffset,
int replacementLength,
final @NotNull List<CompletionSuggestion> completions,
final @NotNull List<IncludedSuggestionSet> includedSuggestionSets,
final @NotNull List<String> includedElementKinds,
final @NotNull List<IncludedSuggestionRelevanceTag> includedSuggestionRelevanceTags,
boolean isLast,
@Nullable String libraryFilePathSD) {
this.myCompletionId = completionId;
this.myOriginalReplacementOffset = replacementOffset;
this.myReplacementLength = replacementLength;
this.myCompletions = completions;
this.myIncludedSuggestionSets = includedSuggestionSets;
this.myIncludedElementKinds = includedElementKinds;
this.myIncludedSuggestionRelevanceTags = includedSuggestionRelevanceTags;
this.isLast = isLast;
this.myLibraryFilePathSD = libraryFilePathSD;
}
}
public static class CompletionInfo2 {
public final int myReplacementOffset;
public final int myReplacementLength;
public final @NotNull List<CompletionSuggestion> mySuggestions;
public final boolean myIsIncomplete;
CompletionInfo2(int replacementOffset,
int replacementLength,
@NotNull List<CompletionSuggestion> suggestions,
boolean isIncomplete) {
myReplacementOffset = replacementOffset;
myReplacementLength = replacementLength;
mySuggestions = suggestions;
myIsIncomplete = isIncomplete;
}
}
/**
* A set of {@link SearchResult}s.
*/
private static class SearchResultsSet {
final @NotNull String id;
final @NotNull List<SearchResult> results;
final boolean isLast;
SearchResultsSet(@NotNull String id, @NotNull List<SearchResult> results, boolean isLast) {
this.id = id;
this.results = results;
this.isLast = isLast;
}
}
public void addOutlineListener(final @NotNull DartServerData.OutlineListener listener) {
myServerData.addOutlineListener(listener);
}
public void removeOutlineListener(final @NotNull DartServerData.OutlineListener listener) {
myServerData.removeOutlineListener(listener);
}
/**
* Generate and return a unique {@link String} id to be used to sent requests.
*/
@SuppressWarnings("unused") // for Flutter plugin
public String generateUniqueId() {
final RemoteAnalysisServerImpl server = myServer;
if (server == null) {
return null;
}
return server.generateUniqueId();
}
/**
* Send the request for which the client that does not expect a response.
*/
@SuppressWarnings("unused") // for Flutter plugin
public void sendRequest(String id, JsonObject request) {
final RemoteAnalysisServerImpl server = myServer;
if (server != null) {
server.sendRequestToServer(id, request);
}
}
/**
* Send the request and associate it with the passed {@link com.google.dart.server.Consumer}.
*/
@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);
}
}
/**
* Subscribe for verbose analysis server `server.log` notifications.
*/
@SuppressWarnings("unused") // for Flutter plugin
public void setServerLogSubscription(boolean subscribeToLog) {
if (mySubscribeToServerLog != subscribeToLog) {
mySubscribeToServerLog = subscribeToLog;
server_setSubscriptions(myServer);
}
}
private void server_setSubscriptions(@Nullable AnalysisServer server) {
if (server != null) {
server.server_setSubscriptions(mySubscribeToServerLog ? Arrays.asList(ServerService.STATUS, ServerService.LOG)
: Collections.singletonList(ServerService.STATUS));
}
}
/**
* Returns a string, which the
* <a href="https://htmlpreview.github.io/?https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/doc/api.html#type_FilePath">Analysis Server API specification</a>
* defines as `FilePath`:
* <ul>
* <li>for SDK version 3.3 and older, it's an absolute file path with OS-dependent slashes
* <li>for SDK version 3.4 and newer, it's a URI, thanks to the `supportsUris` capability defined in the spec
* </ul>
*/
public String getFileUri(@NotNull VirtualFile file) {
if (!isDartSdkVersionSufficientForFileUri(mySdkVersion)) {
// prior to Dart SDK 3.4, the protocol required file paths instead of URIs
return FileUtil.toSystemDependentName(file.getPath());
}
String fileUri = file.getUserData(DartFileInfoKt.DART_NOT_LOCAL_FILE_URI_KEY);
return fileUri != null ? fileUri : getLocalFileUri(file.getPath());
}
/**
* Prefer {@link #getFileUri(VirtualFile)}.
* Use this method only if the corresponding `VirtualFile` is not available at the call site,
* and you are sure that this is a local file path.
*
* @apiNote URI calculation is similar to {@link com.intellij.platform.lsp.api.LspServerDescriptor#getFileUri(VirtualFile)}
* @see #getFileUri(VirtualFile)
*/
public String getLocalFileUri(@NotNull String localFilePath) {
if (!isDartSdkVersionSufficientForFileUri(mySdkVersion)) {
// prior to Dart SDK 3.4, the protocol required file paths instead of URIs
return FileUtil.toSystemDependentName(localFilePath);
}
String escapedPath = URLUtil.encodePath(FileUtil.toSystemIndependentName(localFilePath));
String url = VirtualFileManager.constructUrl(URLUtil.FILE_PROTOCOL, escapedPath);
URI uri = VfsUtil.toUri(url);
return uri != null ? uri.toString() : url;
}
}