Add more tests, special handling for mapped types

This commit is contained in:
Andrew Branch
2019-04-18 16:05:05 -07:00
parent af3d0f0248
commit f98c00ab9d
2 changed files with 207 additions and 54 deletions
+49 -10
View File
@@ -1744,6 +1744,14 @@ namespace ts.server {
};
}
private locationsAreEqual(a: protocol.Location, b: protocol.Location): boolean {
return a.line === b.line && a.offset === b.offset;
}
private locationTextSpansAreEqual(a: protocol.TextSpan, b: protocol.TextSpan): boolean {
return this.locationsAreEqual(a.start, b.start) && this.locationsAreEqual(a.end, b.end);
}
private getNavigationTree(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationTree | NavigationTree | undefined {
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
const tree = languageService.getNavigationTree(file);
@@ -2066,12 +2074,22 @@ namespace ts.server {
const sourceFile = languageService.getNonBoundSourceFile(file);
const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file));
const fullTextSpan = this.toLocationTextSpan(
createTextSpan(sourceFile.getFullStart(), sourceFile.getEnd() - sourceFile.getFullStart()),
createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()),
scriptInfo);
return map(locations, location => {
const pos = this.getPosition(location, scriptInfo);
let selectionRange: protocol.SelectionRange = { textSpan: fullTextSpan };
const pushSelectionRange = (textSpan: protocol.TextSpan, syntaxKind?: SyntaxKind): void => {
// Skip ranges that are identical to the parent
if (!this.locationTextSpansAreEqual(textSpan, selectionRange.textSpan)) {
selectionRange = { textSpan, parent: selectionRange };
if (syntaxKind) {
Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(syntaxKind) });
}
}
};
// Skip top-level SyntaxList
let current: Node | undefined = sourceFile.getChildAt(0);
while (true) {
@@ -2097,16 +2115,37 @@ namespace ts.server {
&& nextNode && nextNode.kind === SyntaxKind.CloseBraceToken;
const start = isBetweenBraces ? prevNode.getEnd() : node.getStart();
const end = isBetweenBraces ? nextNode.getStart() : node.getEnd();
const textSpan = this.toLocationTextSpan(createTextSpan(start, end - start), scriptInfo);
current = node;
// Skip ranges that are identical to the parent
if (selectionRange.textSpan.start !== textSpan.start || selectionRange.textSpan.end !== textSpan.end) {
selectionRange = {
textSpan,
parent: selectionRange,
};
Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(node.kind) });
const textSpan = this.toLocationTextSpan(createTextSpanFromBounds(start, end), scriptInfo);
pushSelectionRange(textSpan, node.kind);
// Mapped types _look_ like ObjectTypes with a single member,
// but in fact don’t contain a SyntaxList or a node containing
// the “key/value” pair like ObjectTypes do, but it seems intuitive
// that the selection would snap to those points. The philosophy
// of choosing a selection range is not so much about what the
// syntax currently _is_ as what the syntax might easily become
// if the user is making a selection; e.g., we synthesize a selection
// around the “key/value” pair not because there’s a node there, but
// because it allows the mapped type to become an object type with a
// few keystrokes.
if (isMappedTypeNode(node)) {
const openBraceToken = Debug.assertDefined(node.getFirstToken());
const firstNonBraceToken = Debug.assertDefined(node.getChildAt(1));
const closeBraceToken = Debug.assertDefined(node.getLastToken());
Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken);
Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken);
const spanWithoutBraces = this.toLocationTextSpan(createTextSpanFromBounds(
openBraceToken.getEnd(),
closeBraceToken.getStart(),
), scriptInfo);
const spanWithoutBracesOrTrivia = this.toLocationTextSpan(createTextSpanFromBounds(
firstNonBraceToken.getStart(),
closeBraceToken.getFullStart(),
), scriptInfo);
pushSelectionRange(spanWithoutBraces);
pushSelectionRange(spanWithoutBracesOrTrivia);
}
current = node;
break;
}
}
@@ -24,61 +24,175 @@ class Foo {
}
}`);
const locations = getSelectionRange([{
line: 4,
offset: 13
}]);
const locations = getSelectionRange([
{
line: 4,
offset: 13,
}, {
line: 5,
offset: 22,
},
]);
// Common to results for both locations
const ifStatementUp: protocol.SelectionRange = {
textSpan: { // IfStatement
start: { line: 4, offset: 9 },
end: { line: 6, offset: 10 } },
parent: {
textSpan: { // SyntaxList + whitespace (body of method)
start: { line: 3, offset: 16 },
end: { line: 8, offset: 5 } },
parent: {
textSpan: { // MethodDeclaration
start: { line: 3, offset: 5 },
end: { line: 8, offset: 6 } },
parent: {
textSpan: { // SyntaxList + whitespace (body of class)
start: { line: 2, offset: 12 },
end: { line: 9, offset: 1 } },
parent: {
textSpan: { // ClassDeclaration
start: { line: 2, offset: 1 },
end: { line: 9, offset: 2 } },
parent: {
textSpan: { // SourceFile (all text)
start: { line: 1, offset: 1 },
end: { line: 9, offset: 2 }, } } } } } } };
assert.deepEqual(locations, [
{
textSpan: { // a
start: { line: 4, offset: 13 },
end: { line: 4, offset: 14 },
},
end: { line: 4, offset: 14 } },
parent: {
textSpan: { // a === b
start: { line: 4, offset: 13 },
end: { line: 4, offset: 20 },
},
end: { line: 4, offset: 20 } },
parent: ifStatementUp } },
{
textSpan: { // true
start: { line: 5, offset: 20 },
end: { line: 5, offset: 24 } },
parent: {
textSpan: { // return true;
start: { line: 5, offset: 13 },
end: { line: 5, offset: 25 } },
parent: {
textSpan: { // IfStatement
start: { line: 4, offset: 9 },
end: { line: 6, offset: 10 },
},
parent: {
textSpan: { // SyntaxList + whitespace (body of method)
start: { line: 3, offset: 16 },
end: { line: 8, offset: 5 },
},
parent: {
textSpan: { // MethodDeclaration
start: { line: 3, offset: 5 },
end: { line: 8, offset: 6 },
},
parent: {
textSpan: { // SyntaxList + whitespace (body of class)
start: { line: 2, offset: 12 },
end: { line: 9, offset: 1 },
},
parent: {
textSpan: { // ClassDeclaration
start: { line: 2, offset: 1 },
end: { line: 9, offset: 2 },
},
parent: {
textSpan: { // SourceFile (all text)
start: { line: 1, offset: 1 },
end: { line: 9, offset: 2 },
}
}
}
}
},
},
},
},
textSpan: { // SyntaxList + whitespace (body of IfStatement)
start: { line: 4, offset: 23 },
end: { line: 6, offset: 9 } },
parent: ifStatementUp } } }
]);
});
it("works for simple TypeScript", () => {
const getSelectionRange = setup("/file.ts", `
export interface IService {
_serviceBrand: any;
open(host: number, data: any): Promise<any>;
}`);
const locations = getSelectionRange([
{
line: 5,
offset: 12,
},
]);
assert.deepEqual(locations, [
{
textSpan: { // host
start: { line: 5, offset: 10 },
end: { line: 5, offset: 14 } },
parent: {
textSpan: { // host: number
start: { line: 5, offset: 10 },
end: { line: 5, offset: 22 } },
parent: {
textSpan: { // host: number, data: any
start: { line: 5, offset: 10 },
end: { line: 5, offset: 33 } },
parent: {
textSpan: { // open(host: number, data: any): Promise<any>;
start: { line: 5, offset: 5 },
end: { line: 5, offset: 49 } },
parent: {
textSpan: { // SyntaxList + whitespace (body of interface)
start: { line: 2, offset: 28 },
end: { line: 6, offset: 1 } },
parent: {
textSpan: { // InterfaceDeclaration
start: { line: 2, offset: 1 },
end: { line: 6, offset: 2 } },
parent: {
textSpan: { // SourceFile
start: { line: 1, offset: 1 },
end: { line: 6, offset: 2 } } } } } } } } },
]);
});
it("works for complex TypeScript", () => {
const getSelectionRange = setup("/file.ts", `
type X<T, P> = IsExactlyAny<P> extends true ? T : ({ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>)
`);
const locations = getSelectionRange([
{
line: 2,
offset: 133,
},
]);
assert.deepEqual(locations, [
{
textSpan: { // K
start: { line: 2, offset: 133 },
end: { line: 2, offset: 134 } },
parent: {
textSpan: { // P[K]
start: { line: 2, offset: 131 },
end: { line: 2, offset: 135 } },
parent: {
textSpan: { // K extends keyof T ? T[K] : P[K]
start: { line: 2, offset: 104 },
end: { line: 2, offset: 135 } },
parent: {
textSpan: { // IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]
start: { line: 2, offset: 70 },
end: { line: 2, offset: 142 } },
parent: {
textSpan: { // [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K];
start: { line: 2, offset: 54 },
end: { line: 2, offset: 143 } },
parent: { // same as above + whitespace
textSpan: {
start: { line: 2, offset: 53 },
end: { line: 2, offset: 144 } },
parent: {
textSpan: { // MappedType: same as above + braces
start: { line: 2, offset: 52 },
end: { line: 2, offset: 145 } },
parent: {
textSpan: { // IntersectionType: { [K in keyof P]: ... } & Pick<T, Exclude<keyof T, keyof P>>
start: { line: 2, offset: 52 },
end: { line: 2, offset: 182 } },
parent: {
textSpan: { // same as above + parens
start: { line: 2, offset: 51 },
end: { line: 2, offset: 183 } },
parent: {
textSpan: { // Whole TypeNode of TypeAliasDeclaration
start: { line: 2, offset: 16 },
end: { line: 2, offset: 183 } },
parent: {
textSpan: { // Whole TypeAliasDeclaration
start: { line: 2, offset: 1 },
end: { line: 2, offset: 183 } },
parent: {
textSpan: { // SourceFile
start: { line: 1, offset: 1 },
end: { line: 2, offset: 184 } } } } } } } } } } } } } },
]);
});
});
}