Skip to content

Commit

Permalink
Basic auto-import machinery via ghc-mod
Browse files Browse the repository at this point in the history
First draft of add import

Prompt user to choose import

Add to import should put popup at cursor

Add to imports support for operators

Add to imports support for data cons

Auto-import type constructor/class

Better name detection in 'not in scope' error messages

Cache responses from ghc-mod

Don't auto-spawn ghc-mod when its settings change

Kill idle ghc-modi processes after configured timeout

Include module name in ghc-modi tool console messages

Cleanup of ghc mod fix handlers

Add-to-import for type hole
  • Loading branch information
Mark Eibes authored and carymrobbins committed Mar 23, 2019
1 parent 5afea4f commit 03e84ed
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 75 deletions.
120 changes: 120 additions & 0 deletions src/com/haskforce/features/intentions/AddToImports.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.haskforce.features.intentions

import com.haskforce.highlighting.annotation.external.{SymbolImportProvider, SymbolImportProviderFactory}
import com.haskforce.psi.{HaskellBody, HaskellImpdecl, HaskellImportt}
import com.haskforce.psi.impl.HaskellElementFactory
import com.haskforce.utils.NotificationUtil
import com.intellij.codeInsight.intention.impl.BaseIntentionAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.psi.{PsiElement, PsiFile}
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.ui.components.JBList
import com.intellij.util.IncorrectOperationException
import scala.collection.JavaConverters._

class AddToImports(val symbolName: String) extends BaseIntentionAction {
override def getFamilyName = "Add to import list"

override def getText: String = "Import symbol " + symbolName

override def isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean = {
SymbolImportProviderFactory.get(file).isDefined
}

@throws[IncorrectOperationException]
override def invoke(project: Project, editor: Editor, file: PsiFile): Unit = {

val provider = SymbolImportProviderFactory.get(file).get

val results = provider.findImport(symbolName)

if (results.isEmpty) {
NotificationUtil.displaySimpleNotification(
NotificationType.INFORMATION, project, "Not found",
s"Could not find any import for the symbol $symbolName"
)
return
}

val list = new JBList(results: _*)

val popup = JBPopupFactory.getInstance()
.createListPopupBuilder(list)
.setTitle("Identifier to Import")
.setItemChoosenCallback(() => doAddImport(project, file, list.getSelectedValue))
.createPopup()

popup.showInBestPositionFor(editor)
}

def doAddImport(project: Project, file: PsiFile, r: SymbolImportProvider.Result): Unit = {
WriteCommandAction.runWriteCommandAction(project, { () =>
val importName = r.importText
val moduleList = importName.split("\\.").toList
val imports = PsiTreeUtil.findChildrenOfType(file, classOf[HaskellImpdecl]).asScala
val addedToExistingImport = imports.foldLeft(false) {
case (true, _) => true // Short circuit
case (false, impDecl) =>
lazy val importedModule = impDecl.getQconidList.asScala.flatMap(_.getConidList.asScala.map(_.getName)).toList
if (impDecl.getQualified == null
&& impDecl.getRparen != null
&& impDecl.getAs == null
&& impDecl.getHiding == null
&& moduleList == importedModule
) { // We already import some qualified symbols, thus we append
AddToImports.appendToExistingImport(project, symbolName, impDecl)
true
}
else false
}
if (!addedToExistingImport) {
AddToImports.createNewImport(file, project, importName, symbolName, imports)
}
}: Runnable)
}
}

