GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Smart Select language service API by andrewbranch · Pull Request #31028 · microsoft/TypeScript

Conversation

@andrewbranch

@andrewbranch Andrew Branch (andrewbranch) commented Apr 19, 2019 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Closes #29071

Protocol for VS & VS Code

export interface Location {
    line: number;
    offset: number;
}

export interface TextSpan {
    start: Location;
    end: Location;
}

export interface SelectionRangeRequest extends FileRequest {
    command: CommandTypes.SelectionRange; // 'selectionRange'
    arguments: SelectionRangeRequestArgs;
}

export interface SelectionRangeRequestArgs extends FileRequestArgs {
    locations: Location[];
}

export interface SelectionRangeResponse extends Response {
    body?: SelectionRange[];
}

export interface SelectionRange {
    textSpan: TextSpan;
    parent?: SelectionRange;
}

Also exposes a selectionRange-full tsserver command which accepts the same request format but responds with the language service response unchanged, which is the same except that TextSpan is a ts.TextSpan:

export interface TextSpan {
    start: number;
    length: number;
}

Implementation notes

Selection ranges walk up the AST, with a few different types of exceptions to the rule.

Adding artificial depth to flat nodes

Some TypeScript nodes feel like they have subtrees but are actually quite flat. Keeping them flat results in a jump from one selection to the next that feels too big. To remedy this, when asking for the children of these nodes, we synthesize SyntaxLists as intermediate nodes that group children together more intuitively, sometimes multiple levels deep. The most complex example is for mapped types. Consider

type M = { -readonly [K in keyof any]-?: any }

The actual tree for the type in this declaration looks like

MappedType
  ├ OpenBraceToken
  ├ MinusToken
  ├ ReadonlyToken
  ├ OpenBracketToken
  ├ TypeParameter
  │  ├ Identifier
  │  ├ InKeyword
  │  └ TypeOperator
  │      ├ KeyofKeyword
  │      └ AnyKeyword
  ├ CloseBracketToken
  ├ MinusToken
  ├ QuestionToken
  ├ ColonToken
  ├ AnyKeyword
  └ CloseBraceToken

But this belies the intuition of the way things are grouped. The two tokens in -readonly go together, the two in -? go together, the square brackets go with their contents, etc. The tree we actually want for selection purposes would look more like

MappedType
  ├ OpenBraceToken
  ├ SyntaxList
  │  ├ SyntaxList
  │  │  ├ MinusToken
  │  │  └ ReadonlyToken
  │  ├ SyntaxList
  │  │  ├ OpenBracketToken
  │  │  ├ TypeParameter
  │  │  │  ├ Identifier
  │  │  │  ├ InKeyword
  │  │  │  └ TypeOperator
  │  │  │     ├ KeyofKeyword
  │  │  │     └ AnyKeyword
  │  │  └ CloseBracketToken
  │  ├ SyntaxList
  │  │  ├ QuestionToken
  │  │  └ ColonToken
  │  └ AnyKeyword
  └ CloseBraceToken

This results in a better selection experience, but it's totally fake which also makes it totally subjective. I used two main strategies to make decisions that are ultimately opinions:

  1. As someone who writes a lot of TypeScript in VS Code myself, I put myself in the shoes of a user selecting some of my own code and thought about the common edits I might want to make around a particular selection, and tried to add selection stops that facilitated those edits with minimal additional keystrokes.
  2. I gauged some of my own opinions and expectations that weren’t reflected in the AST against Matt Bierner (@mjbvz)’s. Where we agreed, I felt confident that synthesizing a node was justified.

Skipping redundant or uninteresting nodes

Identical selection ranges are deduped, but some others vary only in whitespace or semicolons and seemed to add little value. These nodes are skipped.

Adding JSDoc comments

An extra selection is added to nodes with JSDoc annotations so that the node can be selected both alone and with its comment.

Special handling for string literals

String literals don't separate their quotes from their contents in the AST, which is desirable for selection, so there’s special logic there. Template strings are especially unintuitive and have special handling to make sure you can get the whole string, just inside the quotes, around the outside of a substitution (${x}), and on the inside of a substitution.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

mihailik reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Andrew Branch (andrewbranch) added 18 commits April 18, 2019 16:05
@andrewbranch
Start smart select API
af3d0f0
@andrewbranch
Add more tests, special handling for mapped types
f98c00a
@andrewbranch
Add support for string literals
e62c233
@andrewbranch
Start imports
fd88e52
@andrewbranch
Also skip TemplateHeads
039487c
@andrewbranch
Distinguish between same-line and different-line braces
0a4ef0f
@andrewbranch
Move most logic to separate file
61425cb
@andrewbranch
Move to language service
0f7bc02
@andrewbranch
Add rules for expanding selection to sibling nodes
70e2672
@andrewbranch
Rethink sibling expansion by creating fake subtrees
fcb7f01
@andrewbranch
Solidify fake tree approach
4ecdc82
@andrewbranch
Snap to nodes directly behind the cursor, create special rules for Pa…
74fc84f
…rameterNodes
@andrewbranch
Special rules for binding elements, extend brace/whitespace logic to …
d73eabd
…other kinds of bookended lists
@andrewbranch
Add stop for JSDoc comments
fed910f
@andrewbranch
Skip lone variable declarations
5479893
@andrewbranch
Remove debug info
f0f7d82
@andrewbranch
Rename to be smarter
d8936fd
@andrewbranch
Rename test to match
12492a3
@typescript-bot

