Skip to content

Commit f89804e

Browse files
Merge pull request #672 from Microsoft/getSyntacticClassifications
Add support for syntactic classification.
2 parents 358733f + 38d2924 commit f89804e

File tree

8 files changed

+403
-4
lines changed

8 files changed

+403
-4
lines changed

src/compiler/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ module ts {
222222
FirstTypeNode = TypeReference,
223223
LastTypeNode = TupleType,
224224
FirstPunctuation = OpenBraceToken,
225-
LastPunctuation = CaretEqualsToken
225+
LastPunctuation = CaretEqualsToken,
226226
}
227227

228228
export enum NodeFlags {

src/harness/fourslash.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,46 @@ module FourSlash {
14281428
Harness.IO.log(this.getNameOrDottedNameSpan(pos));
14291429
}
14301430

1431+
private verifyClassifications(expected: { classificationType: string; text: string }[], actual: ts.ClassifiedSpan[]) {
1432+
if (actual.length !== expected.length) {
1433+
throw new Error('verifySyntacticClassification failed - expected total classifications to be ' + expected.length + ', but was ' + actual.length);
1434+
}
1435+
1436+
for (var i = 0; i < expected.length; i++) {
1437+
var expectedClassification = expected[i];
1438+
var actualClassification = actual[i];
1439+
1440+
var expectedType: string = (<any>ts.ClassificationTypeNames)[expectedClassification.classificationType];
1441+
if (expectedType !== actualClassification.classificationType) {
1442+
throw new Error('verifySyntacticClassification failed - expected classifications type to be ' +
1443+
expectedType + ', but was ' +
1444+
actualClassification.classificationType);
1445+
}
1446+
1447+
var actualSpan = actualClassification.textSpan;
1448+
var actualText = this.activeFile.content.substr(actualSpan.start(), actualSpan.length());
1449+
if (expectedClassification.text !== actualText) {
1450+
throw new Error('verifySyntacticClassification failed - expected classificatied text to be ' +
1451+
expectedClassification.text + ', but was ' +
1452+
actualText);
1453+
}
1454+
}
1455+
}
1456+
1457+
public verifySemanticClassifications(expected: { classificationType: string; text: string }[]) {
1458+
var actual = this.languageService.getSemanticClassifications(this.activeFile.fileName,
1459+
new TypeScript.TextSpan(0, this.activeFile.content.length));
1460+
1461+
this.verifyClassifications(expected, actual);
1462+
}
1463+
1464+
public verifySyntacticClassifications(expected: { classificationType: string; text: string }[]) {
1465+
var actual = this.languageService.getSyntacticClassifications(this.activeFile.fileName,
1466+
new TypeScript.TextSpan(0, this.activeFile.content.length));
1467+
1468+
this.verifyClassifications(expected, actual);
1469+
}
1470+
14311471
public verifyOutliningSpans(spans: TextSpan[]) {
14321472
this.taoInvalidReason = 'verifyOutliningSpans NYI';
14331473

src/services/services.ts

Lines changed: 226 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ module ts {
7575

7676
var scanner: Scanner = createScanner(ScriptTarget.ES5);
7777

78-
var emptyArray: any [] = [];
78+
var emptyArray: any[] = [];
7979

8080
function createNode(kind: SyntaxKind, pos: number, end: number, flags: NodeFlags, parent?: Node): NodeObject {
8181
var node = <NodeObject> new (getNodeConstructor(kind))();
@@ -259,7 +259,7 @@ module ts {
259259
getProperty(propertyName: string): Symbol {
260260
return this.checker.getPropertyOfType(this, propertyName);
261261
}
262-
getApparentProperties(): Symbol[]{
262+
getApparentProperties(): Symbol[] {
263263
return this.checker.getAugmentedPropertiesOfApparentType(this);
264264
}
265265
getCallSignatures(): Signature[] {
@@ -302,7 +302,7 @@ module ts {
302302
}
303303
}
304304

305-
var incrementalParse: IncrementalParse = TypeScript.IncrementalParser.parse;
305+
var incrementalParse: IncrementalParse = TypeScript.IncrementalParser.parse;
306306

307307
class SourceFileObject extends NodeObject implements SourceFile {
308308
public filename: string;
@@ -430,6 +430,9 @@ module ts {
430430
getSemanticDiagnostics(fileName: string): Diagnostic[];
431431
getCompilerOptionsDiagnostics(): Diagnostic[];
432432

433+
getSyntacticClassifications(fileName: string, span: TypeScript.TextSpan): ClassifiedSpan[];
434+
getSemanticClassifications(fileName: string, span: TypeScript.TextSpan): ClassifiedSpan[];
435+
433436
getCompletionsAtPosition(fileName: string, position: number, isMemberCompletion: boolean): CompletionInfo;
434437
getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails;
435438

@@ -467,6 +470,32 @@ module ts {
467470
dispose(): void;
468471
}
469472

473+
export class ClassificationTypeNames {
474+
public static comment = "comment";
475+
public static identifier = "identifier";
476+
public static keyword = "keyword";
477+
public static numericLiteral = "number";
478+
public static operator = "operator";
479+
public static stringLiteral = "string";
480+
public static whiteSpace = "whitespace";
481+
public static text = "text";
482+
483+
public static punctuation = "punctuation";
484+
485+
public static className = "class name";
486+
public static enumName = "enum name";
487+
public static interfaceName = "interface name";
488+
public static moduleName = "module name";
489+
public static typeParameterName = "type parameter name";
490+
}
491+
492+
export class ClassifiedSpan {
493+
constructor(public textSpan: TypeScript.TextSpan,
494+
public classificationType: string) {
495+
496+
}
497+
}
498+
470499
export class NavigationBarItem {
471500
constructor(public text: string,
472501
public kind: string,
@@ -3214,6 +3243,198 @@ module ts {
32143243
return new TypeScript.Services.NavigationBarItemGetter().getItems(syntaxTree.sourceUnit());
32153244
}
32163245

3246+
function getSemanticClassifications(fileName: string, span: TypeScript.TextSpan): ClassifiedSpan[] {
3247+
synchronizeHostData();
3248+
fileName = TypeScript.switchToForwardSlashes(fileName);
3249+
3250+
var sourceFile = getSourceFile(fileName);
3251+
3252+
var result: ClassifiedSpan[] = [];
3253+
processNode(sourceFile);
3254+
3255+
return result;
3256+
3257+
function classifySymbol(symbol: Symbol) {
3258+
var flags = symbol.getFlags();
3259+
3260+
if (flags & SymbolFlags.Class) {
3261+
return ClassificationTypeNames.className;
3262+
}
3263+
else if (flags & SymbolFlags.Enum) {
3264+
return ClassificationTypeNames.enumName;
3265+
}
3266+
else if (flags & SymbolFlags.Interface) {
3267+
return ClassificationTypeNames.interfaceName;
3268+
}
3269+
else if (flags & SymbolFlags.Module) {
3270+
return ClassificationTypeNames.moduleName;
3271+
}
3272+
else if (flags & SymbolFlags.TypeParameter) {
3273+
return ClassificationTypeNames.typeParameterName;
3274+
}
3275+
}
3276+
3277+
function processNode(node: Node) {
3278+
// Only walk into nodes that intersect the requested span.
3279+
if (node && span.intersectsWith(node.getStart(), node.getWidth())) {
3280+
if (node.kind === SyntaxKind.Identifier && node.getWidth() > 0) {
3281+
var symbol = typeInfoResolver.getSymbolInfo(node);
3282+
if (symbol) {
3283+
var type = classifySymbol(symbol);
3284+
if (type) {
3285+
result.push(new ClassifiedSpan(
3286+
new TypeScript.TextSpan(node.getStart(), node.getWidth()),
3287+
type));
3288+
}
3289+
}
3290+
}
3291+
3292+
forEachChild(node, processNode);
3293+
}
3294+
}
3295+
}
3296+
3297+
function getSyntacticClassifications(fileName: string, span: TypeScript.TextSpan): ClassifiedSpan[] {
3298+
// doesn't use compiler - no need to synchronize with host
3299+
fileName = TypeScript.switchToForwardSlashes(fileName);
3300+
var sourceFile = getCurrentSourceFile(fileName);
3301+
3302+
var result: ClassifiedSpan[] = [];
3303+
processElement(sourceFile.getSourceUnit());
3304+
3305+
return result;
3306+
3307+
function classifyTrivia(trivia: TypeScript.ISyntaxTrivia) {
3308+
if (trivia.isComment() && span.intersectsWith(trivia.fullStart(), trivia.fullWidth())) {
3309+
result.push(new ClassifiedSpan(
3310+
new TypeScript.TextSpan(trivia.fullStart(), trivia.fullWidth()),
3311+
ClassificationTypeNames.comment));
3312+
}
3313+
}
3314+
3315+
function classifyTriviaList(trivia: TypeScript.ISyntaxTriviaList) {
3316+
for (var i = 0, n = trivia.count(); i < n; i++) {
3317+
classifyTrivia(trivia.syntaxTriviaAt(i));
3318+
}
3319+
}
3320+
3321+
function classifyToken(token: TypeScript.ISyntaxToken) {
3322+
if (token.hasLeadingComment()) {
3323+
classifyTriviaList(token.leadingTrivia());
3324+
}
3325+
3326+
if (TypeScript.width(token) > 0) {
3327+
var type = classifyTokenType(token);
3328+
if (type) {
3329+
result.push(new ClassifiedSpan(
3330+
new TypeScript.TextSpan(TypeScript.start(token), TypeScript.width(token)),
3331+
type));
3332+
}
3333+
}
3334+
3335+
if (token.hasTrailingComment()) {
3336+
classifyTriviaList(token.trailingTrivia());
3337+
}
3338+
}
3339+
3340+
function classifyTokenType(token: TypeScript.ISyntaxToken): string {
3341+
var tokenKind = token.kind();
3342+
if (TypeScript.SyntaxFacts.isAnyKeyword(token.kind())) {
3343+
return ClassificationTypeNames.keyword;
3344+
}
3345+
3346+
// Special case < and > If they appear in a generic context they are punctation,
3347+
// not operators.
3348+
if (tokenKind === TypeScript.SyntaxKind.LessThanToken || tokenKind === TypeScript.SyntaxKind.GreaterThanToken) {
3349+
var tokenParentKind = token.parent.kind();
3350+
if (tokenParentKind === TypeScript.SyntaxKind.TypeArgumentList ||
3351+
tokenParentKind === TypeScript.SyntaxKind.TypeParameterList) {
3352+
3353+
return ClassificationTypeNames.punctuation;
3354+
}
3355+
}
3356+
3357+
if (TypeScript.SyntaxFacts.isBinaryExpressionOperatorToken(tokenKind) ||
3358+
TypeScript.SyntaxFacts.isPrefixUnaryExpressionOperatorToken(tokenKind)) {
3359+
return ClassificationTypeNames.operator;
3360+
}
3361+
else if (TypeScript.SyntaxFacts.isAnyPunctuation(tokenKind)) {
3362+
return ClassificationTypeNames.punctuation;
3363+
}
3364+
else if (tokenKind === TypeScript.SyntaxKind.NumericLiteral) {
3365+
return ClassificationTypeNames.numericLiteral;
3366+
}
3367+
else if (tokenKind === TypeScript.SyntaxKind.StringLiteral) {
3368+
return ClassificationTypeNames.stringLiteral;
3369+
}
3370+
else if (tokenKind === TypeScript.SyntaxKind.RegularExpressionLiteral) {
3371+
// TODO: we shoudl get another classification type for these literals.
3372+
return ClassificationTypeNames.stringLiteral;
3373+
}
3374+
else if (tokenKind === TypeScript.SyntaxKind.IdentifierName) {
3375+
var current: TypeScript.ISyntaxNodeOrToken = token;
3376+
var parent = token.parent;
3377+
while (parent.kind() === TypeScript.SyntaxKind.QualifiedName) {
3378+
current = parent;
3379+
parent = parent.parent;
3380+
}
3381+
3382+
switch (parent.kind()) {
3383+
case TypeScript.SyntaxKind.SimplePropertyAssignment:
3384+
if ((<TypeScript.SimplePropertyAssignmentSyntax>parent).propertyName === token) {
3385+
return ClassificationTypeNames.identifier;
3386+
}
3387+
return;
3388+
case TypeScript.SyntaxKind.ClassDeclaration:
3389+
if ((<TypeScript.ClassDeclarationSyntax>parent).identifier === token) {
3390+
return ClassificationTypeNames.className;
3391+
}
3392+
return;
3393+
case TypeScript.SyntaxKind.TypeParameter:
3394+
if ((<TypeScript.TypeParameterSyntax>parent).identifier === token) {
3395+
return ClassificationTypeNames.typeParameterName;
3396+
}
3397+
return;
3398+
case TypeScript.SyntaxKind.InterfaceDeclaration:
3399+
if ((<TypeScript.InterfaceDeclarationSyntax>parent).identifier === token) {
3400+
return ClassificationTypeNames.interfaceName;
3401+
}
3402+
return;
3403+
case TypeScript.SyntaxKind.EnumDeclaration:
3404+
if ((<TypeScript.EnumDeclarationSyntax>parent).identifier === token) {
3405+
return ClassificationTypeNames.enumName;
3406+
}
3407+
return;
3408+
case TypeScript.SyntaxKind.ModuleDeclaration:
3409+
if ((<TypeScript.ModuleDeclarationSyntax>parent).name === current) {
3410+
return ClassificationTypeNames.moduleName;
3411+
}
3412+
return;
3413+
default:
3414+
return ClassificationTypeNames.text;
3415+
}
3416+
}
3417+
}
3418+
3419+
function processElement(element: TypeScript.ISyntaxElement) {
3420+
// Ignore nodes that don't intersect the original span to classify.
3421+
if (!TypeScript.isShared(element) && span.intersectsWith(TypeScript.fullStart(element), TypeScript.fullWidth(element))) {
3422+
for (var i = 0, n = TypeScript.childCount(element); i < n; i++) {
3423+
var child = TypeScript.childAt(element, i);
3424+
if (child) {
3425+
if (TypeScript.isToken(child)) {
3426+
classifyToken(<TypeScript.ISyntaxToken>child);
3427+
}
3428+
else {
3429+
// Recurse into our child nodes.
3430+
processElement(child);
3431+
}
3432+
}
3433+
}
3434+
}
3435+
}
3436+
}
3437+
32173438
function getOutliningSpans(filename: string): OutliningSpan[] {
32183439
// doesn't use compiler - no need to synchronize with host
32193440
filename = TypeScript.switchToForwardSlashes(filename);
@@ -3461,6 +3682,8 @@ module ts {
34613682
getSyntacticDiagnostics: getSyntacticDiagnostics,
34623683
getSemanticDiagnostics: getSemanticDiagnostics,
34633684
getCompilerOptionsDiagnostics: getCompilerOptionsDiagnostics,
3685+
getSyntacticClassifications: getSyntacticClassifications,
3686+
getSemanticClassifications: getSemanticClassifications,
34643687
getCompletionsAtPosition: getCompletionsAtPosition,
34653688
getCompletionEntryDetails: getCompletionEntryDetails,
34663689
getTypeAtPosition: getTypeAtPosition,

src/services/shims.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ module ts {
8080
getSemanticDiagnostics(fileName: string): string;
8181
getCompilerOptionsDiagnostics(): string;
8282

83+
getSyntacticClassifications(fileName: string, start: number, length: number): string;
84+
8385
getCompletionsAtPosition(fileName: string, position: number, isMemberCompletion: boolean): string;
8486
getCompletionEntryDetails(fileName: string, position: number, entryName: string): string;
8587

@@ -477,6 +479,24 @@ module ts {
477479
};
478480
}
479481

482+
public getSyntacticClassifications(fileName: string, start: number, length: number): string {
483+
return this.forwardJSONCall(
484+
"getSyntacticClassifications('" + fileName + "', " + start + ", " + length + ")",
485+
() => {
486+
var classifications = this.languageService.getSyntacticClassifications(fileName, new TypeScript.TextSpan(start, length));
487+
return classifications;
488+
});
489+
}
490+
491+
public getSemanticClassifications(fileName: string, start: number, length: number): string {
492+
return this.forwardJSONCall(
493+
"getSemanticClassifications('" + fileName + "', " + start + ", " + length + ")",
494+
() => {
495+
var classifications = this.languageService.getSemanticClassifications(fileName, new TypeScript.TextSpan(start, length));
496+
return classifications;
497+
});
498+
}
499+
480500
public getSyntacticDiagnostics(fileName: string): string {
481501
return this.forwardJSONCall(
482502
"getSyntacticDiagnostics('" + fileName + "')",

src/services/text/textSpan.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
///<reference path='references.ts' />
22

33
module TypeScript {
4+
45
export interface ISpan {
56
start(): number;
67
end(): number;

0 commit comments

Comments
 (0)