Skip to content

Commit 7c5305b

Browse files
authored
Merge pull request #201 from viperproject/sem-highlight
Add more LSP features
2 parents 5cd18bd + 088161f commit 7c5305b

43 files changed

Lines changed: 3741 additions & 590 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ ter.bat
88
ver.bat
99
.bsp/
1010
logs/
11+
.bloop
12+
.metals
13+
metals.sbt

build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ lazy val server = (project in file("."))
1818
// General settings
1919
name := "ViperServer",
2020
organization := "viper",
21-
version := "2.0.0", // has to be a proper semver
21+
version := "3.0.0", // has to be a proper semver
2222

2323
// Fork test to a different JVM than SBT's, avoiding SBT's classpath interfering with
2424
// classpath used by Scala's reflection.
@@ -30,7 +30,7 @@ lazy val server = (project in file("."))
3030
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.6.10",
3131
libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % "2.6.10" % Test,
3232
libraryDependencies += "com.typesafe.akka" %% "akka-http-testkit" % "10.2.1" % Test,
33-
libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.15.0", // Java implementation of language server protocol
33+
libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.20.1", // Java implementation of language server protocol
3434

3535
silicon / excludeFilter := "logback.xml", /* Ignore Silicon's Logback configuration */
3636
carbon / excludeFilter := "logback.xml", /* Ignore Carbon's Logback configuration */

src/main/scala/viper/server/core/AstWorker.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import viper.server.ViperConfig
1111
import viper.server.utility.AstGenerator
1212
import viper.server.vsi.AstConstructionException
1313
import viper.silver.ast.Program
14+
import viper.silver.ast.utility.FileLoader
1415
import viper.silver.reporter.{Entity, ExceptionReport}
1516
import viper.silver.verifier.VerificationResult
1617

@@ -26,7 +27,8 @@ case class ServerCrashException(e: Throwable) extends Exception(e)
2627

