Skip to content

Commit 98734da

Browse files
Send syntax-highlighted HTML for bindings (#1096)
1 parent 7c3d951 commit 98734da

File tree

5 files changed

+169
-19
lines changed

5 files changed

+169
-19
lines changed

build.sbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ lazy val commonSettings = Seq(
3636
"-language:existentials",
3737
"-language:higherKinds",
3838
"-language:implicitConversions"
39+
),
40+
libraryDependencies ++= Seq(
41+
"org.scala-lang.modules" %%% "scala-xml" % "2.3.0"
3942
)
4043
)
4144

effekt/jvm/src/test/scala/effekt/LSPTests.scala

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,7 +1429,8 @@ class LSPTests extends FunSuite {
14291429
origin = BindingOrigin.Defined,
14301430
`type` = Some(
14311431
"Int"
1432-
)
1432+
),
1433+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">Int</span>")
14331434
)
14341435
)
14351436

@@ -1440,21 +1441,24 @@ class LSPTests extends FunSuite {
14401441
origin = BindingOrigin.Defined,
14411442
`type` = Some(
14421443
"String => Int"
1443-
)
1444+
),
1445+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">String</span> =&gt; <span class=\"effekt-ident pascal-case\">Int</span>"),
14441446
),
14451447
TermBinding(
14461448
qualifier = List(),
14471449
name = "foo",
14481450
origin = BindingOrigin.Defined,
14491451
`type` = Some(
14501452
"Int => Bool"
1451-
)
1453+
),
1454+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">Int</span> =&gt; <span class=\"effekt-ident pascal-case\">Bool</span>"),
14521455
),
14531456
TypeBinding(
14541457
qualifier = Nil,
14551458
name = "MyInt",
14561459
origin = BindingOrigin.Defined,
1457-
definition = "type MyInt = Int"
1460+
definition = "type MyInt = Int",
1461+
definitionHtml = "<span class=\"effekt-keyword\">type</span> <span class=\"effekt-ident pascal-case\">MyInt</span> = <span class=\"effekt-ident pascal-case\">Int</span>",
14581462
)
14591463
)
14601464

@@ -1576,7 +1580,8 @@ class LSPTests extends FunSuite {
15761580
qualifier = List(),
15771581
name = "bar",
15781582
origin = BindingOrigin.Defined,
1579-
`type` = Some("() => Nothing")
1583+
`type` = Some("() => Nothing"),
1584+
typeHtml = Some("() =&gt; <span class=\"effekt-ident pascal-case\">Nothing</span>")
15801585
)
15811586
)
15821587