object AddToImports {

def appendToExistingImport(project: Project, symbolName: String, impDecl: HaskellImpdecl): Unit = {
val importt = impDecl.getImporttList.iterator().next()
val rParen = impDecl.getRparen
importt.addBefore(HaskellElementFactory.createComma(project), rParen)
importt.addBefore(HaskellElementFactory.createSpace(project), rParen)
importt.addBefore(mkImportt(project, symbolName), rParen)
}

def createNewImport(file: PsiFile, project: Project, importName: String, symbolName: String, imports: Iterable[HaskellImpdecl]): Unit = {
val impDecl = mkImpDecl(project, importName, symbolName)
val body = PsiTreeUtil.getChildOfType(file, classOf[HaskellBody])
val newline = HaskellElementFactory.createNewLine(project)
if (imports.nonEmpty) {
body.addAfter(newline, imports.last)
body.addAfter(impDecl, imports.last.getNextSibling)
}
else {
val firstChild = body.getFirstChild
body.addBefore(impDecl, firstChild)
body.addBefore(newline, firstChild)
}
}

private def mkImportt(project: Project, symbolName: String) =
mkImpDecl(project, "Dummy", symbolName).getImporttList.asScala.head

private def mkImpDecl(project: Project, importName: String, symbolName: String) = {
val textImport =
if (isOp(symbolName)) s"import $importName (($symbolName))"
else s"import $importName ($symbolName)"
HaskellElementFactory.createImpdeclFromText(project, textImport)
}


private def isOp(symbolName: String) = notOpRegex.unapplySeq(symbolName).isEmpty

private def notOpRegex = """^\w+$""".r
}
58 changes: 32 additions & 26 deletions src/com/haskforce/highlighting/annotation/external/GhcMod.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.haskforce.highlighting.annotation.external;

import com.haskforce.features.intentions.AddLanguagePragma;
import com.haskforce.features.intentions.AddToImports;
import com.haskforce.features.intentions.AddTypeSignature;
import com.haskforce.features.intentions.RemoveForall;
import com.haskforce.highlighting.annotation.HaskellAnnotationHolder;
Expand Down Expand Up @@ -289,8 +290,9 @@ public Problem(String file, int startLine, int startColumn, String message) {
isUnusedImport = message.contains("import of") && message.contains("is redundant");
}

abstract static class RegisterFixHandler {
abstract public void apply(Matcher matcher, Annotation annotation, Problem problem);
@FunctionalInterface
interface RegisterFixHandler {
void apply(Matcher matcher, Annotation annotation, Problem problem);
}

/**
Expand All @@ -300,30 +302,34 @@ abstract static class RegisterFixHandler {
*/
static final List<Pair<Pattern, RegisterFixHandler>> fixHandlers;
static {
fixHandlers = new ArrayList<>(Arrays.<Pair<Pattern, RegisterFixHandler>>asList(
Pair.create(Pattern.compile("^Top-level binding with no type signature"),
new RegisterFixHandler() {
@Override
public void apply(Matcher matcher, Annotation annotation, Problem problem) {
annotation.registerFix(new AddTypeSignature(problem));
}
}),
Pair.create(Pattern.compile("^Illegal symbol '.' in type"),
new RegisterFixHandler() {
@Override
public void apply(Matcher matcher, Annotation annotation, Problem problem) {
annotation.registerFix(new AddLanguagePragma("RankNTypes"));
annotation.registerFix(new RemoveForall(problem));
}
}),
Pair.create(Pattern.compile(" -X([A-Z][A-Za-z0-9]+)"),
new RegisterFixHandler() {
@Override
public void apply(Matcher matcher, Annotation annotation, Problem problem) {
annotation.registerFix(new AddLanguagePragma(matcher.group(1)));
}
})
));
fixHandlers = Arrays.asList(
Pair.create(
Pattern.compile("^Top-level binding with no type signature"),
(__, annotation, problem) -> annotation.registerFix(new AddTypeSignature(problem))
),
Pair.create(
Pattern.compile("^Illegal symbol '.' in type"),
(__, annotation, problem) -> {
annotation.registerFix(new AddLanguagePragma("RankNTypes"));
annotation.registerFix(new RemoveForall(problem));
}),
Pair.create(
Pattern.compile(" -X([A-Z][A-Za-z0-9]+)"),
(matcher, annotation, problem) -> annotation.registerFix(new AddLanguagePragma(matcher.group(1)))
),
Pair.create(
Pattern.compile(" not in scope:\\s*\\(?([^\\s)]+)"),
(matcher, annotation, problem) -> annotation.registerFix(new AddToImports(matcher.group(1)))
),
Pair.create(
Pattern.compile("Not in scope:[^‘]*‘([^’]+)’"),
(matcher, annotation, problem) -> annotation.registerFix(new AddToImports(matcher.group(1)))
),
Pair.create(
Pattern.compile("Or perhaps ‘(_[^’]+)’ is mis-spelled, or not in scope"),
(matcher, annotation, problem) -> annotation.registerFix(new AddToImports(matcher.group(1)))
)
);
}

public void registerFix(@NotNull Annotation annotation) {
Expand Down
Loading

0 comments on commit 03e84ed

Please sign in to comment.