/*
* Copyright (c) 2015-2015 Vladimir Schneider <vladimir.schneider@gmail.com>
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.vladsch.idea.multimarkdown;
import com.intellij.ProjectTopics;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootAdapter;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsListener;
import com.intellij.openapi.vfs.*;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.refactoring.listeners.RefactoringEventData;
import com.intellij.refactoring.listeners.RefactoringEventListener;
import com.intellij.util.FileContentUtil;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.MessageBusConnection;
import com.vladsch.idea.multimarkdown.psi.MultiMarkdownFile;
import com.vladsch.idea.multimarkdown.psi.MultiMarkdownNamedElement;
import com.vladsch.idea.multimarkdown.settings.MultiMarkdownGlobalSettings;
import com.vladsch.idea.multimarkdown.settings.MultiMarkdownGlobalSettingsListener;
import com.vladsch.idea.multimarkdown.util.*;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class MultiMarkdownProjectComponent implements ProjectComponent, VirtualFileListener, LinkResolver.ProjectResolver {
private static final Logger logger = org.apache.log4j.Logger.getLogger(MultiMarkdownProjectComponent.class);
private final static int LISTENER_ADDED = 0;
private final static int SYMBOL_REF_CHANGED = 1;
public static final Object NULL_VCS_ROOT = new Object();
private ConcurrentHashMap<String, Object> gitHubRepos = null;
private Project project;
protected MultiMarkdownGlobalSettingsListener globalSettingsListener;
protected int refactoringRenameFlags = MultiMarkdownNamedElement.RENAME_NO_FLAGS;
protected int[] refactoringRenameFlagsStack = new int[10];
protected int refactoringRenameStack = 0;
protected boolean needReparseOnDumbModeExit = false;
protected boolean needReparsePsiOnDumbModeExit = false;
private final ConcurrentHashMap<String, ElementNamespace> elementNamespaces = new ConcurrentHashMap<String, ElementNamespace>();
private final ListenerNotifier<ReferenceChangeListener> allNamespacesNotifier = new ListenerNotifier<ReferenceChangeListener>();
private boolean needAllSpacesNotification;
private class ElementNamespace {
final String namespace;
final ConcurrentHashMap<String, MultiMarkdownNamedElement> symbolTable = new ConcurrentHashMap<String, MultiMarkdownNamedElement>();
final ListenerNotifier<ReferenceChangeListener> notifier = new ListenerNotifier<ReferenceChangeListener>();
final ConcurrentHashMap<MultiMarkdownNamedElement, String> rootElements = new ConcurrentHashMap<MultiMarkdownNamedElement, String>();
public ElementNamespace(String namespace) {
this.namespace = namespace;
}
MultiMarkdownNamedElement getSymbol(@NotNull MultiMarkdownNamedElement element, @NotNull String name) {
String oldName = null;
boolean log = false;
MultiMarkdownNamedElement refElement = element;
if (rootElements.containsKey(element)) {
if (!rootElements.get(element).equals(name)) {
// root element's name changed, inform listeners that they need to remap references
oldName = rootElements.get(element);
if (log) logger.info("root element " + element + " renamed from '" + oldName + "' to '" + name + "'");
symbolTable.remove(oldName);
if (!symbolTable.containsKey(name)) {
// still root element but under a new name
symbolTable.put(name, element);
rootElements.put(element, name);
if (log) logger.info(" old root " + element + " now under new name");
} else {
// no longer root element
rootElements.remove(element);
refElement = symbolTable.get(name);
if (log) logger.info("removed old root element " + element + " now referencing " + refElement);
}
}
} else {
if (!symbolTable.containsKey(name)) {
// new root element
symbolTable.put(name, element);
rootElements.put(element, name);
//logger.info("new root for " + namespace + " element " + element);
} else {
refElement = symbolTable.get(name);
if (refElement == element) {
rootElements.put(element, name);
logger.info(namespace + name + "not in rootElements but is root in namespace");
}
}
}
if (oldName != null) {
// do the notifications that the reference symbol for oldName has changed
if (log) logger.info("notifying listeners of " + namespace + " ref changed to '" + oldName + "'");
final String finalOldName = oldName;
notifier.notifyListeners(new ListenerNotifier.RunnableNotifier<ReferenceChangeListener>() {
@Override
public boolean notify(ReferenceChangeListener listener) {
listener.referenceChanged(finalOldName);
return false;
}
});
// TODO: validate that this is not needed, a change of a linkref will cause linkrefs referencing
// to be invalidated but will not invalidate any other elements that depend on the linkref,
// like its text and anchor siblings
//needAllSpacesNotification = true;
}
return refElement;
}
void notifyRefsInvalidated() {
notifier.notifyListeners(new ListenerNotifier.RunnableNotifier<ReferenceChangeListener>() {
@Override
public boolean notify(ReferenceChangeListener listener) {
listener.referenceChanged(null);
return false;
}
});
}
void addListener(ReferenceChangeListener listener) {
notifier.addListener(listener);
}
public void removeListener(ReferenceChangeListener listener) {
notifier.removeListener(listener);
}
}
@NotNull
public MultiMarkdownNamedElement getMissingLinkElement(@NotNull final MultiMarkdownNamedElement element, @NotNull final String namespace, @NotNull final String name) {
ElementNamespace elementNamespace;
MultiMarkdownNamedElement symbol;
if (!elementNamespaces.containsKey(namespace)) {
elementNamespaces.put(namespace, elementNamespace = new ElementNamespace(namespace));
} else {
elementNamespace = elementNamespaces.get(namespace);
}
symbol = elementNamespace.getSymbol(element, name);
if (needAllSpacesNotification) allNamespacesNotifier.notifyListeners(new ListenerNotifier.RunnableNotifier<ReferenceChangeListener>() {
@Override
public boolean notify(ReferenceChangeListener listener) {
listener.referenceChanged(name);
return false;
}
});
return symbol;
}
private void clearNamespaces() {
elementNamespaces.clear();
for (ElementNamespace elementNamespace : elementNamespaces.values()) {
elementNamespace.notifyRefsInvalidated();
}
allNamespacesNotifier.notifyListeners(new ListenerNotifier.RunnableNotifier<ReferenceChangeListener>() {
@Override
public boolean notify(ReferenceChangeListener listener) {
listener.referenceChanged(null);
return false;
}
});
}
protected void reparseMarkdown() {
reparseMarkdown(false);
}
protected void reparseMarkdown(final boolean reparseFilePsi) {
boolean log = false;
if (!project.isDisposed()) {
if (DumbService.isDumb(project)) {
needReparseOnDumbModeExit = true;
needReparsePsiOnDumbModeExit = reparseFilePsi;
return;
}
// reparse all open markdown editors
VirtualFile[] files = FileEditorManager.getInstance(project).getOpenFiles();
clearNamespaces();
PsiManager psiManager = PsiManager.getInstance(project);
if (reparseFilePsi) {
ArrayList<VirtualFile> fileList = new ArrayList<VirtualFile>(files.length);
for (VirtualFile file : files) {
PsiFile psiFile = psiManager.findFile(file);
if (psiFile != null && psiFile instanceof MultiMarkdownFile) {
fileList.add(file);
}
}
if (log) logger.info("reparse file psi start");
FileContentUtil.reparseFiles(fileList);
if (log) logger.info("reparse file psi end");
} else {
if (log) logger.info("reparse open file start");
DaemonCodeAnalyzer instance = DaemonCodeAnalyzer.getInstance(project);
for (VirtualFile file : files) {
PsiFile psiFile = psiManager.findFile(file);
if (psiFile != null && psiFile instanceof MultiMarkdownFile) {
instance.restart(psiFile);
}
}
if (log) logger.info("reparse open file end");
}
}
}
public void addListener(@NotNull String namespace, @NotNull ReferenceChangeListener listener) {
ElementNamespace elementNamespace;
if (!elementNamespaces.containsKey(namespace)) {
elementNamespaces.put(namespace, elementNamespace = new ElementNamespace(namespace));
} else {
elementNamespace = elementNamespaces.get(namespace);
}
elementNamespace.addListener(listener);
}
public void removeListener(@NotNull String namespace, @NotNull ReferenceChangeListener listener) {
if (elementNamespaces.containsKey(namespace)) {
elementNamespaces.get(namespace).removeListener(listener);
}
}
public void addListener(@NotNull ReferenceChangeListener listener) {
allNamespacesNotifier.addListener(listener);
}
public void removeListener(@NotNull ReferenceChangeListener listener) {
allNamespacesNotifier.removeListener(listener);
}
public int getRefactoringRenameFlags() {
return refactoringRenameFlags;
}
public int getRefactoringRenameFlags(int defaultFlags) {
return refactoringRenameFlags == MultiMarkdownNamedElement.RENAME_NO_FLAGS ? defaultFlags : refactoringRenameFlags;
}
public void pushRefactoringRenameFlags(int refactoringReason) {
this.refactoringRenameFlagsStack[refactoringRenameStack++] = this.refactoringRenameFlags;
this.refactoringRenameFlags = refactoringReason;
}
public void popRefactoringRenameFlags() {
if (refactoringRenameStack > 0) {
refactoringRenameFlags = refactoringRenameFlagsStack[--refactoringRenameStack];
}
}
@Nullable
public GitHubVcsRoot getGitHubRepo(@Nullable String baseDirectoryPath) {
if (project.isDisposed()) return null;
String projectBasePath = project.getBasePath();
if (projectBasePath == null) return null;
if (baseDirectoryPath == null) baseDirectoryPath = projectBasePath;
if (gitHubRepos == null) {
gitHubRepos = new ConcurrentHashMap<String, Object>();
}
// TODO: optimize this to reduce directory scanning for git config by using the VcsRoots defined in the IDE to find roots and only read config for defined roots
baseDirectoryPath = StringUtilKt.removeEnd(baseDirectoryPath, '/');
projectBasePath = StringUtilKt.removeEnd(projectBasePath, '/');
if (!gitHubRepos.containsKey(baseDirectoryPath)) {
GitHubVcsRoot gitHubVcsRoot = GitHubVcsRoot.getGitHubVcsRoot(baseDirectoryPath, projectBasePath);
// add all intervening directories to point to this repo or null if none was found so we don't search for it again
String gitRootBaseDir = gitHubVcsRoot == null ? projectBasePath : StringUtilKt.removeEnd(gitHubVcsRoot.getBasePath(), '/');
PathInfo currentBaseDir = new PathInfo(baseDirectoryPath);
do {
//logger.info("getGitHubRepo("+baseDirectoryPath+") : Adding vcsRepoRoot: " + gitHubVcsRoot + " for " + currentBaseDir.getFilePath());
gitHubRepos.put(currentBaseDir.getFilePath(), gitHubVcsRoot != null ? gitHubVcsRoot : NULL_VCS_ROOT);
if (currentBaseDir.getFilePath().equals(gitRootBaseDir)) break;
currentBaseDir = new PathInfo(currentBaseDir.getPath());
} while (!currentBaseDir.isEmpty() && !currentBaseDir.isRoot());
}
Object object = gitHubRepos.get(baseDirectoryPath);
return object instanceof GitHubVcsRoot ? (GitHubVcsRoot) object : null;
}
public boolean isUnderVcs(VirtualFile virtualFile) {
FileStatus status = FileStatusManager.getInstance(project).getStatus(virtualFile);
String id = status.getId();
boolean fileStatus = status.equals(FileStatus.DELETED) || status.equals(FileStatus.ADDED) || status.equals(FileStatus.UNKNOWN) || status.equals(FileStatus.IGNORED)
|| id.startsWith("IGNORE");
//logger.info("isUnderVcs " + (!fileStatus) + " for file " + virtualFile + " status " + status);
return !fileStatus;
}
@Override
public boolean isUnderVcs(@NotNull FileRef fileRef) {
ProjectFileRef projectFileRef = fileRef.projectFileRef(project);
return projectFileRef != null && isUnderVcs(projectFileRef.getVirtualFile());
}
@Nullable
@Override
public GitHubVcsRoot getVcsRoot(@NotNull FileRef fileRef) {
return getGitHubRepo(fileRef.getPath());
}
@Nullable
@Override
public String vcsRootBase(@NotNull FileRef fileRef) {
GitHubVcsRoot gitHubVcsRoot = getGitHubRepo(fileRef.getPath());
return gitHubVcsRoot == null ? null : gitHubVcsRoot.getBasePath();
}
@NotNull
@Override
public String getProjectBasePath() {
String basePath = project.isDisposed() ? null : project.getBasePath();
return basePath != null ? basePath : "";
}
@Nullable
@Override
public String vcsRepoBasePath(@NotNull FileRef fileRef) {
GitHubVcsRoot gitHubVcsRoot = getGitHubRepo(fileRef.getPath());
return gitHubVcsRoot == null ? null : gitHubVcsRoot.getMainRepoBaseDir();
}
@Nullable
@Override
public List<FileRef> projectFileList(@Nullable List<String> fileTypes) {
assert false: "Should never be called";
return null;
}
public MultiMarkdownProjectComponent(final Project project) {
this.project = project;
// Listen to settings changes
MultiMarkdownGlobalSettings.getInstance().addListener(globalSettingsListener = new MultiMarkdownGlobalSettingsListener() {
public void handleSettingsChanged(@NotNull final MultiMarkdownGlobalSettings newSettings) {
reparseMarkdown(true);
}
});
project.getMessageBus().connect(project).subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
@Override
public void enteredDumbMode() {
}
@Override
public void exitDumbMode() {
// need to re-evaluate class link accessibility
if (project.isDisposed()) return;
if (needReparseOnDumbModeExit) {
final boolean reparseFilePsi = needReparsePsiOnDumbModeExit;
needReparseOnDumbModeExit = false;
needReparsePsiOnDumbModeExit = false;
reparseMarkdown(reparseFilePsi);
}
}
});
MessageBusConnection connect = getProject().getMessageBus().connect();
connect.subscribe(RefactoringEventListener.REFACTORING_EVENT_TOPIC, new RefactoringEventListener() {
@Override
public void refactoringStarted(@NotNull String refactoringId, @Nullable RefactoringEventData beforeData) {
//logger.info("refactoring started on " + this.hashCode());
}
@Override
public void refactoringDone(@NotNull String refactoringId, @Nullable RefactoringEventData afterData) {
//logger.info("refactoring done on " + this.hashCode());
}
@Override
public void conflictsDetected(@NotNull String refactoringId, @NotNull RefactoringEventData conflictsData) {
//logger.info("refactoring conflicts on " + this.hashCode());
}
@Override
public void undoRefactoring(@NotNull String refactoringId) {
//logger.info("refactoring undo on " + this.hashCode());
clearNamespaces();
}
});
}
@NotNull
public Project getProject() {
return project;
}
public void projectOpened() {
VirtualFileManager.getInstance().addVirtualFileListener(this);
boolean initialized = project.isInitialized();
final MessageBus messageBus = project.getMessageBus();
messageBus.connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
public void rootsChanged(ModuleRootEvent event) {
if (project.isDisposed()) return;
reparseMarkdown();
}
});
messageBus.connect().subscribe(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED, new VcsListener() {
@Override
public void directoryMappingChanged() {
gitHubRepos = null;
}
});
messageBus.connect().subscribe(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED_IN_PLUGIN, new VcsListener() {
@Override
public void directoryMappingChanged() {
gitHubRepos = null;
}
});
}
public void projectClosed() {
VirtualFileManager.getInstance().removeVirtualFileListener(this);
}
@NonNls
@NotNull
public String getComponentName() {
return this.getClass().getName();
}
public void initComponent() {
}
public void disposeComponent() {
}
protected void updateHighlighters() {
// project files have changed so we need to update the lists and then reparse for link validation
// We get a call back when all have been updated.
if (project.isDisposed()) return;
reparseMarkdown();
}
// TODO: detect extension change in a file and attach our editors if possible
@Override
public void propertyChanged(@NotNull VirtualFilePropertyEvent event) {
updateHighlighters();
}
@Override
public void contentsChanged(@NotNull VirtualFileEvent event) {
//updateHighlighters();
}
@Override
public void fileCreated(@NotNull VirtualFileEvent event) {
updateHighlighters();
}
@Override
public void fileDeleted(@NotNull VirtualFileEvent event) {
updateHighlighters();
}
@Override
public void fileMoved(@NotNull VirtualFileMoveEvent event) {
updateHighlighters();
}
@Override
public void fileCopied(@NotNull VirtualFileCopyEvent event) {
updateHighlighters();
}
@Override
public void beforePropertyChange(@NotNull VirtualFilePropertyEvent event) {
//String s = event.getPropertyName();
//int tmp = 0;
}
@Override
public void beforeContentsChange(@NotNull VirtualFileEvent event) {
//int tmp = 0;
}
// return the files this name from inFile can refer to, wikiPagesOnly is set if the name is a wiki link
// name could be a wiki page ref or a link -
// search type could be markdownFileRefs, wikiFiles, imageFileRefs
@Override
public void beforeFileDeletion(@NotNull VirtualFileEvent event) {
//int tmp = 0;
}
@Override
public void beforeFileMovement(@NotNull VirtualFileMoveEvent event) {
//int tmp = 0;
}
}