Skip to content

(GH-793) Add syntax folding #777

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

Merged
merged 4 commits into from
Nov 28, 2018
Merged

Conversation

glennsarti
Copy link
Contributor

@glennsarti glennsarti commented Oct 21, 2018

This commit adds the the code to return folding ranges when the server receives
a FoldingRangeRequest. This commit;

  • Uses a similar method to that implemented in the VS Code extension [1]
  • Uses the PowerShell tokeniser instead of the AST. This is due to the AST
    ignoring comment sections. Without the comment parsing folding for #region
    etc. will not work
  • Translates the tests in VS Code [1] into equivalent C# tests

[1] PowerShell/vscode-powershell#1355


Note that I cannot use the AST for detecting folding regions as the AST drops the comments in the tree. So I have to use the tokens from the lexer instead (sad-panda)

@glennsarti glennsarti force-pushed the add-syntax-folding branch 4 times, most recently from d22a1a1 to 90a4efb Compare November 12, 2018 14:48
@glennsarti glennsarti changed the title {WIP} (GH-???) Add syntax folding (GH-793) Add syntax folding Nov 13, 2018
@glennsarti
Copy link
Contributor Author

@TylerLeonhardt @rjmholt This is ready for review.

@glennsarti
Copy link
Contributor Author

@rkeithhill Fixed your comments. CI is green

Copy link
Member

@TylerLeonhardt TylerLeonhardt left a comment

Choose a reason for hiding this comment

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

Looking good! 😄

@glennsarti glennsarti force-pushed the add-syntax-folding branch 2 times, most recently from 61014e1 to 72dcd9d Compare November 14, 2018 12:40
@glennsarti
Copy link
Contributor Author

@TylerLeonhardt I've addressed your points except for the Async name change.

Copy link
Contributor

@rjmholt rjmholt left a comment

Choose a reason for hiding this comment

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

Haven't quite finished looking at the technical logic, but it looks really great so far!

Once we have this in, we could even look at using the AST instead for expression and block folding (since we already have the AST).

