Merge pull request #454 from friedbrice/master

Make `system-ghc` and tool paths configurable
This commit is contained in:
Rik 2019-08-04 21:36:21 +02:00 committed by GitHub
commit e4f1abda45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 188 additions and 50 deletions

View File

@ -3,6 +3,7 @@ package intellij.haskell
import java.io.File
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import intellij.haskell.util.HaskellFileUtil
import io.github.soc.directories.ProjectDirectories
@ -38,8 +39,9 @@ object GlobalInfo {
new File(toolsStackRootPath, ToolsBinDirName)
}
def toolPath(toolName: String): File = {
new File(toolsBinPath, toolName)
def toolPath(tool: HTool): File = {
val name = if (SystemInfo.isWindows) tool.name + ".exe" else tool.name
new File(toolsBinPath, name)
}
def getIntelliJProjectDirectory(project: Project): File = {

View File

@ -0,0 +1,24 @@
package intellij.haskell
sealed abstract class HTool extends Product with Serializable {
def name: String
}
object HTool {
case object Hlint extends HTool {
def name: String = "hlint"
}
case object Hindent extends HTool {
def name: String = "hindent"
}
case object Hoogle extends HTool {
def name: String = "hoogle"
}
case object StylishHaskell extends HTool {
def name: String = "stylish-haskell"
}
}

View File

@ -26,8 +26,9 @@ import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import intellij.haskell.external.component.StackProjectManager
import intellij.haskell.external.execution.CommandLine
import intellij.haskell.settings.HaskellSettingsState
import intellij.haskell.util._
import intellij.haskell.{GlobalInfo, HaskellLanguage, HaskellNotificationGroup}
import intellij.haskell.{GlobalInfo, HTool, HaskellLanguage, HaskellNotificationGroup}
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
@ -51,8 +52,7 @@ class HindentReformatAction extends AnAction {
}
object HindentReformatAction {
final val HindentName = "hindent"
private final val HindentPath = GlobalInfo.toolPath(HindentName).toString
private final val HindentPath = HaskellSettingsState.hindentPath.getOrElse(GlobalInfo.toolPath(HTool.Hindent).toString)
def format(psiFile: PsiFile, selectionContext: Option[SelectionContext] = None): Boolean = {
val lineLength = CodeStyle.getSettings(psiFile.getProject).getRightMargin(HaskellLanguage.Instance)
@ -69,11 +69,11 @@ object HindentReformatAction {
}
})
FutureUtil.waitForValue(project, formatAction, s"reformatting by `$HindentName`") match {
FutureUtil.waitForValue(project, formatAction, s"reformatting by `${HTool.Hindent.name}`") match {
case None => false
case Some(r) => r match {
case Left(e) =>
HaskellNotificationGroup.logInfoEvent(project, s"Error while reformatting by `$HindentName`. Error: $e")
HaskellNotificationGroup.logInfoEvent(project, s"Error while reformatting by `${HTool.Hindent.name}`. Error: $e")
false
case Right(sourceCode) =>
selectionContext match {
@ -153,4 +153,3 @@ object HindentReformatAction {
}
}
}

View File

@ -23,8 +23,9 @@ import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import intellij.haskell.external.component.StackProjectManager
import intellij.haskell.external.execution.CommandLine
import intellij.haskell.settings.HaskellSettingsState
import intellij.haskell.util.{FutureUtil, HaskellEditorUtil, HaskellFileUtil, ScalaUtil}
import intellij.haskell.{GlobalInfo, HaskellNotificationGroup}
import intellij.haskell.{GlobalInfo, HTool, HaskellNotificationGroup}
class StylishHaskellReformatAction extends AnAction {
@ -40,8 +41,7 @@ class StylishHaskellReformatAction extends AnAction {
}
object StylishHaskellReformatAction {
final val StylishHaskellName = "stylish-haskell"
private final val StylishHaskellPath = GlobalInfo.toolPath(StylishHaskellName).toString
private final val StylishHaskellPath = HaskellSettingsState.stylishHaskellPath.getOrElse(GlobalInfo.toolPath(HTool.StylishHaskell).toString)
def versionInfo(project: Project): String = {
if (StackProjectManager.isStylishHaskellAvailable(project)) {
@ -61,16 +61,16 @@ object StylishHaskellReformatAction {
CommandLine.run(project, StylishHaskellPath, Seq(path))
})
FutureUtil.waitForValue(project, processOutputFuture, s"reformatting by $StylishHaskellName") match {
FutureUtil.waitForValue(project, processOutputFuture, s"reformatting by ${HTool.StylishHaskell.name}") match {
case None => ()
case Some(processOutput) =>
if (processOutput.getStderrLines.isEmpty) {
HaskellFileUtil.saveFileWithNewContent(psiFile, processOutput.getStdout)
} else {
HaskellNotificationGroup.logInfoEvent(project, s"Error while reformatting by `$StylishHaskellName`. Error: ${processOutput.getStderr}")
HaskellNotificationGroup.logInfoEvent(project, s"Error while reformatting by `${HTool.StylishHaskell.name}`. Error: ${processOutput.getStderr}")
}
}
case None => HaskellNotificationGroup.logWarningBalloonEvent(psiFile.getProject, s"Can not reformat file because could not determine path for file `${psiFile.getName}`. File exists only in memory")
}
}
}
}

View File

@ -21,7 +21,7 @@ import com.intellij.psi.PsiFile
import intellij.haskell.external.execution.CommandLine
import intellij.haskell.settings.HaskellSettingsState
import intellij.haskell.util.HaskellFileUtil
import intellij.haskell.{GlobalInfo, HaskellNotificationGroup}
import intellij.haskell.{GlobalInfo, HTool, HaskellNotificationGroup}
import spray.json.JsonParser.ParsingException
import spray.json.{DefaultJsonProtocol, _}
@ -29,8 +29,7 @@ import scala.concurrent.duration._
object HLintComponent {
final val HLintName = "hlint"
private final val HLintPath = GlobalInfo.toolPath(HLintName).toString
private final val HLintPath = HaskellSettingsState.hlintPath.getOrElse(GlobalInfo.toolPath(HTool.Hlint).toString)
private final val Timeout = 500.millis
def check(psiFile: PsiFile): Seq[HLintInfo] = {
@ -41,7 +40,7 @@ object HLintComponent {
case Some(path) =>
val output = runHLint(project, hlintOptions.toSeq ++ Seq("--json", path), ignoreExitCode = true)
if (output.getExitCode > 0 && output.getStderr.nonEmpty) {
HaskellNotificationGroup.logErrorBalloonEvent(project, s"Error while calling $HLintName: ${output.getStderr}")
HaskellNotificationGroup.logErrorBalloonEvent(project, s"Error while calling ${HTool.Hlint.name}: ${output.getStderr}")
Seq()
} else {
deserializeHLintInfo(project, output.getStdout)
@ -51,7 +50,7 @@ object HLintComponent {
Seq()
}
} else {
HaskellNotificationGroup.logInfoEvent(psiFile.getProject, s"$HLintName is not (yet) available")
HaskellNotificationGroup.logInfoEvent(psiFile.getProject, s"${HTool.Hlint.name} is not (yet) available")
Seq()
}
}

View File

@ -24,8 +24,9 @@ import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import intellij.haskell.external.execution.{CommandLine, StackCommandLine}
import intellij.haskell.psi.{HaskellPsiUtil, HaskellQualifiedNameElement}
import intellij.haskell.settings.HaskellSettingsState
import intellij.haskell.util.{HtmlElement, ScalaFutureUtil}
import intellij.haskell.{GlobalInfo, HaskellNotificationGroup}
import intellij.haskell.{GlobalInfo, HTool, HaskellNotificationGroup}
import scala.collection.mutable
import scala.concurrent.{Future, blocking}
@ -33,8 +34,7 @@ import scala.jdk.CollectionConverters._
object HoogleComponent {
final val HoogleName = "hoogle"
private final val HooglePath = GlobalInfo.toolPath(HoogleName).toString
private final val HooglePath = HaskellSettingsState.hooglePath.getOrElse(GlobalInfo.toolPath(HTool.Hoogle).toString)
private final val HoogleDbName = "hoogle"
def runHoogle(project: Project, pattern: String, count: Int = 100): Option[Seq[String]] = {
@ -113,7 +113,7 @@ object HoogleComponent {
private def isHoogleFeatureAvailable(project: Project): Boolean = {
if (!StackProjectManager.isHoogleAvailable(project)) {
HaskellNotificationGroup.logInfoEvent(project, s"$HoogleName isn't (yet) available")
HaskellNotificationGroup.logInfoEvent(project, s"${HTool.Hoogle.name} isn't (yet) available")
false
} else {
doesHoogleDatabaseExist(project)

View File

@ -36,9 +36,10 @@ import intellij.haskell.external.repl.StackReplsManager
import intellij.haskell.module.{HaskellModuleBuilder, StackProjectImportBuilder}
import intellij.haskell.psi.HaskellPsiUtil
import intellij.haskell.sdk.HaskellSdkType
import intellij.haskell.settings.HaskellSettingsState
import intellij.haskell.util._
import intellij.haskell.util.index.{HaskellFileIndex, HaskellModuleNameIndex}
import intellij.haskell.{GlobalInfo, HaskellNotificationGroup}
import intellij.haskell.{GlobalInfo, HTool, HaskellNotificationGroup}
object StackProjectManager {
@ -109,13 +110,14 @@ object StackProjectManager {
ProgressManager.getInstance().run(new Task.Backgroundable(project, title, false, PerformInBackgroundOption.ALWAYS_BACKGROUND) {
private def isToolAvailable(progressIndicator: ProgressIndicator, toolName: String) = {
val toolNameExe = if (SystemInfo.isWindows) toolName + ".exe" else toolName
if (!GlobalInfo.toolPath(toolNameExe).exists() || update) {
progressIndicator.setText(s"Busy with installing $toolName in ${GlobalInfo.toolsBinPath}")
StackCommandLine.installTool(project, toolName)
} else {
true
private def isToolAvailable(progressIndicator: ProgressIndicator, tool: HTool) = {
HaskellSettingsState.useCustomTool(tool) || {
if (!GlobalInfo.toolPath(tool).exists() || update) {
progressIndicator.setText(s"Busy with installing ${tool.name} in ${GlobalInfo.toolsBinPath}")
StackCommandLine.installTool(project, tool.name)
} else {
true
}
}
}
@ -126,13 +128,13 @@ object StackProjectManager {
StackCommandLine.updateStackIndex(project)
}
getStackProjectManager(project).foreach(_.hlintAvailable = isToolAvailable(progressIndicator, HLintComponent.HLintName))
getStackProjectManager(project).foreach(_.hlintAvailable = isToolAvailable(progressIndicator, HTool.Hlint))
getStackProjectManager(project).foreach(_.hoogleAvailable = isToolAvailable(progressIndicator, HoogleComponent.HoogleName))
getStackProjectManager(project).foreach(_.hoogleAvailable = isToolAvailable(progressIndicator, HTool.Hoogle))
getStackProjectManager(project).foreach(_.stylishHaskellAvailable = isToolAvailable(progressIndicator, StylishHaskellReformatAction.StylishHaskellName))
getStackProjectManager(project).foreach(_.stylishHaskellAvailable = isToolAvailable(progressIndicator, HTool.StylishHaskell))
getStackProjectManager(project).foreach(_.hindentAvailable = isToolAvailable(progressIndicator, HindentReformatAction.HindentName))
getStackProjectManager(project).foreach(_.hindentAvailable = isToolAvailable(progressIndicator, HTool.Hindent))
} finally {
getStackProjectManager(project).foreach(_.installingHaskellTools = false)
}

View File

@ -29,6 +29,7 @@ import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.{CharsetToolkit, VfsUtil}
import intellij.haskell.HaskellNotificationGroup
import intellij.haskell.sdk.HaskellSdkType
import intellij.haskell.settings.HaskellSettingsState
import intellij.haskell.stackyaml.StackYamlComponent
import intellij.haskell.util.{HaskellFileUtil, HaskellProjectUtil}
@ -60,7 +61,7 @@ object StackCommandLine {
def installTool(project: Project, toolName: String): Boolean = {
import intellij.haskell.GlobalInfo._
val systemGhcOption = if (StackYamlComponent.isNixEnabled(project)) {
val systemGhcOption = if (StackYamlComponent.isNixEnabled(project) || !HaskellSettingsState.useSystemGhc) {
Seq()
} else {
Seq("--system-ghc")

View File

@ -21,14 +21,20 @@ import java.awt.{GridBagConstraints, GridBagLayout, Insets}
import com.intellij.openapi.options.{Configurable, ConfigurationException}
import com.intellij.ui.DocumentAdapter
import javax.swing._
import javax.swing.event.DocumentEvent
import javax.swing.event.{ChangeListener, DocumentEvent}
class HaskellConfigurable extends Configurable {
private var isModifiedByUser = false
private val hlintOptionsField = new JTextField
private val useSystemGhcToggle = new JCheckBox
private val replTimeoutField = new JTextField
private val replTimeoutLabel = new JLabel("Changed timeout will take effect after restarting project")
private val afterRestartLabel = new JLabel("Changes will take effect after restarting project")
private val newProjectTemplateNameField = new JTextField
private val hindentPathField = new JTextField
private val hlintPathField = new JTextField
private val hooglePathField = new JTextField
private val stylishHaskellPathField = new JTextField
private val useCustomToolsToggle = new JCheckBox
override def getDisplayName: String = {
"Haskell"
@ -40,16 +46,37 @@ class HaskellConfigurable extends Configurable {
override def createComponent: JComponent = {
def toggleToolPathsVisibility(): Unit = {
val visible = useCustomToolsToggle.isSelected
hindentPathField.setVisible(visible)
hlintPathField.setVisible(visible)
hooglePathField.setVisible(visible)
stylishHaskellPathField.setVisible(visible)
}
toggleToolPathsVisibility()
val settingsPanel = new JPanel(new GridBagLayout())
settingsPanel.getInsets()
val listener: DocumentAdapter = (_: DocumentEvent) => {
val docListener: DocumentAdapter = (_: DocumentEvent) => {
isModifiedByUser = true
}
hlintOptionsField.getDocument.addDocumentListener(listener)
replTimeoutField.getDocument.addDocumentListener(listener)
newProjectTemplateNameField.getDocument.addDocumentListener(listener)
hlintOptionsField.getDocument.addDocumentListener(docListener)
replTimeoutField.getDocument.addDocumentListener(docListener)
newProjectTemplateNameField.getDocument.addDocumentListener(docListener)
hindentPathField.getDocument.addDocumentListener(docListener)
hlintPathField.getDocument.addDocumentListener(docListener)
hooglePathField.getDocument.addDocumentListener(docListener)
stylishHaskellPathField.getDocument.addDocumentListener(docListener)
useSystemGhcToggle.addChangeListener { _ =>
isModifiedByUser = true
}
useCustomToolsToggle.addChangeListener { _ =>
isModifiedByUser = true
toggleToolPathsVisibility()
}
class SettingsGridBagConstraints extends GridBagConstraints {
@ -67,8 +94,8 @@ class HaskellConfigurable extends Configurable {
val baseGridBagConstraints = new SettingsGridBagConstraints
def addLabeledControl(row: Int, label: String, component: JComponent): Unit = {
settingsPanel.add(new JLabel(label), baseGridBagConstraints.setConstraints(
def addLabeledControl(row: Int, label: JLabel, component: JComponent): Unit = {
settingsPanel.add(label, baseGridBagConstraints.setConstraints(
anchor = GridBagConstraints.LINE_START,
gridx = 0,
gridy = row
@ -88,14 +115,33 @@ class HaskellConfigurable extends Configurable {
))
}
addLabeledControl(1, HlintOptions, hlintOptionsField)
addLabeledControl(2, ReplTimeout, replTimeoutField)
addLabeledControl(3, "", replTimeoutLabel)
addLabeledControl(4, NewProjectTemplateName, newProjectTemplateNameField)
val labeledControls = List(
(new JLabel(HlintOptions), hlintOptionsField),
(new JLabel(ReplTimeout), replTimeoutField),
(new JLabel(""), afterRestartLabel),
(new JLabel(NewProjectTemplateName), newProjectTemplateNameField),
(new JLabel(BuildToolsUsingSystemGhc), useSystemGhcToggle),
(new JLabel(UseCustomTool), useCustomToolsToggle),
(new JLabel(""), afterRestartLabel),
(new JLabel(""), {
val x = new JTextArea(PathWarning)
x.setLineWrap(true)
x.setWrapStyleWord(true)
x
}),
(new JLabel(HindentPath), hindentPathField),
(new JLabel(HlintPath), hlintPathField),
(new JLabel(HooglePath), hooglePathField),
(new JLabel(StylishHaskellPath), stylishHaskellPathField)
)
labeledControls.zipWithIndex.foreach {
case ((label, control), row) => addLabeledControl(row, label, control)
}
settingsPanel.add(new JPanel(), baseGridBagConstraints.setConstraints(
gridx = 0,
gridy = 7,
gridy = labeledControls.length,
weighty = 10.0
))
settingsPanel
@ -107,7 +153,13 @@ class HaskellConfigurable extends Configurable {
val state = HaskellSettingsPersistentStateComponent.getInstance().getState
state.replTimeout = validREPLTimeout
state.hlintOptions = hlintOptionsField.getText
state.useSystemGhc = useSystemGhcToggle.isSelected
state.newProjectTemplateName = newProjectTemplateNameField.getText
state.hindentPath = hindentPathField.getText
state.hlintPath = hlintPathField.getText
state.hooglePath = hooglePathField.getText
state.stylishHaskellPath = stylishHaskellPathField.getText
state.customTools = useCustomToolsToggle.isSelected
}
private def validateREPLTimeout(): Integer = {
@ -130,8 +182,14 @@ class HaskellConfigurable extends Configurable {
override def reset(): Unit = {
val state = HaskellSettingsPersistentStateComponent.getInstance().getState
hlintOptionsField.setText(state.hlintOptions)
useSystemGhcToggle.setSelected(state.useSystemGhc)
replTimeoutField.setText(state.replTimeout.toString)
newProjectTemplateNameField.setText(state.newProjectTemplateName)
hindentPathField.setText(state.hindentPath)
hlintPathField.setText(state.hlintPath)
hooglePathField.setText(state.hooglePath)
stylishHaskellPathField.setText(state.stylishHaskellPath)
useCustomToolsToggle.setSelected(state.customTools)
}
}
@ -139,4 +197,18 @@ object HaskellConfigurable {
final val ReplTimeout = "Background REPL timeout in seconds"
final val HlintOptions = "Hlint options"
final val NewProjectTemplateName = "Template name for new project"
}
final val BuildToolsUsingSystemGhc = "Build tools using system GHC"
final val HindentPath = "Hindent path"
final val HlintPath = "Hlint path"
final val HooglePath = "Hoogle path"
final val StylishHaskellPath = "Stylish Haskell path"
final val UseCustomTool = "Use custom Haskell tools"
final val PathWarning =
"""WARNING! Specifying a path for a Haskell tool will override the default
|behavior of building that tool from the Stackage LTS. This plugin was
|tested only with the Haskell tools from the Stackage LTS. Providing a
|path with your own Haskell tool (and thus overriding automatic rebuild
|or download of that tool) could cause some features of this plugin to
|break, because the API that your tool provide may differ from what the
|plugin expects.""".stripMargin.replace('\n', ' ')
}

View File

@ -50,8 +50,14 @@ public class HaskellSettingsPersistentStateComponent implements PersistentStateC
static class HaskellSettingsState {
public Integer replTimeout = 30;
public String hlintOptions = "";
public Boolean useSystemGhc = true;
public Boolean reformatCodeBeforeCommit = false;
public Boolean optimizeImportsBeforeCommit = false;
public String newProjectTemplateName = "new-template";
public String hindentPath = "";
public String hlintPath = "";
public String hooglePath = "";
public String stylishHaskellPath = "";
public Boolean customTools = false;
}
}

View File

@ -16,6 +16,8 @@
package intellij.haskell.settings
import intellij.haskell.HTool
object HaskellSettingsState {
private def state = HaskellSettingsPersistentStateComponent.getInstance().getState
@ -27,6 +29,10 @@ object HaskellSettingsState {
state.hlintOptions
}
def useSystemGhc: Boolean = {
state.useSystemGhc
}
def getNewProjectTemplateName: String = {
state.newProjectTemplateName
}
@ -46,4 +52,31 @@ object HaskellSettingsState {
def setOptimizeImportsBeforeCommit(optimize: Boolean): Unit = {
state.optimizeImportsBeforeCommit = optimize
}
def customTools: Boolean = {
state.customTools
}
def hindentPath: Option[String]= {
Option.when(customTools && state.hindentPath.nonEmpty)(state.hindentPath)
}
def hlintPath: Option[String]= {
Option.when(customTools && state.hlintPath.nonEmpty)(state.hlintPath)
}
def hooglePath: Option[String]= {
Option.when(customTools && state.hooglePath.nonEmpty)(state.hooglePath)
}
def stylishHaskellPath: Option[String]= {
Option.when(customTools && state.stylishHaskellPath.nonEmpty)(state.stylishHaskellPath)
}
def useCustomTool(tool: HTool): Boolean = tool match {
case HTool.Hindent => hindentPath.isDefined
case HTool.Hlint => hlintPath.isDefined
case HTool.Hoogle => hooglePath.isDefined
case HTool.StylishHaskell => stylishHaskellPath.isDefined
}
}