Skip to content

Commit

Permalink
Implement package auto adding in newly created Scala files
Browse files Browse the repository at this point in the history
PackageProvider was introduced and can be used to implement autocompletion and package rename after file move.

The set of created files was introduced because we cannot apply the workspace edit before the file is open.
  • Loading branch information
Tomasz Dudzik committed Aug 12, 2019
1 parent afdcd37 commit db4cbce
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 0 deletions.
Expand Up @@ -105,6 +105,7 @@ class MetalsLanguageServer(
var buildServer = Option.empty[BuildServerConnection]
private val buildTargetClasses = new BuildTargetClasses(() => buildServer)
private val openTextDocument = new AtomicReference[AbsolutePath]()
private val createdFiles = ConcurrentHashSet.empty[AbsolutePath]
private val savedFiles = new ActiveFiles(time)
private val openedFiles = new ActiveFiles(time)
private val messages = new Messages(config.icons)
Expand Down Expand Up @@ -157,6 +158,8 @@ class MetalsLanguageServer(
private var referencesProvider: ReferenceProvider = _
private var workspaceSymbols: WorkspaceSymbolProvider = _
private var foldingRangeProvider: FoldingRangeProvider = _
private val packageProvider: PackageProvider =
new PackageProvider(buildTargets)
private var compilers: Compilers = _
var tables: Tables = _
var statusBar: StatusBar = _
Expand Down Expand Up @@ -588,6 +591,15 @@ class MetalsLanguageServer(
// Update in-memory buffer contents from LSP client
buffers.put(path, params.getTextDocument.getText)
trees.didChange(path)

val wasCreated = createdFiles.remove(path)
if (wasCreated) {
packageProvider
.addPackageWorkspaceEdit(path)
.map(new ApplyWorkspaceEditParams(_))
.foreach(languageClient.applyEdit)
}

if (path.isDependencySource(workspace)) {
CancelTokens { _ =>
// trigger compilation in preparation for definition requests
Expand Down Expand Up @@ -728,6 +740,12 @@ class MetalsLanguageServer(
event.eventType() match {
case EventType.CREATE =>
buildTargets.onCreate(path)

// We cannot apply the workspace edit before the file is open,
// so we need to wait until we receive a proper request from the client.
if (path.isScala) {
createdFiles.add(path)
}
case _ =>
}
onChange(List(path)).asJava
Expand Down
@@ -0,0 +1,41 @@
package scala.meta.internal.metals

import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.TextEdit
import org.eclipse.lsp4j.WorkspaceEdit

import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.io.AbsolutePath

class PackageProvider(private val buildTargets: BuildTargets) {

def addPackageWorkspaceEdit(path: AbsolutePath): Option[WorkspaceEdit] = {
def createWorkspaceEdit(packageName: String): WorkspaceEdit = {
val textEdit = new TextEdit(
new Range(new Position(0, 0), new Position(0, 0)),
s"package $packageName\n\n"
)

val textEdits = List(textEdit).asJava
val changes = Map(path.toString -> textEdits).asJava
new WorkspaceEdit(changes)
}

createPackageName(path).map(createWorkspaceEdit)
}

private def createPackageName(path: AbsolutePath): Option[String] = {
if (path.isScala && path.toFile.length() == 0) {
val sourceItem = buildTargets.inverseSourceItem(path)
val relativeDirectory = sourceItem
.map(path.toRelative)
.flatMap(relativePath => Option(relativePath.toNIO.getParent))
.map(_.toString)

relativeDirectory.map(_.replace("/", "."))
} else {
None
}
}
}
Expand Up @@ -91,6 +91,12 @@ trait MtagsEnrichments {
case _ => false
}
}
def isScala: Boolean = {
toLanguage match {
case Language.SCALA => true
case _ => false
}
}
def isSemanticdb: Boolean = {
file.toNIO.getFileName.toString.endsWith(".semanticdb")
}
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/src/main/scala/tests/TestingClient.scala
Expand Up @@ -5,6 +5,8 @@ import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger

import org.eclipse.lsp4j.ApplyWorkspaceEditParams
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse
import org.eclipse.lsp4j.Diagnostic
import org.eclipse.lsp4j.DidChangeWatchedFilesRegistrationOptions
import org.eclipse.lsp4j.ExecuteCommandParams
Expand All @@ -14,6 +16,7 @@ import org.eclipse.lsp4j.MessageType
import org.eclipse.lsp4j.PublishDiagnosticsParams
import org.eclipse.lsp4j.RegistrationParams
import org.eclipse.lsp4j.ShowMessageRequestParams
import org.eclipse.lsp4j.TextEdit
import org.eclipse.lsp4j.jsonrpc.CompletableFutures
import scala.collection.concurrent.TrieMap
import scala.meta.internal.metals.Buffers
Expand Down Expand Up @@ -66,6 +69,24 @@ final class TestingClient(workspace: AbsolutePath, buffers: Buffers)
): Unit = {
clientCommands.addLast(params)
}

override def applyEdit(
params: ApplyWorkspaceEditParams
): CompletableFuture[ApplyWorkspaceEditResponse] = {
def applyEdits(uri: String, textEdits: java.util.List[TextEdit]): Unit = {
val path = AbsolutePath(uri)

val content = path.readText
val editedContent =
TextEdits.applyEdits(content, textEdits.asScala.toList)

path.writeText(editedContent)
}

params.getEdit.getChanges.forEach(applyEdits)
CompletableFuture.completedFuture(new ApplyWorkspaceEditResponse(true))
}

def workspaceClientCommands: String = {
clientCommands.asScala.map(_.getCommand).mkString("\n")
}
Expand Down
113 changes: 113 additions & 0 deletions tests/unit/src/test/scala/tests/AddPackageSlowSuite.scala
@@ -0,0 +1,113 @@
package tests

import java.nio.file.Files

import scala.meta.internal.metals.RecursivelyDelete
import scala.meta.internal.metals.MetalsEnrichments._

object AddPackageSlowSuite extends BaseSlowSuite("add-package") {

testAsync("single-level") {
cleanCompileCache("a")
RecursivelyDelete(workspace.resolve("a"))
Files.createDirectories(
workspace.resolve("a/src/main/scala/a").toNIO
)
for {
_ <- server.initialize("""
|/metals.json
|{
| "a": { }
|}
""".stripMargin)
_ = workspace
.resolve("a/src/main/scala/a/Main.scala")
.toFile
.createNewFile()
_ <- server.didOpen("a/src/main/scala/a/Main.scala")
_ = assertNoDiff(
workspace.resolve("a/src/main/scala/a/Main.scala").readText,
"""
|package a
""".stripMargin
)
} yield ()
}

testAsync("multilevel") {
cleanCompileCache("a")
RecursivelyDelete(workspace.resolve("a"))
Files.createDirectories(
workspace.resolve("a/src/main/scala/a/b/c").toNIO
)
for {
_ <- server.initialize("""
|/metals.json
|{
| "a": { }
|}
""".stripMargin)
_ = workspace
.resolve("a/src/main/scala/a/b/c/Main.scala")
.toFile
.createNewFile()
_ <- server.didOpen("a/src/main/scala/a/b/c/Main.scala")
_ = assertNoDiff(
workspace.resolve("a/src/main/scala/a/b/c/Main.scala").readText,
"""
|package a.b.c
""".stripMargin
)
} yield ()
}

testAsync("no-package") {
cleanCompileCache("a")
RecursivelyDelete(workspace.resolve("a"))
Files.createDirectories(
workspace.resolve("a/src/main/scala").toNIO
)
for {
_ <- server.initialize("""
|/metals.json
|{
| "a": { }
|}
""".stripMargin)
_ = workspace
.resolve("a/src/main/scala/Main.scala")
.toFile
.createNewFile()
_ <- server.didOpen("a/src/main/scala/Main.scala")
_ = assertNoDiff(
workspace.resolve("a/src/main/scala/Main.scala").readText,
""
)
} yield ()
}

testAsync("java-file") {
cleanCompileCache("a")
RecursivelyDelete(workspace.resolve("a"))
Files.createDirectories(
workspace.resolve("a/src/main/java/a").toNIO
)
for {
_ <- server.initialize("""
|/metals.json
|{
| "a": { }
|}
""".stripMargin)
_ = workspace
.resolve("a/src/main/java/a/Main.java")
.toFile
.createNewFile()
_ <- server.didOpen("a/src/main/java/a/Main.java")
_ = assertNoDiff(
workspace.resolve("a/src/main/java/a/Main.java").readText,
""
)
} yield ()
}
}

0 comments on commit db4cbce

Please sign in to comment.