Skip to content

Commit

Permalink
Implement basic worksheet mode using mdoc and text decorations (#1041)
Browse files Browse the repository at this point in the history
Implement basic worksheet mode using mdoc and text decorations
  • Loading branch information
olafurpg committed Nov 10, 2019
2 parents ae6f677 + a2902a6 commit 90bc490
Show file tree
Hide file tree
Showing 40 changed files with 1,209 additions and 126 deletions.
2 changes: 2 additions & 0 deletions build.sbt
Expand Up @@ -147,6 +147,7 @@ lazy val V = new {
val gradleBloop = bloop
val mavenBloop = bloop
val scalafmt = "2.0.1"
val mdoc = "2.0.0"
// List of supported Scala versions in SemanticDB. Needs to be manually updated
// for every SemanticDB upgrade.
def supportedScalaVersions =
Expand Down Expand Up @@ -261,6 +262,7 @@ lazy val metals = project
// Scala dependencies
// ==================
"org.scala-lang.modules" %% "scala-java8-compat" % "0.9.0",
"org.scalameta" %% "mdoc" % V.mdoc,
"org.scalameta" %% "scalafmt-dynamic" % V.scalafmt,
// For reading classpaths.
// for fetching ch.epfl.scala:bloop-frontend and other library dependencies
Expand Down
115 changes: 115 additions & 0 deletions docs/editors/decoration-protocol.md
@@ -0,0 +1,115 @@
---
id: decoration-protocol
sidebar_label: Decoration Protocol
title: Decoration Protocol v0.1.0
---

Metals implements a Language Server Protocol extension called the "Decoration
Protocol" to display non-editable text in the text editor.

## Base data structures

The Decoration Protocol has several base data structures that are mostly derived
from the [VS Code API](https://code.visualstudio.com/api/references/vscode-api).

### Decoration

A "decoration" represents non-editable code that is display in the text editor
alongside editable code. The GIF below demonstrates an example of green
decorations that are formatted as comments, which contain the evaluated code.

![Example decoration](https://user-images.githubusercontent.com/1408093/68091453-bacbea00-fe77-11e9-80b9-52a9bbd6d98a.gif)

Although decorations appear as text inside the editor, they can't be edited by
the user. In the GIF above, observe that the decoration can optionally include a
message that's displayed on mouse hover.

## DecorationOptions

```ts
export interface DecorationOptions {
/**
* The range position to place the decoration.
* The Range data structure is defined in the Language Server Protocol.
*/
range: Range;
/**
* The text to display when the mouse hovers over the decoration.
* The MarkedString data structure is defined in the LanguageServerProtocol
*/
hoverMessage?: MarkedString;
/** The URI of the text document to place text decorations */
renderOptions: ThemableDecorationInstanceRenderOption;
}
```

## ThemableDecorationInstanceRenderOption

```ts
export interface ThemableDecorationInstanceRenderOption {
/** The decoration to display next to the given range. */
after?: ThemableDecorationAttachmentRenderOptions;
}
```

## ThemableDecorationAttachmentRenderOptions

```ts
export interface ThemableDecorationAttachmentRenderOptions {
/** The text to display in the decoration */
contentText?: string;
/** The color of `contentText`. More colors may be added in the future. */
color?: "green";
/** The font style to use for displaying `contentText. More styles may be added in the future. */
fontStyle?: "italic";
}
```

## Endpoints

The Decoration Protocol is embedded inside the Language Server Protocol and
consists of a single JSON-RPC notification.

### `initialize`

The Decoration Protocol is only enabled when both the client and server declare
support for the protocol by adding an `decorationProvider: true` field to the
experimental section of the server and client capabilities in the `initialize`
response.

```json
{
"capabilities": {
"experimental": {
"decorationProvider": true
}
}
}
```

### `metals/publishDecorations`

The decoration ranges did change notification is sent from the server to the
client to notify that decorations have changes for a given text document.

_Notification_:

- method: `metals/publishDecorations`
- params: `PublishDecorationsParams` as defined below:

```ts
export interface PublishDecorationsParams {
/** The URI of the text document to place text decorations */
uri: string;

/**
* The ranges to publish for this given document.
* Use empty list to clear all decorations.
*/
options: DecorationOptions[];
}
```

## Changelog

- v0.1.0: First release with basic support for worksheets.
11 changes: 11 additions & 0 deletions docs/editors/new-editor.md
Expand Up @@ -347,6 +347,11 @@ To enable Metals extensions, start the main process with the system property
Metals implements several custom JSON-RPC endpoints related to rendering tree
views in the editor client, the [Tree View Protocol](tree-view-protocol.md).

### Decoration Protocol

Metals implements an LSP extension to display non-editable text in the editor,
see the [Decoration Protocol](decoration-protocol.md).

### `metals/slowTask`

The Metals slow task request is sent from the server to the client to notify the
Expand All @@ -369,6 +374,12 @@ _Request_:
interface MetalsSlowTaskParams {
/** The name of this slow task */
message: string;
/**
* If true, the log output from this task does not need to be displayed to the user.
*
* In VS Code, the Metals "Output channel" is not toggled when this flag is true.
*/
quietLogs?: boolean;
}
```

Expand Down
@@ -0,0 +1,18 @@
package scala.meta.internal.async
import java.util.concurrent.ConcurrentLinkedQueue
import scala.collection.mutable

object ConcurrentQueue {

/** Returns all elements in the queue and empties the queue */
def pollAll[T](queue: ConcurrentLinkedQueue[T]): List[T] = {
val buffer = mutable.ListBuffer.empty[T]
var elem = queue.poll()
while (elem != null) {
buffer += elem
elem = queue.poll()
}
buffer.toList
}

}
@@ -0,0 +1,24 @@
package scala.meta.internal.decorations

import javax.annotation.Nullable
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.MarkedString

trait DecorationClient {
@JsonNotification("metals/publishDecorations")
def metalsPublishDecorations(
params: PublishDecorationsParams
): Unit
}

case class DecorationOptions(
range: Range,
@Nullable hoverMessage: MarkedString = null,
@Nullable renderOptions: ThemableDecorationInstanceRenderOptions = null
)

case class PublishDecorationsParams(
uri: String,
options: Array[DecorationOptions]
)
@@ -0,0 +1,21 @@
package scala.meta.internal.decorations

import javax.annotation.Nullable

case class ThemableDecorationAttachmentRenderOptions(
@Nullable contentText: String = null,
@Nullable contentIconPath: String = null,
@Nullable border: String = null,
@Nullable borderColor: String = null,
@Nullable fontStyle: String = null,
@Nullable fontWeight: String = null,
@Nullable textDecoration: String = null,
@Nullable color: String = null,
@Nullable backgroundColor: String = null,
@Nullable margin: String = null,
@Nullable width: String = null,
@Nullable height: String = null,
@Nullable opacity: java.lang.Double = null,
@Nullable light: ThemableDecorationAttachmentRenderOptions = null,
@Nullable dark: ThemableDecorationAttachmentRenderOptions = null
)
@@ -0,0 +1,10 @@
package scala.meta.internal.decorations

import javax.annotation.Nullable

case class ThemableDecorationInstanceRenderOptions(
@Nullable before: ThemableDecorationAttachmentRenderOptions = null,
@Nullable after: ThemableDecorationAttachmentRenderOptions = null,
@Nullable light: ThemableDecorationInstanceRenderOptions = null,
@Nullable dark: ThemableDecorationInstanceRenderOptions = null
)
Expand Up @@ -37,7 +37,7 @@ final class GlobalClassTable(
for {
buildTargetId <- buildTargets.inverseSources(source)
scalaTarget <- buildTargets.scalaTarget(buildTargetId)
classpath = new Classpath(scalaTarget.classpath)
classpath = new Classpath(scalaTarget.jarClasspath)
} yield {
buildTargetsIndexes.getOrElseUpdate(
buildTargetId,
Expand Down
Expand Up @@ -3,11 +3,11 @@ package scala.meta.internal.metals
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import scala.collection.mutable.ListBuffer
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.util.control.NonFatal
import scala.meta.internal.async.ConcurrentQueue

/**
* Helper to batch multiple asynchronous requests and ensure only one request is active at a time.
Expand Down Expand Up @@ -60,13 +60,6 @@ final class BatchedFunction[A, B](

private val queue = new ConcurrentLinkedQueue[Request]()
private case class Request(arguments: Seq[A], result: Promise[B])
private def clearQueue(destination: ListBuffer[Request]): Unit = {
var request = queue.poll()
while (request != null) {
destination += request
request = queue.poll()
}
}

private val lock = new AtomicBoolean()
private def unlock(): Unit = {
Expand All @@ -90,9 +83,8 @@ final class BatchedFunction[A, B](
// - instantly if job queue is empty or unexpected exception
// - asynchronously once `fn` completes if job que is non-empty
// - all pending requests in job queue will be completed
val requests = ListBuffer.empty[Request]
val requests = ConcurrentQueue.pollAll(queue)
try {
clearQueue(requests)
if (requests.nonEmpty) {
val args = requests.flatMap(_.arguments)
val result = fn(args)
Expand Down
Expand Up @@ -3,15 +3,13 @@ import com.google.gson.JsonElement
import org.eclipse.{lsp4j => l}

final case class ClientExperimentalCapabilities(
debuggingProvider: java.lang.Boolean,
treeViewProvider: java.lang.Boolean
debuggingProvider: java.lang.Boolean = false,
treeViewProvider: java.lang.Boolean = false,
decorationProvider: java.lang.Boolean = false
)

object ClientExperimentalCapabilities {
val Default = new ClientExperimentalCapabilities(
debuggingProvider = false,
treeViewProvider = false
)
val Default = new ClientExperimentalCapabilities()

def from(
capabilities: l.ClientCapabilities
Expand Down
Expand Up @@ -16,7 +16,8 @@ final class Compilations(
workspace: () => AbsolutePath,
buildServer: () => Option[BuildServerConnection],
languageClient: MetalsLanguageClient,
isCurrentlyFocused: b.BuildTargetIdentifier => Boolean
isCurrentlyFocused: b.BuildTargetIdentifier => Boolean,
compileWorksheets: Seq[AbsolutePath] => Future[Unit]
)(implicit ec: ExecutionContext) {

// we are maintaining a separate queue for cascade compilation since those must happen ASAP
Expand Down Expand Up @@ -45,13 +46,19 @@ final class Compilations(

def compileFiles(paths: Seq[AbsolutePath]): Future[b.CompileResult] = {
val targets = expand(paths)
compileBatch(targets)
for {
result <- compileBatch(targets)
_ <- compileWorksheets(paths)
} yield result
}

def cascadeCompileFiles(paths: Seq[AbsolutePath]): Future[b.CompileResult] = {
val targets =
expand(paths).flatMap(buildTargets.inverseDependencies).distinct
cascadeBatch(targets)
for {
result <- cascadeBatch(targets)
_ <- compileWorksheets(paths)
} yield result
}

def cancel(): Unit = {
Expand All @@ -63,11 +70,11 @@ final class Compilations(
def isCompilable(path: AbsolutePath): Boolean =
path.isScalaOrJava && !path.isDependencySource(workspace())

val targets =
paths.filter(isCompilable).flatMap(buildTargets.inverseSources).distinct
val compilablePaths = paths.filter(isCompilable)
val targets = compilablePaths.flatMap(buildTargets.inverseSources).distinct

if (targets.isEmpty && paths.nonEmpty) {
scribe.warn(s"no build target for: ${paths.mkString("\n ")}")
if (targets.isEmpty && compilablePaths.nonEmpty) {
scribe.warn(s"no build target for: ${compilablePaths.mkString("\n ")}")
}

targets
Expand Down

0 comments on commit 90bc490

Please sign in to comment.