TypeScript Bot (typescript-bot) commented Apr 19, 2019

Copy link
Copy Markdown
Contributor

Thanks for the PR! It looks like you've changed the TSServer protocol in some way. Please ensure that any changes here don't break consumers of the current TSServer API. For some extra review, we'll ping Sheetal Nandi (@sheetalkamat), Andrew Casey (@amcasey), Matt Bierner (@mjbvz), Mine Starks (@minestarks) for you. Feel free to loop in other consumers/maintainers if necessary

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch
Andrew Branch (andrewbranch) requested review from Matt Bierner (mjbvz) and Sheetal Nandi (sheetalkamat) April 19, 2019 00:20
Andrew Branch (andrewbranch) added 2 commits April 18, 2019 17:21
@andrewbranch
Revert accidental line break added
511cc79
@andrewbranch
Revert accidental line ending change
99ace03
amcasey
Andrew Casey (amcasey) reviewed Apr 19, 2019
View reviewed changes
Comment thread src/server/protocol.ts Resolved

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch
Revert accidental submodule change I guess
6177596
@mjbvz

Matt Bierner (mjbvz) commented Apr 19, 2019

Copy link
Copy Markdown
Contributor

Protocol changes look good. I'll give it another quick test locally and let you know if I run into an big issues

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mjbvz

Matt Bierner (mjbvz) commented Apr 19, 2019

Copy link
Copy Markdown
Contributor

Looks great. Just found a few odd results that cause VS Code to error:


export function loadRepos() {
  return {
    type: /**/LOAD_REPOS,
  };
}

Top level returned text span is:

        "textSpan": {
            "start": {
                "line": 1,
                "offset": 27
            },
            "end": {
                "line": 1,
                "offset": 27
            }
        },

describe('thing', () => {
    it('/**/', () => {});
});

Returned span is:

        "textSpan": {
            "start": {
                "line": 1,
                "offset": 20
            },
            "end": {
                "line": 1,
                "offset": 20
            }
        },

class HomePage {
    componentDidMount() {
        if (this.props.username/**/) { }
    }
}

Returned top level span is

 "textSpan": {
            "start": {
                "line": 2,
                "offset": 23
            },
            "end": {
                "line": 2,
                "offset": 23
            }
        },

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch

Andrew Branch (andrewbranch) commented Apr 19, 2019

Copy link
Copy Markdown
Member Author

Hm, I can see why the empty string one is doing that, but the others seem odd. I'll check and see what it thinks it's doing, but it looks like I just need to add a filter to reject zero-width spans. Will update first thing tomorrow. Thanks for testing!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch
Filter out zero-width selections
f0a3d2b
@andrewbranch

Andrew Branch (andrewbranch) commented Apr 19, 2019

Copy link
Copy Markdown
Member Author

Ohhh. Those really strange ones were caused by a possible bug in ts.positionBelongsToNode that Ryan Cavanaugh (@RyanCavanaugh) and Ron Buckton (@rbuckton) and I were discussing yesterday. I've since switched away from using that API, and just added a filter to ignore empty spans.

I can no longer repro the bugs you gave me, but I thought I stopped using ts.positionBelongsToNode before you tried it yesterday... I just added unit tests for those cases but I'll try the integration with your custom VS Code build too.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

sheetalkamat
Sheetal Nandi (sheetalkamat) reviewed Apr 19, 2019
View reviewed changes
Comment thread src/testRunner/unittests/tsserver/smartSelection.ts Resolved

Uh oh!

There was an error while loading. Please reload this page.

@RyanCavanaugh

Ryan Cavanaugh (RyanCavanaugh) commented Apr 23, 2019

Copy link
Copy Markdown
Member

I agree the fourslash format looks like a disaster.

The motivation, though, is that in general position-based tests end up being a lot less readable and maintainable than DSL-based formats, so we try to avoid the former as much as possible.

It's impossible to look at these tests right now in the diff view, for example, and understand what the expected behavior is, and if something changes in the wire format, there's a bunch of updating that will have to happen. Updating the text is also very difficult - adding a single character in the middle of an existing test is going to require a lot of tedious math.

Having a one-off DSL has generally paid for itself in terms of long-term test maintenance. For example, we could imagine having something like this (bikeshed a bunch):

