Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go to definition in classpath #23

Merged
merged 35 commits into from
Nov 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ab517c3
Implement syntactic definition indexer for scala sources
olafurpg Nov 9, 2017
6547cf3
Implement classpath indexer.
olafurpg Nov 9, 2017
eebbe39
Integrate ctags with compiler and symbol indexer.
olafurpg Nov 9, 2017
445e332
Implement Go to definition in classpath using Ctags module.
olafurpg Nov 9, 2017
062f8e9
Fix trailing comma and run tests in CI
olafurpg Nov 10, 2017
cc537c5
Avoid redundant indexing.
olafurpg Nov 10, 2017
766361a
Update lsp plugin
olafurpg Nov 10, 2017
864821d
Get basic java ctags working.
olafurpg Nov 10, 2017
4b175b9
Refactor ctags into separate package.
olafurpg Nov 11, 2017
6ad69f0
Close ZipInputStream.
olafurpg Nov 11, 2017
bf1b6f9
Update readme
olafurpg Nov 11, 2017
420f11d
Improve deduplication of indexing source jars.
olafurpg Nov 11, 2017
e64a505
Address review feedback
olafurpg Nov 11, 2017
5c7951a
Clean up a few things
olafurpg Nov 11, 2017
bcd3905
Fix compilation errors
olafurpg Nov 11, 2017
01f51b3
Add more detailed explanation why we write jar: contents to disk
olafurpg Nov 11, 2017
db1779a
Improve ctags indexing for java
olafurpg Nov 11, 2017
3de1c85
Clean up ctags
olafurpg Nov 12, 2017
da8e1d8
Handle java fields
olafurpg Nov 12, 2017
dca5a4a
Remove unnecessary code.
olafurpg Nov 12, 2017
2d09433
Update readme to reflect current state
olafurpg Nov 12, 2017
a85dbaf
Implement java ctags with qdox instead of javaparser.
olafurpg Nov 12, 2017
fe8cfa4
Remove javaparser and fix broken tests from qdox migration.
olafurpg Nov 12, 2017
12a4a64
Properly handle java enums
olafurpg Nov 12, 2017
0b29617
Index JDK sources.
olafurpg Nov 12, 2017
d05108d
Address review feedback
olafurpg Nov 14, 2017
c2cb7bd
Fix bug related to default java fields being static.
olafurpg Nov 14, 2017
020faaa
Remove PrintStream from Formatter
olafurpg Nov 14, 2017
81b8520
Clean up java ctags a bit.
olafurpg Nov 14, 2017
dc03def
Use recommended parameter name
olafurpg Nov 14, 2017
ed8e327
Minimize log spam on server startup.
olafurpg Nov 14, 2017
fd2035a
Link issue to open files from jars.
olafurpg Nov 14, 2017
50191c1
Remove noise from ctags if shouldIndex filters a lot of paths
olafurpg Nov 14, 2017
3576919
Skip broken and slow tests.
olafurpg Nov 14, 2017
5e9272f
Make JDK test pass regardless of JAVA_HOME filename length.
olafurpg Nov 14, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ Please share your thoughts in
- [x] Linting with Scalafix
- [x] Formatting with Scalafmt
- [x] Auto completions as you type with presentation compiler
- [x] Go to definition from project Scala sources to project Scala sources with Semanticdb
- [x] Go to definition from project Scala sources to project Scala sources
- [x] Show type at position
- [ ] Go to definition from project sources to dependency sources
- [ ] Go to definition from dependency sources to dependency sources
- [ ] Go to definition in Java sources
- [x] Go to definition from project sources to Scala dependency source files
- [x] Go to definition from project sources to Java dependency source files
- [ ] Show red squigglies as you type
- [ ] Show red squigglies on compile
- [ ] Show parameter list as you type, signature helper
- [ ] Find symbol references
- [ ] Show docstring on hover
- [ ] Rename symbol

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion bin/runci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ case "$TEST" in
./scalafmt --test
;;
* )
sbt metaserver/compile
sbt test
;;
esac

