mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-15 21:36:50 -05:00
Solidify fake tree approach
This commit is contained in:
@@ -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 they’re 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 don’t 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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user