@@ -1605,7 +1610,8 @@ class LSPTests extends FunSuite {
16051610
qualifier = List(),
16061611
name = "x",
16071612
origin = BindingOrigin.Defined,
1608-
`type` = Some("Int")
1613+
`type` = Some("Int"),
1614+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">Int</span>")
16091615
)
16101616
),
16111617
outer = Some(ScopeInfo(
@@ -1620,7 +1626,8 @@ class LSPTests extends FunSuite {
16201626
qualifier = List(),
16211627
name = "MyInt",
16221628
origin = BindingOrigin.Defined,
1623-
definition = "type MyInt = Int"
1629+
definition = "type MyInt = Int",
1630+
definitionHtml = "<span class=\"effekt-keyword\">type</span> <span class=\"effekt-ident pascal-case\">MyInt</span> = <span class=\"effekt-ident pascal-case\">Int</span>"
16241631
)),
16251632
outer = None
16261633
))
@@ -1654,6 +1661,7 @@ class LSPTests extends FunSuite {
16541661
| "name": "x",
16551662
| "origin": "Defined",
16561663
| "type": "Int",
1664+
| "typeHtml": "<span class=\"effekt-ident pascal-case\">Int</span>",
16571665
| "kind": "Term"
16581666
| }
16591667
| ],
@@ -1669,6 +1677,7 @@ class LSPTests extends FunSuite {
16691677
| "name": "MyInt",
16701678
| "origin": "Defined",
16711679
| "definition": "type MyInt = Int",
1680+
| "definitionHtml": "<span class=\"effekt-keyword\">type</span> <span class=\"effekt-ident pascal-case\">MyInt</span> = <span class=\"effekt-ident pascal-case\">Int</span>",
16721681
| "kind": "Type"
16731682
| }
16741683
| ]
@@ -1928,6 +1937,9 @@ class LSPTests extends FunSuite {
19281937
origin = "Defined",
19291938
definition = """type Foo1 {
19301939
def Foo1(theField: String): Foo1 / {}
1940+
}""",
1941+
definitionHtml = """<span class="effekt-keyword">type</span> <span class="effekt-ident pascal-case">Foo1</span> {
1942+
<span class="effekt-keyword">def</span> <span class="effekt-ident pascal-case">Foo1</span>(<span class="effekt-ident camel-case">theField</span>: <span class="effekt-ident pascal-case">String</span>): <span class="effekt-ident pascal-case">Foo1</span> / {}
19311943
}""",
19321944
kind = "Type"
19331945
),
@@ -1938,7 +1950,8 @@ class LSPTests extends FunSuite {
19381950
`type` = Some(
19391951
value = "String => Foo1"
19401952
),
1941-
kind = "Term"
1953+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">String</span> =&gt; <span class=\"effekt-ident pascal-case\">Foo1</span>"),
1954+
kind = "Term",
19421955
),
19431956
TermBinding(
19441957
qualifier = Nil,
@@ -1947,14 +1960,18 @@ class LSPTests extends FunSuite {
19471960
`type` = Some(
19481961
value = "Foo1 => String"
19491962
),
1950-
kind = "Term"
1963+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">Foo1</span> =&gt; <span class=\"effekt-ident pascal-case\">String</span>"),
1964+
kind = "Term",
19511965
),
19521966
TypeBinding(
19531967
qualifier = Nil,
19541968
name = "Foo2",
19551969
origin = "Defined",
19561970
definition = """type Foo2 {
19571971
def Foo2(theField: String): Foo2 / {}
1972+
}""",
1973+
definitionHtml = """<span class="effekt-keyword">type</span> <span class="effekt-ident pascal-case">Foo2</span> {
1974+
<span class="effekt-keyword">def</span> <span class="effekt-ident pascal-case">Foo2</span>(<span class="effekt-ident camel-case">theField</span>: <span class="effekt-ident pascal-case">String</span>): <span class="effekt-ident pascal-case">Foo2</span> / {}
19581975
}""",
19591976
kind = "Type"
19601977
),
@@ -1965,7 +1982,8 @@ class LSPTests extends FunSuite {
19651982
`type` = Some(
19661983
value = "String => Foo2"
19671984
),
1968-
kind = "Term"
1985+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">String</span> =&gt; <span class=\"effekt-ident pascal-case\">Foo2</span>"),
1986+
kind = "Term",
19691987
),
19701988
TermBinding(
19711989
qualifier = Nil,
@@ -1974,14 +1992,18 @@ class LSPTests extends FunSuite {
19741992
`type` = Some(
19751993
value = "Foo2 => String"
19761994
),
1977-
kind = "Term"
1995+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">Foo2</span> =&gt; <span class=\"effekt-ident pascal-case\">String</span>"),
1996+
kind = "Term",
19781997
),
19791998
TypeBinding(
19801999
qualifier = Nil,
19812000
name = "Bar",
19822001
origin = "Defined",
19832002
definition = """type Bar {
19842003
def Bar(theField: Int): Bar / {}
2004+
}""",
2005+
definitionHtml = """<span class="effekt-keyword">type</span> <span class="effekt-ident pascal-case">Bar</span> {
2006+
<span class="effekt-keyword">def</span> <span class="effekt-ident pascal-case">Bar</span>(<span class="effekt-ident camel-case">theField</span>: <span class="effekt-ident pascal-case">Int</span>): <span class="effekt-ident pascal-case">Bar</span> / {}
19852007
}""",
19862008
kind = "Type"
19872009
),
@@ -1992,7 +2014,8 @@ class LSPTests extends FunSuite {
19922014
`type` = Some(
19932015
value = "Int => Bar"
19942016
),
1995-
kind = "Term"
2017+
typeHtml = Some("<span class=\"effekt-ident pascal-case\">Int</span> =&gt; <span class=\"effekt-ident pascal-case\">Bar</span>"),
2018+
kind = "Term",
19962019
),
19972020
TermBinding(
19982021
qualifier = Nil,
@@ -2001,7 +2024,8 @@ class LSPTests extends FunSuite {
20012024
`type` = Some(
20022025
value = "() => Nothing"
20032026
),
2004-
kind = "Term"
2027+
typeHtml = Some("() =&gt; <span class=\"effekt-ident pascal-case\">Nothing</span>"),
2028+
kind = "Term",
20052029
)
20062030
)
20072031