8 changes: 5 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ lazy val languageserver = project
"org.slf4j" % "slf4j-api" % "1.7.21",
"ch.qos.logback" % "logback-classic" % "1.1.7",
"org.codehaus.groovy" % "groovy" % "2.4.0",
"org.scalatest" %% "scalatest" % "3.0.1" % "test"
"org.scalatest" %% "scalatest" % "3.0.1" % Test
)
)

Expand All @@ -24,11 +24,13 @@ lazy val metaserver = project
resolvers += "dhpcs at bintray" at "https://dl.bintray.com/dhpcs/maven",
libraryDependencies ++= List(
"io.monix" %% "monix" % "2.3.0",
"com.lihaoyi" %% "pprint" % "0.5.3",
"com.thoughtworks.qdox" % "qdox" % "2.0-M7", // for java ctags
"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,
"com.geirsson" %% "scalafmt-core" % "1.3.0"
"org.scalatest" %% "scalatest" % "3.0.3" % Test,
"org.scalameta" %% "testkit" % "2.0.1" % Test
)
)
.dependsOn(languageserver)
.aggregate(languageserver)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ case class TextDocument(uri: String, contents: Array[Char]) {
copy(contents = change.text.toArray)
}

private def peek(idx: Int) =
private def peek(idx: Int): Int =
if (idx < contents.size) contents(idx) else -1

def toFile: File =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.langmeta.languageserver

import org.langmeta.inputs.Input
import org.langmeta.inputs.Position

object InputEnrichments {
implicit class XtensionInputOffset(val input: Input) extends AnyVal {

/** Returns an offset for this input */
def toOffset(line: Int, column: Int): Int =
input.lineToOffset(line) + column

/** Returns an offset position for this input */
def toPosition(startLine: Int, startColumn: Int): Position.Range =
toPosition(startLine, startColumn, startLine, startColumn)

/** Returns a range position for this input */
def toPosition(
startLine: Int,
startColumn: Int,
endLine: Int,
endColumn: Int
): Position.Range =
Position.Range(
input,
toOffset(startLine, startColumn),
toOffset(endLine, endColumn)
)
}
}
82 changes: 48 additions & 34 deletions metaserver/src/main/scala/scala/meta/languageserver/Compiler.scala
Original file line number Diff line number Diff line change
@@ -1,54 +1,46 @@
package scala.meta.languageserver

import java.io.File
import java.nio.file.Files
import java.util.Properties
import java.io.PrintStream
import java.util.concurrent.ConcurrentHashMap
import scala.collection.mutable
import scala.reflect.io
import scala.tools.nsc.Settings
import scala.tools.nsc.interactive.{Global, Response}
import scala.tools.nsc.interactive.Global
import scala.tools.nsc.interactive.Response
import scala.tools.nsc.reporters.StoreReporter
import com.typesafe.scalalogging.LazyLogging
import langserver.core.Connection
import langserver.messages.MessageType
import monix.eval.Task
import monix.execution.Scheduler
import monix.reactive.MulticastStrategy
import monix.reactive.Observable
import org.langmeta.io.AbsolutePath
import org.langmeta.semanticdb.Document

