Skip to content

Commit cc76e0e

Browse files
Merge pull request #813 from Microsoft/genericClassificationTweak
Add some heuristics in the lexical classifier to make it play better wit...
2 parents 00e5b15 + 422324f commit cc76e0e

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed

src/services/services.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4692,6 +4692,27 @@ module ts {
46924692
entries: []
46934693
};
46944694

4695+
// We can run into an unfortunate interaction between the lexical and syntactic classifier
4696+
// when the user is typing something generic. Consider the case where the user types:
4697+
//
4698+
// Foo<number
4699+
//
4700+
// From the lexical classifier's perspective, 'number' is a keyword, and so the word will
4701+
// be classified as such. However, from the syntactic classifier's tree-based perspective
4702+
// this is simply an expression with the identifier 'number' on the RHS of the less than
4703+
// token. So the classification will go back to being an identifier. The moment the user
4704+
// types again, number will become a keyword, then an identifier, etc. etc.
4705+
//
4706+
// To try to avoid this problem, we avoid classifying contextual keywords as keywords
4707+
// when the user is potentially typing something generic. We just can't do a good enough
4708+
// job at the lexical level, and so well leave it up to the syntactic classifier to make
4709+
// the determination.
4710+
//
4711+
// In order to determine if the user is potentially typing something generic, we use a
4712+
// weak heuristic where we track < and > tokens. It's a weak heuristic, but should
4713+
// work well enough in practice.
4714+
var angleBracketStack = 0;
4715+
46954716
do {
46964717
token = scanner.scan();
46974718

@@ -4711,6 +4732,28 @@ module ts {
47114732
// we recognize that 'var' is actually an identifier here.
47124733
token = SyntaxKind.Identifier;
47134734
}
4735+
else if (lastNonTriviaToken === SyntaxKind.Identifier &&
4736+
token === SyntaxKind.LessThanToken) {
4737+
// Could be the start of something generic. Keep track of that by bumping
4738+
// up the current count of generic contexts we may be in.
4739+
angleBracketStack++;
4740+
}
4741+
else if (token === SyntaxKind.GreaterThanToken && angleBracketStack > 0) {
4742+
// If we think we're currently in something generic, then mark that that
4743+
// generic entity is complete.
4744+
angleBracketStack--;
4745+
}
4746+
else if (token === SyntaxKind.AnyKeyword ||
4747+
token === SyntaxKind.StringKeyword ||
4748+
token === SyntaxKind.NumberKeyword ||
4749+
token === SyntaxKind.BooleanKeyword) {
4750+
if (angleBracketStack > 0) {
4751+
// If it looks like we're could be in something generic, don't classify this
4752+
// as a keyword. We may just get overwritten by the syntactic classifier,
4753+
// causing a noisy experience for the user.
4754+
token = SyntaxKind.Identifier;
4755+
}
4756+
}
47144757

47154758
lastNonTriviaToken = token;
47164759
}

tests/cases/unittests/services/colorization.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,5 +231,43 @@ describe('Colorization', function () {
231231
identifier("var"),
232232
finalEndOfLineState(ts.EndOfLineState.Start));
233233
});
234+
235+
it("classifies partially written generics correctly.", function () {
236+
test("Foo<number",
237+
ts.EndOfLineState.Start,
238+
identifier("Foo"),
239+
operator("<"),
240+
identifier("number"),
241+
finalEndOfLineState(ts.EndOfLineState.Start));
242+
243+
// Looks like a cast, should get classified as a keyword.
244+
test("<number",
245+
ts.EndOfLineState.Start,
246+
operator("<"),
247+
keyword("number"),
248+
finalEndOfLineState(ts.EndOfLineState.Start));
249+
250+
// handle nesting properly.
251+
test("Foo<Foo,Foo<number",
252+
ts.EndOfLineState.Start,
253+
identifier("Foo"),
254+
operator("<"),
255+
identifier("Foo"),
256+
punctuation(","),
257+
identifier("Foo"),
258+
operator("<"),
259+
identifier("number"),
260+
finalEndOfLineState(ts.EndOfLineState.Start));
261+
262+
// no longer in something that looks generic.
263+
test("Foo<Foo> number",
264+
ts.EndOfLineState.Start,
265+
identifier("Foo"),
266+
operator("<"),
267+
identifier("Foo"),
268+
operator(">"
269+
identifier("keyword"),
270+
finalEndOfLineState(ts.EndOfLineState.Start));
271+
});
234272
});
235273
});

0 commit comments

Comments
 (0)