Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

Commit

Permalink
Implemented Haskell Problems View.
Browse files Browse the repository at this point in the history
  • Loading branch information
rikvdkleij committed Jul 24, 2018
1 parent 417b6a8 commit 7545bc3
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 43 deletions.
1 change: 1 addition & 0 deletions intellij-haskell/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@
<checkinHandlerFactory implementation="intellij.haskell.action.HaskellReformatCheckinHandlerFactory"/>
<checkinHandlerFactory implementation="intellij.haskell.action.HaskellOptimizeImportsCheckinHandlerFactory"/>

<projectService serviceInterface="com.intellij.compiler.ProblemsView" serviceImplementation="intellij.haskell.editor.HaskellProblemsView" overrides="true"/>
</extensions>

<application-components>
Expand Down
54 changes: 42 additions & 12 deletions src/main/scala/intellij/haskell/annotator/HaskellAnnotator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ package intellij.haskell.annotator
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.daemon.impl._
import com.intellij.codeInsight.intention.impl.BaseIntentionAction
import com.intellij.compiler.{CompilerMessageImpl, ProblemsView}
import com.intellij.lang.annotation.{AnnotationHolder, ExternalAnnotator, HighlightSeverity}
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.compiler.CompilerMessageCategory
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.impl.source.tree.TreeUtil
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.{PsiElement, PsiFile}
import intellij.haskell.editor.HaskellImportOptimizer
import intellij.haskell.editor.{HaskellImportOptimizer, HaskellProblemsView}
import intellij.haskell.external.component._
import intellij.haskell.external.execution._
import intellij.haskell.psi._
Expand Down Expand Up @@ -73,7 +76,33 @@ class HaskellAnnotator extends ExternalAnnotator[(PsiFile, Option[PsiElement]),
}

