Skip to content

Commit

Permalink
Merge pull request #116 from olafurpg/scalafix-on-type
Browse files Browse the repository at this point in the history
scalafix/definition/squigglies as you type
  • Loading branch information
gabro committed Dec 14, 2017
2 parents cec8b98 + 2ad41c2 commit 3e35435
Show file tree
Hide file tree
Showing 24 changed files with 463 additions and 187 deletions.
3 changes: 3 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rules = [
NoInfer
]
41 changes: 24 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,39 @@ Even if some checkbox is marked it does not mean that feature works perfectly.
Some of those features are a likely outside the scope of this project, we are
still learning and exploring what's possible.

* Compile errors with the Scala Presentation Compiler (`textDocument/publishDiagnostics`):
- [ ] On build compile
- [x] As you type
* Linting with Scalafix (`textDocument/publishDiagnostics`):
- [x] On build compile
- [x] As you type
* Refactoring with Scalafix:
- [ ] Quick-fix inspections (`textDocument/codeAction`)
- [ ] Rename local symbol (`textDocument/rename`)
- [ ] Rename global symbol (`textDocument/rename`)
* Formatting with Scalafmt:
- [x] Whole file (`textDocument/formatting`)
- [ ] Selected range (`textDocument/rangeFormatting`)
- [ ] As you type (`textDocument/onTypeFormatting`)
* Context-aware code completion:
- [x] Autocompletion as you type (`textDocument/completions`)
* Code assistance:
- [x] Auto-complete symbols in scope as you type (`textDocument/completions`)
- [ ] Auto-complete global symbol and insert missing imports (`textDocument/completions`)
- [x] Show parameter list as you type (`textDocument/signatureHelp`)
- [x] Show type at position (`textDocument/hover`)
* Symbols outline:
- [x] Current file symbols tree as you type (`textDocument/documentSymbol`)
- [ ] Workspace global symbols list (`workspace/symbol`)
* Go to definition (`textDocument/definition`):
* Go to definition with SemanticDB (`textDocument/definition`):
- [x] Inside the project
- [x] From project files to Scala dependency source files
- [x] From project files to Java dependency source files
* Symbol references:
- [x] Find all references in the project (`textDocument/references`)
- [x] Highlight local references in the file (`textDocument/documentHighlight`)
* Linting with Scalafix (`textDocument/publishDiagnostics`):
- [x] On compile
- [ ] As you type
* Refactoring with Scalafix:
- [ ] Code actions (`textDocument/codeAction`)
- [ ] Auto-insert missing import when completing a global symbol (`textDocument/completions`)
- [ ] Rename local symbol (`textDocument/rename`)
- [ ] Rename global symbol (`textDocument/rename`)
- [ ] From project dependency to project dependency
* Find references with SemanticDB (`textDocument/references`):
- [x] In file (`textDocument/documentHighlight`)
- [x] In project
- [ ] In dependencies
* Lookup symbol definition by name:
- [x] In file (`textDocument/documentSymbol`)
- [ ] In workspace (`workspace/symbol`)
* Symbol outline:
- [x] In file as you type (`textDocument/documentSymbol`)

## Contributing

Expand Down
8 changes: 5 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ inThisBuild(
if (sys.env.contains("CI")) old
else "0.1-SNAPSHOT" // to avoid manually updating extension.js
},
scalaVersion := "2.12.3",
scalaVersion := V.scala212,
organization := "org.scalameta",
licenses := Seq(
"Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")
Expand Down Expand Up @@ -55,7 +55,9 @@ inThisBuild(
)

lazy val V = new {
val scalameta = "2.1.2"
val scala212 = "2.12.4"
val scalameta = "2.1.5"
val scalafix = "0.5.7"
}

lazy val noPublish = List(
Expand Down Expand Up @@ -112,7 +114,7 @@ lazy val metaserver = project
"com.thoughtworks.qdox" % "qdox" % "2.0-M7", // for java mtags
"io.get-coursier" %% "coursier" % coursier.util.Properties.version,
"io.get-coursier" %% "coursier-cache" % coursier.util.Properties.version,
"ch.epfl.scala" % "scalafix-cli" % "0.5.3" cross CrossVersion.full,
"ch.epfl.scala" % "scalafix-cli" % V.scalafix cross CrossVersion.full,
"org.scalameta" %% "semanticdb-scalac" % V.scalameta cross CrossVersion.full,
"com.lihaoyi" %% "utest" % "0.6.0" % Test,
"org.scalameta" %% "testkit" % V.scalameta % Test,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ object ClientCommand extends CommandCompanion[ClientCommand] {
case class ShowMessageParams(`type`: MessageType, message: String) extends Notification
case class LogMessageParams(`type`: MessageType, message: String) extends Notification
case class PublishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]) extends Notification
object PublishDiagnostics {
implicit val format: OFormat[PublishDiagnostics] = Json.format[PublishDiagnostics]
}

