package com.jetbrains.lang.dart.analyzer;
import com.google.common.collect.Sets;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.impl.source.resolve.ResolveCache;
import com.intellij.psi.search.SearchScope;
import com.intellij.util.SmartList;
import gnu.trove.THashMap;
import org.dartlang.analysis.server.protocol.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class DartServerData {
private final DartAnalysisServerService myService;
private final Map<String, List<DartError>> myErrorData =
Collections.synchronizedMap(new THashMap<String, List<DartError>>());
private final Map<String, List<DartHighlightRegion>> myHighlightData =
Collections.synchronizedMap(new THashMap<String, List<DartHighlightRegion>>());
private final Map<String, List<DartNavigationRegion>> myNavigationData =
Collections.synchronizedMap(new THashMap<String, List<DartNavigationRegion>>());
private final Map<String, List<DartOverrideMember>> myOverrideData =
Collections.synchronizedMap(new THashMap<String, List<DartOverrideMember>>());
private final Map<String, List<DartRegion>> myImplementedClassData =
Collections.synchronizedMap(new THashMap<String, List<DartRegion>>());
private final Map<String, List<DartRegion>> myImplementedMemberData =
Collections.synchronizedMap(new THashMap<String, List<DartRegion>>());
private final Set<String> myFilePathsWithUnsentChanges = Sets.newConcurrentHashSet();
// keeps track of files in which error regions have been deleted by DocumentListener (typing inside an error region)
private final Set<String> myFilePathsWithLostErrorInfo = Sets.newConcurrentHashSet();
DartServerData(@NotNull final DartAnalysisServerService service) {
myService = service;
}
boolean isErrorInfoLost(@NotNull final String filePath) {
return myFilePathsWithLostErrorInfo.contains(filePath);
}
/**
* @return <code>true</code> if <code>errors</code> were processes, <code>false</code> if ignored;
* errors are ignored if the file has been edited and new contents has not yet been sent to the server.
*/
boolean computedErrors(@NotNull final String filePath, @NotNull final List<AnalysisError> errors, final boolean restartHighlighting) {
if (myFilePathsWithUnsentChanges.contains(filePath)) return false;
final List<DartError> newErrors = new ArrayList<>(errors.size());
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
for (AnalysisError error : errors) {
final int offset = myService.getConvertedOffset(file, error.getLocation().getOffset());
final int length = myService.getConvertedOffset(file, error.getLocation().getOffset() + error.getLocation().getLength()) - offset;
newErrors.add(new DartError(error, offset, length));
}
myFilePathsWithLostErrorInfo.remove(filePath);
myErrorData.put(filePath, newErrors);
if (restartHighlighting) {
forceFileAnnotation(file, false);
}
return true;
}
void computedHighlights(@NotNull final String filePath, @NotNull final List<HighlightRegion> regions) {
if (myFilePathsWithUnsentChanges.contains(filePath)) return;
final List<DartHighlightRegion> newRegions = new ArrayList<>(regions.size());
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
for (HighlightRegion region : regions) {
if (region.getLength() > 0) {
final int offset = myService.getConvertedOffset(file, region.getOffset());
final int length = myService.getConvertedOffset(file, region.getOffset() + region.getLength()) - offset;
newRegions.add(new DartHighlightRegion(offset, length, region.getType()));
}
}
myHighlightData.put(filePath, newRegions);
forceFileAnnotation(file, false);
}
void computedNavigation(@NotNull final String filePath, @NotNull final List<NavigationRegion> regions) {
if (myFilePathsWithUnsentChanges.contains(filePath)) return;
final List<DartNavigationRegion> newRegions = new ArrayList<>(regions.size());
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
for (NavigationRegion region : regions) {
if (region.getLength() > 0) {
final DartNavigationRegion dartNavigationRegion = createDartNavigationRegion(myService, file, region);
newRegions.add(dartNavigationRegion);
}
}
myNavigationData.put(filePath, newRegions);
forceFileAnnotation(file, true);
}
@NotNull
static DartNavigationRegion createDartNavigationRegion(@NotNull final DartAnalysisServerService service,
@Nullable final VirtualFile file,
@NotNull final NavigationRegion region) {
final int offset = service.getConvertedOffset(file, region.getOffset());
final int length = service.getConvertedOffset(file, region.getOffset() + region.getLength()) - offset;
final SmartList<DartNavigationTarget> targets = new SmartList<>();
for (NavigationTarget target : region.getTargetObjects()) {
targets.add(new DartNavigationTarget(target));
}
return new DartNavigationRegion(offset, length, targets);
}
void computedOverrides(@NotNull final String filePath, @NotNull final List<OverrideMember> overrides) {
if (myFilePathsWithUnsentChanges.contains(filePath)) return;
final List<DartOverrideMember> newOverrides = new ArrayList<>(overrides.size());
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
for (OverrideMember override : overrides) {
if (override.getLength() > 0) {
final int offset = myService.getConvertedOffset(file, override.getOffset());
final int length = myService.getConvertedOffset(file, override.getOffset() + override.getLength()) - offset;
newOverrides.add(new DartOverrideMember(offset, length, override.getSuperclassMember(), override.getInterfaceMembers()));
}
}
myOverrideData.put(filePath, newOverrides);
forceFileAnnotation(file, false);
}
void computedImplemented(@NotNull final String filePath,
@NotNull final List<ImplementedClass> implementedClasses,
@NotNull final List<ImplementedMember> implementedMembers) {
if (myFilePathsWithUnsentChanges.contains(filePath)) return;
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
final List<DartRegion> newImplementedClasses = new ArrayList<>(implementedClasses.size());
for (ImplementedClass implementedClass : implementedClasses) {
final int offset = myService.getConvertedOffset(file, implementedClass.getOffset());
final int length = myService.getConvertedOffset(file, implementedClass.getOffset() + implementedClass.getLength()) - offset;
newImplementedClasses.add(new DartRegion(offset, length));
}
final List<DartRegion> newImplementedMembers = new ArrayList<>(implementedMembers.size());
for (ImplementedMember implementedMember : implementedMembers) {
final int offset = myService.getConvertedOffset(file, implementedMember.getOffset());
final int length = myService.getConvertedOffset(file, implementedMember.getOffset() + implementedMember.getLength()) - offset;
newImplementedMembers.add(new DartRegion(offset, length));
}
boolean hasChanges = false;
final List<DartRegion> oldClasses = myImplementedClassData.get(filePath);
if (oldClasses == null || !oldClasses.equals(newImplementedClasses)) {
hasChanges = true;
myImplementedClassData.put(filePath, newImplementedClasses);
}
final List<DartRegion> oldMembers = myImplementedMemberData.get(filePath);
if (oldMembers == null || !oldMembers.equals(newImplementedMembers)) {
hasChanges = true;
myImplementedMemberData.put(filePath, newImplementedMembers);
}
if (hasChanges) {
forceFileAnnotation(file, false);
}
}
@NotNull
List<DartError> getErrors(@NotNull final SearchScope scope) {
final List<DartError> errors = new ArrayList<>();
synchronized (myErrorData) {
for (Map.Entry<String, List<DartError>> entry : myErrorData.entrySet()) {
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(entry.getKey());
if (file != null && scope.contains(file)) {
errors.addAll(entry.getValue());
}
}
}
return errors;
}
@NotNull
List<DartError> getErrors(@NotNull final VirtualFile file) {
final List<DartError> errors = myErrorData.get(file.getPath());
return errors != null ? errors : Collections.emptyList();
}
@NotNull
List<DartHighlightRegion> getHighlight(@NotNull final VirtualFile file) {
final List<DartHighlightRegion> regions = myHighlightData.get(file.getPath());
return regions != null ? regions : Collections.emptyList();
}
@NotNull
List<DartNavigationRegion> getNavigation(@NotNull final VirtualFile file) {
final List<DartNavigationRegion> regions = myNavigationData.get(file.getPath());
return regions != null ? regions : Collections.emptyList();
}
@NotNull
List<DartOverrideMember> getOverrideMembers(@NotNull final VirtualFile file) {
final List<DartOverrideMember> regions = myOverrideData.get(file.getPath());
return regions != null ? regions : Collections.emptyList();
}
@NotNull
List<DartRegion> getImplementedClasses(@NotNull final VirtualFile file) {
final List<DartRegion> classes = myImplementedClassData.get(file.getPath());
return classes != null ? classes : Collections.emptyList();
}
@NotNull
List<DartRegion> getImplementedMembers(@NotNull final VirtualFile file) {
final List<DartRegion> classes = myImplementedMemberData.get(file.getPath());
return classes != null ? classes : Collections.emptyList();
}
private void forceFileAnnotation(@Nullable final VirtualFile file, final boolean clearCache) {
if (file != null) {
final Project project = myService.getProject();
if (clearCache) {
ResolveCache.getInstance(project).clearCache(true);
}
// It's ok to call DaemonCodeAnalyzer.restart() right in this thread, without invokeLater(),
// but it would cache RemoteAnalysisServerImpl$ServerResponseReaderThread in FileStatusMap.threads and as a result,
// DartAnalysisServerService.myProject would be leaked in tests
ApplicationManager.getApplication().invokeLater(() -> DaemonCodeAnalyzer.getInstance(project).restart(),
ModalityState.NON_MODAL,
project.getDisposed());
}
}
void onFilesContentUpdated() {
myFilePathsWithUnsentChanges.clear();
}
void onFileClosed(@NotNull final VirtualFile file) {
// do not remove from myErrorData, this map is always kept up-to-date for all files, not only for visible
myHighlightData.remove(file.getPath());
myNavigationData.remove(file.getPath());
myOverrideData.remove(file.getPath());
myImplementedClassData.remove(file.getPath());
myImplementedMemberData.remove(file.getPath());
}
void onFlushedResults(@NotNull final List<String> filePaths) {
if (!myErrorData.isEmpty()) {
for (String path : filePaths) {
myErrorData.remove(path);
}
}
if (!myHighlightData.isEmpty()) {
for (String path : filePaths) {
myHighlightData.remove(path);
}
}
if (!myNavigationData.isEmpty()) {
for (String path : filePaths) {
myNavigationData.remove(path);
}
}
if (!myOverrideData.isEmpty()) {
for (String path : filePaths) {
myOverrideData.remove(path);
}
}
if (!myImplementedClassData.isEmpty()) {
for (String path : filePaths) {
myImplementedClassData.remove(path);
}
}
if (!myImplementedMemberData.isEmpty()) {
for (String path : filePaths) {
myImplementedMemberData.remove(path);
}
}
}
void clearData() {
myErrorData.clear();
myHighlightData.clear();
myNavigationData.clear();
myOverrideData.clear();
myImplementedClassData.clear();
myImplementedMemberData.clear();
}
void onDocumentChanged(@NotNull final DocumentEvent e) {
final VirtualFile file = FileDocumentManager.getInstance().getFile(e.getDocument());
if (!DartAnalysisServerService.isLocalAnalyzableFile(file)) return;
final String filePath = file.getPath();
myFilePathsWithUnsentChanges.add(filePath);
boolean someRegionDeleted = updateRegionsDeletingTouched(filePath, myErrorData.get(filePath), e);
if (someRegionDeleted) {
myFilePathsWithLostErrorInfo.add(filePath);
}
updateRegionsUpdatingTouched(myHighlightData.get(filePath), e);
updateRegionsDeletingTouched(filePath, myNavigationData.get(filePath), e);
updateRegionsDeletingTouched(filePath, myOverrideData.get(filePath), e);
updateRegionsDeletingTouched(filePath, myImplementedClassData.get(filePath), e);
updateRegionsDeletingTouched(filePath, myImplementedMemberData.get(filePath), e);
}
/**
* @return <code>true</code> if at least one region has been deleted, <code>false</code> if updated only or nothing done at all
*/
private static boolean updateRegionsDeletingTouched(@NotNull final String filePath,
@Nullable final List<? extends DartRegion> regions,
@NotNull final DocumentEvent e) {
if (regions == null) return false;
boolean regionDeleted = false;
// delete touched regions, shift untouched
final int eventOffset = e.getOffset();
final int deltaLength = e.getNewLength() - e.getOldLength();
final Iterator<? extends DartRegion> iterator = regions.iterator();
while (iterator.hasNext()) {
final DartRegion region = iterator.next();
if (region instanceof DartNavigationRegion) {
// may be we'd better delete target touched by editing?
for (DartNavigationTarget target : ((DartNavigationRegion)region).getTargets()) {
if (target.myFile.equals(filePath) && target.myConvertedOffset >= eventOffset) {
target.myConvertedOffset += deltaLength;
}
}
}
if (deltaLength > 0) {
// Something was typed. Shift untouched regions, delete touched.
if (eventOffset <= region.myOffset) {
region.myOffset += deltaLength;
}
else if (region.myOffset < eventOffset && eventOffset < region.myOffset + region.myLength) {
iterator.remove();
regionDeleted = true;
}
}
else if (deltaLength < 0) {
// Some text was deleted. Shift untouched regions, delete touched.
final int eventRightOffset = eventOffset - deltaLength;
if (eventRightOffset <= region.myOffset) {
region.myOffset += deltaLength;
}
else if (eventOffset < region.myOffset + region.myLength) {
iterator.remove();
regionDeleted = true;
}
}
}
return regionDeleted;
}
private static void updateRegionsUpdatingTouched(@Nullable final List<? extends DartRegion> regions,
@NotNull final DocumentEvent e) {
if (regions == null) return;
final int eventOffset = e.getOffset();
final int deltaLength = e.getNewLength() - e.getOldLength();
final Iterator<? extends DartRegion> iterator = regions.iterator();
while (iterator.hasNext()) {
final DartRegion region = iterator.next();
if (deltaLength > 0) {
// Something was typed. Shift untouched regions, update touched.
if (eventOffset <= region.myOffset) {
region.myOffset += deltaLength;
}
else if (region.myOffset < eventOffset && eventOffset < region.myOffset + region.myLength) {
region.myLength += deltaLength;
}
}
else if (deltaLength < 0) {
// Some text was deleted. Shift untouched regions, delete or update touched.
final int eventRightOffset = eventOffset - deltaLength;
final int regionRightOffset = region.myOffset + region.myLength;
if (eventRightOffset <= region.myOffset) {
region.myOffset += deltaLength;
}
else if (region.myOffset <= eventOffset && eventRightOffset <= regionRightOffset && region.myLength != -deltaLength) {
region.myLength += deltaLength;
}
else if (eventOffset < regionRightOffset) {
iterator.remove();
}
}
}
}
public static class DartRegion {
protected int myOffset;
protected int myLength;
DartRegion(final int offset, final int length) {
myOffset = offset;
myLength = length;
}
public final int getOffset() {
return myOffset;
}
public final int getLength() {
return myLength;
}
@Override
public boolean equals(Object o) {
return o instanceof DartRegion && myOffset == ((DartRegion)o).myOffset && myLength == ((DartRegion)o).myLength;
}
@Override
public int hashCode() {
return myOffset * 31 + myLength;
}
}
public static class DartHighlightRegion extends DartRegion {
private final String type;
private DartHighlightRegion(final int offset, final int length, @NotNull final String type) {
super(offset, length);
this.type = type.intern();
}
public String getType() {
return type;
}
}
public static class DartError extends DartRegion {
private final String myAnalysisErrorFileSD;
private final String mySeverity;
@Nullable private final String myCode;
private final String myMessage;
private DartError(@NotNull final AnalysisError error, final int correctedOffset, final int correctedLength) {
super(correctedOffset, correctedLength);
myAnalysisErrorFileSD = error.getLocation().getFile().intern();
mySeverity = error.getSeverity().intern();
myCode = error.getCode() == null ? null : error.getCode().intern();
myMessage = error.getMessage();
}
public String getAnalysisErrorFileSD() {
return myAnalysisErrorFileSD;
}
public String getSeverity() {
return mySeverity;
}
public boolean isError() {
return mySeverity.equals(AnalysisErrorSeverity.ERROR);
}
@Nullable
public String getCode() {
return myCode;
}
public String getMessage() {
return myMessage;
}
}
public static class DartNavigationRegion extends DartRegion {
private final List<DartNavigationTarget> myTargets;
DartNavigationRegion(final int offset, final int length, @NotNull final List<DartNavigationTarget> targets) {
super(offset, length);
myTargets = targets;
}
@Override
public String toString() {
return "DartNavigationRegion(" + myOffset + ", " + myLength + ")";
}
public List<DartNavigationTarget> getTargets() {
return myTargets;
}
}
public static class DartNavigationTarget {
private final String myFile;
private final int myOriginalOffset;
private final String myKind;
private int myConvertedOffset = -1;
private DartNavigationTarget(@NotNull final NavigationTarget target) {
myFile = FileUtil.toSystemIndependentName(target.getFile().trim()).intern();
myOriginalOffset = target.getOffset();
myKind = target.getKind().intern();
}
public String getFile() {
return myFile;
}
public int getOffset(@NotNull final Project project, @Nullable final VirtualFile file) {
if (myConvertedOffset == -1) {
myConvertedOffset = DartAnalysisServerService.getInstance(project).getConvertedOffset(file, myOriginalOffset);
}
return myConvertedOffset;
}
public String getKind() {
return myKind;
}
}
public static class DartOverrideMember extends DartRegion {
@Nullable private final OverriddenMember mySuperclassMember;
@Nullable private final List<OverriddenMember> myInterfaceMembers;
private DartOverrideMember(final int offset,
final int length,
@Nullable final OverriddenMember superclassMember,
@Nullable final List<OverriddenMember> interfaceMembers) {
super(offset, length);
mySuperclassMember = superclassMember;
myInterfaceMembers = interfaceMembers;
}
@Nullable
public OverriddenMember getSuperclassMember() {
return mySuperclassMember;
}
@Nullable
public List<OverriddenMember> getInterfaceMembers() {
return myInterfaceMembers;
}
}
}