/// <summary>
/// Describes the kind of the folding range such as `comment' or 'region'.
/// </summary>
public string Kind { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this based on the VSCode type? Would it be better as an enum? We could extend that as easily as a string, but it's faster to check, less error prone and can't be null.

Copy link
Contributor

Choose a reason for hiding this comment

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

Given that the VSCode API can have this as null, a common convention in C# is to have a None value in the enum:

enum MatchKind
{
    None = 0,
    Comment,
    Region
}

(Or similar)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See discussion above about strings and enums. This type is String (https://github.com/Microsoft/vscode-languageserver-node/blob/master/protocol/src/protocol.foldingRange.ts#L94-L99)

With 3 (at this time) well known strings (https://github.com/Microsoft/vscode-languageserver-node/blob/master/protocol/src/protocol.foldingRange.ts#L51-L67)

 * Enum of known range kinds
 */
export enum FoldingRangeKind {
	/**
	 * Folding range for a comment
	 */
	Comment = 'comment',
	/**
	 * Folding range for a imports or includes
	 */
	Imports = 'imports',
	/**
	 * Folding range for a region (e.g. `#region`)
	 */
	Region = 'region'
}

I couldn't use an enum as C# enums are ints (well, ordinal) not strings. I later just used string constants as I only needed it in one place

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it because it's a JSON deserialisation thing? If so we should take a quick look at how we deserialise messages and see if we can use [JsonConverter(typeof(StringEnumConverter))] or something like that on the type. Being able to use enums would just be really nice here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case it's serialisation only. We don't receive this from a client.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I'm good with how we do this. I looked into restructuring it to allow enums, but given that it took several changes in various places, I might save it to go with some other work I've been wanting to do.

@glennsarti glennsarti force-pushed the add-syntax-folding branch 2 times, most recently from 9249f46 to 38b045e Compare November 16, 2018 03:48
@glennsarti
Copy link
Contributor Author

I think I've addressed all questions again. Ready for review.

Copy link
Contributor

@rjmholt rjmholt left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Member

@TylerLeonhardt TylerLeonhardt left a comment

Choose a reason for hiding this comment

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

Had a few more comments 😄

// It's possible to have duplicate or overlapping ranges, that is, regions which have the same starting
// line number as the previous region. Therefore only emit ranges which have a different starting line
// than the previous range.
foldableRegions.RemoveAll( (FoldingReference item) => {
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 using a HashSet?

HashSet<int> startLines = new HashSet();
foldableRegions.RemoveAll((item) => {
    if ( startLines.Contains(item.StartLine))
    {
        return true;
    }
    else
    {
        startLines.Add(item.StartLine);
        return false;
    }
});
  • disclaimer: compiled with my mind *

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mean you can, but;

  1. All of the matching functions return an array of folding references anyway
  2. Not sure if a hash would be any faster
  3. I need to sort the hashes anyway so may as well keep it as an array
  4. The data/objects I'm manipulating here don't feel very "hash-y", they feel better represented as an array of stuff.

Copy link
Member

Choose a reason for hiding this comment

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

I could be misunderstanding the problem...

So doing a Binary search in Big-O is O(logn) on each element (O(n) makes this an O(nlogn) operation.

By using a HashSet which has an O(1) insert and lookup, that means you only have to go through each element making it an O(n) operation overall.

The HashSet suggested was only used as a helper to prevent using a search. It wouldn't be used anywhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can try and add it in...it'll make the creation logic look a bit funky. I'll slap on another commit and see what it looks like ...

I mean, we're not talking big numbers here normally, even for 200 elements do you really think there's a perceptual difference?

Copy link
Contributor Author

@glennsarti glennsarti Nov 23, 2018

Choose a reason for hiding this comment

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

Ok so I did some performance testing.

Given a PowerShell script with 3840 foldable regions (Which is a HUGE amount) the FoldableRegions method took 121.4589ms. If I remove the foldableRegions.Sort(); and the duplicate checking that comes done to 120.0091ms

Net gain of 1.5ms if I do no sorting or dupe checking at all. That's a speed increase of 1%. If you factor in actually tokenizing the document first, that speed improvement becomes < 0.5%

IMO, the choice of sorting algorithm used here is statistically insignificant compared to overall operation for a large level of folding regions. The speed increase becomes practically zero if you're only talking < 100 regions.

Ref: https://gist.github.com/glennsarti/b4af7c9b8aa6bcfc0b7cd70ae027530b

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like rather than stripping unwanted things out of the list after the fact, the ideal would be to never add them in the first place (same deal with the nulls). But to make sure that new regions displace others, we'd need to use a new data structure to accumulate the values.

So I'm happy enough with how it's done currently, and we can look at it again later if we need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a lot of room for speed improvements in how I'm currently getting the tokens. I've raised #798 to track to the work post-merge so it doesn't get forgotten.

This commit adds the skeleton for a Folding Range Provider.
This commit adds the the code to return folding ranges when the server receives
a FoldingRangeRequest.  This commit;

* Uses a similar method to that implemented in the VS Code extension [1]
* Uses the PowerShell tokeniser instead of the AST.  This is due to the AST
  ignoring comment sections.  Without the comment parsing folding for #region
  etc. will not work
* Translates the tests in VS Code [1] into equivalent C# tests

[1] PowerShell/vscode-powershell#1355
This commit uses the the client configuration settings to modify the behaviour
of the server. In particular the Enable and ShowLastLine setting.

* The enable setting will change the provider to return null when disabled
* The ShowLastLine setting emulates the behaviour in PowerShell/vscode-powershell#1557
  * Modifies conversion method for the internal region class to FoldingRange
    object to show/hide the last line based on the extension settings
  * Modifies the tests for the default value and adds tests for the show and hide
    scenarios
This commit renames methods to conform to version 2.0 naming standard;

* Async method names should be suffixed with Async
Copy link
Contributor

@rjmholt rjmholt left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks @glennsarti

// It's possible to have duplicate or overlapping ranges, that is, regions which have the same starting
// line number as the previous region. Therefore only emit ranges which have a different starting line
// than the previous range.
foldableRegions.RemoveAll( (FoldingReference item) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like rather than stripping unwanted things out of the list after the fact, the ideal would be to never add them in the first place (same deal with the nulls). But to make sure that new regions displace others, we'd need to use a new data structure to accumulate the values.

So I'm happy enough with how it's done currently, and we can look at it again later if we need to.

/// <summary>
/// Describes the kind of the folding range such as `comment' or 'region'.
/// </summary>
public string Kind { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I'm good with how we do this. I looked into restructuring it to allow enums, but given that it took several changes in various places, I might save it to go with some other work I've been wanting to do.

Copy link
Contributor

@rkeithhill rkeithhill left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for making the code style changes!

@glennsarti
Copy link
Contributor Author

@TylerLeonhardt I can't find any comments you raised that I haven't addressed. Is there anything else stopping approval?

Copy link
Member

@TylerLeonhardt TylerLeonhardt left a comment

Choose a reason for hiding this comment

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

LGTM! Glad to see this 😊 more editors can take advantage of this awesome feature. I left a small nit

@@ -42,6 +42,8 @@ public class ServerCapabilities
public ExecuteCommandOptions ExecuteCommandProvider { get; set; }

public object Experimental { get; set; }

public bool FoldingRangeProvider { get; set; } = false;
Copy link
Member

Choose a reason for hiding this comment

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

bool defaults to false so the = false shouldn't be needed I think. Since bool is a non-nullable type

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMO, in this case, being explicit makes it more obvious what's going on. I don't feel there's any maintenance cost with leaving this in.

Copy link
Member

Choose a reason for hiding this comment

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

Works for me 👍

@rjmholt rjmholt merged commit fbe8871 into PowerShell:master Nov 28, 2018
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.

4 participants