/* * The MIT License (MIT) * * Copyright (c) 2017 hsz Jakub Chrzanowski <jakub@hsz.mobi> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package mobi.hsz.idea.gitignore.util; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.IdeaPluginDescriptorImpl; import com.intellij.ide.plugins.PluginManager; import com.intellij.ide.projectView.PresentationData; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.EditorSettings; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModifiableRootModel; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.ui.SimpleTextAttributes; import com.intellij.util.containers.ContainerUtil; import mobi.hsz.idea.gitignore.IgnoreBundle; import mobi.hsz.idea.gitignore.command.CreateFileCommandAction; import mobi.hsz.idea.gitignore.file.type.IgnoreFileType; import mobi.hsz.idea.gitignore.lang.IgnoreLanguage; import mobi.hsz.idea.gitignore.psi.IgnoreFile; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import static com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES; /** * {@link Utils} class that contains various methods. * * @author Jakub Chrzanowski <jakub@hsz.mobi> * @since 0.3.3 */ public class Utils { /** Private constructor to prevent creating {@link Utils} instance. */ private Utils() { } /** * Gets relative path of given @{link VirtualFile} and root directory. * * @param directory root directory * @param file file to get it's path * @return relative path */ @Nullable public static String getRelativePath(@NotNull VirtualFile directory, @NotNull VirtualFile file) { return VfsUtilCore.getRelativePath(file, directory, '/') + (file.isDirectory() ? '/' : ""); } /** * Gets Ignore file for given {@link Project} root directory. * * @param project current project * @param fileType current ignore file type * @return Ignore file */ @Nullable public static PsiFile getIgnoreFile(@NotNull Project project, @NotNull IgnoreFileType fileType) { return getIgnoreFile(project, fileType, null, false); } /** * Gets Ignore file for given {@link Project} and root {@link PsiDirectory}. * * @param project current project * @param fileType current ignore file type * @param directory root directory * @return Ignore file */ @Nullable public static PsiFile getIgnoreFile(@NotNull Project project, @NotNull IgnoreFileType fileType, @Nullable PsiDirectory directory) { return getIgnoreFile(project, fileType, directory, false); } /** * Gets Ignore file for given {@link Project} and root {@link PsiDirectory}. * If file is missing - creates new one. * * @param project current project * @param fileType current ignore file type * @param directory root directory * @param createIfMissing create new file if missing * @return Ignore file */ @Nullable public static PsiFile getIgnoreFile(@NotNull Project project, @NotNull IgnoreFileType fileType, @Nullable PsiDirectory directory, boolean createIfMissing) { if (directory == null) { directory = PsiManager.getInstance(project).findDirectory(project.getBaseDir()); } assert directory != null; String filename = fileType.getIgnoreLanguage().getFilename(); PsiFile file = directory.findFile(filename); VirtualFile virtualFile = file == null ? directory.getVirtualFile().findChild(filename) : file.getVirtualFile(); if (file == null && virtualFile == null && createIfMissing) { file = new CreateFileCommandAction(project, directory, fileType).execute().getResultObject(); } return file; } /** * Finds {@link PsiFile} for the given {@link VirtualFile} instance. If file is outside current project, * it's required to create new {@link PsiFile} manually. * * @param project current project * @param virtualFile to handle * @return {@link PsiFile} instance */ @Nullable public static PsiFile getPsiFile(@NotNull Project project, @NotNull VirtualFile virtualFile) { PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile); if (psiFile == null) { FileViewProvider viewProvider = PsiManager.getInstance(project).findViewProvider(virtualFile); if (viewProvider != null) { IgnoreLanguage language = IgnoreBundle.obtainLanguage(virtualFile); if (language != null) { psiFile = language.createFile(viewProvider); } } } return psiFile; } /** * Opens given file in editor. * * @param project current project * @param file file to open */ public static void openFile(@NotNull Project project, @NotNull PsiFile file) { openFile(project, file.getVirtualFile()); } /** * Opens given file in editor. * * @param project current project * @param file file to open */ public static void openFile(@NotNull Project project, @NotNull VirtualFile file) { FileEditorManager.getInstance(project).openFile(file, true); } /** * Returns all Ignore files in given {@link Project} that can match current passed file. * * @param project current project * @param file current file * @return collection of suitable Ignore files * * @throws ExternalFileException */ public static List<VirtualFile> getSuitableIgnoreFiles(@NotNull Project project, @NotNull IgnoreFileType fileType, @NotNull VirtualFile file) throws ExternalFileException { List<VirtualFile> files = ContainerUtil.newArrayList(); if (file.getCanonicalPath() == null || project.getBaseDir() == null || !VfsUtilCore.isAncestor(project.getBaseDir(), file, true)) { throw new ExternalFileException(); } VirtualFile baseDir = project.getBaseDir(); if (baseDir != null && !baseDir.equals(file)) { do { file = file.getParent(); VirtualFile ignoreFile = file.findChild(fileType.getIgnoreLanguage().getFilename()); ContainerUtil.addIfNotNull(ignoreFile, files); } while (!file.equals(project.getBaseDir())); } return files; } /** * Checks if given directory is a {@link IgnoreLanguage#getVcsDirectory()}. * * @param directory to check * @return given file is VCS directory */ public static boolean isVcsDirectory(@NotNull VirtualFile directory) { if (!directory.isDirectory()) { return false; } for (IgnoreLanguage language : IgnoreBundle.VCS_LANGUAGES) { String vcsName = language.getVcsDirectory(); if (language.isEnabled() && directory.getName().equals(vcsName)) { return true; } } return false; } /** * Searches for excluded roots in given {@link Project}. * * @param project current project * @return list of excluded roots */ public static List<VirtualFile> getExcludedRoots(@NotNull Project project) { List<VirtualFile> roots = ContainerUtil.newArrayList(); ModuleManager manager = ModuleManager.getInstance(project); for (Module module : manager.getModules()) { ModifiableRootModel model = ModuleRootManager.getInstance(module).getModifiableModel(); Collections.addAll(roots, model.getExcludeRoots()); model.dispose(); } return roots; } /** * Gets list of words for given {@link String} excluding special characters. * * @param filter input string * @return list of words without special characters */ public static List<String> getWords(@NotNull String filter) { List<String> words = ContainerUtil.newArrayList(filter.toLowerCase().split("\\W+")); words.removeAll(Arrays.asList(null, "")); return words; } /** * Returns Gitignore plugin information. * * @return {@link IdeaPluginDescriptor} */ public static IdeaPluginDescriptor getPlugin() { return PluginManager.getPlugin(PluginId.getId(IgnoreBundle.PLUGIN_ID)); } /** * Returns plugin major version. * * @return major version */ public static String getMajorVersion() { return getVersion().split("\\.")[0]; } /** * Returns plugin minor version. * * @return minor version */ public static String getMinorVersion() { return StringUtil.join(getVersion().split("\\."), 0, 2, "."); } /** * Returns plugin version. * * @return version */ public static String getVersion() { return getPlugin().getVersion(); } /** * Checks if lists are equal. * * @param l1 first list * @param l2 second list * @return lists are equal */ public static boolean equalLists(@NotNull List<?> l1, @NotNull List<?> l2) { return l1.size() == l2.size() && l1.containsAll(l2) && l2.containsAll(l1); } /** * Returns {@link IgnoreFileType} basing on the {@link VirtualFile} file. * * @param virtualFile current file * @return file type */ public static IgnoreFileType getFileType(@Nullable VirtualFile virtualFile) { if (virtualFile != null) { FileType fileType = virtualFile.getFileType(); if (fileType instanceof IgnoreFileType) { return (IgnoreFileType) fileType; } } return null; } /** * Checks if file is under given directory. * * @param file file * @param directory directory * @return file is under directory */ public static boolean isUnder(@NotNull VirtualFile file, @NotNull VirtualFile directory) { VirtualFile parent = file.getParent(); while (parent != null) { if (directory.equals(parent)) { return true; } parent = parent.getParent(); } return false; } /** * Checks if file is in project directory. * * @param file file * @param project project * @return file is under directory */ public static boolean isInProject(@NotNull final VirtualFile file, @NotNull final Project project) { return project.getBaseDir() != null && (isUnder(file, project.getBaseDir()) || StringUtil.startsWith(file.getUrl(), "temp://")); } /** * Creates and configures template preview editor. * * @param document virtual editor document * @param project current project * @return editor */ @NotNull public static Editor createPreviewEditor(@NotNull Document document, @Nullable Project project, boolean isViewer) { EditorEx editor = (EditorEx) EditorFactory.getInstance().createEditor(document, project, IgnoreFileType.INSTANCE, isViewer); editor.setCaretEnabled(!isViewer); final EditorSettings settings = editor.getSettings(); settings.setLineNumbersShown(false); settings.setAdditionalColumnsCount(1); settings.setAdditionalLinesCount(0); settings.setRightMarginShown(false); settings.setFoldingOutlineShown(false); settings.setLineMarkerAreaShown(false); settings.setIndentGuidesShown(false); settings.setVirtualSpace(false); settings.setWheelFontChangeEnabled(false); EditorColorsScheme colorsScheme = editor.getColorsScheme(); colorsScheme.setColor(EditorColors.CARET_ROW_COLOR, null); return editor; } /** * Checks if specified plugin is enabled. * * @param id plugin id * @return plugin is enabled */ private static boolean isPluginEnabled(@NotNull final String id) { IdeaPluginDescriptor p = PluginManager.getPlugin(PluginId.getId(id)); return p instanceof IdeaPluginDescriptorImpl && p.isEnabled(); } /** * Checks if Git plugin is enabled. * * @return Git plugin is enabled */ public static boolean isGitPluginEnabled() { return isPluginEnabled("Git4Idea"); } /** * Resolves user directory with the <code>user.home</code> property. * * @param path path with leading ~ * @return resolved path */ public static String resolveUserDir(@Nullable String path) { if (StringUtil.startsWithChar(path, '~')) { assert path != null; path = System.getProperty("user.home") + path.substring(1); } return path; } /** * Sorts {@link IgnoreFile} ascending using files path. * Uses passed argument by reference and additionally returns the same object. * * @param files {@link IgnoreFile} list * @return sorted list */ public static List<IgnoreFile> ignoreFilesSort(final List<IgnoreFile> files) { ContainerUtil.sort(files, new Comparator<IgnoreFile>() { @Override public int compare(IgnoreFile file1, IgnoreFile file2) { return StringUtil.naturalCompare(file1.getVirtualFile().getPath(), file2.getVirtualFile().getPath()); } }); return files; } /** * Escapes character in the given {@link String}. * Method is copied from the {@link StringUtil} class to keep the backward compatibility with IDEA 12.x * * @param string to parse * @param character to escape * @return escaped string */ @NotNull @Contract(pure = true) public static String escapeChar(@NotNull final String string, final char character) { final StringBuilder buf = new StringBuilder(string); int idx = 0; while ((idx = StringUtil.indexOf(buf, character, idx)) >= 0) { buf.insert(idx, "\\"); idx += 2; } return buf.toString(); } /** * Trims leading character in the given {@link String}. * Method is copied from the {@link StringUtil} class to keep the backward compatibility with IDEA 12.x * * @param string to parse * @param character to trim * @return trimmed string */ @NotNull @Contract(pure = true) public static String trimLeading(@NotNull String string, final char character) { int index = 0; while (index < string.length() && string.charAt(index) == character) { index++; } return string.substring(index); } /** * Intersection method cloned from {@link ContainerUtil#intersection(Collection, Collection)} because of * NoSuchMethodError exception errors related to the some API changes. * * @param collection1 left * @param collection2 right * @return read-only collection consisting of elements from both collections */ @NotNull @Contract(pure = true) public static <T> List<T> intersection(@NotNull Collection<? extends T> collection1, @NotNull Collection<? extends T> collection2) { List<T> result = new ArrayList<T>(); for (T t : collection1) { if (collection2.contains(t)) { result.add(t); } } return result.isEmpty() ? ContainerUtil.<T>emptyList() : result; } /** * Method cloned from {@link ContainerUtil#notNullize(List)} because of NoSuchMethodError exception * errors related to the some API changes. * * @param list method to check * @param <T> container type * @return not null container */ @NotNull public static <T> List<T> notNullize(@Nullable List<T> list) { return list == null ? ContainerUtil.<T>newArrayList() : list; } /** * Method cloned from {@link ContainerUtil#getFirstItem(List)} because of NoSuchMethodError exception * errors related to the some API changes. * * @param items method to check * @param <T> container type * @return not null container */ public static <T> T getFirstItem(@Nullable List<T> items) { return items == null || items.isEmpty() ? null : items.get(0); } /** * Adds ColoredFragment to the node's presentation. * * @param data node's presentation data * @param text text to add * @param attributes custom {@link SimpleTextAttributes} */ public static void addColoredText(@NotNull PresentationData data, @NotNull String text, @NotNull SimpleTextAttributes attributes) { if (data.getColoredText().isEmpty()) { data.addText(data.getPresentableText(), REGULAR_ATTRIBUTES); } data.addText(" " + text, attributes); } }