package com.haskforce.highlighting.annotation.external; import com.haskforce.settings.HaskellBuildSettings; import com.haskforce.utils.ExecUtil; import com.haskforce.utils.NotificationUtil; import com.haskforce.utils.EitherUtil; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.configurations.ParametersList; import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.VisualPosition; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import scala.util.Either; import java.io.File; import java.util.*; import java.util.HashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class should contain the common code between GhcMod and GhcModi. Right now this class * only contains static methods, as there is no 'state' in common between GhcMod and GhcModi. */ public class GhcModUtil { private static Logger LOG = Logger.getInstance(GhcModUtil.class); /** * Returns a String to display to the user as the type info. * If we are unable to parse the type info from ghc-mod, a * java.util.InputMismatchException might be thrown from the Scanner. */ @SuppressWarnings("ObjectAllocationInLoop") // Should only be 3-5 loops. public static String unsafeHandleTypeInfo(VisualPosition selectionStartPosition, VisualPosition selectionStopPosition, @NotNull String stdout) { Scanner typeInfosScanner = new Scanner(stdout); String lineSeparator = System.getProperty("line.separator"); typeInfosScanner.useDelimiter(lineSeparator); while (typeInfosScanner.hasNext()){ Scanner typeInfoScanner = new Scanner(typeInfosScanner.next()); typeInfoScanner.useDelimiter("\""); String rowAndColInfo = typeInfoScanner.next(); Scanner rowAndColScanner = new Scanner(rowAndColInfo); int startRow = rowAndColScanner.nextInt(); int startCol = rowAndColScanner.nextInt(); int endRow = rowAndColScanner.nextInt(); int endCol = rowAndColScanner.nextInt(); String typeOnRowAndCol = typeInfoScanner.next(); if (! (new VisualPosition(startRow, startCol).after(selectionStartPosition)) && ! selectionStopPosition.after(new VisualPosition(endRow, endCol))){ typeInfosScanner.close(); typeInfoScanner.close(); rowAndColScanner.close(); return typeOnRowAndCol; } typeInfoScanner.close(); rowAndColScanner.close(); } typeInfosScanner.close(); return "No enclosing type found"; } public static String handleTypeInfo(VisualPosition selectionStartPosition, VisualPosition selectionStopPosition, @NotNull String stdout) throws TypeInfoParseException { try { return unsafeHandleTypeInfo(selectionStartPosition, selectionStopPosition, stdout); } catch (InputMismatchException e) { throw new TypeInfoParseException(stdout, e); } } public static class TypeInfoParseException extends Exception { public final String stdout; public TypeInfoParseException(String stdout, Throwable cause) { super("Could not parse type info output", cause); this.stdout = stdout; } } /** * Updates the environment with path hacks so that ghc-mod(i) can find ghc, cabal, and stack. */ public static void updateEnvironment(@NotNull Project project, @NotNull Map<String, String> env) { HaskellBuildSettings settings = HaskellBuildSettings.getInstance(project); updateEnvironment(env, settings.getGhcPath(), settings.getCabalPath(), settings.getStackPath()); } public static void updateEnvironment(@NotNull Map<String, String> env, String... paths) { Set<String> newPaths = new OrderedSet<String>(); for (String path : paths) { //noinspection ObjectAllocationInLoop File exe = new File(path); if (exe.canExecute()) newPaths.add(exe.getParent()); } String pathValue = env.get("PATH"); if (pathValue != null && !pathValue.isEmpty()) newPaths.add(pathValue); newPaths.add(System.getenv("PATH")); env.put("PATH", StringUtil.join(newPaths, SystemInfo.isWindows ? ";" : ":")); } /** * If stack is enabled for the project, returns the path to stack. The .getFlags * method will then take care of pointing to the correct ghc-mod executable. * Otherwise, the ghc-mod path is returned directly. If ghc-mod is not configured, * the method returns null. */ public static String changedPathIfStack(@NotNull Project project, @Nullable String ghcModPath) { if (ghcModPath == null) return null; String stackPath = GhcModUtil.maybeStackPath(project); if (stackPath == null) return ghcModPath; return stackPath; } /** * If stack is enabled for the project, returns the arguments needed to point stack * to ghc-mod, including any flags provided. Otherwise, returns the flags configured * for ghc-mod. */ @NotNull public static String changedFlagsIfStack(@NotNull Project project, @Nullable String ghcModPath, @NotNull String ghcModFlags) { if (GhcModUtil.maybeStackPath(project) == null) { return ghcModFlags; } return "exec -- " + ghcModPath + " " + ghcModFlags; } @NotNull public static GhcVersionValidation validateGhcVersion(@Nullable GhcVersionValidation v, @NotNull Project project, @NotNull String ghcModPath, @NotNull String ghcModFlags) { if (v == null || v == GhcVersionValidation.PENDING_VALIDATION) { return validateGhcVersion(project, ghcModPath, ghcModFlags) ? GhcVersionValidation.VALID : GhcVersionValidation.INVALID; } return v; } private static boolean validateGhcVersion(@NotNull Project project, @NotNull String ghcModPath, @NotNull String ghcModFlags) { String workDir = project.getBasePath(); if (workDir == null) { LOG.warn( "Project base path is null, unable to use it as the work directory for" + "ghc-mod version validation" ); } GeneralCommandLine ghcModVersionCmdLine = new GeneralCommandLine(ghcModPath); ghcModVersionCmdLine.withWorkDirectory(workDir); ParametersList ghcModVersionParamsList = ghcModVersionCmdLine.getParametersList(); ghcModVersionParamsList.addParametersString(ghcModFlags); ghcModVersionParamsList.add("--version"); Either<ExecUtil.ExecError, String> ghcModVersionResult = ExecUtil.readCommandLine(ghcModVersionCmdLine); if (ghcModVersionResult.isLeft()) { //noinspection ThrowableResultOfMethodCallIgnored ExecUtil.ExecError e = EitherUtil.unsafeGetLeft(ghcModVersionResult); NotificationUtil.displayToolsNotification(NotificationType.ERROR, project, "ghc-mod", e.getMessage() ); return false; } String ghcModVersionInfo = EitherUtil.unsafeGetRight(ghcModVersionResult).trim(); Matcher m = GHC_VERSION_REGEX.matcher(ghcModVersionInfo); if (!m.find()) { NotificationUtil.displayToolsNotification(NotificationType.ERROR, project, "ghc-mod", "Could not find GHC version in ghc-mod version info: '" + ghcModVersionInfo + "'" ); return false; } String ghcModGhcVersion = m.group(1); String stackPath = maybeStackPath(project); GeneralCommandLine ghcVersionCmdLine = new GeneralCommandLine(); ghcVersionCmdLine.withWorkDirectory(workDir); HaskellBuildSettings settings = HaskellBuildSettings.getInstance(project); if (stackPath != null) { ghcVersionCmdLine.setExePath(stackPath); ghcVersionCmdLine.addParameters( "--stack-yaml", settings.getStackFile(), "ghc", "--", "--numeric-version" ); } else { String ghcPath = settings.getGhcPath(); ghcVersionCmdLine.setExePath(ghcPath); ghcVersionCmdLine.addParameter("--numeric-version"); } Either<ExecUtil.ExecError, String> ghcVersionResult = ExecUtil.readCommandLine(ghcVersionCmdLine); if (ghcVersionResult.isLeft()) { //noinspection ThrowableResultOfMethodCallIgnored ExecUtil.ExecError e = EitherUtil.unsafeGetLeft(ghcVersionResult); NotificationUtil.displayToolsNotification(NotificationType.ERROR, project, "ghc", e.getMessage() ); return false; } String ghcVersion = EitherUtil.unsafeGetRight(ghcVersionResult).trim(); if (!ghcVersion.trim().equals(ghcModGhcVersion)) { NotificationUtil.displayToolsNotification(NotificationType.ERROR, project, "ghc-mod", "Attempting to use a ghc-mod compiled with a different version of ghc:\n" + "GHC version: '" + ghcVersion + "'\n" + "ghc-mod compiled with ghc version: '" + ghcModGhcVersion + "'\n" + "Please reconfigure ghc-mod to use a version compiled with GHC " + ghcVersion ); return false; } return true; } /** * If stack is enabled for the project, return the stack path. If this method * returns null, stack is not enabled for the project. */ @Nullable private static String maybeStackPath(@NotNull Project project) { HaskellBuildSettings s = HaskellBuildSettings.getInstance(project); if (!s.isStackEnabled()) return null; return s.getStackPath(); } /** * Used for parsing the GHC version from `ghc-mod --version`, for example - * $ ghc-mod --version * ghc-mod version 5.4.0.0 compiled by GHC 7.10.2 */ private static Pattern GHC_VERSION_REGEX = Pattern.compile("GHC (\\d+(\\.\\d+)+)"); /** * Values for keeping track of ghc-mod processes' validation of their ghc version. */ public enum GhcVersionValidation { PENDING_VALIDATION("PENDING_VALIDATION"), VALID("VALID"), INVALID("INVALID"); private final String name; GhcVersionValidation(String name) { this.name = name; } @Override public String toString() { return getClass().getSimpleName() + '.' + name; } } }