effekt/shared/src/main/scala/effekt/Intelligence.scala

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import effekt.symbols.{CaptureSet, Hole}
77
import kiama.util.{Position, Source}
88
import effekt.symbols.scopes.Scope
99
import effekt.source.sourceOf
10+
import effekt.util.HtmlHighlight
1011
import effekt.source.Spans
1112

1213
trait Intelligence {
@@ -201,9 +202,21 @@ trait Intelligence {
201202

202203
sorted.flatMap((name, path, sym) => sym match {
203204
// TODO this is extremely hacky, printing is not defined for all types at the moment
204-
case sym: TypeSymbol => try { Some(TypeBinding(path, name, origin, DeclPrinter(sym))) } catch { case e => None }
205-
case sym: ValueSymbol => Some(TermBinding(path, name, origin, C.valueTypeOption(sym).map(t => pp"${t}")))
206-
case sym: BlockSymbol => Some(TermBinding(path, name, origin, C.blockTypeOption(sym).map(t => pp"${t}")))
205+
case sym: TypeSymbol => try {
206+
val definition = DeclPrinter(sym)
207+
val definitionHtml = HtmlHighlight(definition)
208+
Some(TypeBinding(path, name, origin, definition, definitionHtml))
209+
} catch { case e => None }
210+
case sym: ValueSymbol => {
211+
val `type` = C.valueTypeOption(sym).map(t => pp"${t}")
212+
val typeHtml = `type`.map(HtmlHighlight(_))
213+
Some(TermBinding(path, name, origin, `type`, typeHtml))
214+
}
215+
case sym: BlockSymbol => {
216+
val `type` = C.blockTypeOption(sym).map(t => pp"${t}")
217+
val typeHtml = `type`.map(HtmlHighlight(_))
218+
Some(TermBinding(path, name, origin, `type`, typeHtml))
219+
}
207220
}).toList
208221

209222
def allSymbols(origin: String, bindings: Bindings, path: List[String] = Nil)(using C: Context): Array[(String, List[String], TypeSymbol | TermSymbol)] = {
@@ -454,8 +467,22 @@ object Intelligence {
454467
val kind: String
455468
}
456469

457-
case class TermBinding(qualifier: List[String], name: String, origin: String, `type`: Option[String], kind: String = BindingKind.Term) extends BindingInfo
458-
case class TypeBinding(qualifier: List[String], name: String, origin: String, definition: String, kind: String = BindingKind.Type) extends BindingInfo
470+
case class TermBinding(
471+
qualifier: List[String],
472+
name: String,
473+
origin: String,
474+
`type`: Option[String],
475+
typeHtml: Option[String],
476+
kind: String = BindingKind.Term
477+
) extends BindingInfo
478+
case class TypeBinding(
479+
qualifier: List[String],
480+
name: String,
481+
origin: String,
482+
definition: String,
483+
definitionHtml: String,
484+
kind: String = BindingKind.Type
485+
) extends BindingInfo
459486

460487
// These need to be strings (rather than cases of an enum) so that they get serialized correctly
461488
object ScopeKind {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package effekt.util
2+
3+
import effekt.lexer.TokenKind.*
4+
import effekt.lexer.{Lexer, Token, TokenKind}
5+
import kiama.util.{Source, StringSource}
6+
7+
/**
8+
* Highlights Effekt code using HTML <span> tags.
9+
*
10+
* The generated output preserves the original input (including whitespace)
11+
* and escapes HTML special characters.
12+
*
13+
* Usage:
14+
* HtmlHighlight("val x = 3; return x + 1")
15+
*/
16+
object HtmlHighlight {
17+
private def htmlSpan(cls: String, text: String): String =
18+
s"<span class=\"$cls\">$text</span>"
19+
20+
// Highlighting
21+
// ------------
22+
23+
private def highlight(kind: TokenKind, raw: String): String = {
24+
val text = scala.xml.Utility.escape(raw)
25+
kind match {
26+
case EOF => ""
27+
case Error(_) => htmlSpan("effekt-error", text)
28+
29+
case Integer(_) => htmlSpan("effekt-number", text)
30+
case Float(_) => htmlSpan("effekt-number", text)
31+
32+
case Str(_, _) => htmlSpan("effekt-string", text)
33+
case HoleStr(_) => htmlSpan("effekt-string", text)
34+
case Chr(_) => htmlSpan("effekt-string", text)
35+
36+
case Ident(id) =>
37+
if (id.headOption.exists(_.isUpper))
38+
htmlSpan("effekt-ident pascal-case", text)
39+
else
40+
htmlSpan("effekt-ident camel-case", text)
41+
42+
case Comment(_) => htmlSpan("effekt-comment", text)
43+
case DocComment(_) => htmlSpan("effekt-comment", text)
44+
case Shebang(_) => htmlSpan("effekt-comment", text)
45+
46+
// keywords
47+
case
48+
`let` | `val` | `var` | `if` | `else` | `while` |
49+
`type` | `effect` | `interface` | `fun` | `do` | `case` | `with` | `try` |
50+
`true` | `false` |
51+
`match` | `def` | `module`| `import`| `export`| `extern`| `include`|
52+
`record`| `box`| `unbox`| `return`| `region`|
53+
`resource`| `new`| `and`| `is`| `namespace`| `pure`
54+
=> htmlSpan("effekt-keyword", text)
55+
56+
case _ => text
57+
}
58+
}
59+
60+
extension (token: Token) {
61+
def content(using src: Source): String = token.kind match {
62+
case EOF => ""
63+
case _ => src.content.substring(token.start, token.end + 1)
64+
}
65+
}
66+
67+
final def apply(input: String): String = {
68+
69+
given source: Source = StringSource(input)
70+
71+
val in = Lexer(source)
72+
73+
// to track whitespace (and other input skipped by the lexer)
74+
//
75+
// def foo() = 1 + 2
76+
// ^ ^
77+
// 2 4
78+
//
79+
// so we need to emit from offset 3 to 4 (non-inclusive).
80+
var previousOffset = -1
81+
82+
val out = new StringBuffer
83+
84+
while (in.hasNext) {
85+
val token = in.next()
86+
87+
if (token.start > previousOffset) {
88+
out.append(input.substring(previousOffset + 1, token.start))
89+
}
90+
previousOffset = token.end
91+
92+
out.append(highlight(token.kind, token.content))
93+
}
94+
out.toString
95+
}
96+
}

project/plugins.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")
33

44
// to generate a javascript file and run the compiler in the browser
5-
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0")
5+
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")
66

77
// to have separate projects for jvm and js details
88
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0")

0 commit comments

Comments
 (0)