Solidify fake tree approach

This commit is contained in:
Andrew Branch
2019-04-15 14:51:52 -07:00
parent fcb7f0152f
commit 4ecdc82736
2 changed files with 204 additions and 80 deletions

View File

@@ -1,14 +1,11 @@
/* @internal */
namespace ts.SelectionRange {
const isImport = or(isImportDeclaration, isImportEqualsDeclaration);
export function getSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange {
let selectionRange: SelectionRange = {
textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd())
};
// Skip top-level SyntaxList
let parentNode = sourceFile.getChildAt(0);
let parentNode: Node = sourceFile;
outer: while (true) {
const children = getSelectionChildren(parentNode);
if (!children.length) break;
@@ -36,13 +33,6 @@ namespace ts.SelectionRange {
const end = nextNode.getStart() + "}".length;
pushSelectionRange(start, end, node.kind);
}
// Synthesize a stop for group of adjacent imports
else if (isImport(node)) {
const [firstImportIndex, lastImportIndex] = getGroupBounds(children, i, isImport);
pushSelectionRange(
children[firstImportIndex].getStart(),
children[lastImportIndex].getEnd());
}
// Blocks with braces on separate lines should be selected from brace to brace,
// including whitespace but not including the braces themselves.
@@ -79,7 +69,22 @@ namespace ts.SelectionRange {
}
}
const isImport = or(isImportDeclaration, isImportEqualsDeclaration);
/**
* Gets the children of a node to be considered for selection ranging,
* transforming them into an artificial tree according to their intuitive
* grouping where no grouping actually exists in the parse tree. For example,
* top-level imports are grouped into their own SyntaxList so they can be
* selected all together, even though in the AST theyre just siblings of each
* other as well as of other top-level statements and declarations.
*/
function getSelectionChildren(node: Node): ReadonlyArray<Node> {
// Group top-level imports
if (isSourceFile(node)) {
return groupChildren(node.getChildAt(0).getChildren(), isImport);
}
// Mapped types _look_ like ObjectTypes with a single member,
// but in fact dont contain a SyntaxList or a node containing
// the “key/value” pair like ObjectTypes do, but it seems intuitive
@@ -95,58 +100,108 @@ namespace ts.SelectionRange {
const closeBraceToken = Debug.assertDefined(children.pop());
Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken);
Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken);
const colonTokenIndex = findIndex(children, child => child.kind === SyntaxKind.ColonToken);
const typeNodeIndex = node.type && children.indexOf(node.type);
const leftChildren = children.slice(0, colonTokenIndex);
const colonToken = Debug.assertDefined(children[colonTokenIndex]);
const rightChildren = children.slice(colonTokenIndex + 1, typeNodeIndex && (typeNodeIndex + 1));
// Possible semicolon
const extraChildren = typeNodeIndex && typeNodeIndex > -1 ? children.slice(typeNodeIndex + 1) : [];
const syntaxList = createSyntaxList([
createSyntaxList(leftChildren),
colonToken,
createSyntaxList(rightChildren),
createSyntaxList(extraChildren),
]);
const [leftOfColon, ...rest] = splitChildren(children, child => child.kind === SyntaxKind.ColonToken);
// Group `-/+readonly` and `-/+?`
const leftChildren = groupChildren(getChildrenOrSingleNode(leftOfColon), child =>
child === node.readonlyToken || child.kind === SyntaxKind.ReadonlyKeyword ||
child === node.questionToken || child.kind === SyntaxKind.QuestionToken);
return [
openBraceToken,
syntaxList,
createSyntaxList([
// Group type parameter with surrounding brackets
createSyntaxList(groupChildren(leftChildren, ({ kind }) =>
kind === SyntaxKind.OpenBracketToken ||
kind === SyntaxKind.TypeParameter ||
kind === SyntaxKind.CloseBracketToken
)),
...rest,
]),
closeBraceToken,
];
}
// Split e.g. `readonly foo?: string` into left and right sides of the colon,
// the group `readonly foo` without the QuestionToken.
if (isPropertySignature(node)) {
const [leftOfColon, ...rest] = splitChildren(node.getChildren(), child => child.kind === SyntaxKind.ColonToken);
return [
createSyntaxList(groupChildren(getChildrenOrSingleNode(leftOfColon), child => child !== node.questionToken)),
...rest,
];
}
return node.getChildren();
}
/**
* Groups sibling nodes together into their own SyntaxList if they
* a) are adjacent, AND b) match a predicate function.
*/
function groupChildren(children: Node[], groupOn: (child: Node) => boolean): Node[] {
const result: Node[] = [];
let group: Node[] | undefined;
for (const child of children) {
if (groupOn(child)) {
group = group || [];
group.push(child);
}
else {
if (group) {
result.push(createSyntaxList(group));
group = undefined;
}
result.push(child);
}
}
if (group) {
result.push(createSyntaxList(group));
}
return result;
}
/**
* Splits sibling nodes into up to four partitions:
* 1) everything left of the first node matched by `pivotOn`,
* 2) the first node matched by `pivotOn`,
* 3) everything right of the first node matched by `pivotOn`,
* 4) a trailing semicolon, if `separateTrailingSemicolon` is enabled.
* The left and right groups, if not empty, will each be grouped into their own containing SyntaxList.
* @param children The sibling nodes to split.
* @param pivotOn The predicate function to match the node to be the pivot. The first node that matches
* the predicate will be used; any others that may match will be included into the right-hand group.
* @param separateTrailingSemicolon If the last token is a semicolon, it will be returned as a separate
* child rather than be included in the right-hand group.
*/
function splitChildren(children: Node[], pivotOn: (child: Node) => boolean, separateTrailingSemicolon = true): Node[] {
if (children.length < 2) {
return children;
}
const splitTokenIndex = findIndex(children, pivotOn);
if (splitTokenIndex === -1) {
return children;
}
const leftChildren = children.slice(0, splitTokenIndex);
const splitToken = children[splitTokenIndex];
const lastToken = last(children);
const separateLastToken = separateTrailingSemicolon && lastToken.kind === SyntaxKind.SemicolonToken;
const rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined);
const result = compact([
leftChildren.length ? createSyntaxList(leftChildren) : undefined,
splitToken,
rightChildren.length ? createSyntaxList(rightChildren) : undefined,
]);
return separateLastToken ? result.concat(lastToken) : result;
}
function getChildrenOrSingleNode(node: Node): Node[] {
return isSyntaxList(node) ? node.getChildren() : [node];
}
function createSyntaxList(children: Node[]): SyntaxList {
Debug.assertGreaterThanOrEqual(children.length, 1);
const syntaxList = createNode(SyntaxKind.SyntaxList, children[0].pos, last(children).end) as SyntaxList;
syntaxList._children = children;
return syntaxList;
}
function getGroupBounds<T>(array: ArrayLike<T>, index: number, predicate: (element: T) => boolean): [number, number] {
let first = index;
let last = index;
let i = index;
while (i > 0) {
const element = array[--i];
if (predicate(element)) {
first = i;
}
else {
break;
}
}
i = index;
while (i < array.length - 1) {
const element = array[++i];
if (predicate(element)) {
last = i;
}
else {
break;
}
}
return [first, last];
}
}