case class CompilerConfig(
sources: List[AbsolutePath],
scalacOptions: List[String],
classpath: String
)
object CompilerConfig {
def fromPath(
path: AbsolutePath
)(implicit cwd: AbsolutePath): CompilerConfig = {
val input = Files.newInputStream(path.toNIO)
try {
val props = new Properties()
props.load(input)
val sources = props
.getProperty("sources")
.split(File.pathSeparator)
.iterator
.map(AbsolutePath(_))
.toList
val scalacOptions = props.getProperty("scalacOptions").split(" ").toList
val classpath = props.getProperty("classpath")
CompilerConfig(sources, scalacOptions, classpath)
} finally input.close()
}
}
class Compiler(
out: PrintStream,
config: Observable[AbsolutePath],
connection: Connection,
buffers: Buffers
)(implicit cwd: AbsolutePath)
)(implicit cwd: AbsolutePath, s: Scheduler)
extends LazyLogging {
val onNewCompilerConfig: Observable[Unit] =
private val (documentSubscriber, myDocumentPublisher) =
Observable.multicast[Document](MulticastStrategy.Publish)
val documentPublisher: Observable[Document] = myDocumentPublisher
private val indexedJars: ConcurrentHashMap[AbsolutePath, Unit] =
new ConcurrentHashMap[AbsolutePath, Unit]()
val onNewCompilerConfig: Observable[
(Effects.InstallPresentationCompiler, Effects.IndexSourcesClasspath)
] =
config
.map(path => CompilerConfig.fromPath(path))
.map(onNewConfig)
.flatMap { config =>
Observable.fromTask(
Task(loadNewCompilerGlobals(config))
.zip(Task(indexDependencyClasspath(config)))
)
}

def autocomplete(
path: AbsolutePath,
Expand Down Expand Up @@ -89,7 +81,9 @@ class Compiler(
}

private val compilerByPath = mutable.Map.empty[AbsolutePath, Global]
private def onNewConfig(config: CompilerConfig): Unit = {
private def loadNewCompilerGlobals(
config: CompilerConfig
): Effects.InstallPresentationCompiler = {
logger.info(s"Loading new compiler from config $config")
val vd = new io.VirtualDirectory("(memory)", None)
val settings = new Settings
Expand All @@ -99,11 +93,31 @@ class Compiler(
("-Ypresentation-any-thread" :: config.scalacOptions).mkString(" ")
)
val compiler = new Global(settings, new StoreReporter)

config.sources.foreach { path =>
// TODO(olafur) garbage collect compilers from removed files.
compilerByPath(path) = compiler
}
Effects.InstallPresentationCompiler
}
private def indexDependencyClasspath(
config: CompilerConfig
): Effects.IndexSourcesClasspath = {
val buf = List.newBuilder[AbsolutePath]
val sourceJars = config.sourceJars
sourceJars.foreach { jar =>
// ensure we only index each jar once even under race conditions.
indexedJars.computeIfAbsent(jar, _ => buf += jar)
}
val sourcesClasspath = buf.result()
if (sourcesClasspath.nonEmpty) {
logger.info(
s"Indexing classpath with ${sourcesClasspath.length} entries..."
)
}
ctags.Ctags.index(sourcesClasspath) { doc =>
documentSubscriber.onNext(doc)
}
Effects.IndexSourcesClasspath
}
private def noCompletions: List[(String, String)] = {
connection.showMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package scala.meta.languageserver

import java.io.File
import java.nio.file.Files
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using IO and NIO?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java.io is only for File.pathSeparator, otherwise we use nio for I/O work

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIO Filesystem has a platform independent separator if you want to stick with the NIO api.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer java.io.File.pathSeparator over FileSystems.getDefault.getSeparator

import java.nio.file.Paths
import java.util.Properties
import com.typesafe.scalalogging.LazyLogging
import org.langmeta.io.AbsolutePath
import org.langmeta.io.Classpath

/**
* Configuration to load up a presentation compiler.
*
* In sbt, one compiler config typically corresponds to one project+config.
* For example one sbt project with test/main/it configurations has three
* CompilerConfig.
*
* @param sources list of source files for this project
* @param scalacOptions space separated list of flags to pass to the Scala compiler
* @param dependencyClasspath File.pathSeparated list of *.jar and classDirectories.
* Includes both dependencyClasspath and classDirectory.
* @param classDirectory The output directory where *.class files are emitted
* for this project.
* @param sourceJars File.pathSeparated list of *-sources.jar from the
* dependencyClasspath.
*/
case class CompilerConfig(
sources: List[AbsolutePath],
scalacOptions: List[String],
classDirectory: AbsolutePath,
dependencyClasspath: List[AbsolutePath],
sourceJars: List[AbsolutePath]
) {
override def toString: String =
s"CompilerConfig(" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we have pprint as a dependency, you can probably just do pprint.stringify(this)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find pprint.stringify, it seems to have been removed in 0.5. I don't think pprint includes the names of the fields. This string appears in the logs whenever a new presentation compiler is loaded so I customized it to make it compact and readable (full classpath can be huge).

s"sources={+${sources.length}}, " +
s"scalacOptions=${scalacOptions.mkString(" ")}, " +
s"dependencyClasspath={+${dependencyClasspath.length}}, " +
s"classDirectory=$classDirectory, " +
s"sourceJars={+${sourceJars.length}})"
def classpath: String =
(classDirectory :: dependencyClasspath).mkString(File.pathSeparator)
}

object CompilerConfig extends LazyLogging {

def jdkSourcePath: Option[AbsolutePath] =
sys.env.get("JAVA_HOME").map(AbsolutePath(_).resolve("src.zip"))

def jdkSources: Option[AbsolutePath] =
for {
path <- jdkSourcePath
if Files.isRegularFile(path.toNIO)
} yield path

def fromPath(
path: AbsolutePath
)(implicit cwd: AbsolutePath): CompilerConfig = {
val input = Files.newInputStream(path.toNIO)
try {
val props = new Properties()
props.load(input)
fromProperties(props)
} finally input.close()
}

def fromProperties(
props: Properties
)(implicit cwd: AbsolutePath): CompilerConfig = {
val sources = props
.getProperty("sources")
.split(File.pathSeparator)
.iterator
.map(AbsolutePath(_))
.toList
val scalacOptions =
props.getProperty("scalacOptions").split(" ").toList
val dependencyClasspath =
Classpath(props.getProperty("dependencyClasspath")).shallow
val sourceJars = {
val result = Classpath(props.getProperty("sourceJars")).shallow
jdkSources.fold(result)(_ :: result)
}
val classDirectory =
AbsolutePath(props.getProperty("classDirectory"))
CompilerConfig(
sources,
scalacOptions,
classDirectory,
dependencyClasspath,
sourceJars
)
}
}
18 changes: 18 additions & 0 deletions metaserver/src/main/scala/scala/meta/languageserver/Effects.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package scala.meta.languageserver

/**
* The ScalametaLanguageServer effects.
*
* Observable[Unit] is not descriptive of what the observable represents.
* Instead, we create Unit-like types to better document what effects are
* flowing through our application.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor man's type-safety. I refactored to this in fact because I hit on bugs related to accidentally creating Observable[Observable[T]] from a .map when I should have been using .flatMap to build Observable[Unit]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these phantom types?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really since those values also exist at runtime. I'm still not sure if this was necessary, maybe we can avoid it by using stricter scalac options (no discard unit). However, I think it's a nice replacement for Observable[Unit].

*/
sealed abstract class Effects
object Effects {
final class IndexSemanticdb extends Effects
final val IndexSemanticdb = new IndexSemanticdb
final class IndexSourcesClasspath extends Effects
final val IndexSourcesClasspath = new IndexSourcesClasspath
final class InstallPresentationCompiler extends Effects
final val InstallPresentationCompiler = new InstallPresentationCompiler
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ package scala.meta.languageserver

import scala.language.reflectiveCalls

import java.io.File
import java.io.PrintStream
import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader
import com.typesafe.scalalogging.LazyLogging

abstract class Formatter {
def format(code: String, configFile: String, filename: String): String
}
object Formatter {
def classloadScalafmt(version: String, out: PrintStream): Formatter = {
object Formatter extends LazyLogging {
def classloadScalafmt(version: String): Formatter = {
val urls = Jars
.fetch("com.geirsson", "scalafmt-cli_2.12", version, out)
.fetch("com.geirsson", "scalafmt-cli_2.12", version, System.out)
.iterator
.map(_.toURI.toURL)
.toArray
logger.info(s"Classloading scalafmt with ${urls.length} downloaded jars")
type Scalafmt210 = {
def format(code: String, configFile: String, filename: String): String
}
Expand Down
Loading