// from client to server

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.langmeta.languageserver
import org.langmeta.inputs.Input
import org.langmeta.inputs.Position
import scala.meta.languageserver.{index => i}
import langserver.{types => l}

object InputEnrichments {
implicit class XtensionInputOffset(val input: Input) extends AnyVal {
Expand All @@ -17,6 +18,16 @@ object InputEnrichments {
)
}

/** Returns a scala.meta.Position from an index range. */
def toPosition(range: l.Range): Position = {
toPosition(
range.start.line,
range.start.character,
range.end.line,
range.end.character
)
}

/** Returns a scala.meta.Position from an index range. */
def toPosition(range: i.Range): Position = {
toPosition(
Expand All @@ -27,6 +38,9 @@ object InputEnrichments {
)
}

def toOffset(pos: l.Position): Int =
toOffset(pos.line, pos.character)

/** Returns an offset for this input */
def toOffset(line: Int, column: Int): Int =
input.lineToOffset(line) + column
Expand Down
29 changes: 16 additions & 13 deletions metaserver/src/main/scala/scala/meta/languageserver/Buffers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package scala.meta.languageserver
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Paths
import java.util.concurrent.ConcurrentHashMap
import java.util.{Map => JMap}
import com.typesafe.scalalogging.LazyLogging
Expand All @@ -11,6 +12,7 @@ import langserver.types.VersionedTextDocumentIdentifier
import org.langmeta.io.AbsolutePath
import org.langmeta.io.RelativePath
import scala.meta.Source
import org.langmeta.inputs.Input

/**
* Utility to keep local state of file contents.
Expand All @@ -23,30 +25,31 @@ import scala.meta.Source
* https://github.com/sourcegraph/language-server-protocol/blob/master/extension-files.md
*/
class Buffers private (
contents: JMap[AbsolutePath, String],
contents: JMap[String, String],
cwd: AbsolutePath
) extends LazyLogging {
private def readFromDisk(path: AbsolutePath): String = {
logger.info(s"Reading $path from disk")
new String(Files.readAllBytes(path.toNIO), StandardCharsets.UTF_8)
}
def changed(path: AbsolutePath, newContents: String): Unit =
contents.put(path, newContents)
def closed(path: AbsolutePath): Unit = {
contents.remove(path)
sources.remove(path)
def changed(input: Input.VirtualFile): Effects.UpdateBuffers = {
contents.put(input.path, input.value)
Effects.UpdateBuffers
}
def closed(uri: String): Unit = {
contents.remove(uri)
sources.remove(uri)
}

def read(td: TextDocumentIdentifier): String =
read(URI.create(td.uri))
read(td.uri)
def read(td: VersionedTextDocumentIdentifier): String =
read(URI.create(td.uri))
def read(uri: URI): String =
read(AbsolutePath(uri.getPath))
def read(path: RelativePath): String = // TODO(olafur) remove?
read(cwd.resolve(path))
read(td.uri)
def read(path: AbsolutePath): String =
Option(contents.get(path)).getOrElse(readFromDisk(path))
read(s"file:$path")
def read(uri: String): String =
Option(contents.get(uri))
.getOrElse(readFromDisk(AbsolutePath(Paths.get(URI.create(uri)))))

private val sources: JMap[AbsolutePath, Source] = new ConcurrentHashMap()
// Tries to parse and record it or fallback to an old source if it existed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ object Effects {
final val IndexSourcesClasspath = new IndexSourcesClasspath
final class InstallPresentationCompiler extends Effects
final val InstallPresentationCompiler = new InstallPresentationCompiler
final class PublishLinterDiagnostics extends Effects
final val PublishLinterDiagnostics = new PublishLinterDiagnostics
final class PublishSquigglies extends Effects
final val PublishSquigglies = new PublishSquigglies
final class PublishScalacDiagnostics extends Effects
final val PublishScalacDiagnostics = new PublishScalacDiagnostics
final class UpdateBuffers extends Effects
final val UpdateBuffers = new UpdateBuffers
}
95 changes: 34 additions & 61 deletions metaserver/src/main/scala/scala/meta/languageserver/Linter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,89 +2,78 @@ package scala.meta.languageserver

import java.io.PrintStream
import scala.meta.internal.tokenizers.PlatformTokenizerCache
import scala.meta.languageserver.ScalametaEnrichments._
import scala.meta.parsers.Parsed
import scala.tools.nsc.interpreter.OutputStream
import scala.{meta => m}
import scalafix._
import scalafix.internal.config.LazySemanticdbIndex
import scalafix.internal.config.ScalafixConfig
import scalafix.internal.config.ScalafixReporter
import scalafix.internal.util.EagerInMemorySemanticdbIndex
import scalafix.languageserver.ScalafixEnrichments._
import scalafix.lint.LintSeverity
import scalafix.patch.Patch
import scalafix.reflect.ScalafixReflect
import scalafix.rule.RuleCtx
import scalafix.rule.RuleName
import scalafix.util.SemanticdbIndex
import langserver.core.Connection
import langserver.messages.PublishDiagnostics
import com.typesafe.scalalogging.LazyLogging
import langserver.types.Diagnostic
import langserver.{types => l}
import metaconfig.ConfDecoder
import monix.reactive.Observable
import org.langmeta.internal.io.PathIO
import org.langmeta.io.AbsolutePath
import org.langmeta.io.RelativePath

class Linter(
cwd: AbsolutePath,
out: OutputStream,
connection: Connection,
) {
) extends LazyLogging {

// Simple method to run syntactic scalafix rules on a string.
def onSyntacticInput(
filename: String,
contents: String
): Seq[PublishDiagnostics] = {
): Seq[Diagnostic] = {
val mdoc = m.Document(
m.Input.VirtualFile(filename, contents),
"scala212",
Nil,
Nil,
Nil,
Nil
)
analyzeIndex(
mdoc,
EagerInMemorySemanticdbIndex(
m.Database(
m.Document(
m.Input.VirtualFile(filename, contents),
"scala212",
Nil,
Nil,
Nil,
Nil
) :: Nil
),
m.Database(mdoc :: Nil),
m.Sourcepath(Nil),
m.Classpath(Nil)
)
)
}

def reportLinterMessages(
mdb: m.Database
): Effects.PublishLinterDiagnostics = {
val messages = analyzeIndex(mdb)
messages.foreach(connection.sendNotification)
Effects.PublishLinterDiagnostics
}
private def analyzeIndex(mdb: m.Database): Seq[PublishDiagnostics] =
def linterMessages(mdoc: m.Document): Seq[Diagnostic] =
analyzeIndex(
EagerInMemorySemanticdbIndex(mdb, m.Sourcepath(Nil), m.Classpath(Nil))
mdoc,
EagerInMemorySemanticdbIndex(
m.Database(mdoc :: Nil),
m.Sourcepath(Nil),
m.Classpath(Nil)
)
)
private def analyzeIndex(index: SemanticdbIndex): Seq[PublishDiagnostics] =

private def analyzeIndex(
document: m.Document,
index: SemanticdbIndex
): Seq[Diagnostic] =
withConfig { configInput =>
val lazyIndex = lazySemanticdbIndex(index)
val configDecoder = ScalafixReflect.fromLazySemanticdbIndex(lazyIndex)
val (rule, config) =
ScalafixConfig.fromInput(configInput, lazyIndex)(configDecoder).get
val results: Seq[PublishDiagnostics] = index.database.documents.flatMap {
d =>
Parser
.parse(d)
.toOption
.map { tree =>
val ctx = RuleCtx.applyInternal(tree, config)
val patches = rule.fixWithNameInternal(ctx)
val diagnostics =
Patch.lintMessagesInternal(patches, ctx).map(toDiagnostic)
val uri = d.input.syntax
PublishDiagnostics(uri, diagnostics)
}
.toList
val results: Seq[Diagnostic] = Parser.parse(document.input) match {
case Parsed.Error(_, _, _) => Nil
case Parsed.Success(tree) =>
val ctx = RuleCtx.applyInternal(tree, config)
val patches = rule.fixWithNameInternal(ctx)
Patch.lintMessagesInternal(patches, ctx).map(_.toLSP)
}

// megaCache needs to die, if we forget this we will read stale
Expand All @@ -111,20 +100,4 @@ class Linter(
ScalafixReporter.default.copy(outStream = new PrintStream(out))
)

private def toDiagnostic(msg: LintMessage): l.Diagnostic = {
l.Diagnostic(
range = msg.position.toRange,
severity = Some(toSeverity(msg.category.severity)),
code = Some(msg.category.id),
source = Some("scalafix"),
message = msg.message
)
}

private def toSeverity(s: LintSeverity): l.DiagnosticSeverity = s match {
case LintSeverity.Error => l.DiagnosticSeverity.Error
case LintSeverity.Warning => l.DiagnosticSeverity.Warning
case LintSeverity.Info => l.DiagnosticSeverity.Information
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import scala.meta.parsers.Parsed
import org.langmeta.inputs.Position
import org.langmeta.semanticdb.Document
import scalafix.internal.config.ScalafixConfig
import org.langmeta.inputs.Input

// Small utility to parse inputs into scala.meta.Tree,
// this is missing in the API after semanticdb went language agnostics with langmeta.
Expand All @@ -19,6 +20,8 @@ object Parser {
Parsed.Error(Position.None, err, new IllegalArgumentException(err))
}

def parse(input: Input): Parsed[Source] =
ScalafixConfig.DefaultDialect(input).parse[Source]
def parse(content: String): Parsed[Source] =
scala.meta.dialects.Scala212(content).parse[Source]
parse(Input.String(content))
}
Loading

0 comments on commit 3e35435

Please sign in to comment.