View File

@@ -191,35 +191,37 @@ type X<T, P> = IsExactlyAny<P> extends true ? T : ({ [K in keyof P]: IsExactlyAn
]);
});
it.skip("works for object types", () => {
it("works for object types", () => {
const getSelectionRange = setup("/file.js", `
type X = {
foo?: string;
readonly bar: { x: number };
meh
}`);
const locations = getSelectionRange([
{ line: 3, offset: 5 },
{ line: 4, offset: 5 },
{ line: 4, offset: 14 },
{ line: 4, offset: 27 },
{ line: 5, offset: 5 },
]);
const allMembersUp: protocol.SelectionRange = {
textSpan: { // all members + whitespace (just inside braces)
start: { line: 2, offset: 11 },
end: { line: 5, offset: 1 } },
end: { line: 6, offset: 1 } },
parent: {
textSpan: { // add braces
start: { line: 2, offset: 10 },
end: { line: 5, offset: 2 } },
end: { line: 6, offset: 2 } },
parent: {
textSpan: { // whole TypeAliasDeclaration
start: { line: 2, offset: 1 },
end: { line: 5, offset: 2 } },
end: { line: 6, offset: 2 } },
parent: {
textSpan: { // SourceFile
start: { line: 1, offset: 1 },
end: { line: 5, offset: 2 } } } } } };
end: { line: 6, offset: 2 } } } } } };
const readonlyBarUp: protocol.SelectionRange = {
textSpan: { // readonly bar
@@ -270,6 +272,12 @@ type X = {
start: { line: 4, offset: 19 },
end: { line: 4, offset: 32 } },
parent: readonlyBarUp.parent } } });
assert.deepEqual(locations![4], {
textSpan: { // meh
start: { line: 5, offset: 5 },
end: { line: 5, offset: 8 } },
parent: allMembersUp });
});
it("works for string literals and template strings", () => {
@@ -355,7 +363,7 @@ console.log(1);`);
]);
});
it.skip("works for complex mapped types", () => {
it("works for complex mapped types", () => {
const getSelectionRange = setup("/file.ts", `
type M = { -readonly [K in keyof any]-?: any };`);
@@ -368,38 +376,99 @@ type M = { -readonly [K in keyof any]-?: any };`);
{ line: 2, offset: 39 }, // ?
]);
const leftOfColonUp: protocol.SelectionRange = {
textSpan: { // -readonly [K in keyof any]-?
start: { line: 2, offset: 12 },
end: { line: 2, offset: 40 } },
parent: {
textSpan: { // -readonly [K in keyof any]-?: any
start: { line: 2, offset: 12 },
end: { line: 2, offset: 45 } },
parent: {
textSpan: { // { -readonly [K in keyof any]-?: any }
start: { line: 2, offset: 10 },
end: { line: 2, offset: 47 } },
parent: {
textSpan: { // whole line
start: { line: 2, offset: 1 },
end: { line: 2, offset: 48 } },
parent: {
textSpan: { // SourceFile
start: { line: 1, offset: 1 },
end: { line: 2, offset: 48 } } } } } } };
assert.deepEqual(locations![0], {
textSpan: { // -
textSpan: { // - (in -readonly)
start: { line: 2, offset: 12 },
end: { line: 2, offset: 13 } },
parent: {
textSpan: { // -readonly
start: { line: 2, offset: 12 },
end: { line: 2, offset: 21 } },
parent: leftOfColonUp },
});
assert.deepEqual(locations![1], {
textSpan: { // readonly
start: { line: 2, offset: 13 },
end: { line: 2, offset: 21 } },
parent: {
textSpan: { // -readonly
start: { line: 2, offset: 12 },
end: { line: 2, offset: 21 } },
parent: leftOfColonUp },
});
assert.deepEqual(locations![2], {
textSpan: { // [
start: { line: 2, offset: 22 },
end: { line: 2, offset: 23 } },
parent: {
textSpan: { // [K in keyof any]
start: { line: 2, offset: 22 },
end: { line: 2, offset: 38 } },
parent: leftOfColonUp }
});
assert.deepEqual(locations![3], {
textSpan: { // keyof
start: { line: 2, offset: 28 },
end: { line: 2, offset: 33 } },
parent: {
textSpan: { // keyof any
start: { line: 2, offset: 28 },
end: { line: 2, offset: 37 } },
parent: {
textSpan: { // -readonly [K in keyof any]
start: { line: 2, offset: 12 },
end: { line: 2, offset: 38 } },
textSpan: { // K in keyof any
start: { line: 2, offset: 23 },
end: { line: 2, offset: 37 } },
parent: {
textSpan: { // -readonly [K in keyof any]-?
start: { line: 2, offset: 12 },
end: { line: 2, offset: 40 } },
parent: {
textSpan: { // -readonly [K in keyof any]-?: any
start: { line: 2, offset: 12 },
end: { line: 2, offset: 45 } },
parent: {
textSpan: { // { -readonly [K in keyof any]-?: any }
start: { line: 2, offset: 10 },
end: { line: 2, offset: 47 } },
parent: {
textSpan: { // whole line
start: { line: 2, offset: 1 },
end: { line: 2, offset: 48 } },
parent: {
textSpan: { // SourceFile
start: { line: 1, offset: 1 },
end: { line: 2, offset: 48 } } } } } } } } }
textSpan: { // [K in keyof any]
start: { line: 2, offset: 22 },
end: { line: 2, offset: 38 } },
parent: leftOfColonUp } } },
});
assert.deepEqual(locations![4], {
textSpan: { // - (in -?)
start: { line: 2, offset: 38 },
end: { line: 2, offset: 39 } },
parent: {
textSpan: { // -?
start: { line: 2, offset: 38 },
end: { line: 2, offset: 40 } },
parent: leftOfColonUp },
});
assert.deepEqual(locations![5], {
textSpan: { // ?
start: { line: 2, offset: 39 },
end: { line: 2, offset: 40 } },
parent: {
textSpan: { // -?
start: { line: 2, offset: 38 },
end: { line: 2, offset: 40 } },
parent: leftOfColonUp },
});
});
});