package com.jetbrains.lang.dart.analyzer;
import com.google.common.collect.EvictingQueue;
import com.google.common.collect.Lists;
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.intellij.codeInsight.intention.IntentionManager;
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.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
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.module.Module;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
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.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.SearchScope;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import com.intellij.util.PathUtil;
import com.intellij.util.Processor;
import com.intellij.util.concurrency.QueueProcessor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.xml.util.HtmlUtil;
import com.jetbrains.lang.dart.DartBundle;
import com.jetbrains.lang.dart.DartFileListener;
import com.jetbrains.lang.dart.DartFileType;
import com.jetbrains.lang.dart.DartYamlFileTypeFactory;
import com.jetbrains.lang.dart.assists.DartQuickAssistIntention;
import com.jetbrains.lang.dart.assists.QuickAssistSet;
import com.jetbrains.lang.dart.ide.errorTreeView.DartFeedbackBuilder;
import com.jetbrains.lang.dart.ide.errorTreeView.DartProblemsView;
import com.jetbrains.lang.dart.sdk.DartSdk;
import com.jetbrains.lang.dart.sdk.DartSdkLibUtil;
import com.jetbrains.lang.dart.sdk.DartSdkUpdateChecker;
import com.jetbrains.lang.dart.util.PubspecYamlUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TObjectIntHashMap;
import org.dartlang.analysis.server.protocol.*;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class DartAnalysisServerService implements Disposable {
public static final String MIN_SDK_VERSION = "1.12";
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 = TimeUnit.MILLISECONDS.toMillis(100);
private static final long GET_FIXES_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long STATEMENT_COMPLETION_TIMEOUT = TimeUnit.MILLISECONDS.toMillis(100);
private static final long GET_SUGGESTIONS_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
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 TESTS_TIMEOUT_COEFF = 10;
private static final Logger LOG = Logger.getInstance("#com.jetbrains.lang.dart.analyzer.DartAnalysisServerService");
private static final String STACK_TRACE_MARKER = "#0";
private static final long MIN_DISRUPTION_TIME = 5000L; // 5 seconds minimum between error report balloons
private static final int MAX_DISRUPTIONS_PER_SESSION = 20; // Do not annoy the user too many times
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
@NotNull private final Project myProject;
private boolean myInitializationOnServerStartupDone = false;
// Do not wait for server response under lock. Do not take read/write action under lock.
private final Object myLock = new Object();
@Nullable private AnalysisServer myServer;
@Nullable private StdioServerSocket myServerSocket;
@NotNull private String myServerVersion = "";
@NotNull private String mySdkVersion = "";
@Nullable private String mySdkHome = null;
private final DartServerRootsHandler myRootsHandler;
private final Map<String, Long> myFilePathWithOverlaidContentToTimestamp = new THashMap<>();
private final List<String> myVisibleFiles = new ArrayList<>();
private final Set<Document> myChangedDocuments = new THashSet<>();
private final Alarm myUpdateFilesAlarm;
@NotNull private final Queue<CompletionInfo> myCompletionInfos = new LinkedList<>();
@NotNull private final Queue<SearchResultsSet> mySearchResultSets = new LinkedList<>();
@NotNull private final DartServerData myServerData;
private volatile boolean myAnalysisInProgress;
private volatile boolean myPubListInProgress;
@NotNull private final Alarm myShowServerProgressAlarm;
@Nullable private ProgressIndicator myProgressIndicator;
private final Object myProgressLock = new Object();
private boolean myHaveShownInitialProgress;
private boolean mySentAnalysisBusy;
// files with red squiggles in Project View. This field is also used as a lock to access these 3 collections
@NotNull private final Set<String> myFilePathsWithErrors = new THashSet<>();
// how many files with errors are in this folder (recursively)
@NotNull private final TObjectIntHashMap<String> myFolderPathsWithErrors = new TObjectIntHashMap<>();
// errors hash is tracked to optimize error notification listener: do not handle equal notifications more than once
@NotNull private final TObjectIntHashMap<String> myFilePathToErrorsHash = new TObjectIntHashMap<>();
public long maxMillisToWaitForServerResponse = 0L;
@NotNull private final InteractiveErrorReporter myErrorReporter = new InteractiveErrorReporter();
@NotNull private final EvictingQueue<String> myDebugLog = EvictingQueue.create(DEBUG_LOG_CAPACITY);
public static String getClientId() {
return ApplicationNamesInfo.getInstance().getFullProductName().replaceAll(" ", "-");
}
private static String getClientVersion() {
return ApplicationInfo.getInstance().getApiVersion();
}
private final AnalysisServerListener myAnalysisServerListener = new AnalysisServerListenerAdapter() {
@Override
public void computedAnalyzedFiles(List<String> filePaths) {
configureImportedLibraries(filePaths);
}
@Override
public void computedErrors(@NotNull final String filePathSD, @NotNull final List<AnalysisError> errors) {
final String fileName = PathUtil.getFileName(filePathSD);
final ProgressIndicator indicator = myProgressIndicator;
if (indicator != null) {
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()) || AnalysisErrorSeverity.WARNING.equals(error.getSeverity())) {
hasSevereProblems = true;
}
if (!AnalysisErrorType.TODO.equals(error.getType())) {
errorsWithoutTodo.add(error);
}
}
final String filePathSI = FileUtil.toSystemIndependentName(filePathSD);
final 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.get(filePathSI);
}
final int newHash = errorsWithoutTodo.isEmpty() ? 0 : ensureNotZero(errorsWithoutTodo.hashCode());
// do nothing if errors are the same as were already handled previously
if (oldHash == newHash && !myServerData.isErrorInfoLost(filePathSI)) return;
final boolean visible = myVisibleFiles.contains(filePathSD);
if (myServerData.computedErrors(filePathSI, errorsWithoutTodo, visible)) {
onErrorsUpdated(filePathSI, errorsWithoutTodo, hasSevereProblems, newHash);
}
}
@Override
public void computedHighlights(@NotNull final String filePath, @NotNull final List<HighlightRegion> regions) {
myServerData.computedHighlights(FileUtil.toSystemIndependentName(filePath), regions);
}
@Override
public void computedImplemented(String _filePath,
List<ImplementedClass> implementedClasses,
List<ImplementedMember> implementedMembers) {
myServerData.computedImplemented(FileUtil.toSystemIndependentName(_filePath), implementedClasses, implementedMembers);
}
@Override
public void computedNavigation(@NotNull final String _filePath, @NotNull final List<NavigationRegion> regions) {
myServerData.computedNavigation(FileUtil.toSystemIndependentName(_filePath), regions);
}
@Override
public void computedOverrides(@NotNull final String _filePath, @NotNull final List<OverrideMember> overrides) {
myServerData.computedOverrides(FileUtil.toSystemIndependentName(_filePath), overrides);
}
@Override
public void flushedResults(@NotNull final List<String> _filePaths) {
final List<String> filePaths = new ArrayList<>(_filePaths.size());
for (String path : _filePaths) {
filePaths.add(FileUtil.toSystemIndependentName(path));
}
myServerData.onFlushedResults(filePaths);
for (String filePath : filePaths) {
onErrorsUpdated(filePath, AnalysisError.EMPTY_LIST, false, 0);
}
}
@Override
public void computedCompletion(@NotNull final String completionId,
final int replacementOffset,
final int replacementLength,
@NotNull final List<CompletionSuggestion> completions,
final boolean isLast) {
synchronized (myCompletionInfos) {
myCompletionInfos.add(new CompletionInfo(completionId, replacementOffset, replacementLength, completions, isLast));
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 : "";
}
@Override
public void serverError(boolean isFatal, @Nullable String message, @Nullable String stackTrace) {
if (message == null) message = "<no error message>";
if (stackTrace == null) stackTrace = "<no stack trace>";
if (!isFatal && stackTrace.startsWith("#0 checkValidPackageUri (package:package_config/src/util.dart:72)")) {
return;
}
String errorMessage =
"Dart analysis server, SDK version " + mySdkVersion +
", server version " + myServerVersion +
", " + (isFatal ? "FATAL " : "") + "error: " + message + "\n" + stackTrace;
myErrorReporter.report(errorMessage);
}
@Override
public void serverStatus(@Nullable final AnalysisStatus analysisStatus, @Nullable final 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();
}
}
};
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(@NotNull final 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(@Nullable final VirtualFile file, final int originalOffset) {
if (originalOffset <= 0 || file == null) 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
*/
private int getOriginalOffset(@Nullable final VirtualFile file, final int convertedOffset) {
if (file == null) return convertedOffset;
return myFilePathWithOverlaidContentToTimestamp.containsKey(file.getPath())
? convertedOffset
: FileOffsetsManager.getInstance().getOriginalOffset(file, convertedOffset);
}
public int[] getConvertedOffsets(@NotNull final 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(@NotNull final 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(@NotNull final DartSdk sdk) {
return StringUtil.compareVersionNumbers(sdk.getVersion(), MIN_SDK_VERSION) >= 0;
}
public void addCompletions(@NotNull final VirtualFile file,
@NotNull final String completionId,
@NotNull final CompletionSuggestionConsumer consumer) {
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);
final int convertedReplacementLength = getConvertedOffset(file, completionInfo.myOriginalReplacementLength);
consumer.consumeCompletionSuggestion(convertedReplacementOffset, convertedReplacementLength, completion);
}
return;
}
try {
myCompletionInfos.wait(CHECK_CANCELLED_PERIOD);
}
catch (InterruptedException e) {
return;
}
}
}
}
public static class FormatResult {
@Nullable private final List<SourceEdit> myEdits;
private final int myOffset;
private final int myLength;
public FormatResult(@Nullable final 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;
}
@Nullable
public List<SourceEdit> getEdits() {
return myEdits;
}
}
private void configureImportedLibraries(@NotNull final Collection<String> filePaths) {
DumbService.getInstance(myProject).smartInvokeLater(() -> doConfigureImportedLibraries(myProject, filePaths));
}
private static void doConfigureImportedLibraries(@NotNull final Project project, @NotNull final Collection<String> filePaths) {
final DartSdk sdk = DartSdk.getDartSdk(project);
if (sdk == null) return;
final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
final SortedSet<String> folderPaths = new TreeSet<>();
final Collection<String> rootsToAddToLib = new THashSet<>();
for (final String path : filePaths) {
if (path != null) {
folderPaths.add(PathUtil.getParentPath(FileUtil.toSystemIndependentName(path)));
}
}
outer:
for (final String path : folderPaths) {
final VirtualFile vFile = LocalFileSystem.getInstance().findFileByPath(path);
if (!path.startsWith(sdk.getHomePath() + "/") && (vFile == null || !fileIndex.isInContent(vFile))) {
for (String configuredPath : rootsToAddToLib) {
if (path.startsWith(configuredPath + "/")) {
continue outer; // folderPaths is sorted so subfolders go after parent folder
}
}
rootsToAddToLib.add(path);
}
}
final Processor<? super PsiFileSystemItem> falseProcessor = (Processor<PsiFileSystemItem>)item -> false;
final Condition<Module> moduleFilter = module -> DartSdkLibUtil.isDartSdkEnabled(module) &&
!FilenameIndex.processFilesByName(PubspecYamlUtil.PUBSPEC_YAML, false,
falseProcessor, module.getModuleContentScope(),
project, null);
final DartFileListener.DartLibInfo libInfo = new DartFileListener.DartLibInfo(true);
libInfo.addRoots(rootsToAddToLib);
final Library library = DartFileListener.updatePackagesLibraryRoots(project, libInfo);
DartFileListener.updateDependenciesOnDartPackagesLibrary(project, moduleFilter, library);
}
public DartAnalysisServerService(@NotNull final Project project) {
myProject = project;
myRootsHandler = new DartServerRootsHandler(project);
myServerData = new DartServerData(this);
myUpdateFilesAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, project);
myShowServerProgressAlarm = new Alarm(project);
}
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(@NotNull final FileEditorManager source, @NotNull final VirtualFile file) {
if (!Registry.is("dart.projects.without.pubspec", false) &&
(PubspecYamlUtil.PUBSPEC_YAML.equals(file.getName()) || file.getFileType() == 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(@NotNull final FileEditorManager source, @NotNull final 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(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, myProject);
}
@NotNull
public static DartAnalysisServerService getInstance(@NotNull final Project project) {
return ServiceManager.getService(project, DartAnalysisServerService.class);
}
@NotNull
public Project getProject() {
return myProject;
}
@Override
public void dispose() {
stopServer();
}
@NotNull
public List<DartServerData.DartError> getErrors(@NotNull final VirtualFile file) {
return myServerData.getErrors(file);
}
public List<DartServerData.DartError> getErrors(@NotNull final SearchScope scope) {
return myServerData.getErrors(scope);
}
@NotNull
public List<DartServerData.DartHighlightRegion> getHighlight(@NotNull final VirtualFile file) {
return myServerData.getHighlight(file);
}
@NotNull
public List<DartServerData.DartNavigationRegion> getNavigation(@NotNull final VirtualFile file) {
return myServerData.getNavigation(file);
}
@NotNull
public List<DartServerData.DartOverrideMember> getOverrideMembers(@NotNull final VirtualFile file) {
return myServerData.getOverrideMembers(file);
}
@NotNull
public List<DartServerData.DartRegion> getImplementedClasses(@NotNull final VirtualFile file) {
return myServerData.getImplementedClasses(file);
}
@NotNull
public List<DartServerData.DartRegion> getImplementedMembers(@NotNull final VirtualFile file) {
return myServerData.getImplementedMembers(file);
}
void updateCurrentFile() {
UIUtil.invokeLaterIfNeeded(() -> {
final VirtualFile[] files = FileEditorManager.getInstance(myProject).getSelectedFiles();
if (files.length > 0) {
DartProblemsView.getInstance(myProject).setCurrentFile(files[0]);
}
});
}
void updateVisibleFiles() {
ApplicationManager.getApplication().assertReadAccessAllowed();
synchronized (myLock) {
final List<String> newVisibleFiles = new ArrayList<>();
for (VirtualFile file : FileEditorManager.getInstance(myProject).getSelectedFiles()) {
if (isLocalAnalyzableFile(file)) {
newVisibleFiles.add(FileUtil.toSystemDependentName(file.getPath()));
}
}
if (!Comparing.haveEqualElements(myVisibleFiles, newVisibleFiles)) {
myVisibleFiles.clear();
myVisibleFiles.addAll(newVisibleFiles);
analysis_setPriorityFiles();
analysis_setSubscriptions();
}
}
}
/**
* Return true if the given file can be analyzed by Dart Analysis Server.
*/
@Contract("null->false")
public static boolean isLocalAnalyzableFile(@Nullable final VirtualFile file) {
if (file != null && file.isInLocalFileSystem()) {
return file.getFileType() == DartFileType.INSTANCE ||
HtmlUtil.isHtmlFile(file) ||
file.getName().equals(DartYamlFileTypeFactory.DOT_ANALYSIS_OPTIONS);
}
return false;
}
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> filesToUpdate = new THashMap<>();
ApplicationManager.getApplication().assertReadAccessAllowed();
synchronized (myLock) {
final Set<String> oldTrackedFiles = new THashSet<>(myFilePathWithOverlaidContentToTimestamp.keySet());
final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
// some documents in myChangedDocuments may be updated by external change, suxh as switch branch, that's why we track them,
// getUnsavedDocuments() is not enough, we must make sure that overlaid content is sent for for myChangedDocuments as well (to trigger DAS notifications)
final Set<Document> documents = new THashSet<>(myChangedDocuments);
myChangedDocuments.clear();
ContainerUtil.addAll(documents, fileDocumentManager.getUnsavedDocuments());
for (Document document : documents) {
final VirtualFile file = fileDocumentManager.getFile(document);
if (isLocalAnalyzableFile(file)) {
oldTrackedFiles.remove(file.getPath());
final Long oldTimestamp = myFilePathWithOverlaidContentToTimestamp.get(file.getPath());
if (oldTimestamp == null || document.getModificationStamp() != oldTimestamp) {
filesToUpdate.put(FileUtil.toSystemDependentName(file.getPath()), new AddContentOverlay(document.getText()));
myFilePathWithOverlaidContentToTimestamp.put(file.getPath(), document.getModificationStamp());
}
}
}
// oldTrackedFiles at this point contains only those files that are not in FileDocumentManager.getUnsavedDocuments() any more
for (String oldPath : oldTrackedFiles) {
final Long removed = myFilePathWithOverlaidContentToTimestamp.remove(oldPath);
LOG.assertTrue(removed != null, oldPath);
filesToUpdate.put(FileUtil.toSystemDependentName(oldPath), new RemoveContentOverlay());
}
if (LOG.isDebugEnabled()) {
final Set<String> overlaid = new THashSet<>(filesToUpdate.keySet());
for (String removeOverlaid : oldTrackedFiles) {
overlaid.remove(FileUtil.toSystemDependentName(removeOverlaid));
}
if (!overlaid.isEmpty()) {
LOG.debug("Sending overlaid content: " + StringUtil.join(overlaid, ",\n"));
}
if (!oldTrackedFiles.isEmpty()) {
LOG.debug("Removing overlaid content: " + StringUtil.join(oldTrackedFiles, ",\n"));
}
}
}
if (!filesToUpdate.isEmpty()) {
server.analysis_updateContent(filesToUpdate, myServerData::onFilesContentUpdated);
}
}
public boolean updateRoots(@NotNull final List<String> includedRoots, @NotNull final List<String> excludedRoots) {
AnalysisServer server = myServer;
if (server == null) {
return false;
}
if (LOG.isDebugEnabled()) {
LOG.debug("analysis_setAnalysisRoots, included:\n" + StringUtil.join(includedRoots, ",\n") +
"\nexcluded:\n" + StringUtil.join(excludedRoots, ",\n"));
}
server.analysis_setAnalysisRoots(includedRoots, excludedRoots, null);
return true;
}
private void onErrorsUpdated(@NotNull final String filePath,
@NotNull final List<AnalysisError> errors,
final boolean hasSevereProblems,
final int errorsHash) {
updateFilesWithErrorsSet(filePath, hasSevereProblems, errorsHash);
DartProblemsView.getInstance(myProject).updateErrorsForFile(filePath, errors);
}
private void updateFilesWithErrorsSet(@NotNull final String filePath, final boolean hasSevereProblems, final int errorsHash) {
synchronized (myFilePathsWithErrors) {
if (errorsHash == 0) {
// no errors
myFilePathToErrorsHash.remove(filePath);
}
else {
myFilePathToErrorsHash.put(filePath, errorsHash);
}
if (hasSevereProblems) {
if (myFilePathsWithErrors.add(filePath)) {
String parentPath = PathUtil.getParentPath(filePath);
while (!parentPath.isEmpty()) {
final int count = myFolderPathsWithErrors.get(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.remove(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 (!myProject.isDisposed() && myInitializationOnServerStartupDone) {
DartProblemsView.getInstance(myProject).clearAll();
}
}
@NotNull
public List<HoverInformation> analysis_getHover(@NotNull final VirtualFile file, final int _offset) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final List<HoverInformation> result = Lists.newArrayList();
final AnalysisServer server = myServer;
if (server == null) {
return HoverInformation.EMPTY_LIST;
}
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.analysis_getHover(filePath, offset, new GetHoverConsumer() {
@Override
public void computedHovers(HoverInformation[] hovers) {
Collections.addAll(result, hovers);
latch.countDown();
}
@Override
public void onError(RequestError error) {
logError("analysis_getHover()", filePath, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_HOVER_TIMEOUT);
return result;
}
@Nullable
public List<DartServerData.DartNavigationRegion> analysis_getNavigation(@NotNull final VirtualFile file,
final int _offset,
final int length) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final Ref<List<DartServerData.DartNavigationRegion>> resultRef = Ref.create();
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final CountDownLatch latch = new CountDownLatch(1);
LOG.debug("analysis_getNavigation(" + filePath + ")");
final int offset = getOriginalOffset(file, _offset);
server.analysis_getNavigation(filePath, 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()", filePath, error));
}
else {
logError("analysis_getNavigation()", filePath, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_NAVIGATION_TIMEOUT);
if (latch.getCount() > 0) {
LOG.info("analysis_getNavigation() took more than " + GET_NAVIGATION_TIMEOUT + "ms for file " + filePath);
}
return resultRef.get();
}
@NotNull
public List<SourceChange> edit_getAssists(@NotNull final VirtualFile file, final int _offset, final int _length) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final List<SourceChange> results = Lists.newArrayList();
final AnalysisServer server = myServer;
if (server == null) {
return results;
}
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
final int length = getOriginalOffset(file, _offset + _length) - offset;
server.edit_getAssists(filePath, 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()", filePath, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_ASSISTS_TIMEOUT);
return results;
}
@Nullable
public SourceChange edit_getStatementCompletion(@NotNull final VirtualFile file, final int _offset) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final Ref<SourceChange> resultRef = Ref.create();
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.edit_getStatementCompletion(filePath, offset, new GetStatementCompletionConsumer() {
@Override
public void computedSourceChange(SourceChange sourceChange) {
resultRef.set(sourceChange);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, STATEMENT_COMPLETION_TIMEOUT);
return resultRef.get();
}
public void diagnostic_getServerPort(GetServerPortConsumer consumer) {
final AnalysisServer server = myServer;
if (server == null) {
consumer.onError(new RequestError(ExtendedRequestErrorCode.INVALID_SERVER_RESPONSE,
"The analysis server is not running.", null));
}
else {
server.diagnostic_getServerPort(consumer);
}
}
/**
* If server responds in less than <code>GET_FIXES_TIMEOUT</code> then this method can be considered synchronous: when exiting this method
* <code>consumer</code> is already notified. Otherwise this method is async.
*/
public void askForFixesAndWaitABitIfReceivedQuickly(@NotNull final VirtualFile file,
final int _offset,
@NotNull final Consumer<List<AnalysisErrorFixes>> consumer) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final AnalysisServer server = myServer;
if (server == null) return;
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.edit_getFixes(filePath, 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()", filePath, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_FIXES_TIMEOUT);
}
public void search_findElementReferences(@NotNull final VirtualFile file,
final int _offset,
@NotNull final Consumer<SearchResult> consumer) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final Ref<String> searchIdRef = new Ref<>();
final AnalysisServer server = myServer;
if (server == null) return;
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.search_findElementReferences(filePath, 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()", filePath, error));
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, FIND_ELEMENT_REFERENCES_TIMEOUT);
if (latch.getCount() > 0) {
LOG.info("search_findElementReferences() took too long for " + filePath + "@" + 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;
}
}
}
}
@NotNull
public List<TypeHierarchyItem> search_getTypeHierarchy(@NotNull final VirtualFile file, final int _offset, final boolean superOnly) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final List<TypeHierarchyItem> results = Lists.newArrayList();
final AnalysisServer server = myServer;
if (server == null) {
return results;
}
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.search_getTypeHierarchy(filePath, 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()", filePath, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_TYPE_HIERARCHY_TIMEOUT);
return results;
}
@Nullable
public String completion_getSuggestions(@NotNull final VirtualFile file, final int _offset) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final Ref<String> resultRef = new Ref<>();
final AnalysisServer server = myServer;
if (server == null) {
return null;
}
final CountDownLatch latch = new CountDownLatch(1);
final int offset = getOriginalOffset(file, _offset);
server.completion_getSuggestions(filePath, offset, new GetSuggestionsConsumer() {
@Override
public void computedCompletionId(@NotNull final String completionId) {
resultRef.set(completionId);
latch.countDown();
}
@Override
public void onError(@NotNull final RequestError error) {
// Not a problem. Happens if a file is outside of the project, or server is just not ready yet.
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, GET_SUGGESTIONS_TIMEOUT);
return resultRef.get();
}
@Nullable
public FormatResult edit_format(@NotNull final VirtualFile file,
final int _selectionOffset,
final int _selectionLength,
final int lineLength) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final Ref<FormatResult> resultRef = new Ref<>();
final AnalysisServer server = myServer;
if (server == null) return null;
final CountDownLatch latch = new CountDownLatch(1);
final int selectionOffset = getOriginalOffset(file, _selectionOffset);
final int selectionLength = getOriginalOffset(file, _selectionOffset + _selectionLength) - selectionOffset;
server.edit_format(filePath, 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()", filePath, error));
}
else {
logError("edit_format()", filePath, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EDIT_FORMAT_TIMEOUT);
if (latch.getCount() > 0) {
LOG.info("edit_format() took too long for file " + filePath);
}
return resultRef.get();
}
public boolean edit_getRefactoring(String kind,
VirtualFile file,
int _offset,
int _length,
boolean validateOnly,
RefactoringOptions options,
GetRefactoringConsumer consumer) {
final String filePath = FileUtil.toSystemDependentName(file.getPath());
final AnalysisServer server = myServer;
if (server == null) return false;
final int offset = getOriginalOffset(file, _offset);
final int length = getOriginalOffset(file, _offset + _length) - offset;
server.edit_getRefactoring(kind, filePath, offset, length, validateOnly, options, consumer);
return true;
}
@Nullable
public SourceFileEdit edit_organizeDirectives(@NotNull final String _filePath) {
final String filePath = FileUtil.toSystemDependentName(_filePath);
final Ref<SourceFileEdit> resultRef = new Ref<>();
final AnalysisServer server = myServer;
if (server == null) return null;
final CountDownLatch latch = new CountDownLatch(1);
server.edit_organizeDirectives(filePath, new OrganizeDirectivesConsumer() {
@Override
public void computedEdit(final SourceFileEdit edit) {
resultRef.set(edit);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
if (RequestErrorCode.FILE_NOT_ANALYZED.equals(error.getCode()) ||
RequestErrorCode.ORGANIZE_DIRECTIVES_ERROR.equals(error.getCode())) {
LOG.info(getShortErrorMessage("edit_organizeDirectives()", filePath, error));
}
else {
logError("edit_organizeDirectives()", filePath, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EDIT_ORGANIZE_DIRECTIVES_TIMEOUT);
if (latch.getCount() > 0) {
LOG.info("edit_organizeDirectives() took too long for file " + filePath);
}
return resultRef.get();
}
@Nullable
public SourceFileEdit edit_sortMembers(@NotNull final String _filePath) {
final String filePath = FileUtil.toSystemDependentName(_filePath);
final Ref<SourceFileEdit> resultRef = new Ref<>();
final AnalysisServer server = myServer;
if (server == null) return null;
final CountDownLatch latch = new CountDownLatch(1);
server.edit_sortMembers(filePath, 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()", filePath, error));
}
else {
logError("edit_sortMembers()", filePath, error);
}
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EDIT_SORT_MEMBERS_TIMEOUT);
if (latch.getCount() > 0) {
LOG.info("edit_sortMembers() took too long for file " + filePath);
}
return resultRef.get();
}
public void analysis_reanalyze() {
final AnalysisServer server = myServer;
if (server == null) return;
server.analysis_reanalyze(null);
ApplicationManager.getApplication().invokeLater(this::clearAllErrors, ModalityState.NON_MODAL);
}
private void analysis_setPriorityFiles() {
synchronized (myLock) {
if (myServer == null) return;
if (LOG.isDebugEnabled()) {
LOG.debug("analysis_setPriorityFiles, files:\n" + StringUtil.join(myVisibleFiles, ",\n"));
}
myServer.analysis_setPriorityFiles(myVisibleFiles);
}
}
private void analysis_setSubscriptions() {
synchronized (myLock) {
if (myServer == null) return;
final Map<String, List<String>> subscriptions = new THashMap<>();
subscriptions.put(AnalysisService.HIGHLIGHTS, myVisibleFiles);
subscriptions.put(AnalysisService.NAVIGATION, myVisibleFiles);
subscriptions.put(AnalysisService.OVERRIDES, myVisibleFiles);
if (StringUtil.compareVersionNumbers(mySdkVersion, "1.13") >= 0) {
subscriptions.put(AnalysisService.IMPLEMENTED, myVisibleFiles);
}
if (LOG.isDebugEnabled()) {
LOG.debug("analysis_setSubscriptions, subscriptions:\n" + subscriptions);
}
myServer.analysis_setSubscriptions(subscriptions);
}
}
@Nullable
public String execution_createContext(@NotNull final String _filePath) {
final String filePath = FileUtil.toSystemDependentName(_filePath);
final Ref<String> resultRef = new Ref<>();
final AnalysisServer server = myServer;
if (server == null) return null;
final CountDownLatch latch = new CountDownLatch(1);
server.execution_createContext(filePath, new CreateContextConsumer() {
@Override
public void computedExecutionContext(final String contextId) {
resultRef.set(contextId);
latch.countDown();
}
@Override
public void onError(final RequestError error) {
logError("execution_createContext()", filePath, error);
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EXECUTION_CREATE_CONTEXT_TIMEOUT);
if (latch.getCount() > 0) {
LOG.info("execution_createContext() took too long for file " + filePath);
}
return resultRef.get();
}
public void execution_deleteContext(@NotNull final String contextId) {
final AnalysisServer server = myServer;
if (server != null) {
server.execution_deleteContext(contextId);
}
}
@Nullable
public String execution_mapUri(@NotNull final String _id, @Nullable final String _filePath, @Nullable final String _uri) {
// 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 ((_filePath == null && _uri == null) || (_filePath != null && _uri != null)) {
LOG.error("One of _filePath and _uri must be non-null.");
return null;
}
final String filePath = _filePath != null ? FileUtil.toSystemDependentName(_filePath) : null;
final Ref<String> resultRef = new Ref<>();
final AnalysisServer server = myServer;
if (server == null) return null;
final CountDownLatch latch = new CountDownLatch(1);
server.execution_mapUri(_id, filePath, _uri, 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) {
LOG.warn(
"execution_mapUri(" + _id + ", " + filePath + ", " + _uri + ") returned error " + error.getCode() + ": " + error.getMessage());
latch.countDown();
}
});
awaitForLatchCheckingCanceled(server, latch, EXECUTION_MAP_URI_TIMEOUT);
if (latch.getCount() > 0) {
LOG.info("execution_mapUri() took too long for contextID " + _id + " and file or uri " + (filePath != null ? filePath : _uri));
return null;
}
if (_uri != null && !resultRef.isNull()) {
return FileUtil.toSystemIndependentName(resultRef.get());
}
return resultRef.get();
}
private void startServer(@NotNull final DartSdk sdk) {
synchronized (myLock) {
mySdkHome = sdk.getHomePath();
final String runtimePath = FileUtil.toSystemDependentName(mySdkHome + "/bin/dart");
String analysisServerPath = FileUtil.toSystemDependentName(mySdkHome + "/bin/snapshots/analysis_server.dart.snapshot");
analysisServerPath = System.getProperty("dart.server.path", analysisServerPath);
final DebugPrintStream debugStream = str -> {
str = str.substring(0, Math.min(str.length(), MAX_DEBUG_LOG_LINE_LENGTH));
synchronized (myDebugLog) {
myDebugLog.add(str);
}
};
String vmArgsRaw;
try {
vmArgsRaw = Registry.stringValue("dart.server.vm.options");
}
catch (MissingResourceException e) {
vmArgsRaw = "";
}
String serverArgsRaw = "";
serverArgsRaw += " --useAnalysisHighlight2";
//serverArgsRaw += " --file-read-mode=normalize-eol-always";
try {
serverArgsRaw += " " + Registry.stringValue("dart.server.additional.arguments");
}
catch (MissingResourceException e) {
// NOP
}
myServerSocket =
new StdioServerSocket(runtimePath, StringUtil.split(vmArgsRaw, " "), analysisServerPath, StringUtil.split(serverArgsRaw, " "),
debugStream);
myServerSocket.setClientId(getClientId());
myServerSocket.setClientVersion(getClientVersion());
final AnalysisServer startedServer = new RemoteAnalysisServerImpl(myServerSocket);
try {
startedServer.start();
startedServer.server_setSubscriptions(Collections.singletonList(ServerService.STATUS));
if (Registry.is("dart.projects.without.pubspec", false)) {
startedServer.analysis_setGeneralSubscriptions(Collections.singletonList(GeneralAnalysisService.ANALYZED_FILES));
}
if (!myInitializationOnServerStartupDone) {
myInitializationOnServerStartupDone = true;
registerFileEditorManagerListener();
registerDocumentListener();
setDasLogger();
registerQuickAssistIntentions();
}
startedServer.addAnalysisServerListener(myAnalysisServerListener);
myHaveShownInitialProgress = false;
startedServer.addStatusListener(isAlive -> {
if (!isAlive) {
synchronized (myLock) {
if (startedServer == myServer) {
stopServer();
}
}
}
});
mySdkVersion = sdk.getVersion();
startedServer.analysis_updateOptions(new AnalysisOptions(true, true, true, true, true, false, true, false));
myServer = startedServer;
}
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 isServerResponsive() {
if (maxMillisToWaitForServerResponse == 0L) return true; // UI has not finished initialization yet.
if (!(myAnalysisInProgress || myPubListInProgress)) return true;
long responseMillis, requestMillis;
synchronized (myLock) {
if (myServer == null) return false;
responseMillis = myServer.getLastResponseMillis();
requestMillis = myServer.getLastRequestMillis();
}
if (responseMillis == 0L) return true; // Allow UI to start in good state even if it becomes unknown later.
if (requestMillis <= responseMillis) return true;
long delta = System.currentTimeMillis() - requestMillis;
return delta > maxMillisToWaitForServerResponse;
}
public boolean serverReadyForRequest(@NotNull final Project project) {
final DartSdk sdk = DartSdk.getDartSdk(project);
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();
startServer(sdk);
if (myServer != null) {
myRootsHandler.ensureProjectServed();
}
}
return myServer != null;
}
}
public void restartServer() {
stopServer();
serverReadyForRequest(myProject);
}
void stopServer() {
synchronized (myLock) {
if (myServer != null) {
LOG.debug("stopping server");
myServer.removeAnalysisServerListener(myAnalysisServerListener);
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;
myFilePathWithOverlaidContentToTimestamp.clear();
myVisibleFiles.clear();
myChangedDocuments.clear();
myServerData.clearData();
myRootsHandler.reset();
if (myProject.isOpen() && !myProject.isDisposed()) {
ApplicationManager.getApplication().invokeLater(this::clearAllErrors, ModalityState.NON_MODAL, myProject.getDisposed());
}
}
}
public void waitForAnalysisToComplete_TESTS_ONLY(@NotNull final VirtualFile file) {
assert ApplicationManager.getApplication().isUnitTestMode();
final AnalysisServer server = myServer;
if (server == null) return;
final CountDownLatch latch = new CountDownLatch(1);
server.analysis_getErrors(FileUtil.toSystemDependentName(file.getPath()), new GetErrorsConsumer() {
@Override
public void computedErrors(AnalysisError[] errors) {
latch.countDown();
}
@Override
public void onError(RequestError requestError) {
latch.countDown();
LOG.error(requestError.getMessage());
}
});
awaitForLatchCheckingCanceled(server, latch, ANALYSIS_IN_TESTS_TIMEOUT / TESTS_TIMEOUT_COEFF);
assert latch.getCount() == 0 : "Analysis did't complete in " + ANALYSIS_IN_TESTS_TIMEOUT + "ms.";
}
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(@NotNull final VirtualFile file) {
synchronized (myFilePathsWithErrors) {
return file.isDirectory() ? myFolderPathsWithErrors.get(file.getPath()) > 0 : myFilePathsWithErrors.contains(file.getPath());
}
}
public int getFilePathsWithErrorsHash() {
synchronized (myFilePathsWithErrors) {
return myFilePathsWithErrors.hashCode();
}
}
private void logError(@NotNull final String methodName, @Nullable final String filePath, @NotNull final RequestError error) {
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 String getShortErrorMessage(@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 static boolean awaitForLatchCheckingCanceled(@NotNull final AnalysisServer server,
@NotNull final 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;
}
}
}
/**
* see {@link DartQuickAssistIntention}
*/
private static void registerQuickAssistIntentions() {
final IntentionManager intentionManager = IntentionManager.getInstance();
final QuickAssistSet quickAssistSet = new QuickAssistSet();
for (int i = 0; i < 20; i++) {
final DartQuickAssistIntention intention = new DartQuickAssistIntention(quickAssistSet, i);
intentionManager.addAction(intention);
}
}
public interface CompletionSuggestionConsumer {
void consumeCompletionSuggestion(final int replacementOffset,
final int replacementLength,
final @NotNull CompletionSuggestion completionSuggestion);
}
private static class CompletionInfo {
@NotNull private final String myCompletionId;
/**
* must be converted before any usage
*/
private final int myOriginalReplacementOffset;
/**
* must be converted before any usage
*/
private final int myOriginalReplacementLength;
@NotNull private final List<CompletionSuggestion> myCompletions;
private final boolean isLast;
public CompletionInfo(@NotNull final String completionId,
int replacementOffset,
int originalReplacementLength,
@NotNull final List<CompletionSuggestion> completions,
boolean isLast) {
this.myCompletionId = completionId;
this.myOriginalReplacementOffset = replacementOffset;
this.myOriginalReplacementLength = originalReplacementLength;
this.myCompletions = completions;
this.isLast = isLast;
}
}
/**
* A set of {@link SearchResult}s.
*/
private static class SearchResultsSet {
@NotNull final String id;
@NotNull final List<SearchResult> results;
final boolean isLast;
public SearchResultsSet(@NotNull String id, @NotNull List<SearchResult> results, boolean isLast) {
this.id = id;
this.results = results;
this.isLast = isLast;
}
}
/**
* Ask the user to report an error in the analysis server, subject to these constraints:
* - The same message is not reported twice in a row
* - The user is not interrupted too often
*/
private class InteractiveErrorReporter {
@NotNull private QueueProcessor<Runnable> myErrorReporter = QueueProcessor.createRunnableQueueProcessor();
private long myPreviousTime;
@NotNull private String myPreviousMessage = "";
private int myDisruptionCount = 0;
public void report(@NotNull String errorMessage) {
if (myDisruptionCount > MAX_DISRUPTIONS_PER_SESSION) return;
long timeStamp = System.currentTimeMillis();
if (timeStamp - myPreviousTime < MIN_DISRUPTION_TIME) {
if (messageDiffers(errorMessage)) {
LOG.warn(errorMessage);
if (myDisruptionCount > 0) {
myDisruptionCount++; // The red flashing icon is somewhat disruptive, but we only count if the user has already been queried.
}
}
return;
}
myPreviousTime = timeStamp;
if (messageDiffers(errorMessage)) {
String debugLog = debugLogContent();
myErrorReporter.add(() -> {
DartFeedbackBuilder builder = DartFeedbackBuilder.getFeedbackBuilder();
myDisruptionCount++;
builder.showNotification(DartBundle.message("dart.analysis.server.error"), myProject, errorMessage, debugLog);
});
}
myPreviousMessage = errorMessage;
}
private boolean messageDiffers(@NotNull String errorMessage) {
int prevIdx = myPreviousMessage.indexOf(STACK_TRACE_MARKER);
if (prevIdx < 0) return !errorMessage.equals(myPreviousMessage);
int errIdx = errorMessage.indexOf(STACK_TRACE_MARKER);
if (errIdx < 0) return !errorMessage.equals(myPreviousMessage);
// Compare Dart stack traces
return !errorMessage.substring(errIdx).equals(myPreviousMessage.substring(prevIdx));
}
private String debugLogContent() {
StringBuilder log = new StringBuilder();
log.append("```\n");
synchronized (myDebugLog) {
for (String s : myDebugLog) {
log.append(s).append('\n');
}
}
log.append("```\n");
return log.toString();
}
}
}