IntelliJ IDEA上でTerminalからsbt立ち上げてerrorやwarn出た時にclickで飛べるようにする

記憶違いなのか、IDEAのversionか設定で変わって出来なくなったのか、本当に謎なんですが、大量の警告やエラー直す場合に、これの有無でけっこう生産性が変わるので。

「sbt pluginではない」というのは、IDEA関係ない普通のsbt pluginとしてではないし、IDEA自体のsbtやScala pluginも関係ない、という話です。 「IDEA自体のsbtやScala plugin」は存在して、そこからもsbtのshellは立ち上がり、それ経由だと昔から勝手にクリック可能なのですが、そちらはtab補完が壊滅的になるので使いたくない、という事情があります。 Scalaとはおそらく全く関係ないIDEAに標準搭載されてる(よくわかってない?)terminalだと、sbtのshellのtab補完その他がそのままいい感じに動きます。

こちらがsbt shell(pathがクリック可能だけどtab補完死ぬので使いたくない方)

こちらがterminal(tab補完いい感じになるけど、デフォルトでは?pathがクリックできない方)

解決策ですが、普通のerrorやwarnの場合は、以下のようにbuild.sbtに書いて無理やり file:/// 付与すれば、リンクになっていけました。 (動作確認したsbtは1.9.9)

ただし、これScala 3だとダメですね・・・(調査中 => 下に別の方法追記した)

build.sbt

sourcePositionMappers += { (p: xsbti.Position) =>
  Option {
    new xsbti.Position {
      override def line() = p.line()
      override def lineContent() = p.lineContent()
      override def offset() = p.offset()
      override def pointer() = p.pointer()
      override def pointerSpace() = p.pointerSpace()
      override def sourcePath() = p.sourcePath().map(a => "file:///" + a)
      override def sourceFile() = p.sourceFile()
    }
  }
}

// 以下の2つはこれのテストのためで、場合による
scalaVersion := "2.13.13"
scalacOptions += "-deprecation"

わざと警告出すための適当なファイル

class A {
  def x = Stream(2)
}

before

after

また、sbt-scalafixは、ほぼ同じような形式で出すのに、独自に出しているので、上記では変わらないのですが、これも以下のように若干無理やり独自にやるといけました

(ただし自分がつい最近追加した機能を使ってるので0.12.0以降が必要)

(これのためにこのkey追加したわけではなく、他にやりたいことあったのがきっかけだが)

ThisBuild / scalafixCallback := {
  val log = sbt.ConsoleLogger(System.out)

  import _root_.scalafix.interfaces.ScalafixSeverity

  { (x: _root_.scalafix.interfaces.ScalafixDiagnostic) =>
    val msg = s"file:///${x.getClass.getMethod("diagnostic").invoke(x)}"

    x.severity() match {
      case ScalafixSeverity.INFO =>
        log.info(msg)
      case ScalafixSeverity.WARNING =>
        log.warn(msg)
      case ScalafixSeverity.ERROR =>
        log.err(msg)
    }
  }
}

追記:

Scala 3の場合は、とりあえず以下のようにすれば、2回出てしまうが、とりあえず出ます

// https://github.com/sbt/sbt/blob/48c23761dc5ffe191a79424ba830dafcd2ec7631/main/src/main/scala/sbt/Keys.scala#L639
val compilerReporter = taskKey[xsbti.Reporter]("")

Seq(Compile, Test).map { x =>
  x / compile / compilerReporter := {
    val underlying = (x / compile / compilerReporter).value
    val logger = streams.value.log
    if (scalaBinaryVersion.value == "3") {
      new xsbti.Reporter {
        override def reset() =
          underlying.reset()
        override def hasErrors =
          underlying.hasErrors
        override def hasWarnings =
          underlying.hasWarnings
        override def printSummary() =
          underlying.printSummary()
        override def problems() =
          underlying.problems()
        override def log(problem: xsbti.Problem): Unit = {
          problem.position().sourceFile().ifPresent { f =>
            val x = s"file:///${f.getCanonicalPath}:${problem.position().startLine().orElseGet(() => -1)}"
            problem.severity() match {
              case xsbti.Severity.Info =>
                logger.info(x)
              case xsbti.Severity.Warn =>
                logger.warn(x)
              case xsbti.Severity.Error =>
                logger.error(x)
            }
          }
          underlying.log(problem)
        }
        override def comment(position: xsbti.Position, s: String): Unit =
          underlying.comment(position, s)
      }
    } else {
      underlying
    }
  }
}