override def apply(psiFile: PsiFile, loadResult: CompilationResult, holder: AnnotationHolder): Unit = {
for (annotation <- HaskellAnnotator.createAnnotations(psiFile, loadResult)) {
val haskellProblemsView = ProblemsView.SERVICE.getInstance(psiFile.getProject).asInstanceOf[HaskellProblemsView]
val project = psiFile.getProject

HaskellFileUtil.findVirtualFile(psiFile).foreach { currentFile =>
ApplicationManager.getApplication.invokeLater { () =>
if (!project.isDisposed) {
haskellProblemsView.clearProgress()
haskellProblemsView.clearOldMessages(currentFile)

for (problem <- loadResult.currentFileProblems) {
val message = createCompilerMessage(currentFile, project, problem)
haskellProblemsView.addMessage(message)
}

for (problem <- loadResult.otherFileProblems) {
HaskellProjectUtil.findVirtualFile(problem.filePath, project).foreach { file =>
val message = createCompilerMessage(file, project, problem)
haskellProblemsView.addMessage(message)
}
}
}
}
}

HaskellCompilationResultHelper.createNotificationsForErrorsNotInCurrentFile(project, loadResult)

for (annotation <- HaskellAnnotator.createAnnotations(project, psiFile, loadResult.currentFileProblems)) {
annotation match {
case ErrorAnnotation(textRange, message, htmlMessage) => holder.createAnnotation(HighlightSeverity.ERROR, textRange, message, htmlMessage)
case ErrorAnnotationWithIntentionActions(textRange, message, htmlMessage, intentionActions) =>
Expand All @@ -86,6 +115,11 @@ class HaskellAnnotator extends ExternalAnnotator[(PsiFile, Option[PsiElement]),
}
}
}

private def createCompilerMessage(file: VirtualFile, project: Project, problem: CompilationProblem) = {
val category = if (problem.isWarning) CompilerMessageCategory.WARNING else CompilerMessageCategory.ERROR
new CompilerMessageImpl(project, category, problem.message, file, problem.lineNr, problem.columnNr, null)
}
}

object HaskellAnnotator {
Expand Down Expand Up @@ -122,13 +156,9 @@ object HaskellAnnotator {
}
}

private def createAnnotations(psiFile: PsiFile, loadResult: CompilationResult): Iterable[Annotation] = {
val problems = loadResult.currentFileProblems.filter(problem => HaskellFileUtil.getAbsolutePath(psiFile).contains(problem.filePath))
val project = psiFile.getProject

HaskellCompilationResultHelper.createNotificationsForErrorsNotInCurrentFile(project, loadResult)
private def createAnnotations(project: Project, psiFile: PsiFile, problems: Iterable[CompilationProblem]): Iterable[Annotation] = {

def createErrorAnnotationWithMultiplePerhapsIntentions(problem: CompilationProblemInCurrentFile, tr: TextRange, notInScopeMessage: String, suggestionsList: String) = {
def createErrorAnnotationWithMultiplePerhapsIntentions(problem: CompilationProblem, tr: TextRange, notInScopeMessage: String, suggestionsList: String) = {
val notInScopeName = extractName(notInScopeMessage)
val annotations = suggestionsList.split(",").flatMap(s => extractPerhapsYouMeantAction(s))
ErrorAnnotationWithIntentionActions(tr, problem.plainMessage, problem.htmlMessage, annotations.toStream ++ createNotInScopeIntentionActions(psiFile, notInScopeName))
Expand Down Expand Up @@ -206,19 +236,19 @@ object HaskellAnnotator {
moduleIdentifiers.map(mi => new NotInScopeIntentionAction(mi.name, mi.moduleName, psiFile))
}

private def createLanguageExtensionIntentionsAction(problem: CompilationProblemInCurrentFile, tr: TextRange, languageExtensions: Iterable[String]): ErrorAnnotationWithIntentionActions = {
private def createLanguageExtensionIntentionsAction(problem: CompilationProblem, tr: TextRange, languageExtensions: Iterable[String]): ErrorAnnotationWithIntentionActions = {
ErrorAnnotationWithIntentionActions(tr, problem.plainMessage, problem.htmlMessage, languageExtensions.map(le => new LanguageExtensionIntentionAction(le)))
}

private def importAloneInstancesAction(problem: CompilationProblemInCurrentFile, tr: TextRange, importDecl: String): WarningAnnotationWithIntentionActions = {
private def importAloneInstancesAction(problem: CompilationProblem, tr: TextRange, importDecl: String): WarningAnnotationWithIntentionActions = {
WarningAnnotationWithIntentionActions(tr, problem.plainMessage, problem.htmlMessage, Stream(new ImportAloneInstancesAction(importDecl)))
}

private def redundantImportAction(problem: CompilationProblemInCurrentFile, tr: TextRange, moduleName: String, redundants: String): WarningAnnotationWithIntentionActions = {
private def redundantImportAction(problem: CompilationProblem, tr: TextRange, moduleName: String, redundants: String): WarningAnnotationWithIntentionActions = {
WarningAnnotationWithIntentionActions(tr, problem.plainMessage, problem.htmlMessage, Stream(new RedundantImportAction(moduleName, redundants)))
}

private def getProblemTextRange(psiFile: PsiFile, problem: CompilationProblemInCurrentFile): Option[TextRange] = {
private def getProblemTextRange(psiFile: PsiFile, problem: CompilationProblem): Option[TextRange] = {
LineColumnPosition.getOffset(psiFile, LineColumnPosition(problem.lineNr, problem.columnNr)).map(offset => {
findTextRange(psiFile, offset)
})
Expand Down
133 changes: 133 additions & 0 deletions src/main/scala/intellij/haskell/editor/HaskellProblemsView.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package intellij.haskell.editor

import java.util
import java.util.UUID

import com.intellij.compiler.ProblemsView
import com.intellij.compiler.impl.ProblemsViewPanel
import com.intellij.compiler.progress.CompilerTask
import com.intellij.icons.AllIcons
import com.intellij.ide.errorTreeView.{ErrorTreeElement, ErrorTreeElementKind, GroupingElement}
import com.intellij.openapi.compiler.{CompileScope, CompilerMessage}
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.{Disposer, IconLoader}
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.{ToolWindowAnchor, ToolWindowManager}
import com.intellij.pom.Navigatable
import com.intellij.ui.content.ContentFactory
import com.intellij.util.concurrency.SequentialTaskExecutor
import com.intellij.util.ui.UIUtil

class HaskellProblemsView(project: Project, toolWindowManager: ToolWindowManager) extends ProblemsView(project) {

private final val ProblemsToolWindowId = "Haskell Problems"
private final val ActiveIcon = AllIcons.Toolwindows.Problems
private final val PassiveIcon = IconLoader.getDisabledIcon(ActiveIcon)

private val viewUpdater = SequentialTaskExecutor.createSequentialApplicationPoolExecutor("ProblemsView Pool")

private val problemsPanel = new ProblemsViewPanel(project)

Disposer.register(project, () => {
Disposer.dispose(problemsPanel)
})

UIUtil.invokeLaterIfNeeded(() => {
if (!project.isDisposed) {
val toolWindow = toolWindowManager.registerToolWindow(ProblemsToolWindowId, false, ToolWindowAnchor.LEFT, project, true)
val content = ContentFactory.SERVICE.getInstance.createContent(problemsPanel, "", false)
content.setHelpId("reference.problems.tool.window")
toolWindow.getContentManager.addContent(content)
Disposer.register(project, () => {
toolWindow.getContentManager.removeAllContents(true)
})
updateIcon()
}
})

def clearOldMessages(currentFile: VirtualFile): Unit = {
viewUpdater.execute(() => {
cleanupChildrenRecursively(problemsPanel.getErrorViewStructure.getRootElement.asInstanceOf[ErrorTreeElement], currentFile)
updateIcon()
problemsPanel.reload()
})
}

// See previous clearOldMessages
def clearOldMessages(scope: CompileScope, currentSessionId: UUID): Unit = {
throw new RuntimeException("Not implemented because no session id is available and need current file to clear only old messages of that file.")
}

override def addMessage(messageCategoryIndex: Int, text: Array[String], groupName: String, navigatable: Navigatable, exportTextPrefix: String, rendererTextPrefix: String, sessionId: UUID): Unit = {
viewUpdater.execute(() => {
if (navigatable != null) {
problemsPanel.addMessage(messageCategoryIndex, text, groupName, navigatable, exportTextPrefix, rendererTextPrefix, sessionId)
}
else {
problemsPanel.addMessage(messageCategoryIndex, text, null, -1, -1, sessionId)
}
updateIcon()
})
}

def addMessage(message: CompilerMessage): Unit = {
val file = message.getVirtualFile
val navigatable = if (message.getNavigatable == null && file != null && !file.getFileType.isBinary) {
new OpenFileDescriptor(myProject, file, -1, -1)
} else {
message.getNavigatable
}
val category = message.getCategory
val categoryIndex = CompilerTask.translateCategory(category)
val messageText = splitMessage(message)
val groupName = if (file != null) file.getPresentableUrl else category.getPresentableText
addMessage(categoryIndex, messageText, groupName, navigatable, message.getExportTextPrefix, message.getRenderTextPrefix, null)
}

override def setProgress(text: String, fraction: Float): Unit = {
problemsPanel.setProgress(text, fraction)
}

override def setProgress(text: String): Unit = {
problemsPanel.setProgressText(text)
}

override def clearProgress(): Unit = {
problemsPanel.clearProgressData()
}

private def splitMessage(message: CompilerMessage): Array[String] = {
val messageText = message.getMessage
if (messageText.contains("\n")) {
messageText.split("\n")
} else {
Array[String](messageText)
}
}

private def cleanupChildrenRecursively(errorTreeElement: ErrorTreeElement, currentFile: VirtualFile): Unit = {
val errorViewStructure = problemsPanel.getErrorViewStructure
for (element <- errorViewStructure.getChildElements(errorTreeElement)) {
element match {
case groupElement: GroupingElement =>
if (groupElement.getFile == currentFile) {
cleanupChildrenRecursively(element, currentFile)
}
case _ => errorViewStructure.removeElement(element)
}
}
}

private def updateIcon() = {
UIUtil.invokeLaterIfNeeded(() => {
if (!myProject.isDisposed) {
val toolWindow = ToolWindowManager.getInstance(myProject).getToolWindow(ProblemsToolWindowId)
if (toolWindow != null) {
val active = problemsPanel.getErrorViewStructure.hasMessages(util.EnumSet.of(ErrorTreeElementKind.ERROR, ErrorTreeElementKind.WARNING, ErrorTreeElementKind.NOTE))
toolWindow.setIcon(if (active) ActiveIcon else PassiveIcon)
}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,52 +26,37 @@ object HaskellCompilationResultHelper {
private final val ProblemPattern = """(.+):([\d]+):([\d]+):(.+)""".r

def createCompilationResult(currentPsiFile: PsiFile, errorLines: Seq[String], failed: Boolean): CompilationResult = {
val filePath = HaskellFileUtil.getAbsolutePath(currentPsiFile).getOrElse(throw new IllegalStateException(s"File `${currentPsiFile.getName}` exists only in memory"))
val currentFilePath = HaskellFileUtil.getAbsolutePath(currentPsiFile).getOrElse(throw new IllegalStateException(s"File `${currentPsiFile.getName}` exists only in memory"))

val compilationProblems = errorLines.flatMap(l => parseErrorLine(Some(filePath), l))
val compilationProblems = errorLines.flatMap(parseErrorLine)

val currentFileProblems = compilationProblems.flatMap(convertToCompilationProblemInCurrentFile)
val otherFileProblems = compilationProblems.diff(currentFileProblems)
val (currentFileProblems, otherFileProblems) = compilationProblems.partition(_.filePath == currentFilePath)

CompilationResult(currentFileProblems, otherFileProblems, failed)
}


def createNotificationsForErrorsNotInCurrentFile(project: Project, compilationResult: CompilationResult): Unit = {
if (compilationResult.currentFileProblems.isEmpty) {
compilationResult.otherFileProblems.foreach {
case cpf: CompilationProblemInOtherFile if !cpf.isWarning => HaskellNotificationGroup.logErrorBalloonEventWithLink(project, cpf.filePath, cpf.htmlMessage, cpf.lineNr, cpf.columnNr)
case cpf: CompilationProblem if !cpf.isWarning => HaskellNotificationGroup.logErrorBalloonEventWithLink(project, cpf.filePath, cpf.htmlMessage, cpf.lineNr, cpf.columnNr)
case _ => ()
}
}
}

private def convertToCompilationProblemInCurrentFile(problem: CompilationProblem) = {
problem match {
case p: CompilationProblemInCurrentFile => Some(p)
case _ => None
}
}

def parseErrorLine(filePath: Option[String], errorLine: String): Option[CompilationProblem] = {
def parseErrorLine(errorLine: String): Option[CompilationProblem] = {
errorLine match {
case ProblemPattern(problemFilePath, lineNr, columnNr, message) =>
case ProblemPattern(filePath, lineNr, columnNr, message) =>
val displayMessage = message.trim.replaceAll("""(\s\s\s\s+)""", "\n" + "$1")
if (filePath.contains(problemFilePath)) {
Some(CompilationProblemInCurrentFile(problemFilePath, lineNr.toInt, columnNr.toInt, displayMessage))
} else {
Some(CompilationProblemInOtherFile(problemFilePath, lineNr.toInt, columnNr.toInt, displayMessage))
}
Some(CompilationProblem(filePath, lineNr.toInt, columnNr.toInt, displayMessage))
case _ => None
}
}
}

case class CompilationResult(currentFileProblems: Iterable[CompilationProblemInCurrentFile], otherFileProblems: Iterable[CompilationProblem], failed: Boolean)

trait CompilationProblem {
case class CompilationResult(currentFileProblems: Iterable[CompilationProblem], otherFileProblems: Iterable[CompilationProblem], failed: Boolean)

def message: String
case class CompilationProblem(filePath: String, lineNr: Int, columnNr: Int, message: String) {

def plainMessage: String = {
message.split("\n").mkString.replaceAll("\\s+", " ")
Expand All @@ -86,7 +71,4 @@ trait CompilationProblem {
}
}

case class CompilationProblemInCurrentFile private(filePath: String, lineNr: Int, columnNr: Int, message: String) extends CompilationProblem

case class CompilationProblemInOtherFile private(filePath: String, lineNr: Int, columnNr: Int, message: String) extends CompilationProblem

Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@ object StackCommandLine {

private def addMessage() = {
val errorMessageLine = previousMessageLines.mkString(" ")
val compilationProblem = HaskellCompilationResultHelper.parseErrorLine(None, errorMessageLine.replaceAll("\n", " "))
val compilationProblem = HaskellCompilationResultHelper.parseErrorLine(errorMessageLine.replaceAll("\n", " "))
compilationProblem match {
case Some(p@CompilationProblemInOtherFile(filePath, lineNr, columnNr, message)) if p.isWarning =>
case Some(p@CompilationProblem(filePath, lineNr, columnNr, message)) if p.isWarning =>
compileContext.addMessage(CompilerMessageCategory.WARNING, message, HaskellFileUtil.getUrlByPath(filePath), lineNr, columnNr)
case Some(CompilationProblemInOtherFile(filePath, lineNr, columnNr, message)) =>
case Some(CompilationProblem(filePath, lineNr, columnNr, message)) =>
compileContext.addMessage(CompilerMessageCategory.ERROR, message, HaskellFileUtil.getUrlByPath(filePath), lineNr, columnNr)
case _ =>
val compilerMessageCategory =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class HaskellCompilationResultHelperSpec extends FunSpec with Matchers with Give
val output = "/file/path/HaskellFile.hs:1:11:parse error on input and so on"

When("parsed to problem")
val problem = HaskellCompilationResultHelper.parseErrorLine(Some("/file/path/HaskellFile.hs"), output).asInstanceOf[Some[CompilationProblemInCurrentFile]].get
val problem = HaskellCompilationResultHelper.parseErrorLine(output).get

Then("it should contain right data")
problem.lineNr should equal(1)
Expand Down

0 comments on commit 7545bc3

Please sign in to comment.