Skip to content
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

Fixing symbols hierarchy #34

Merged
merged 9 commits into from
Nov 19, 2017

Conversation

laughedelic
Copy link
Member

WIP fix for #33.

Here's how it looks with 379a735:
screen shot 2017-11-12 at 21 24 06

The problem is, it shows only one level of nesting, which is wrong. I compared the responses with other servers (TypeScript and Python) and didn't find any difference: containerName just matches the corresponding symbol's name field, nothing special.

@laughedelic laughedelic self-assigned this Nov 12, 2017
Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

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

Change looks good, I propose we let this wait until #31 is merged to avoid merge conflicts for @gabro

@@ -29,19 +30,25 @@ class SymbolIndexer(

def documentSymbols(
path: RelativePath
): Seq[(Position.Range, Denotation)] =
): Seq[SymbolInformation] = {
Copy link
Member

Choose a reason for hiding this comment

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

👍

denotation.name,
denotation.symbolKind,
path.toAbsolute.toLocation(position),
Option(denotations.get(symbol.owner)).map(_.name)
Copy link
Member

Choose a reason for hiding this comment

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

How about using symbol.owner.syntax? .owner is likely a Symbol.Global which has a .signature.name we can use

Copy link
Member Author

@laughedelic laughedelic Nov 12, 2017

Choose a reason for hiding this comment

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

See #33 (comment). Also I didn't get you about .signature.name.

@laughedelic
Copy link
Member Author

Yes, I thought that it will need a rebase on top of #31, just wanted to show what I have now. The change is little so it will be easy to adapt it to #31 once it's in a usable state.

@laughedelic
Copy link
Member Author

Regarding broken nested hierarchy: facebookarchive/atom-ide-ui#121

@laughedelic
Copy link
Member Author

@olafurpg how can we get the range of symbol's definition body? i.e. class/method/etc. with everything inside the braces {}

@olafurpg
Copy link
Member

@laughedelic you mean the source file positions? You would have to parse the file and find the tree node that defines that symbol and then use Tree.pos.{start,end} to get it's position. Is that necessary for the outline feature? I feel like you should be able to somewhat infer it from the Symbol structure

a -> line 1
a.b -> line 2
a.b.c -> line 3
a.c -> line 4
d -> line 5
d.e -> line 6

@laughedelic
Copy link
Member Author

you mean the source file positions?

Yes. Location range where the symbol starts and where its definition ends.

Is that necessary for the outline feature?

There are two things:

You would have to parse the file and find the tree node that defines that symbol and then use Tree.pos.{start,end} to get it's position.

Sounds like too much extra work for this basic feature. I hoped that it's something that's already available in semanticdb, but just needs some digging..

I feel like you should be able to somewhat infer it from the Symbol structure

This is a good idea for a workaround. I'll see how to do it.

@gabro
Copy link
Member

gabro commented Nov 14, 2017

Generally speaking, it's not necessary and VS Code doesn't even have this tree representation, it shows symbols in a flat list: microsoft/vscode#5605. Yet it would be nice to support if it's possible to do.

I agree it would be nice to support it. I believe eventually also VSCode will support a tree outline

@gabro
Copy link
Member

gabro commented Nov 14, 2017

Also, apparently some extensions are already providing it. E.g. https://marketplace.visualstudio.com/items?itemName=patrys.vscode-code-outline

@laughedelic
Copy link
Member Author

Nice! And it also relies on nested ranges. It even selects the region when you click on the symbol.

@gabro
Copy link
Member

gabro commented Nov 14, 2017

Yep, I've tested it with TypeScript and it works quite well!

@olafurpg
Copy link
Member

Cool! Let's do this then 💪 parsing the tree and finding the tree node of the symbol definition should not be too hard, which message needs this information?

Rough sketch

import scala.meta._, contrib._
val code: String = ???
def findSymbol(pos: Position) = ???
code.parse[Source].get.collect { 
  case node @ Defn.Object(_, name, _, _) =>
    findSymbol(name.pos) -> node.pos
// ... same for Defn.Trait and Defn.Class
}

@laughedelic
Copy link
Member Author

@olafurpg thanks! I'll try to implement it. I was just concerned about reparsing source on every such request.

@olafurpg
Copy link
Member

olafurpg commented Nov 14, 2017

Parsing is cheap, we use the parser to index all the *.scala source files of your dependencies at 30k LOC/s 😄 How often is this message called?

@laughedelic
Copy link
Member Author

I have to check, but I think it's called every time you open the outline panel and if it's open, on every file change 😓

@olafurpg
Copy link
Member

We actually don't even need the compiler for this, once #23 is merged we can use the ctags indexer to build the semanticdbs on the fly. It's not prohibitive to run on every file change IMO, and if it turns out to be slow we can probably optimize it somehow. I agree this is a useful feature!

Its containerName field should be the symbol.owner's name, not its type
It searches a Member node in the parsed tree that has the same name and
same startLine. This widens position range to the symbol definition,
which is necessary for correct symbols hierarchy nesting.
@laughedelic
Copy link
Member Author

laughedelic commented Nov 15, 2017

I rebased this branch on master (after #23 was merged) and here's what I got :shipit:

  • First some crazy example with different nested things and awful formatting (on purpose):
    screen shot 2017-11-15 at 06 35 34
  • And an example of different SymbolKinds (some icons don't make much sense to me, but that's what atom-ide-ui has to offer):
    screen shot 2017-11-15 at 06 11 22
    There are some more SymbolKinds such as String, Number, Boolean, Array. I was doubting if it's worth implementing them and how exactly. I think they should be used for literals, but literals normally don't float around and don't appear in this outline..

Notice that I didn't want to nest everything under the package symbol, because it's always top-level and wouldn't give you any additional information. Even with more than one package in the same file it's fine, because they kind of work like "separators". This is discussable, of course.

About implementation: after some experimenting, I figured out how to do it using only parsing and traversing the tree. It's fast and updates immediately when you're editing (btw, how do you guys make those nice GIFs?). It was a good exercise for me to familiarize myself with the Tree 🌳 and some other things in Scalameta.

Unsolved problem: requests are sent on edits and responses are immediate, so when you edit to a non-parseable state, the outline is empty. This could be solved by storing the last generated symbol list and sending it until it can be updated. Any better ideas?

@gabro
Copy link
Member

gabro commented Nov 15, 2017

Wow, that looks great! I personally use RecordIt for the gifs, but I think there's many options available.
For the state, one may think of using an observable and only return the latest valid state for that buffer.

@laughedelic
Copy link
Member Author

@gabro thanks, I will try it.

Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

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

Nice work! Falling back to last outline makes sense, you can probably get away with a regular map keyed by the uri instead of using Observable.

Some(name)
case Term.Select(qual, name) =>
qualifiedName(qual).map { prefix => s"${prefix}.${name}" }
case Pkg(sel: Term.Select, _) =>
Copy link
Member

Choose a reason for hiding this comment

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

What about package a?

Copy link
Member Author

@laughedelic laughedelic Nov 15, 2017

Choose a reason for hiding this comment

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

It falls into the Member case, so it will be just its name a.
This whole qualifiedName method is here only for the packages with Select, because their default name is just the last part and I think it's useful to see the full name in the outline.

Copy link
Member

Choose a reason for hiding this comment

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

I see, nice!

qualifiedName(qual).map { prefix => s"${prefix}.${name}" }
case Pkg(sel: Term.Select, _) =>
qualifiedName(sel)
case m: Member =>
Copy link
Member

Choose a reason for hiding this comment

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

Glad you found this! Took me a while to learn about it :)

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean? 😅

Copy link
Member

Choose a reason for hiding this comment

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

I wasn't aware of Member until quite recently, always found it frustrating to handle all val/val/def/class/trait/object cases

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yes, it's very helpful. I just searched for whatever with name, when I wanted to match it with symbols from the indexer 😄

@olafurpg
Copy link
Member

@laughedelic I use GIPHY on OSX.

Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

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

I just realized this is not too different from the recently added ScalaCtags https://github.com/scalameta/language-server/blob/8bbef8eb20e4cef9c8586cecf33b0093a09c6905/metaserver/src/main/scala/scala/meta/languageserver/ctags/ScalaCtags.scala

The ctags indexers will return a more useful schema after #35 which will probably make it more convenient to re-use for features like outline.

However, this PR is immediately useful and the feature looks really cool! I will keep my eyes open for sharing the implementation between ctags and outline if possible later down the road, but we should not let it block us now.

LGTM 👍 Nice work @laughedelic ! Feel free to merge.

val contents = buffers.read(path)
for {
tree <- contents.parse[Source].toOption.toList
node <- tree.collect {
Copy link
Member

Choose a reason for hiding this comment

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

FYI scala.meta.contrib._ adds a .filter on Tree

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I was looking for it but found after I wrote this and then forgot to change it.

@laughedelic
Copy link
Member Author

laughedelic commented Nov 15, 2017

@olafurpg thanks for the review, but please don't merge it yet. I tested it today with some real projects and found a couple of corner-cases which should be handled more carefully.
I will also take a closer at the ctags code.

@gabro
Copy link
Member

gabro commented Nov 19, 2017

@laughedelic what do you think about merging this and addressing the corner cases in a separate issue/PR? The more this PR stays open the more conflicts we risk to introduce.

@olafurpg
Copy link
Member

I agree we should merge this to avoid conflicts with #37 👍

@laughedelic
Copy link
Member Author

OK, I just pushed a couple of little fixes and I'm going to merge it. I'll submit another PR later with other improvements.

@laughedelic laughedelic merged commit 641dfb8 into scalameta:master Nov 19, 2017
@laughedelic laughedelic deleted the symbols-hierarchy branch November 19, 2017 16:32
Copy link
Member

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

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

🎉

case Defn.Object(_) => SymbolKind.Namespace
case Pkg.Object(_) => SymbolKind.Module
case Defn.Object(_) => SymbolKind.Module
case Pkg.Object(_) => SymbolKind.Namespace
Copy link
Member

Choose a reason for hiding this comment

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

Strictly speaking, package object are modules like regular objects. But having a different icon in the UI to distinguish package objects from objects makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, that's what I thought

@olafurpg
Copy link
Member

Nice! This PR fixes one of the TODOs in #37 😄

@laughedelic
Copy link
Member Author

I didn't have time yet to check all the big changes in #37, but I just noticed that merging this introduced some merge conflicts there

@olafurpg
Copy link
Member

I've fixed those, the conflicts were only related to document symbol provider which I had commented out

@gabro
Copy link
Member

gabro commented Nov 20, 2017

yay! Nice job @laughedelic!

@olafurpg olafurpg mentioned this pull request Nov 20, 2017
@laughedelic laughedelic mentioned this pull request Dec 4, 2017
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.

3 participants