const test = `const { x, y: a, ...zs = {} } = {};`;
const expected = [
    `const { x, y: a, ...zs = {} } = {};`,
    `             [a]                    `,
    `          [y: a]                   `,
    `     [{ x, y: a, ...zs = {} }]     `,
];

Alternatively, this might be an appropriate place to use baselines that just render a human-readable format of what happened. Writing a one-off renderer to make an inspectable text file for what the selection spans were would be pretty easy, and lends itself to reviewers being able to see what the behavior is.

andrewbranch and mihailik reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch

Andrew Branch (andrewbranch) commented Apr 23, 2019

Copy link
Copy Markdown
Member Author

👍🏽 I like the idea of a baseline. The assertions are difficult to write no matter what, but there are formats that are easy to read—eliminating the writing step seems like the best approach.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Andrew Branch (andrewbranch) added 3 commits April 23, 2019 15:34
@andrewbranch
Add custom baseline format for smart selection
6fc2e4a
@andrewbranch
Copy smartSelect tests to fourslash
e28b9b2
@andrewbranch
Remove all but one server unit test
3e30a7c
@andrewbranch
Andrew Branch (andrewbranch) requested review from Ryan Cavanaugh (RyanCavanaugh) and Sheetal Nandi (sheetalkamat) April 23, 2019 23:47
@andrewbranch
Fix baseline file name changes
eff3960
DanielRosenwasser
Daniel Rosenwasser (DanielRosenwasser) reviewed Apr 25, 2019
View reviewed changes
Comment thread src/services/smartSelection.ts Resolved

Uh oh!

There was an error while loading. Please reload this page.

@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) assigned Andrew Branch (andrewbranch) Apr 30, 2019
@andrewbranch

Andrew Branch (andrewbranch) commented May 16, 2019

Copy link
Copy Markdown
Member Author

Ryan just gave me a physical thumbs up on this in the room because his computer currently has a blue screen of death 😄

j-oliveras reacted with laugh emoji mihailik reacted with rocket emoji
All reactions
  • 😄 1 reaction
  • 🚀 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch
Andrew Branch (andrewbranch) merged commit 15daf42 into microsoft:master May 16, 2019
@andrewbranch
Andrew Branch (andrewbranch) deleted the feature/smart-select branch May 16, 2019 16:45
@sheetalkamat

Sheetal Nandi (sheetalkamat) commented May 16, 2019

Copy link
Copy Markdown
Member

Andrew Branch (@andrewbranch) yey on first big feature merge 👍

andrewbranch reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mihailik

mihailik commented May 17, 2019

Copy link
Copy Markdown
Contributor

External observer question: why is selectionRange-full is needed?

   export interface TextSpan {
      start: Location;
      end: Location;
  }

. . .

Also exposes a selectionRange-full tsserver command which accepts the same request format but responds with the language service response unchanged, which is the same except that TextSpan is a ts.TextSpan:

 export interface TextSpan {
     start: number;
     length: number;
 }

Given general brevity of the docs on ts APIs, adding odd-shaped nearly-identical calls can easily lead people astray.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch

Andrew Branch (andrewbranch) commented May 17, 2019

Copy link
Copy Markdown
Member Author

Good question. Almost every TSServer command has a -full counterpart that gets used by Visual Studio. I believe Visual Studio internally uses 0-based lines and columns, while I know VS Code uses 1-based lines and columns.

I actually wrote up something about this for the wiki, I just haven’t added it yet.

mihailik reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mihailik

mihailik commented May 17, 2019

Copy link
Copy Markdown
Contributor

Sorry, I didn't realise it's already consistent with other calls. Makes total sense!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@amcasey

Andrew Casey (amcasey) commented May 17, 2019

Copy link
Copy Markdown
Member

I believe the primary difference between the -full and regular versions is that -full uses a character offset from the beginning of the file, rather than line and column.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch

Andrew Branch (andrewbranch) commented May 17, 2019

Copy link
Copy Markdown
Member Author

Yep, that’s right, and that’s the same way that the compiler itself typically represents positions in files. Maybe Visual Studio doesn't use lines and columns much internally at all ¯_(ツ)_/¯

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@microsoft Microsoft (microsoft) locked as resolved and limited conversation to collaborators Oct 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@DanielRosenwasser Daniel Rosenwasser (DanielRosenwasser) DanielRosenwasser left review comments
@mjbvz Matt Bierner (mjbvz) Awaiting requested review from mjbvz
@sheetalkamat Sheetal Nandi (sheetalkamat) Awaiting requested review from sheetalkamat
@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) Awaiting requested review from RyanCavanaugh
+1 more reviewer
@amcasey Andrew Casey (amcasey) amcasey left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@andrewbranch Andrew Branch (andrewbranch)

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

TSServer: Smart select API

Uh oh!

There was an error while loading. Please reload this page.

8 participants

@andrewbranch @typescript-bot @mjbvz @RyanCavanaugh @sheetalkamat @mihailik @amcasey @DanielRosenwasser
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.