2728
class AstWorker(val arg_list: List[String],
2829
override val logger: Logger,
29-
private val config: ViperConfig
30+
private val config: ViperConfig,
31+
private val loader: Option[FileLoader]
3032
)(override val executor: VerificationExecutionContext)
3133
extends MessageReportingTask[Option[Program]] {
3234

@@ -37,7 +39,7 @@ class AstWorker(val arg_list: List[String],
3739
val astGen = new AstGenerator(logger, reporter, arg_list, disablePlugins = config.disablePlugins())
3840

3941
val ast_option: Option[Program] = try {
40-
astGen.generateViperAst(file)
42+
astGen.generateViperAst(file, loader)
4143
} catch {
4244
case _: java.nio.file.NoSuchFileException =>
4345
logger.error(s"The file ($file) for which verification has been requested was not found.")

src/main/scala/viper/server/core/ViperCoreServer.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ch.qos.logback.classic.Logger
1313
import viper.server.ViperConfig
1414
import viper.server.vsi.{AstHandle, AstJobId, VerJobId, VerificationServer}
1515
import viper.silver.ast.Program
16+
import viper.silver.ast.utility.FileLoader
1617
import viper.silver.logger.ViperLogger
1718

1819
import scala.concurrent.duration._
@@ -58,10 +59,10 @@ abstract class ViperCoreServer(val config: ViperConfig)(implicit val executor: V
5859
}
5960
}
6061

61-
def requestAst(arg_list: List[String], localLogger: Option[Logger] = None): AstJobId = {
62+
def requestAst(arg_list: List[String], localLogger: Option[Logger] = None, loader: Option[FileLoader] = None): AstJobId = {
6263
require(config != null)
6364
val logger = combineLoggers(localLogger)
64-
val task_backend = new AstWorker(arg_list, logger, config)(executor)
65+
val task_backend = new AstWorker(arg_list, logger, config, loader)(executor)
6566
val ast_id = initializeAstConstruction(task_backend)
6667

6768
if (ast_id.id >= 0) {
@@ -72,6 +73,7 @@ abstract class ViperCoreServer(val config: ViperConfig)(implicit val executor: V
7273
}
7374
ast_id
7475
}
76+
def discardAstJobOnCompletion(jid: AstJobId, jobActor: ActorRef): Unit = discardAstOnCompletion(jid, jobActor)
7577

7678
def verifyWithAstJob(programId: String, ast_id: AstJobId, backend_config: ViperBackendConfig, localLogger: Option[Logger] = None): VerJobId = {
7779
val logger = combineLoggers(localLogger)
@@ -123,7 +125,11 @@ abstract class ViperCoreServer(val config: ViperConfig)(implicit val executor: V
123125
ver_id
124126
}
125127

126-
override def streamMessages(jid: VerJobId, clientActor: ActorRef): Option[Future[Done]] = {
128+
override def streamMessages(jid: VerJobId, clientActor: ActorRef, full: Boolean): Option[Future[Done]] = {
129+
globalLogger.info(s"Streaming results for job #${jid.id}.")
130+
super.streamMessages(jid, clientActor, full)
131+
}
132+
override def streamMessages(jid: AstJobId, clientActor: ActorRef): Option[Future[Done]] = {
127133
globalLogger.info(s"Streaming results for job #${jid.id}.")
128134
super.streamMessages(jid, clientActor)
129135
}

src/main/scala/viper/server/core/ViperCoreServerUtils.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ object ViperCoreServerUtils {
5656
import scala.language.postfixOps
5757

5858
val actor = executor.actorSystem.actorOf(SeqActor.props(jid, core.globalLogger))
59-
val complete_future = core.streamMessages(jid, actor).getOrElse(Future.failed(JobNotFoundException))
59+
val complete_future = core.streamMessages(jid, actor, true).getOrElse(Future.failed(JobNotFoundException))
6060
complete_future.flatMap(_ => {
6161
implicit val askTimeout: Timeout = Timeout(core.config.actorCommunicationTimeout() milliseconds)
6262
(actor ? SeqActor.Result).mapTo[Future[List[Message]]].flatten

src/main/scala/viper/server/frontends/http/jsonWriters/ViperIDEProtocol.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ object ViperIDEProtocol extends akka.http.scaladsl.marshallers.sprayjson.SprayJs
493493
case q: QuantifierInstantiationsMessage => q.toJson
494494
case q: QuantifierChosenTriggersMessage => q.toJson
495495
case v: VerificationTerminationMessage => v.toJson
496+
case p: PProgramReport => p.semanticAnalysisSuccess.toJson
497+
case w: WarningsDuringVerification => w.toJson
496498
}))
497499
})
498500

src/main/scala/viper/server/frontends/lsp/ClientCoordinator.scala

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
package viper.server.frontends.lsp
88

99
import ch.qos.logback.classic.Logger
10-
import org.eclipse.lsp4j.DocumentSymbol
10+
import org.eclipse.lsp4j.Range
1111
import viper.server.core.VerificationExecutionContext
1212
import viper.server.frontends.lsp.VerificationState.Ready
1313

1414
import java.util.concurrent.{ConcurrentHashMap, ConcurrentMap}
15-
import scala.collection.mutable.ArrayBuffer
1615
import scala.concurrent.Future
1716
import scala.jdk.CollectionConverters._
1817
import scala.jdk.FutureConverters._
18+
import viper.server.frontends.lsp.file.FileManager
19+
import viper.server.frontends.lsp.file.VerificationManager
1920

2021
/** manages per-client state and interacts with the server instance (which is shared among all clients) */
2122
class ClientCoordinator(val server: ViperServerService)(implicit executor: VerificationExecutionContext) {
@@ -46,59 +47,41 @@ class ClientCoordinator(val server: ViperServerService)(implicit executor: Verif
4647
!_exited
4748
}
4849

49-
def addFileIfNecessary(uri: String): Unit = {
50-
logger.trace(s"Adding FileManager for $uri if it does not exist yet")
51-
val coordinator = this
52-
val createFMFunc = new java.util.function.Function[String, FileManager]() {
53-
override def apply(t: String): FileManager = {
54-
logger.trace(s"FileManager created for $uri")
55-
new FileManager(coordinator, uri)
56-
}
50+
def closeFile(uri: String): Unit = {
51+
val toRemove = Option(_files.get(uri)).map(fm => {
52+
fm.isOpen = false
53+
fm.removeDiagnostics()
54+
fm.isRoot
55+
}).getOrElse(false)
56+
if (toRemove) {
57+
logger.trace(s"Removing FileManager for $uri")
58+
_files.remove(uri)
5759
}
58-
// we use `computeIfAbsent` instead of `putIfAbsent` such that a new FileManager is only created if it's absent
59-
_files.computeIfAbsent(uri, createFMFunc)
60-
}
61-
62-
def removeFileIfExists(uri: String): Unit = {
63-
logger.trace(s"Removing FileManager for $uri")
64-
_files.remove(uri)
65-
}
66-
67-
/** clears definitions and symbols associated with a file */
68-
def resetFile(uri: String): Unit = {
69-
Option(_files.get(uri))
70-
.foreach(fm => {
71-
fm.symbolInformation = ArrayBuffer.empty
72-
fm.definitions = ArrayBuffer.empty
73-
})
7460
}
7561

7662
def resetDiagnostics(uri: String): Unit = {
77-
Option(_files.get(uri))
78-
.foreach(fm => fm.resetDiagnostics())
63+
getFileManager(uri).removeDiagnostics()
7964
}
8065

81-
def getSymbolsForFile(uri: String): Array[DocumentSymbol]= {
82-
Option(_files.get(uri))
83-
.map(fm => fm.symbolInformation.toArray)
84-
.getOrElse(Array.empty)
85-
}
86-
87-
def getDefinitionsForFile(uri: String): ArrayBuffer[Definition] = {
88-
Option(_files.get(uri))
89-
.map(fm => fm.definitions)
90-
.getOrElse(ArrayBuffer.empty)
66+
def handleChange(uri: String, range: Range, text: String): Unit = {
67+
val fm = getFileManager(uri)
68+
fm.synchronized {
69+
fm.handleContentChange(range, text)
70+
}
9171
}
9272

9373
/** Checks if verification can be started for a given file.
9474
*
9575
* Informs client differently depending on whether or not verification attempt was triggered manually
9676
* */
97-
def canVerificationBeStarted(uri: String, manuallyTriggered: Boolean): Boolean = {
77+
def canVerificationBeStarted(uri: String, content: String, manuallyTriggered: Boolean): Boolean = {
9878
logger.trace("canVerificationBeStarted")
9979
if (server.isRunning) {
10080
logger.trace("server is running")
101-
addFileIfNecessary(uri)
81+
// This should only be necessary if one wants to verify a closed file for some reason
82+
val fm = getFileManager(uri, Some(content))
83+
// This will be the new project root
84+
makeEmptyRoot(fm)
10285
true
10386
} else {
10487
logger.trace("server is not running")
@@ -111,16 +94,15 @@ class ClientCoordinator(val server: ViperServerService)(implicit executor: Verif
11194
}
11295

11396
def stopRunningVerification(uri: String): Future[Boolean] = {
114-
Option(_files.get(uri))
115-
.map(fm => fm.stopVerification()
116-
.map(_ => {
117-
logger.trace(s"stopVerification has completed for ${fm.uri}")
118-
val params = StateChangeParams(Ready.id, verificationCompleted = 0, verificationNeeded = 0, uri = uri)
119-
sendStateChangeNotification(params, Some(fm))
120-
true
121-
})
122-
.recover(_ => false))
123-
.getOrElse(Future.successful(false))
97+
val fm = getFileManager(uri)
98+
fm.stop()
99+
.map(_ => {
100+
logger.trace(s"stopVerification has completed for ${fm.file.uri}")
101+
val params = StateChangeParams(Ready.id, verificationCompleted = 0, verificationNeeded = 0, uri = uri)
102+
sendStateChangeNotification(params, Some(fm))
103+
true
104+
})
105+
.recover(_ => false)
124106
}
125107

126108
/** Stops all running verifications.
@@ -130,18 +112,26 @@ class ClientCoordinator(val server: ViperServerService)(implicit executor: Verif
130112
* */
131113
def stopAllRunningVerifications(): Future[Unit] = {
132114
val tasks = _files.values().asScala.map(fm =>
133-
fm.stopVerification().map(_ => {
134-
logger.trace(s"stopVerification has completed for ${fm.uri}")
115+
fm.stop().map(_ => {
116+
logger.trace(s"stopVerification has completed for ${fm.file.uri}")
135117
}))
136118
Future.sequence(tasks).map(_ => {
137119
logger.debug("all running verifications have been stopped")
138120
})
139121
}
140122

141123
/** returns true if verification was started */
142-
def startVerification(backendClassName: String, customArgs: String, uri: String, manuallyTriggered: Boolean): Boolean = {
143-
Option(_files.get(uri))
144-
.exists(fm => fm.startVerification(backendClassName, customArgs, manuallyTriggered))
124+
def startVerification(backendClassName: String, customArgs: String, uri: String, manuallyTriggered: Boolean): Future[Boolean] = {
125+
val fm = getFileManager(uri)
126+
fm.startVerification(backendClassName, customArgs, fm.content, manuallyTriggered)
127+
}
128+
129+
/** returns true if parse/typecheck was started */
130+
def startParseTypecheck(uri: String): Boolean = {
131+
val fm = getFileManager(uri)
132+
val project = Option(_files.get(uri)).flatMap(_.projectRoot).getOrElse(uri)
133+
val root = getFileManager(project)
134+
root.runParseTypecheck(fm.content)
145135
}
146136

147137
/** flushes verification cache, optionally only for a particular file */
@@ -173,9 +163,9 @@ class ClientCoordinator(val server: ViperServerService)(implicit executor: Verif
173163
*
174164
* If state change is related to a particular file, its manager's state is also updated.
175165
* */
176-
def sendStateChangeNotification(params: StateChangeParams, task: Option[FileManager]): Unit = {
166+
def sendStateChangeNotification(params: StateChangeParams, task: Option[VerificationManager]): Unit = {
177167
// update file manager's state:
178-
task.foreach(fm => fm.state = VerificationState(params.newState))
168+
task.foreach(vm => vm.state = VerificationState(params.newState))
179169
try {
180170
client.notifyStateChanged(params)
181171
} catch {
@@ -204,4 +194,48 @@ class ClientCoordinator(val server: ViperServerService)(implicit executor: Verif
204194
if (!isAlive) return
205195
client.notifyHint(HintMessage(message, showSettingsButton, showViperToolsUpdateButton ))
206196
}
197+
198+
private def getFileManager(uri: String, content: Option[String] = None): FileManager = {
199+
var createdNew = false
200+
val coordinator = this
201+
val createFMFunc = new java.util.function.Function[String, FileManager]() {
202+
override def apply(t: String): FileManager = {
203+
logger.trace(s"FileManager created for $uri")
204+
createdNew = true
205+
FileManager(uri, coordinator, content)
206+
}
207+
}
208+
// we use `computeIfAbsent` instead of `putIfAbsent` such that a new FileManager is only created if it's absent
209+
val fm = _files.computeIfAbsent(uri, createFMFunc)
210+
// Override the content if we are given one and the file manager was not just created
211+
if (!createdNew && content.isDefined) fm.content.set(content.get)
212+
fm
213+
}
214+
def ensureFmExists(uri: String, content: String): FileManager = {
215+
getFileManager(uri, Some(content))
216+
}
217+
def getRoot(uri: String): FileManager = {
218+
val fm = getFileManager(uri)
219+
fm.projectRoot.map(getFileManager(_)).getOrElse(fm)
220+
}
221+
222+
///////////////////////
223+
// Project management
224+
///////////////////////
225+
226+
def addToProject(uri: String, root: String, getContents: Boolean): (Option[String], Option[Set[String]]) = {
227+
getFileManager(uri).addToProject(root, getContents)
228+
}
229+
def removeFromProject(uri: String, root: String) = {
230+
Option(_files.get(uri)).map(_.removeFromProject(root))
231+
}
232+
def makeEmptyRoot(fm: FileManager) = {
233+
for (leaves <- fm.projectLeaves; leaf <- leaves) {
234+
removeFromProject(leaf, fm.file_uri)
235+
}
236+
fm.project = Left(Map())
237+
}
238+
def handleChangeInLeaf(root: String, leaf: String, range: Range, text: String): Unit = {
239+
Option(_files.get(root)).map(_.handleChangeInLeaf(leaf, range, text))
240+
}
207241
}

src/main/scala/viper/server/frontends/lsp/CommandProtocol.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ object C2S_Commands {
2121
// final val GetExecutionTrace = "GetExecutionTrace"
2222
final val RemoveDiagnostics = "RemoveDiagnostics"
2323
final val GetViperFileEndings = "GetViperFileEndings"
24+
final val SetupProject = "SetupProject"
2425
final val FlushCache = "FlushCache"
2526
final val GetIdentifier = "GetIdentifier"
27+
final val GetRange = "GetRange"
2628
}
2729

2830
object S2C_Commands {

0 commit comments

Comments
 (0)