Skip to content

Commit f877018

Browse files
committed
Add language server implementation
1 parent 9140599 commit f877018

File tree

8 files changed

+979
-7
lines changed

8 files changed

+979
-7
lines changed

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ libraryDependencies ++= {
7777
}
7878
}
7979

80+
libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.12.0"
81+
8082
////////////////////////////////////////////////////////////////////////////////
8183
// Testing dependencies
8284
////////////////////////////////////////////////////////////////////////////////

src/main/antlr4/AlogicLexer.g4

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99

1010
lexer grammar AlogicLexer;
1111

12-
fragment LCMT: '//' ~[\n]* ; // Line comment
13-
fragment BCMT: '/*' .*? '*/'; // Block comment
14-
CMT: (LCMT | BCMT) -> channel(HIDDEN) ; // Any comment
12+
channels {
13+
COMMENT
14+
}
15+
16+
fragment LCMT: '//' ~[\n]* ; // Line comment
17+
fragment BCMT: '/*' .*? '*/'; // Block comment
18+
CMT: (LCMT | BCMT) -> channel(COMMENT) ; // Any comment
1519

1620
UINTTYPE: 'u' [0-9]+;
1721

src/main/scala/com/argondesign/alogic/antlr/AlogicTokenFactory.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ class AlogicTokenFactory(val alogicSource: Source, mb: MessageBuffer) extends To
5555
line: Int,
5656
charPositionInLine: Int
5757
): Token = {
58-
require(channel == Token.DEFAULT_CHANNEL || channel == Token.HIDDEN_CHANNEL)
58+
require(
59+
channel == Token.DEFAULT_CHANNEL || channel == Token.HIDDEN_CHANNEL || channel == AlogicLexer.COMMENT
60+
)
5961
require(source.getItem1.isInstanceOf[AlogicLexer])
6062

6163
def mkToken(channel: Int): AlogicToken = {
@@ -69,15 +71,15 @@ class AlogicTokenFactory(val alogicSource: Source, mb: MessageBuffer) extends To
6971
token
7072
}
7173
// Creates normal token passed to the parser
72-
def normalToken: AlogicToken = mkToken(Token.DEFAULT_CHANNEL)
74+
def normalToken: AlogicToken = mkToken(channel)
7375
// Creates hidden token not passed to the parser
7476
def hiddenToken: AlogicToken = mkToken(Token.HIDDEN_CHANNEL)
7577

7678
if (channel == Token.HIDDEN_CHANNEL) {
7779
// Hidden tokens, nothing special
7880
hiddenToken
79-
} else if (kind == Token.EOF) {
80-
// Pass through EOF
81+
} else if (kind == Token.EOF || channel == AlogicLexer.COMMENT) {
82+
// Pass through EOF and comments
8183
normalToken
8284
} else {
8385
// #line state machine
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// Copyright (c) 2017-2021 Argon Design Ltd. All rights reserved.
3+
// This file is covered by the BSD (with attribution) license.
4+
// See the LICENSE file for the precise wording of the license.
5+
//
6+
// DESCRIPTION:
7+
// Language server implementation
8+
////////////////////////////////////////////////////////////////////////////////
9+
10+
package com.argondesign.alogic.lsp
11+
12+
import com.argondesign.alogic.Compiler
13+
import com.argondesign.alogic.core.CompilerContext
14+
import com.argondesign.alogic.core.Loc
15+
import com.argondesign.alogic.core.MessageBuffer
16+
import com.argondesign.alogic.core.Messages._
17+
import com.argondesign.alogic.core.Source
18+
import com.argondesign.alogic.frontend.Frontend
19+
import com.google.gson.JsonObject
20+
import org.eclipse.lsp4j._
21+
import org.eclipse.lsp4j.services._
22+
23+
import java.io.File
24+
import java.io.PrintWriter
25+
import java.util.concurrent.CompletableFuture
26+
import scala.concurrent.ExecutionContext.Implicits.global
27+
import scala.jdk.CollectionConverters._
28+
import scala.jdk.FutureConverters._
29+
import scala.util.Failure
30+
import scala.util.Success
31+
32+
class AlogicLanguageServer extends LanguageServer with LanguageClientAware {
33+
34+
private var client: LanguageClient = null
35+
36+
var workspaceFolders: Seq[WorkspaceFolder] = Nil
37+
var extraCommandLineOpts: Seq[String] = Nil
38+
39+
def initialize(x: InitializeParams): CompletableFuture[InitializeResult] = {
40+
workspaceFolders = x.getWorkspaceFolders().asScala.toList
41+
val capabilities = new ServerCapabilities();
42+
capabilities.setTextDocumentSync(TextDocumentSyncKind.Full);
43+
val semTokenLegend = new SemanticTokensLegend(
44+
SemanticTokenType.values.toList.map(_.toString).asJava,
45+
SemanticTokenModifier.values.toList.map(_.toString).asJava
46+
)
47+
capabilities.setSemanticTokensProvider(
48+
new SemanticTokensWithRegistrationOptions(
49+
semTokenLegend,
50+
new SemanticTokensServerFull(false),
51+
false,
52+
List(new DocumentFilter("alogic", "", "")).asJava
53+
)
54+
)
55+
CompletableFuture.completedFuture(new InitializeResult(capabilities))
56+
}
57+
58+
def shutdown(): CompletableFuture[Object] = {
59+
CompletableFuture.completedFuture(null)
60+
}
61+
62+
def exit() = {}
63+
64+
val fullTextDocumentService = new FullTextDocumentService() {
65+
66+
override def didChange(params: DidChangeTextDocumentParams) = {
67+
super.didChange(params);
68+
validateDocument(documents(params.getTextDocument().getUri()))
69+
}
70+
71+
override def didOpen(params: DidOpenTextDocumentParams) = {
72+
super.didOpen(params)
73+
validateDocument(documents(params.getTextDocument().getUri()))
74+
}
75+
76+
}
77+
78+
def getTextDocumentService(): TextDocumentService = {
79+
fullTextDocumentService
80+
}
81+
82+
def getWorkspaceService(): WorkspaceService = {
83+
new WorkspaceService() {
84+
override def symbol(params: WorkspaceSymbolParams) = {
85+
null;
86+
}
87+
88+
def didChangeConfiguration(params: DidChangeConfigurationParams) = {
89+
val gloabalSettings = params.getSettings.asInstanceOf[JsonObject]
90+
extraCommandLineOpts = gloabalSettings
91+
.getAsJsonObject("alogic-lang")
92+
.getAsJsonArray("extraCommandLineOpts")
93+
.asScala
94+
.map(_.getAsString)
95+
.toSeq
96+
client.workspaceFolders.asScala andThen {
97+
case Success(folders) => {
98+
val perWSOpts = gloabalSettings
99+
.getAsJsonObject("alogic-lang")
100+
.getAsJsonArray("perWorkspaceCmdOpts")
101+
.asScala
102+
.map(_.getAsString)
103+
.toSeq
104+
val prefix = "file://"
105+
extraCommandLineOpts =
106+
extraCommandLineOpts ++ folders.asScala.filter(_.getUri.startsWith(prefix)).flatMap {
107+
folder =>
108+
perWSOpts map {
109+
_.replaceAll("\\$\\{workspaceFolder}", folder.getUri.substring(prefix.length))
110+
}
111+
}
112+
fullTextDocumentService.documents.values.foreach(validateDocument)
113+
}
114+
case Failure(e) => e.printStackTrace
115+
}
116+
}
117+
118+
def didChangeWatchedFiles(params: DidChangeWatchedFilesParams) = {}
119+
}
120+
}
121+
122+
override def connect(client: LanguageClient) = {
123+
this.client = client
124+
}
125+
126+
private def validateDocument(document: TextDocumentItem) = {
127+
val prefix = "file://"
128+
var tempFile: File = null
129+
val path = if (document.getUri.startsWith(prefix)) {
130+
document.getUri.substring(prefix.length)
131+
} else {
132+
tempFile = File.createTempFile("alogic-lang-", ".alogic")
133+
new PrintWriter(tempFile) {
134+
try {
135+
write(document.getText)
136+
} finally {
137+
close()
138+
}
139+
}
140+
tempFile.getAbsolutePath
141+
}
142+
143+
val source = Source(path, document.getText)
144+
145+
val mb = new MessageBuffer
146+
147+
Compiler.parseArgs(
148+
mb,
149+
extraCommandLineOpts :++ List("-o", System.getProperty("java.io.tmpdir"), path),
150+
None
151+
) match {
152+
case Some((settings, _, params)) => {
153+
154+
implicit val cc = new CompilerContext(mb, settings)
155+
val fe = new Frontend
156+
fe(source, Loc(document.getUri, 1, source, 0, 0, 0), Nil)
157+
158+
val sources = mb.messages.map(_.loc.source.path).toSet
159+
sources.foreach(src => {
160+
val uri = if (src == path) document.getUri else "file://" + src
161+
client.publishDiagnostics(
162+
new PublishDiagnosticsParams(
163+
uri,
164+
mb.messages
165+
.filter(_.loc.source.path == src)
166+
.map(msg => {
167+
val startLineOffset =
168+
msg.loc.source.offsetFor(msg.loc.source.lineFor(msg.loc.start))
169+
new Diagnostic(
170+
new Range(
171+
new Position(msg.loc.line - 1, msg.loc.start - startLineOffset),
172+
new Position(msg.loc.line - 1, msg.loc.end - startLineOffset)
173+
),
174+
msg.msg.mkString("\n"),
175+
msg.category match {
176+
case WarningCategory => DiagnosticSeverity.Warning
177+
case ErrorCategory => DiagnosticSeverity.Error
178+
case NoteCategory => DiagnosticSeverity.Information
179+
case FatalCategory => DiagnosticSeverity.Error
180+
case IceCategory => DiagnosticSeverity.Error
181+
},
182+
"alogic-lang"
183+
)
184+
})
185+
.asJava
186+
)
187+
)
188+
})
189+
// Push empty diagnostics for current file if not in sources
190+
if (!sources.contains(path)) {
191+
client.publishDiagnostics(new PublishDiagnosticsParams(document.getUri, Nil.asJava))
192+
}
193+
194+
}
195+
case None => {
196+
client.showMessage(new MessageParams(MessageType.Error, "Command line parsing failed"))
197+
println((extraCommandLineOpts :+ path).mkString("\n"))
198+
mb.messages.foreach(msg => println(msg.msg.mkString("\n")))
199+
}
200+
}
201+
if (tempFile != null) {
202+
tempFile.delete
203+
}
204+
}
205+
206+
}

0 commit comments

Comments
 (0)