package requirejs; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.intellij.lang.ASTNode; import com.intellij.lang.javascript.JSElementTypes; import com.intellij.lang.javascript.JSTokenTypes; import com.intellij.lang.javascript.psi.impl.JSFileImpl; import com.intellij.notification.Notification; import com.intellij.notification.NotificationType; import com.intellij.notification.Notifications; import com.intellij.openapi.components.ProjectComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileAdapter; import com.intellij.openapi.vfs.VirtualFileEvent; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.psi.impl.source.tree.TreeElement; import com.intellij.psi.impl.source.xml.XmlFileImpl; import com.intellij.psi.tree.TokenSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import requirejs.settings.Settings; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; public class RequirejsProjectComponent implements ProjectComponent { protected Project project; protected Settings settings; protected boolean settingValidStatus; protected String settingValidVersion; protected String settingVersionLastShowNotification; protected final Logger LOG = Logger.getInstance("Requirejs-Plugin"); protected String requirejsBaseUrl; protected RequirePaths requirePaths; protected RequireMap requireMap = new RequireMap(); private RequireConfigVfsListener vfsListener; public PackageConfig packageConfig; public RequirejsProjectComponent(Project project) { this.project = project; settings = Settings.getInstance(project); requirePaths = new RequirePaths(this); packageConfig = new PackageConfig(this); } @Override public void projectOpened() { if (isEnabled()) { validateSettings(); } } public void watchConfigFile() { // Add the Virtual File listener vfsListener = new RequireConfigVfsListener(); VirtualFileManager.getInstance().addVirtualFileListener(vfsListener, project); } public void stopWatchConfigFile() { if (vfsListener == null) { return; } VirtualFileManager.getInstance().removeVirtualFileListener(vfsListener); } @Override public void projectClosed() { } @Override public void initComponent() { if (isEnabled()) { validateSettings(); } } @Override public void disposeComponent() { } @NotNull @Override public String getComponentName() { return "RequirejsProjectComponent"; } public Logger getLogger() { return LOG; } public boolean isEnabled() { return Settings.getInstance(project).pluginEnabled; } public boolean isSettingsValid() { if (!settings.getVersion().equals(settingValidVersion)) { validateSettings(); settingValidVersion = settings.getVersion(); } return settingValidStatus; } public boolean validateSettings() { if (null == getWebDir()) { showErrorConfigNotification( "Public directory not found. Path " + getContentRoot().getPath() + '/' + settings.publicPath + " not found in project" ); LOG.debug("Public directory not found"); settingValidStatus = false; return false; } if (isEnabled()) { watchConfigFile(); } else { stopWatchConfigFile(); } settingValidStatus = true; return true; } public void clearParse() { requirejsBaseUrl = null; requirePaths.clear(); requireMap.clear(); packageConfig.clear(); } protected void showErrorConfigNotification(String content) { if (!settings.getVersion().equals(settingVersionLastShowNotification)) { settingVersionLastShowNotification = settings.getVersion(); showInfoNotification(content, NotificationType.ERROR); } } public VirtualFile getWebDir(VirtualFile elementFile) { if (null != elementFile) { if (settings.publicPath.isEmpty()) { return getContentRoot(elementFile); } } VirtualFile vfWebDir = findPathInContentRoot(settings.publicPath); if (null != vfWebDir) { return vfWebDir; } else { return null; } } public VirtualFile getWebDir() { return getWebDir(null); } protected VirtualFile findPathInWebDir(String path) { if (settings.publicPath.isEmpty()) { return findPathInContentRoot(path); } VirtualFile vfWebDir = getWebDir(); if (null != vfWebDir) { return vfWebDir.findFileByRelativePath(path); } else { return null; } } public VirtualFile getContentRoot() { VirtualFile[] contentRoots = ProjectRootManager.getInstance(project).getContentRoots(); if (contentRoots.length > 0) { return contentRoots[0]; } else { return project.getBaseDir(); } } public VirtualFile getContentRoot(VirtualFile file) { return ProjectRootManager.getInstance(project).getFileIndex().getContentRootForFile(file); } protected VirtualFile findPathInContentRoot(String path) { VirtualFile[] contentRoots = ProjectRootManager.getInstance(project).getContentRoots(); if (contentRoots.length > 0) { for(VirtualFile contentRoot : contentRoots) { VirtualFile vfPath = contentRoot.findFileByRelativePath(path); if (null != vfPath) { return vfPath; } } return null; } else { return project.getBaseDir().findFileByRelativePath(path); } } public void showInfoNotification(String content, NotificationType type) { Notification errorNotification = new Notification("Require.js plugin", "Require.js plugin", content, type); Notifications.Bus.notify(errorNotification, this.project); } public List<String> getModulesNames() { List<String> modules = new ArrayList<String>(); if (requirePaths.isEmpty()) { if (!parseRequirejsConfig()) { return modules; } } modules.addAll(requirePaths.getAliasToFiles()); Collection<Package> filteredPackages = Collections2.filter(packageConfig.packages, new Predicate<Package>() { @Override public boolean apply(Package aPackage) { return aPackage != null && aPackage.mainExists; } }); Collection<String> ret = Collections2.transform(filteredPackages, new Function<Package, String>() { @Override public String apply(Package aPackage) { return aPackage.name; } }); modules.addAll(ret); return modules; } public String getBaseUrl() { if (null == requirejsBaseUrl) { VirtualFile baseUrlPath = getBaseUrlPath(true); if (null != baseUrlPath) { requirejsBaseUrl = baseUrlPath.getPath().replace(getWebDir().getPath(), ""); requirejsBaseUrl = StringUtil.trimEnd(requirejsBaseUrl, "/"); if (requirejsBaseUrl.startsWith("/")) { requirejsBaseUrl = requirejsBaseUrl.substring(1); } } else { requirejsBaseUrl = ""; } } return requirejsBaseUrl; } public VirtualFile getBaseUrlPath(boolean parseConfig) { if (null == requirejsBaseUrl) { if (parseConfig) { parseRequirejsConfig(); } if (null == requirejsBaseUrl) { return getConfigFileDir(); } } return findPathInWebDir(requirejsBaseUrl); } protected VirtualFile getConfigFileDir() { VirtualFile mainJsVirtualFile = findPathInWebDir(settings.configFilePath); if (null != mainJsVirtualFile) { return mainJsVirtualFile.getParent(); } else { return null; } } // private Date lastParse; public boolean parseRequirejsConfig() { VirtualFile mainJsVirtualFile = findPathInWebDir(settings.configFilePath); if (null == mainJsVirtualFile) { this.showErrorConfigNotification("Config file not found. File " + settings.publicPath + '/' + settings.configFilePath + " not found in project"); LOG.debug("Config not found"); return false; } else { PsiFile mainJs = PsiManager.getInstance(project).findFile(mainJsVirtualFile); if (mainJs instanceof JSFileImpl || mainJs instanceof XmlFileImpl) { Map<String, VirtualFile> allConfigPaths; packageConfig.clear(); requireMap.clear(); requirePaths.clear(); if (((PsiFileImpl) mainJs).getTreeElement() == null) { parseMainJsFile(((PsiFileImpl) mainJs).calcTreeElement()); } else { parseMainJsFile(((PsiFileImpl) mainJs).getTreeElement()); } } else { this.showErrorConfigNotification("Config file wrong format"); LOG.debug("Config file wrong format"); return false; } } return true; } public void parseMainJsFile(TreeElement node) { TreeElement firstChild = node.getFirstChildNode(); if (firstChild != null) { parseMainJsFile(firstChild); } TreeElement nextNode = node.getTreeNext(); if (nextNode != null) { parseMainJsFile(nextNode); } if (node.getElementType() == JSTokenTypes.IDENTIFIER) { if (node.getText().equals("requirejs") || node.getText().equals("require")) { TreeElement treeParent = node.getTreeParent(); if (null != treeParent) { ASTNode firstTreeChild = treeParent.findChildByType(JSElementTypes.OBJECT_LITERAL_EXPRESSION); TreeElement nextTreeElement = treeParent.getTreeNext(); if (null != firstTreeChild) { parseRequirejsConfig((TreeElement) firstTreeChild .getFirstChildNode() ); } else if (null != nextTreeElement && nextTreeElement.getElementType() == JSTokenTypes.DOT) { nextTreeElement = nextTreeElement.getTreeNext(); if (null != nextTreeElement && nextTreeElement.getText().equals("config")) { treeParent = nextTreeElement.getTreeParent(); findAndParseConfig(treeParent); } } else { findAndParseConfig(treeParent); } } } } } protected void findAndParseConfig(TreeElement treeParent) { TreeElement nextTreeElement; if (null != treeParent) { nextTreeElement = treeParent.getTreeNext(); if (null != nextTreeElement) { ASTNode nextChild = nextTreeElement.findChildByType(JSElementTypes.OBJECT_LITERAL_EXPRESSION); if (null != nextChild) { parseRequirejsConfig( (TreeElement) nextChild.getFirstChildNode() ); } } } } public static String dequote(String text) { return text.replace("\"", "").replace("'", ""); } public static String dequoteAll(String text) { return text.replaceAll("\"", "").replaceAll("'", ""); } public void parseRequirejsConfig(TreeElement node) { if (null == node) { return ; } try { if (node.getElementType() == JSElementTypes.PROPERTY) { TreeElement identifier = (TreeElement) node.findChildByType(JSTokenTypes.IDENTIFIER); String identifierName = null; if (null != identifier) { identifierName = identifier.getText(); } else { TreeElement identifierString = (TreeElement) node.findChildByType(JSTokenTypes.STRING_LITERAL); if (null != identifierString) { identifierName = dequote(identifierString.getText()); } } if (null != identifierName) { if (identifierName.equals("baseUrl")) { String baseUrl = null; if (!settings.overrideBaseUrl) { ASTNode baseUrlNode = node .findChildByType(JSElementTypes.LITERAL_EXPRESSION); if (null != baseUrlNode) { baseUrl = dequote(baseUrlNode.getText()); } } else { LOG.info("baseUrl override is enabled, overriding with '" + settings.baseUrl + "'"); baseUrl = settings.baseUrl; } if (null != baseUrl) { LOG.info("Setting baseUrl to '" + baseUrl + "'"); setBaseUrl(baseUrl); packageConfig.baseUrl = baseUrl; } else { LOG.debug("BaseUrl not set"); } } else if (identifierName.equals("paths")) { ASTNode pathsNode = node .findChildByType(JSElementTypes.OBJECT_LITERAL_EXPRESSION); if (null != pathsNode) { parseRequireJsPaths( (TreeElement) pathsNode.getFirstChildNode() ); } } else if (identifierName.equals("packages")) { TreeElement packages = (TreeElement) node.findChildByType(JSElementTypes.ARRAY_LITERAL_EXPRESSION); LOG.debug("parsing packages"); parsePackages(packages); LOG.debug("parsing packages done, found " + packageConfig.packages.size() + " packages"); } else if (identifierName.equals("map")) { TreeElement mapElement = (TreeElement) node.findChildByType(JSElementTypes.OBJECT_LITERAL_EXPRESSION); parseMapsConfig(mapElement); } } } } catch (NullPointerException exception) { LOG.error(exception.getMessage(), exception); } TreeElement next = node.getTreeNext(); if (null != next) { parseRequirejsConfig(next); } } protected void parseMapsConfig(TreeElement mapElement) { TreeElement firstMapConfigElement = (TreeElement) mapElement.findChildByType(JSElementTypes.PROPERTY); parseMapConfigElement(firstMapConfigElement); } protected void parseMapConfigElement(TreeElement mapConfigElement) { if (null == mapConfigElement) { return; } if (mapConfigElement.getElementType() == JSElementTypes.PROPERTY) { String module = getJSPropertyName(mapConfigElement); TreeElement mapAliasesObject = (TreeElement) mapConfigElement .findChildByType(JSElementTypes.OBJECT_LITERAL_EXPRESSION); if (null != mapAliasesObject) { RequireMapModule requireMapModule = new RequireMapModule(); requireMapModule.module = module; TreeElement mapAliasProperty = (TreeElement) mapAliasesObject.findChildByType(JSElementTypes.PROPERTY); parseMapAliasProperty(requireMapModule, mapAliasProperty); requireMap.addModule(requireMapModule); } } parseMapConfigElement(mapConfigElement.getTreeNext()); } protected void parseMapAliasProperty(RequireMapModule requireMapModule, TreeElement mapAliasProperty) { if (null == mapAliasProperty) { return; } if (mapAliasProperty.getElementType() == JSElementTypes.PROPERTY) { RequirePathAlias alias = new RequirePathAlias(); alias.alias = getJSPropertyName(mapAliasProperty); alias.path = getJSPropertyLiteralValue(mapAliasProperty); if (null != alias.alias && alias.path != null) { requireMapModule.addAlias(alias); } else { LOG.debug("Error parse require js path", alias); } } parseMapAliasProperty(requireMapModule, mapAliasProperty.getTreeNext()); } protected String getJSPropertyName(TreeElement jsProperty) { TreeElement identifier = (TreeElement) jsProperty.findChildByType( TokenSet.create(JSTokenTypes.IDENTIFIER, JSTokenTypes.STRING_LITERAL, JSTokenTypes.PUBLIC_KEYWORD) ); String identifierName = null; if (null != identifier) { identifierName = dequote(identifier.getText()); } return identifierName; } private void parsePackages(TreeElement node) { TokenSet tokenSet = TokenSet.create( JSElementTypes.OBJECT_LITERAL_EXPRESSION, JSElementTypes.LITERAL_EXPRESSION); TreeElement packageNode = (TreeElement) node.findChildByType(tokenSet); parsePackage(packageNode); } private void parsePackage(TreeElement node) { if (null == node) { return; } if (node.getElementType() == JSElementTypes.OBJECT_LITERAL_EXPRESSION || node.getElementType() == JSElementTypes.LITERAL_EXPRESSION ) { // TODO: Not adding not resolve package Package p = new Package(); packageConfig.packages.add(p); if (node.getElementType() == JSElementTypes.OBJECT_LITERAL_EXPRESSION) { TreeElement prop = (TreeElement) node.findChildByType(JSElementTypes.PROPERTY); parsePackageObject(prop, p); } else { p.name = dequote(node.getText()); } normalizeParsedPackage(p); validatePackage(p); } TreeElement next = node.getTreeNext(); parsePackage(next); } private void normalizeParsedPackage(Package p) { if (null == p.location) { p.location = p.name; } if (null == p.main) { p.main = Package.DEFAULT_MAIN; } } private void validatePackage(Package p) { if (null == getConfigFileDir().findFileByRelativePath(p.location + '/' + p.main + ".js")) { p.mainExists = false; } else { p.mainExists = true; } } private static void parsePackageObject(TreeElement node, Package p) { if (null == node) { return; } if (node.getElementType() == JSElementTypes.PROPERTY) { TreeElement identifier = (TreeElement) node.findChildByType(JSTokenTypes.IDENTIFIER); String identifierName = null; if (null != identifier) { identifierName = identifier.getText(); } else { TreeElement identifierString = (TreeElement) node.findChildByType(JSTokenTypes.STRING_LITERAL); if (null != identifierString) { identifierName = dequote(identifierString.getText()); } } if (null != identifierName) { if (identifierName.equals("name")) { p.name = getJSPropertyLiteralValue(node); } else if (identifierName.equals("location")) { p.location = getJSPropertyLiteralValue(node); } else if (identifierName.equals("main")) { p.main = getJSPropertyLiteralValue(node); } } } TreeElement next = node.getTreeNext(); parsePackageObject(next, p); } @Nullable private static String getJSPropertyLiteralValue(TreeElement jsProperty) { TokenSet availablePropertyValueTokenSet = TokenSet.create( JSElementTypes.LITERAL_EXPRESSION, JSElementTypes.BINARY_EXPRESSION, JSElementTypes.CONDITIONAL_EXPRESSION); TreeElement jsPropertyValue = (TreeElement) jsProperty.findChildByType(availablePropertyValueTokenSet); if (null == jsPropertyValue) { return null; } if (jsPropertyValue.getElementType() != JSElementTypes.LITERAL_EXPRESSION) { jsPropertyValue = (TreeElement) jsPropertyValue.findChildByType(JSElementTypes.LITERAL_EXPRESSION); if (null == jsPropertyValue) { return null; } } return dequote(jsPropertyValue.getText()); } protected void setBaseUrl(String baseUrl) { if (baseUrl.startsWith("/")) { baseUrl = baseUrl.substring(1); } if (baseUrl.endsWith("/")) { baseUrl = StringUtil.trimEnd(baseUrl, "/"); } requirejsBaseUrl = baseUrl; } protected void parseRequireJsPaths(TreeElement node) { if (null == node) { return ; } if (node.getElementType() == JSElementTypes.PROPERTY) { RequirePathAlias pathAlias = new RequirePathAlias(); pathAlias.alias = getJSPropertyName(node); pathAlias.path = getJSPropertyLiteralValue(node); requirePaths.addPath(pathAlias); } TreeElement next = node.getTreeNext(); if (null != next) { parseRequireJsPaths(next); } } public VirtualFile resolvePath(String path) { VirtualFile rootDirectory; if (path.startsWith(".")) { rootDirectory = getBaseUrlPath(false); } else if (path.startsWith("/")) { // TODO: Check work on multi modules idea project and empty web path rootDirectory = getWebDir(); } else { rootDirectory = getBaseUrlPath(false); } if (null != rootDirectory) { VirtualFile directoryVF = rootDirectory.findFileByRelativePath(path); if (null != directoryVF) { return directoryVF; } else { VirtualFile fileVF = rootDirectory.findFileByRelativePath(path + ".js"); if (null != fileVF) { return fileVF; } } } return null; } public PsiElement requireResolve(PsiElement element) { Path path = new Path(element, this); return path.resolve(); } public List<String> getCompletion(PsiElement element) { List<String> completions = new ArrayList<String>(); String value = element.getText().replace("'", "").replace("\"", "").replace("IntellijIdeaRulezzz ", ""); String valuePath = value; boolean exclamationMark = value.contains("!"); String plugin = ""; int doubleDotCount = 0; boolean notEndSlash = false; String pathOnDots = ""; String dotString = ""; VirtualFile elementFile = element .getContainingFile() .getOriginalFile() .getVirtualFile(); if (exclamationMark) { String[] exclamationMarkSplit = valuePath.split("!"); plugin = exclamationMarkSplit[0]; if (exclamationMarkSplit.length == 2) { valuePath = exclamationMarkSplit[1]; } else { valuePath = ""; } } if (exclamationMark) { for (String moduleName : getModulesNames()) { completions.add(plugin + '!' + moduleName); } } else { completions.addAll(getModulesNames()); // expand current package } PsiDirectory fileDirectory = element .getContainingFile() .getOriginalFile() .getContainingDirectory(); if (null == fileDirectory) { return completions; } String filePath = fileDirectory .getVirtualFile() .getPath() .replace(getWebDir(elementFile).getPath(), ""); if (filePath.startsWith("/")) { filePath = filePath.substring(1); } boolean startSlash = valuePath.startsWith("/"); if (startSlash) { valuePath = valuePath.substring(1); } boolean oneDot = valuePath.startsWith("./"); if (oneDot) { if (filePath.isEmpty()) { valuePath = valuePath.substring(2); } else { valuePath = valuePath.replaceFirst(".", filePath); } } if (valuePath.startsWith("..")) { doubleDotCount = FileUtils.getDoubleDotCount(valuePath); String[] pathsOfPath = filePath.split("/"); if (pathsOfPath.length > 0) { if (doubleDotCount > 0) { if (doubleDotCount > pathsOfPath.length || filePath.isEmpty()) { return new ArrayList<String>(); } pathOnDots = FileUtils.getNormalizedPath(doubleDotCount, pathsOfPath); dotString = StringUtil.repeat("../", doubleDotCount); if (valuePath.endsWith("..")) { notEndSlash = true; } if (valuePath.endsWith("..") || !StringUtil.isEmpty(pathOnDots)) { dotString = dotString.substring(0, dotString.length() - 1); } valuePath = valuePath.replaceFirst(dotString, pathOnDots); } } } List<String> allFiles = FileUtils.getAllFilesInDirectory( getWebDir(elementFile), getWebDir(elementFile).getPath() + '/', "" ); List<String> aliasFiles = requirePaths.getAllFilesOnPaths(); aliasFiles.addAll(packageConfig.getAllFilesOnPackages()); String requireMapModule = FileUtils.removeExt(element .getContainingFile() .getOriginalFile() .getVirtualFile() .getPath() .replace( getWebDir(elementFile).getPath() + '/', "" ), ".js" ); completions.addAll(requireMap.getCompletionByModule(requireMapModule)); String valuePathForAlias = valuePath; if (!oneDot && 0 == doubleDotCount && !startSlash && !getBaseUrl().isEmpty()) { valuePath = FileUtils.join(getBaseUrl(), valuePath); } for (String file : allFiles) { if (file.startsWith(valuePath)) { // Prepare file path if (oneDot) { if (filePath.isEmpty()) { file = "./" + file; } else { file = file.replaceFirst(filePath, "."); } } if (doubleDotCount > 0) { if (!StringUtil.isEmpty(valuePath)) { file = file.replace(pathOnDots, ""); } if (notEndSlash) { file = '/' + file; } file = dotString + file; } if (!oneDot && 0 == doubleDotCount && !startSlash && !getBaseUrl().isEmpty()) { file = file.substring(getBaseUrl().length() + 1); } if (startSlash) { file = '/' + file; } addToCompletion(completions, file, exclamationMark, plugin); } } for (String file : aliasFiles) { if (file.startsWith(valuePathForAlias)) { addToCompletion(completions, file, exclamationMark, plugin); } } return completions; } private boolean isModuleAccessible(Package pkg, String file, String moduleId) { return file.endsWith(".js") && !file.equals(moduleId) && !file.equals(pkg.name + '/' + pkg.main + ".js"); // return (restrictAccessToPackage && !file.equals(pkg.name + '/' + pkg.main + ".js")) && file.endsWith(".js") && !file.equals(moduleId); } private static void addToCompletion(List<String> completions, String file, boolean exclamationMark, String plugin) { if (exclamationMark) { file = FileUtils.removeExt(file, ".js"); completions.add(plugin + '!' + file); } else if (file.endsWith(".js")) { completions.add(file.replace(".js", "")); } } // ------------------------------------------------------------------------- // RequireConfigVfsListener // ------------------------------------------------------------------------- private class RequireConfigVfsListener extends VirtualFileAdapter { public void contentsChanged(@NotNull VirtualFileEvent event) { VirtualFile confFile = findPathInWebDir(settings.configFilePath); if (confFile == null || !confFile.exists() || !event.getFile().equals(confFile)) { return; } LOG.debug("RequireConfigVfsListener contentsChanged"); // RequirejsProjectComponent.this.project.getComponent(RequirejsProjectComponent.class).parseRequirejsConfig(); RequirejsProjectComponent.this.parseRequirejsConfig(); } } }