Skip to content

Reimplement SyntaxHighlighter using Scanner and Parser #4537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from

Conversation

slothspot
Copy link
Contributor

@slothspot slothspot commented May 16, 2018

WORK IN PROGRESS

Problems

  • annotations are not supported
  • comments are not supported
  • tests are broken
  • list of keywords which should not be highlighted is not defined
  • something else I forgot

Nice parts:

Next steps:
fix tests and reach features parity with current highlighter implementation

@allanrenucci I would appreciate your feedback on the current state of this experiment and clarification on comments highlighting and list of keywords which shouldn't be highlighted

Copy link
Member

@dottybot dottybot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, and thank you for opening this PR! 🎉

All contributors have signed the CLA, thank you! ❤️

Have an awesome day! ☀️

Copy link
Contributor

@allanrenucci allanrenucci left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job! Once you're done, you can remove all the code from the old implementation


val sf = new SourceFile("<highlighting>", in.toCharArray)
val p = new Parser(sf)
val s = new Scanner(sf)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could use better names: s/sf/source, s/p/parser, s/s/scanner, s/p/pos...


val outputH = Array.fill(in.length)(NoColor)

def highlightRange(p: Position, color: String): Unit = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually don't type annotate inner methods

def highlightRange(p: Position, color: String): Unit = {
if(p.exists) {
for {
i <- p.start until math.min(p.end, outputH.length)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will hide bugs with positions. Let's catch IndexOutOfBoundException and print something like:

Encountered tree with invalid position, please open an issue with the code snippet that caused the error

val outputH = Array.fill(in.length)(NoColor)

def highlightRange(p: Position, color: String): Unit = {
if(p.exists) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing space after if

if(p.exists) {
for {
i <- p.start until math.min(p.end, outputH.length)
} outputH(i) = color
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer syntax with parentheses:

for (i <- p.start until p.end)
  outputH(i) = color

}
sb.append(in(idx))
}
if(outputH.last != NoColor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the input is empty? I would test against this at the beginning

for {
idx <- outputH.indices
} {
if(idx == 0 || outputH(idx-1) != outputH(idx)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If color at index 0 is NoColor, we should not add it

val offsetStart = s.offset

if(s.token == IDENTIFIER && s.name == nme.???) {
highlightRange(Position(s.offset, s.offset + s.name.length), Console.RED_B)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

define highlightRange and highlightPosition as:

def highlightRange(from: Int, to: Int, color: String) = ...
def highlightPosition(pos: Postition, color: String) =
  if (pos.exists) highlightRange(pos.start, pos.end, color)

sb.append(NoColor)
}

sb.mkString
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if it does the same thing, we prefer calling toString on StringBuilder


val sf = new SourceFile("<highlighting>", in.toCharArray)
val p = new Parser(sf)
val s = new Scanner(sf)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the scanner declaration closer to use site.

@slothspot
Copy link
Contributor Author

@allanrenucci PR was updated addressing your comments.
I would like to get some suggestions:
1/ How to match annotations? (Annotated node type doesn't work for me)
2/ What to add to the list of excluded keywords?

@allanrenucci
Copy link
Contributor

1/ How to match annotations? (Annotated node type doesn't work for me)

You should be able to match on MemberDef. Then you can do .rawMods.annotations. Another (more limited) possibility would be to use the scanner and highlight Tokens.AT as well as the following identifier.

2/ What to add to the list of excluded keywords?

In Tokens.scala, we have keywords = alphaKeywords + symbolicKeywords, we could decide to only highlight alphaKeywords. So instead of doing isKeyword(scanner.token), you could do alphaKeywords.contains(scanner.token)

colorAt(i) = color
} catch {
case _: IndexOutOfBoundsException =>
ctx.error("Encountered tree with invalid position, please open an issue with the code snippet that caused the error")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctx.error will not print anything since your are using a dummy reporter. Simply use println

@allanrenucci
Copy link
Contributor

@slothspot Let's not bother about annotations in this PR. This can be fixed in a separate PR.

I'd like to get this in and close all bugs related to syntax highlighting. Can you update the tests cases and add @Ignore on the tests for annotations and comments that are currently failing.

@slothspot
Copy link
Contributor Author

Preliminary support for annotations was added.
Have next problem:
this @deprecated class Zoo matches to ValOrDefDef with name <init> and namePos pointing to range [0..6] which results in first half of @deprecated highlighted with ValDefColor.

Questions:
1/ Shouldn't this be matched with TypeDef?
2/ Shouldn't namePos be <no position> in this case?

@slothspot
Copy link
Contributor Author

And due to your last request:
1/ I may mark annotations and comments related tests as ignored and create separate PR to handle these.
2/ Is it ok to use LiteralColor for strings?
3/ I'm not happy with interpolated strings:
for s"Some str with $v inside" current implementation gives:
s<L|"Some str with> <V|$v> $<L|inside">
but new one gives:
s<L|"Some str with $>v <L|inside">
May try to fix this with this PR or create separate one

@allanrenucci
Copy link
Contributor

this @deprecated class Zoo matches to ValOrDefDef with name and namePos pointing to range [0..6] which results in first half of @deprecated highlighted with ValDefColor.

You are matching the contstructor of the class. The position looks weird to me too. I will have to investigate. What you can try to do is not print synthetic positions: if (pos.exists && pos.isSourceDerived) highlightRange(...)

I may mark annotations and comments related tests as ignored and create separate PR to handle these

Yes, let's handle annoations in a separate PR. Please remove the code for annotations. You can keep the tests and mark them as Pending.

Is it ok to use LiteralColor for strings?

Yes, we can always change it later

I'm not happy with interpolated strings:

You could special case the tree InterpolatedString. Let's do this in another PR


if (isKwd) {
val offsetEnd = scanner.lastOffset
highlightPosition(Position(offsetStart, offsetEnd), KeywordColor)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

highlightRange?

|}
""".stripMargin

test(source, expected)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer simple single line test case:

test("@inline val bar = 42", ...)

@@ -89,12 +111,12 @@ class SyntaxHighlightingTests {

@Test
def unionTypes = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let remove this test and move it to def types. Also I don't think we need all these combinations. Let's keep:

test("type A = String|Int", "<K|type> <T|A> = <T|String|Int>")
test("type B = String & Int", "<K|type> <T|A> = <T|String & Int>")
test("fn[String](input)", "fn[<T|String>](input)")

import util.Chars
import util.{Chars, SourceFile}

import scala.collection.mutable

/** This object provides functions for syntax highlighting in the REPL */
object SyntaxHighlighting {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove all the legacy code

@danielyli
Copy link
Contributor

danielyli commented Jun 20, 2018

@smarter
Copy link
Member

smarter commented Jun 20, 2018

@danielyli Thanks! Pushed to the PR.

slothspot and others added 8 commits June 29, 2018 14:37
- Highlight is based on MemberDef with annotations present
- move scanner highlighter before parser so that annotation @inline
wholdn't be highlighted as keyword
- annotations are highlighted when full member definition is available
If we ignore trees named <error> and <init>, it is not so
fragile after all...
Prior to this commit, an annotation with one or more parameter lists
would have incorrect positions attached to each parameter list.  For
example, the annotation

    @foo("bar")

would have the erroneous position `<4..10>` attached to its `Apply` node
instead of the correct position `<4..11>`.  This was caused by forget-
ting to take into account the closing parenthesis.
These tests currently fail because of a bug in the syntax highlighter.
Test for `1Lx` currently fails; tests for float literals pass.
@allanrenucci
Copy link
Contributor

I rebased the PR. The fix in d31f32e is not correct. It breaks a lot of stuff. The compiler does not bootstrap anymore

The end position was correctly set but the start position was wrong: the
start position of `fn(args)` should be the same as the start position of
`fn` itself.
@allanrenucci
Copy link
Contributor

Closed in favor of #5137. Thanks @slothspot for the original PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants