From 791af381395d5e0ce1af5f2227c1f70a41d30d66 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Thu, 16 May 2019 13:10:50 -0700 Subject: [PATCH] Update LKG. --- lib/protocol.d.ts | 17 +- lib/tsserver.js | 310 +++++++++++++++++++++++++++++++++++- lib/tsserverlibrary.d.ts | 24 ++- lib/tsserverlibrary.js | 310 +++++++++++++++++++++++++++++++++++- lib/typescript.d.ts | 5 + lib/typescript.js | 281 +++++++++++++++++++++++++++++++- lib/typescriptServices.d.ts | 5 + lib/typescriptServices.js | 281 +++++++++++++++++++++++++++++++- 8 files changed, 1223 insertions(+), 10 deletions(-) diff --git a/lib/protocol.d.ts b/lib/protocol.d.ts index 483bc2aa28761..b56304021ea94 100644 --- a/lib/protocol.d.ts +++ b/lib/protocol.d.ts @@ -63,7 +63,8 @@ declare namespace ts.server.protocol { GetEditsForRefactor = "getEditsForRefactor", OrganizeImports = "organizeImports", GetEditsForFileRename = "getEditsForFileRename", - ConfigurePlugin = "configurePlugin" + ConfigurePlugin = "configurePlugin", + SelectionRange = "selectionRange" } /** * A TypeScript Server message @@ -1026,6 +1027,20 @@ declare namespace ts.server.protocol { } interface ConfigurePluginResponse extends Response { } + interface SelectionRangeRequest extends FileRequest { + command: CommandTypes.SelectionRange; + arguments: SelectionRangeRequestArgs; + } + interface SelectionRangeRequestArgs extends FileRequestArgs { + locations: Location[]; + } + interface SelectionRangeResponse extends Response { + body?: SelectionRange[]; + } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Information found in an "open" request. */ diff --git a/lib/tsserver.js b/lib/tsserver.js index 8d3f91a6a558b..a36958ec6b226 100644 --- a/lib/tsserver.js +++ b/lib/tsserver.js @@ -107032,6 +107032,275 @@ var ts; })(ts || (ts = {})); /* @internal */ var ts; +(function (ts) { + var SmartSelectionRange; + (function (SmartSelectionRange) { + function getSmartSelectionRange(pos, sourceFile) { + var selectionRange = { + textSpan: ts.createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; + var parentNode = sourceFile; + outer: while (true) { + var children = getSelectionChildren(parentNode); + if (!children.length) + break; + for (var i = 0; i < children.length; i++) { + var prevNode = children[i - 1]; + var node = children[i]; + var nextNode = children[i + 1]; + if (node.getStart(sourceFile) > pos) { + break outer; + } + if (positionShouldSnapToNode(pos, node, nextNode)) { + // 1. Blocks are effectively redundant with SyntaxLists. + // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping + // of things that should be considered independently. + // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. + // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. + // + // Dive in without pushing a selection range. + if (ts.isBlock(node) + || ts.isTemplateSpan(node) || ts.isTemplateHead(node) + || prevNode && ts.isTemplateHead(prevNode) + || ts.isVariableDeclarationList(node) && ts.isVariableStatement(parentNode) + || ts.isSyntaxList(node) && ts.isVariableDeclarationList(parentNode) + || ts.isVariableDeclaration(node) && ts.isSyntaxList(parentNode) && children.length === 1) { + parentNode = node; + break; + } + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (ts.isTemplateSpan(parentNode) && nextNode && ts.isTemplateMiddleOrTemplateTail(nextNode)) { + var start_2 = node.getFullStart() - "${".length; + var end_2 = nextNode.getStart() + "}".length; + pushSelectionRange(start_2, end_2); + } + // Blocks with braces, brackets, parens, or JSX tags on separate lines should be + // selected from open to close, including whitespace but not including the braces/etc. themselves. + var isBetweenMultiLineBookends = ts.isSyntaxList(node) + && isListOpener(prevNode) + && isListCloser(nextNode) + && !ts.positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + var jsDocCommentStart = ts.hasJSDocNodes(node) && node.jsDoc[0].getStart(); + var start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + var end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); + if (ts.isNumber(jsDocCommentStart)) { + pushSelectionRange(jsDocCommentStart, end); + } + pushSelectionRange(start, end); + // String literals should have a stop both inside and outside their quotes. + if (ts.isStringLiteral(node) || ts.isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + parentNode = node; + break; + } + } + } + return selectionRange; + function pushSelectionRange(start, end) { + // Skip empty ranges + if (start !== end) { + // Skip ranges that are identical to the parent + var textSpan = ts.createTextSpanFromBounds(start, end); + if (!selectionRange || !ts.textSpansEqual(textSpan, selectionRange.textSpan)) { + selectionRange = __assign({ textSpan: textSpan }, selectionRange && { parent: selectionRange }); + } + } + } + } + SmartSelectionRange.getSmartSelectionRange = getSmartSelectionRange; + /** + * Like `ts.positionBelongsToNode`, except positions immediately after nodes + * count too, unless that position belongs to the next node. In effect, makes + * selections able to snap to preceding tokens when the cursor is on the tail + * end of them with only whitespace ahead. + * @param pos The position to check. + * @param node The candidate node to snap to. + * @param nextNode The next sibling node in the tree. + * @param sourceFile The source file containing the nodes. + */ + function positionShouldSnapToNode(pos, node, nextNode) { + // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts + // for missing nodes, which can’t really be considered when deciding what + // to select. + ts.Debug.assert(node.pos <= pos); + if (pos < node.end) { + return true; + } + var nodeEnd = node.getEnd(); + var nextNodeStart = nextNode && nextNode.getStart(); + if (nodeEnd === pos) { + return pos !== nextNodeStart; + } + return false; + } + var isImport = ts.or(ts.isImportDeclaration, ts.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) { + // Group top-level imports + if (ts.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 + // 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 (ts.isMappedTypeNode(node)) { + var _a = node.getChildren(), openBraceToken = _a[0], children = _a.slice(1); + var closeBraceToken = ts.Debug.assertDefined(children.pop()); + ts.Debug.assertEqual(openBraceToken.kind, 18 /* OpenBraceToken */); + ts.Debug.assertEqual(closeBraceToken.kind, 19 /* CloseBraceToken */); + // Group `-/+readonly` and `-/+?` + var groupedWithPlusMinusTokens = groupChildren(children, function (child) { + return child === node.readonlyToken || child.kind === 134 /* ReadonlyKeyword */ || + child === node.questionToken || child.kind === 56 /* QuestionToken */; + }); + // Group type parameter with surrounding brackets + var groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, function (_a) { + var kind = _a.kind; + return kind === 22 /* OpenBracketToken */ || + kind === 151 /* TypeParameter */ || + kind === 23 /* CloseBracketToken */; + }); + return [ + openBraceToken, + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + })), + closeBraceToken, + ]; + } + // Group modifiers and property name, then pivot on `:`. + if (ts.isPropertySignature(node)) { + var children = groupChildren(node.getChildren(), function (child) { + return child === node.name || ts.contains(node.modifiers, child); + }); + return splitChildren(children, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + }); + } + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (ts.isParameter(node)) { + var groupedDotDotDotAndName_1 = groupChildren(node.getChildren(), function (child) { + return child === node.dotDotDotToken || child === node.name; + }); + var groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName_1, function (child) { + return child === groupedDotDotDotAndName_1[0] || child === node.questionToken; + }); + return splitChildren(groupedWithQuestionToken, function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + // Pivot on '=' + if (ts.isBindingElement(node)) { + return splitChildren(node.getChildren(), function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + 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, groupOn) { + var result = []; + var group; + for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { + var child = children_1[_i]; + 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, pivotOn, separateTrailingSemicolon) { + if (separateTrailingSemicolon === void 0) { separateTrailingSemicolon = true; } + if (children.length < 2) { + return children; + } + var splitTokenIndex = ts.findIndex(children, pivotOn); + if (splitTokenIndex === -1) { + return children; + } + var leftChildren = children.slice(0, splitTokenIndex); + var splitToken = children[splitTokenIndex]; + var lastToken = ts.last(children); + var separateLastToken = separateTrailingSemicolon && lastToken.kind === 26 /* SemicolonToken */; + var rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); + var result = ts.compact([ + leftChildren.length ? createSyntaxList(leftChildren) : undefined, + splitToken, + rightChildren.length ? createSyntaxList(rightChildren) : undefined, + ]); + return separateLastToken ? result.concat(lastToken) : result; + } + function createSyntaxList(children) { + ts.Debug.assertGreaterThanOrEqual(children.length, 1); + var syntaxList = ts.createNode(312 /* SyntaxList */, children[0].pos, ts.last(children).end); + syntaxList._children = children; + return syntaxList; + } + function isListOpener(token) { + var kind = token && token.kind; + return kind === 18 /* OpenBraceToken */ + || kind === 22 /* OpenBracketToken */ + || kind === 20 /* OpenParenToken */ + || kind === 263 /* JsxOpeningElement */; + } + function isListCloser(token) { + var kind = token && token.kind; + return kind === 19 /* CloseBraceToken */ + || kind === 23 /* CloseBracketToken */ + || kind === 21 /* CloseParenToken */ + || kind === 264 /* JsxClosingElement */; + } + })(SmartSelectionRange = ts.SmartSelectionRange || (ts.SmartSelectionRange = {})); +})(ts || (ts = {})); +/* @internal */ +var ts; (function (ts) { var SignatureHelp; (function (SignatureHelp) { @@ -116229,8 +116498,8 @@ var ts; } } if (ts.isBlock(statement.parent)) { - var end_2 = start + length; - var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_2; })); + var end_3 = start + length; + var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_3; })); changes.deleteNodeRange(sourceFile, statement, lastStatement); } else { @@ -122169,6 +122438,9 @@ var ts; preferences: preferences, }; } + function getSmartSelectionRange(fileName, position) { + return ts.SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + } function getApplicableRefactors(fileName, positionOrRange, preferences) { if (preferences === void 0) { preferences = ts.emptyOptions; } synchronizeHostData(); @@ -122209,6 +122481,7 @@ var ts; getBreakpointStatementAtPosition: getBreakpointStatementAtPosition, getNavigateToItems: getNavigateToItems, getRenameInfo: getRenameInfo, + getSmartSelectionRange: getSmartSelectionRange, findRenameLocations: findRenameLocations, getNavigationBarItems: getNavigationBarItems, getNavigationTree: getNavigationTree, @@ -123416,6 +123689,10 @@ var ts; var _this = this; return this.forwardJSONCall("getRenameInfo('" + fileName + "', " + position + ")", function () { return _this.languageService.getRenameInfo(fileName, position, options); }); }; + LanguageServiceShimObject.prototype.getSmartSelectionRange = function (fileName, position) { + var _this = this; + return this.forwardJSONCall("getSmartSelectionRange('" + fileName + "', " + position + ")", function () { return _this.languageService.getSmartSelectionRange(fileName, position); }); + }; LanguageServiceShimObject.prototype.findRenameLocations = function (fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) { var _this = this; return this.forwardJSONCall("findRenameLocations('" + fileName + "', " + position + ", " + findInStrings + ", " + findInComments + ", " + providePrefixAndSuffixTextForRename + ")", function () { return _this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename); }); @@ -124440,6 +124717,9 @@ var ts; /* @internal */ CommandTypes["GetEditsForFileRenameFull"] = "getEditsForFileRename-full"; CommandTypes["ConfigurePlugin"] = "configurePlugin"; + CommandTypes["SelectionRange"] = "selectionRange"; + /* @internal */ + CommandTypes["SelectionRangeFull"] = "selectionRange-full"; // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. })(CommandTypes = protocol.CommandTypes || (protocol.CommandTypes = {})); var IndentStyle; @@ -129768,6 +130048,12 @@ var ts; _this.doOutput(/*info*/ undefined, server.CommandNames.ConfigurePlugin, request.seq, /*success*/ true); return _this.notRequired(); }, + _a[server.CommandNames.SelectionRange] = function (request) { + return _this.requiredResponse(_this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ true)); + }, + _a[server.CommandNames.SelectionRangeFull] = function (request) { + return _this.requiredResponse(_this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ false)); + }, _a)); this.host = opts.host; this.cancellationToken = opts.cancellationToken; @@ -131104,6 +131390,26 @@ var ts; Session.prototype.configurePlugin = function (args) { this.projectService.configurePlugin(args); }; + Session.prototype.getSmartSelectionRange = function (args, simplifiedResult) { + var _this = this; + var locations = args.locations; + var _a = this.getFileAndLanguageServiceForSyntacticOperation(args), file = _a.file, languageService = _a.languageService; + var scriptInfo = ts.Debug.assertDefined(this.projectService.getScriptInfo(file)); + return ts.map(locations, function (location) { + var pos = _this.getPosition(location, scriptInfo); + var selectionRange = languageService.getSmartSelectionRange(file, pos); + return simplifiedResult ? _this.mapSelectionRange(selectionRange, scriptInfo) : selectionRange; + }); + }; + Session.prototype.mapSelectionRange = function (selectionRange, scriptInfo) { + var result = { + textSpan: this.toLocationTextSpan(selectionRange.textSpan, scriptInfo), + }; + if (selectionRange.parent) { + result.parent = this.mapSelectionRange(selectionRange.parent, scriptInfo); + } + return result; + }; Session.prototype.getCanonicalFileName = function (fileName) { var name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); return ts.normalizePath(name); diff --git a/lib/tsserverlibrary.d.ts b/lib/tsserverlibrary.d.ts index ca97395891a31..4b06bc9e8392a 100644 --- a/lib/tsserverlibrary.d.ts +++ b/lib/tsserverlibrary.d.ts @@ -4809,6 +4809,7 @@ declare namespace ts { getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; + getSmartSelectionRange(fileName: string, position: number): SelectionRange; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -5253,6 +5254,10 @@ declare namespace ts { displayParts: SymbolDisplayPart[]; isOptional: boolean; } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Represents a single signature to show in signature help. * The id is used for subsequent calls into the language service to ask questions about the @@ -5800,7 +5805,8 @@ declare namespace ts.server.protocol { GetEditsForRefactor = "getEditsForRefactor", OrganizeImports = "organizeImports", GetEditsForFileRename = "getEditsForFileRename", - ConfigurePlugin = "configurePlugin" + ConfigurePlugin = "configurePlugin", + SelectionRange = "selectionRange", } /** * A TypeScript Server message @@ -6763,6 +6769,20 @@ declare namespace ts.server.protocol { } interface ConfigurePluginResponse extends Response { } + interface SelectionRangeRequest extends FileRequest { + command: CommandTypes.SelectionRange; + arguments: SelectionRangeRequestArgs; + } + interface SelectionRangeRequestArgs extends FileRequestArgs { + locations: Location[]; + } + interface SelectionRangeResponse extends Response { + body?: SelectionRange[]; + } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Information found in an "open" request. */ @@ -9039,6 +9059,8 @@ declare namespace ts.server { private getBraceMatching; private getDiagnosticsForProject; private configurePlugin; + private getSmartSelectionRange; + private mapSelectionRange; getCanonicalFileName(fileName: string): string; exit(): void; private notRequired; diff --git a/lib/tsserverlibrary.js b/lib/tsserverlibrary.js index e577c4ae9bef0..7158a5452faa6 100644 --- a/lib/tsserverlibrary.js +++ b/lib/tsserverlibrary.js @@ -107375,6 +107375,275 @@ var ts; })(ts || (ts = {})); /* @internal */ var ts; +(function (ts) { + var SmartSelectionRange; + (function (SmartSelectionRange) { + function getSmartSelectionRange(pos, sourceFile) { + var selectionRange = { + textSpan: ts.createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; + var parentNode = sourceFile; + outer: while (true) { + var children = getSelectionChildren(parentNode); + if (!children.length) + break; + for (var i = 0; i < children.length; i++) { + var prevNode = children[i - 1]; + var node = children[i]; + var nextNode = children[i + 1]; + if (node.getStart(sourceFile) > pos) { + break outer; + } + if (positionShouldSnapToNode(pos, node, nextNode)) { + // 1. Blocks are effectively redundant with SyntaxLists. + // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping + // of things that should be considered independently. + // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. + // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. + // + // Dive in without pushing a selection range. + if (ts.isBlock(node) + || ts.isTemplateSpan(node) || ts.isTemplateHead(node) + || prevNode && ts.isTemplateHead(prevNode) + || ts.isVariableDeclarationList(node) && ts.isVariableStatement(parentNode) + || ts.isSyntaxList(node) && ts.isVariableDeclarationList(parentNode) + || ts.isVariableDeclaration(node) && ts.isSyntaxList(parentNode) && children.length === 1) { + parentNode = node; + break; + } + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (ts.isTemplateSpan(parentNode) && nextNode && ts.isTemplateMiddleOrTemplateTail(nextNode)) { + var start_2 = node.getFullStart() - "${".length; + var end_2 = nextNode.getStart() + "}".length; + pushSelectionRange(start_2, end_2); + } + // Blocks with braces, brackets, parens, or JSX tags on separate lines should be + // selected from open to close, including whitespace but not including the braces/etc. themselves. + var isBetweenMultiLineBookends = ts.isSyntaxList(node) + && isListOpener(prevNode) + && isListCloser(nextNode) + && !ts.positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + var jsDocCommentStart = ts.hasJSDocNodes(node) && node.jsDoc[0].getStart(); + var start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + var end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); + if (ts.isNumber(jsDocCommentStart)) { + pushSelectionRange(jsDocCommentStart, end); + } + pushSelectionRange(start, end); + // String literals should have a stop both inside and outside their quotes. + if (ts.isStringLiteral(node) || ts.isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + parentNode = node; + break; + } + } + } + return selectionRange; + function pushSelectionRange(start, end) { + // Skip empty ranges + if (start !== end) { + // Skip ranges that are identical to the parent + var textSpan = ts.createTextSpanFromBounds(start, end); + if (!selectionRange || !ts.textSpansEqual(textSpan, selectionRange.textSpan)) { + selectionRange = __assign({ textSpan: textSpan }, selectionRange && { parent: selectionRange }); + } + } + } + } + SmartSelectionRange.getSmartSelectionRange = getSmartSelectionRange; + /** + * Like `ts.positionBelongsToNode`, except positions immediately after nodes + * count too, unless that position belongs to the next node. In effect, makes + * selections able to snap to preceding tokens when the cursor is on the tail + * end of them with only whitespace ahead. + * @param pos The position to check. + * @param node The candidate node to snap to. + * @param nextNode The next sibling node in the tree. + * @param sourceFile The source file containing the nodes. + */ + function positionShouldSnapToNode(pos, node, nextNode) { + // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts + // for missing nodes, which can’t really be considered when deciding what + // to select. + ts.Debug.assert(node.pos <= pos); + if (pos < node.end) { + return true; + } + var nodeEnd = node.getEnd(); + var nextNodeStart = nextNode && nextNode.getStart(); + if (nodeEnd === pos) { + return pos !== nextNodeStart; + } + return false; + } + var isImport = ts.or(ts.isImportDeclaration, ts.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) { + // Group top-level imports + if (ts.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 + // 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 (ts.isMappedTypeNode(node)) { + var _a = node.getChildren(), openBraceToken = _a[0], children = _a.slice(1); + var closeBraceToken = ts.Debug.assertDefined(children.pop()); + ts.Debug.assertEqual(openBraceToken.kind, 18 /* OpenBraceToken */); + ts.Debug.assertEqual(closeBraceToken.kind, 19 /* CloseBraceToken */); + // Group `-/+readonly` and `-/+?` + var groupedWithPlusMinusTokens = groupChildren(children, function (child) { + return child === node.readonlyToken || child.kind === 134 /* ReadonlyKeyword */ || + child === node.questionToken || child.kind === 56 /* QuestionToken */; + }); + // Group type parameter with surrounding brackets + var groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, function (_a) { + var kind = _a.kind; + return kind === 22 /* OpenBracketToken */ || + kind === 151 /* TypeParameter */ || + kind === 23 /* CloseBracketToken */; + }); + return [ + openBraceToken, + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + })), + closeBraceToken, + ]; + } + // Group modifiers and property name, then pivot on `:`. + if (ts.isPropertySignature(node)) { + var children = groupChildren(node.getChildren(), function (child) { + return child === node.name || ts.contains(node.modifiers, child); + }); + return splitChildren(children, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + }); + } + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (ts.isParameter(node)) { + var groupedDotDotDotAndName_1 = groupChildren(node.getChildren(), function (child) { + return child === node.dotDotDotToken || child === node.name; + }); + var groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName_1, function (child) { + return child === groupedDotDotDotAndName_1[0] || child === node.questionToken; + }); + return splitChildren(groupedWithQuestionToken, function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + // Pivot on '=' + if (ts.isBindingElement(node)) { + return splitChildren(node.getChildren(), function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + 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, groupOn) { + var result = []; + var group; + for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { + var child = children_1[_i]; + 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, pivotOn, separateTrailingSemicolon) { + if (separateTrailingSemicolon === void 0) { separateTrailingSemicolon = true; } + if (children.length < 2) { + return children; + } + var splitTokenIndex = ts.findIndex(children, pivotOn); + if (splitTokenIndex === -1) { + return children; + } + var leftChildren = children.slice(0, splitTokenIndex); + var splitToken = children[splitTokenIndex]; + var lastToken = ts.last(children); + var separateLastToken = separateTrailingSemicolon && lastToken.kind === 26 /* SemicolonToken */; + var rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); + var result = ts.compact([ + leftChildren.length ? createSyntaxList(leftChildren) : undefined, + splitToken, + rightChildren.length ? createSyntaxList(rightChildren) : undefined, + ]); + return separateLastToken ? result.concat(lastToken) : result; + } + function createSyntaxList(children) { + ts.Debug.assertGreaterThanOrEqual(children.length, 1); + var syntaxList = ts.createNode(312 /* SyntaxList */, children[0].pos, ts.last(children).end); + syntaxList._children = children; + return syntaxList; + } + function isListOpener(token) { + var kind = token && token.kind; + return kind === 18 /* OpenBraceToken */ + || kind === 22 /* OpenBracketToken */ + || kind === 20 /* OpenParenToken */ + || kind === 263 /* JsxOpeningElement */; + } + function isListCloser(token) { + var kind = token && token.kind; + return kind === 19 /* CloseBraceToken */ + || kind === 23 /* CloseBracketToken */ + || kind === 21 /* CloseParenToken */ + || kind === 264 /* JsxClosingElement */; + } + })(SmartSelectionRange = ts.SmartSelectionRange || (ts.SmartSelectionRange = {})); +})(ts || (ts = {})); +/* @internal */ +var ts; (function (ts) { var SignatureHelp; (function (SignatureHelp) { @@ -116572,8 +116841,8 @@ var ts; } } if (ts.isBlock(statement.parent)) { - var end_2 = start + length; - var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_2; })); + var end_3 = start + length; + var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_3; })); changes.deleteNodeRange(sourceFile, statement, lastStatement); } else { @@ -122512,6 +122781,9 @@ var ts; preferences: preferences, }; } + function getSmartSelectionRange(fileName, position) { + return ts.SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + } function getApplicableRefactors(fileName, positionOrRange, preferences) { if (preferences === void 0) { preferences = ts.emptyOptions; } synchronizeHostData(); @@ -122552,6 +122824,7 @@ var ts; getBreakpointStatementAtPosition: getBreakpointStatementAtPosition, getNavigateToItems: getNavigateToItems, getRenameInfo: getRenameInfo, + getSmartSelectionRange: getSmartSelectionRange, findRenameLocations: findRenameLocations, getNavigationBarItems: getNavigationBarItems, getNavigationTree: getNavigationTree, @@ -123759,6 +124032,10 @@ var ts; var _this = this; return this.forwardJSONCall("getRenameInfo('" + fileName + "', " + position + ")", function () { return _this.languageService.getRenameInfo(fileName, position, options); }); }; + LanguageServiceShimObject.prototype.getSmartSelectionRange = function (fileName, position) { + var _this = this; + return this.forwardJSONCall("getSmartSelectionRange('" + fileName + "', " + position + ")", function () { return _this.languageService.getSmartSelectionRange(fileName, position); }); + }; LanguageServiceShimObject.prototype.findRenameLocations = function (fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) { var _this = this; return this.forwardJSONCall("findRenameLocations('" + fileName + "', " + position + ", " + findInStrings + ", " + findInComments + ", " + providePrefixAndSuffixTextForRename + ")", function () { return _this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename); }); @@ -124439,6 +124716,9 @@ var ts; /* @internal */ CommandTypes["GetEditsForFileRenameFull"] = "getEditsForFileRename-full"; CommandTypes["ConfigurePlugin"] = "configurePlugin"; + CommandTypes["SelectionRange"] = "selectionRange"; + /* @internal */ + CommandTypes["SelectionRangeFull"] = "selectionRange-full"; // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. })(CommandTypes = protocol.CommandTypes || (protocol.CommandTypes = {})); var IndentStyle; @@ -129767,6 +130047,12 @@ var ts; _this.doOutput(/*info*/ undefined, server.CommandNames.ConfigurePlugin, request.seq, /*success*/ true); return _this.notRequired(); }, + _a[server.CommandNames.SelectionRange] = function (request) { + return _this.requiredResponse(_this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ true)); + }, + _a[server.CommandNames.SelectionRangeFull] = function (request) { + return _this.requiredResponse(_this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ false)); + }, _a)); this.host = opts.host; this.cancellationToken = opts.cancellationToken; @@ -131103,6 +131389,26 @@ var ts; Session.prototype.configurePlugin = function (args) { this.projectService.configurePlugin(args); }; + Session.prototype.getSmartSelectionRange = function (args, simplifiedResult) { + var _this = this; + var locations = args.locations; + var _a = this.getFileAndLanguageServiceForSyntacticOperation(args), file = _a.file, languageService = _a.languageService; + var scriptInfo = ts.Debug.assertDefined(this.projectService.getScriptInfo(file)); + return ts.map(locations, function (location) { + var pos = _this.getPosition(location, scriptInfo); + var selectionRange = languageService.getSmartSelectionRange(file, pos); + return simplifiedResult ? _this.mapSelectionRange(selectionRange, scriptInfo) : selectionRange; + }); + }; + Session.prototype.mapSelectionRange = function (selectionRange, scriptInfo) { + var result = { + textSpan: this.toLocationTextSpan(selectionRange.textSpan, scriptInfo), + }; + if (selectionRange.parent) { + result.parent = this.mapSelectionRange(selectionRange.parent, scriptInfo); + } + return result; + }; Session.prototype.getCanonicalFileName = function (fileName) { var name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); return ts.normalizePath(name); diff --git a/lib/typescript.d.ts b/lib/typescript.d.ts index e136f2c96e4df..f70916a882be6 100644 --- a/lib/typescript.d.ts +++ b/lib/typescript.d.ts @@ -4809,6 +4809,7 @@ declare namespace ts { getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; + getSmartSelectionRange(fileName: string, position: number): SelectionRange; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -5253,6 +5254,10 @@ declare namespace ts { displayParts: SymbolDisplayPart[]; isOptional: boolean; } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Represents a single signature to show in signature help. * The id is used for subsequent calls into the language service to ask questions about the diff --git a/lib/typescript.js b/lib/typescript.js index f723a652839a5..97393be7cf53d 100644 --- a/lib/typescript.js +++ b/lib/typescript.js @@ -107364,6 +107364,275 @@ var ts; })(ts || (ts = {})); /* @internal */ var ts; +(function (ts) { + var SmartSelectionRange; + (function (SmartSelectionRange) { + function getSmartSelectionRange(pos, sourceFile) { + var selectionRange = { + textSpan: ts.createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; + var parentNode = sourceFile; + outer: while (true) { + var children = getSelectionChildren(parentNode); + if (!children.length) + break; + for (var i = 0; i < children.length; i++) { + var prevNode = children[i - 1]; + var node = children[i]; + var nextNode = children[i + 1]; + if (node.getStart(sourceFile) > pos) { + break outer; + } + if (positionShouldSnapToNode(pos, node, nextNode)) { + // 1. Blocks are effectively redundant with SyntaxLists. + // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping + // of things that should be considered independently. + // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. + // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. + // + // Dive in without pushing a selection range. + if (ts.isBlock(node) + || ts.isTemplateSpan(node) || ts.isTemplateHead(node) + || prevNode && ts.isTemplateHead(prevNode) + || ts.isVariableDeclarationList(node) && ts.isVariableStatement(parentNode) + || ts.isSyntaxList(node) && ts.isVariableDeclarationList(parentNode) + || ts.isVariableDeclaration(node) && ts.isSyntaxList(parentNode) && children.length === 1) { + parentNode = node; + break; + } + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (ts.isTemplateSpan(parentNode) && nextNode && ts.isTemplateMiddleOrTemplateTail(nextNode)) { + var start_2 = node.getFullStart() - "${".length; + var end_2 = nextNode.getStart() + "}".length; + pushSelectionRange(start_2, end_2); + } + // Blocks with braces, brackets, parens, or JSX tags on separate lines should be + // selected from open to close, including whitespace but not including the braces/etc. themselves. + var isBetweenMultiLineBookends = ts.isSyntaxList(node) + && isListOpener(prevNode) + && isListCloser(nextNode) + && !ts.positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + var jsDocCommentStart = ts.hasJSDocNodes(node) && node.jsDoc[0].getStart(); + var start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + var end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); + if (ts.isNumber(jsDocCommentStart)) { + pushSelectionRange(jsDocCommentStart, end); + } + pushSelectionRange(start, end); + // String literals should have a stop both inside and outside their quotes. + if (ts.isStringLiteral(node) || ts.isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + parentNode = node; + break; + } + } + } + return selectionRange; + function pushSelectionRange(start, end) { + // Skip empty ranges + if (start !== end) { + // Skip ranges that are identical to the parent + var textSpan = ts.createTextSpanFromBounds(start, end); + if (!selectionRange || !ts.textSpansEqual(textSpan, selectionRange.textSpan)) { + selectionRange = __assign({ textSpan: textSpan }, selectionRange && { parent: selectionRange }); + } + } + } + } + SmartSelectionRange.getSmartSelectionRange = getSmartSelectionRange; + /** + * Like `ts.positionBelongsToNode`, except positions immediately after nodes + * count too, unless that position belongs to the next node. In effect, makes + * selections able to snap to preceding tokens when the cursor is on the tail + * end of them with only whitespace ahead. + * @param pos The position to check. + * @param node The candidate node to snap to. + * @param nextNode The next sibling node in the tree. + * @param sourceFile The source file containing the nodes. + */ + function positionShouldSnapToNode(pos, node, nextNode) { + // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts + // for missing nodes, which can’t really be considered when deciding what + // to select. + ts.Debug.assert(node.pos <= pos); + if (pos < node.end) { + return true; + } + var nodeEnd = node.getEnd(); + var nextNodeStart = nextNode && nextNode.getStart(); + if (nodeEnd === pos) { + return pos !== nextNodeStart; + } + return false; + } + var isImport = ts.or(ts.isImportDeclaration, ts.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) { + // Group top-level imports + if (ts.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 + // 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 (ts.isMappedTypeNode(node)) { + var _a = node.getChildren(), openBraceToken = _a[0], children = _a.slice(1); + var closeBraceToken = ts.Debug.assertDefined(children.pop()); + ts.Debug.assertEqual(openBraceToken.kind, 18 /* OpenBraceToken */); + ts.Debug.assertEqual(closeBraceToken.kind, 19 /* CloseBraceToken */); + // Group `-/+readonly` and `-/+?` + var groupedWithPlusMinusTokens = groupChildren(children, function (child) { + return child === node.readonlyToken || child.kind === 134 /* ReadonlyKeyword */ || + child === node.questionToken || child.kind === 56 /* QuestionToken */; + }); + // Group type parameter with surrounding brackets + var groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, function (_a) { + var kind = _a.kind; + return kind === 22 /* OpenBracketToken */ || + kind === 151 /* TypeParameter */ || + kind === 23 /* CloseBracketToken */; + }); + return [ + openBraceToken, + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + })), + closeBraceToken, + ]; + } + // Group modifiers and property name, then pivot on `:`. + if (ts.isPropertySignature(node)) { + var children = groupChildren(node.getChildren(), function (child) { + return child === node.name || ts.contains(node.modifiers, child); + }); + return splitChildren(children, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + }); + } + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (ts.isParameter(node)) { + var groupedDotDotDotAndName_1 = groupChildren(node.getChildren(), function (child) { + return child === node.dotDotDotToken || child === node.name; + }); + var groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName_1, function (child) { + return child === groupedDotDotDotAndName_1[0] || child === node.questionToken; + }); + return splitChildren(groupedWithQuestionToken, function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + // Pivot on '=' + if (ts.isBindingElement(node)) { + return splitChildren(node.getChildren(), function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + 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, groupOn) { + var result = []; + var group; + for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { + var child = children_1[_i]; + 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, pivotOn, separateTrailingSemicolon) { + if (separateTrailingSemicolon === void 0) { separateTrailingSemicolon = true; } + if (children.length < 2) { + return children; + } + var splitTokenIndex = ts.findIndex(children, pivotOn); + if (splitTokenIndex === -1) { + return children; + } + var leftChildren = children.slice(0, splitTokenIndex); + var splitToken = children[splitTokenIndex]; + var lastToken = ts.last(children); + var separateLastToken = separateTrailingSemicolon && lastToken.kind === 26 /* SemicolonToken */; + var rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); + var result = ts.compact([ + leftChildren.length ? createSyntaxList(leftChildren) : undefined, + splitToken, + rightChildren.length ? createSyntaxList(rightChildren) : undefined, + ]); + return separateLastToken ? result.concat(lastToken) : result; + } + function createSyntaxList(children) { + ts.Debug.assertGreaterThanOrEqual(children.length, 1); + var syntaxList = ts.createNode(312 /* SyntaxList */, children[0].pos, ts.last(children).end); + syntaxList._children = children; + return syntaxList; + } + function isListOpener(token) { + var kind = token && token.kind; + return kind === 18 /* OpenBraceToken */ + || kind === 22 /* OpenBracketToken */ + || kind === 20 /* OpenParenToken */ + || kind === 263 /* JsxOpeningElement */; + } + function isListCloser(token) { + var kind = token && token.kind; + return kind === 19 /* CloseBraceToken */ + || kind === 23 /* CloseBracketToken */ + || kind === 21 /* CloseParenToken */ + || kind === 264 /* JsxClosingElement */; + } + })(SmartSelectionRange = ts.SmartSelectionRange || (ts.SmartSelectionRange = {})); +})(ts || (ts = {})); +/* @internal */ +var ts; (function (ts) { var SignatureHelp; (function (SignatureHelp) { @@ -116561,8 +116830,8 @@ var ts; } } if (ts.isBlock(statement.parent)) { - var end_2 = start + length; - var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_2; })); + var end_3 = start + length; + var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_3; })); changes.deleteNodeRange(sourceFile, statement, lastStatement); } else { @@ -122501,6 +122770,9 @@ var ts; preferences: preferences, }; } + function getSmartSelectionRange(fileName, position) { + return ts.SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + } function getApplicableRefactors(fileName, positionOrRange, preferences) { if (preferences === void 0) { preferences = ts.emptyOptions; } synchronizeHostData(); @@ -122541,6 +122813,7 @@ var ts; getBreakpointStatementAtPosition: getBreakpointStatementAtPosition, getNavigateToItems: getNavigateToItems, getRenameInfo: getRenameInfo, + getSmartSelectionRange: getSmartSelectionRange, findRenameLocations: findRenameLocations, getNavigationBarItems: getNavigationBarItems, getNavigationTree: getNavigationTree, @@ -123748,6 +124021,10 @@ var ts; var _this = this; return this.forwardJSONCall("getRenameInfo('" + fileName + "', " + position + ")", function () { return _this.languageService.getRenameInfo(fileName, position, options); }); }; + LanguageServiceShimObject.prototype.getSmartSelectionRange = function (fileName, position) { + var _this = this; + return this.forwardJSONCall("getSmartSelectionRange('" + fileName + "', " + position + ")", function () { return _this.languageService.getSmartSelectionRange(fileName, position); }); + }; LanguageServiceShimObject.prototype.findRenameLocations = function (fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) { var _this = this; return this.forwardJSONCall("findRenameLocations('" + fileName + "', " + position + ", " + findInStrings + ", " + findInComments + ", " + providePrefixAndSuffixTextForRename + ")", function () { return _this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename); }); diff --git a/lib/typescriptServices.d.ts b/lib/typescriptServices.d.ts index 5e66245d3eda7..747fac8526ac3 100644 --- a/lib/typescriptServices.d.ts +++ b/lib/typescriptServices.d.ts @@ -4809,6 +4809,7 @@ declare namespace ts { getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; + getSmartSelectionRange(fileName: string, position: number): SelectionRange; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -5253,6 +5254,10 @@ declare namespace ts { displayParts: SymbolDisplayPart[]; isOptional: boolean; } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Represents a single signature to show in signature help. * The id is used for subsequent calls into the language service to ask questions about the diff --git a/lib/typescriptServices.js b/lib/typescriptServices.js index 9974b9a33e035..db5763584538e 100644 --- a/lib/typescriptServices.js +++ b/lib/typescriptServices.js @@ -107364,6 +107364,275 @@ var ts; })(ts || (ts = {})); /* @internal */ var ts; +(function (ts) { + var SmartSelectionRange; + (function (SmartSelectionRange) { + function getSmartSelectionRange(pos, sourceFile) { + var selectionRange = { + textSpan: ts.createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; + var parentNode = sourceFile; + outer: while (true) { + var children = getSelectionChildren(parentNode); + if (!children.length) + break; + for (var i = 0; i < children.length; i++) { + var prevNode = children[i - 1]; + var node = children[i]; + var nextNode = children[i + 1]; + if (node.getStart(sourceFile) > pos) { + break outer; + } + if (positionShouldSnapToNode(pos, node, nextNode)) { + // 1. Blocks are effectively redundant with SyntaxLists. + // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping + // of things that should be considered independently. + // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. + // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. + // + // Dive in without pushing a selection range. + if (ts.isBlock(node) + || ts.isTemplateSpan(node) || ts.isTemplateHead(node) + || prevNode && ts.isTemplateHead(prevNode) + || ts.isVariableDeclarationList(node) && ts.isVariableStatement(parentNode) + || ts.isSyntaxList(node) && ts.isVariableDeclarationList(parentNode) + || ts.isVariableDeclaration(node) && ts.isSyntaxList(parentNode) && children.length === 1) { + parentNode = node; + break; + } + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (ts.isTemplateSpan(parentNode) && nextNode && ts.isTemplateMiddleOrTemplateTail(nextNode)) { + var start_2 = node.getFullStart() - "${".length; + var end_2 = nextNode.getStart() + "}".length; + pushSelectionRange(start_2, end_2); + } + // Blocks with braces, brackets, parens, or JSX tags on separate lines should be + // selected from open to close, including whitespace but not including the braces/etc. themselves. + var isBetweenMultiLineBookends = ts.isSyntaxList(node) + && isListOpener(prevNode) + && isListCloser(nextNode) + && !ts.positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + var jsDocCommentStart = ts.hasJSDocNodes(node) && node.jsDoc[0].getStart(); + var start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + var end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); + if (ts.isNumber(jsDocCommentStart)) { + pushSelectionRange(jsDocCommentStart, end); + } + pushSelectionRange(start, end); + // String literals should have a stop both inside and outside their quotes. + if (ts.isStringLiteral(node) || ts.isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + parentNode = node; + break; + } + } + } + return selectionRange; + function pushSelectionRange(start, end) { + // Skip empty ranges + if (start !== end) { + // Skip ranges that are identical to the parent + var textSpan = ts.createTextSpanFromBounds(start, end); + if (!selectionRange || !ts.textSpansEqual(textSpan, selectionRange.textSpan)) { + selectionRange = __assign({ textSpan: textSpan }, selectionRange && { parent: selectionRange }); + } + } + } + } + SmartSelectionRange.getSmartSelectionRange = getSmartSelectionRange; + /** + * Like `ts.positionBelongsToNode`, except positions immediately after nodes + * count too, unless that position belongs to the next node. In effect, makes + * selections able to snap to preceding tokens when the cursor is on the tail + * end of them with only whitespace ahead. + * @param pos The position to check. + * @param node The candidate node to snap to. + * @param nextNode The next sibling node in the tree. + * @param sourceFile The source file containing the nodes. + */ + function positionShouldSnapToNode(pos, node, nextNode) { + // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts + // for missing nodes, which can’t really be considered when deciding what + // to select. + ts.Debug.assert(node.pos <= pos); + if (pos < node.end) { + return true; + } + var nodeEnd = node.getEnd(); + var nextNodeStart = nextNode && nextNode.getStart(); + if (nodeEnd === pos) { + return pos !== nextNodeStart; + } + return false; + } + var isImport = ts.or(ts.isImportDeclaration, ts.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) { + // Group top-level imports + if (ts.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 + // 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 (ts.isMappedTypeNode(node)) { + var _a = node.getChildren(), openBraceToken = _a[0], children = _a.slice(1); + var closeBraceToken = ts.Debug.assertDefined(children.pop()); + ts.Debug.assertEqual(openBraceToken.kind, 18 /* OpenBraceToken */); + ts.Debug.assertEqual(closeBraceToken.kind, 19 /* CloseBraceToken */); + // Group `-/+readonly` and `-/+?` + var groupedWithPlusMinusTokens = groupChildren(children, function (child) { + return child === node.readonlyToken || child.kind === 134 /* ReadonlyKeyword */ || + child === node.questionToken || child.kind === 56 /* QuestionToken */; + }); + // Group type parameter with surrounding brackets + var groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, function (_a) { + var kind = _a.kind; + return kind === 22 /* OpenBracketToken */ || + kind === 151 /* TypeParameter */ || + kind === 23 /* CloseBracketToken */; + }); + return [ + openBraceToken, + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + })), + closeBraceToken, + ]; + } + // Group modifiers and property name, then pivot on `:`. + if (ts.isPropertySignature(node)) { + var children = groupChildren(node.getChildren(), function (child) { + return child === node.name || ts.contains(node.modifiers, child); + }); + return splitChildren(children, function (_a) { + var kind = _a.kind; + return kind === 57 /* ColonToken */; + }); + } + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (ts.isParameter(node)) { + var groupedDotDotDotAndName_1 = groupChildren(node.getChildren(), function (child) { + return child === node.dotDotDotToken || child === node.name; + }); + var groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName_1, function (child) { + return child === groupedDotDotDotAndName_1[0] || child === node.questionToken; + }); + return splitChildren(groupedWithQuestionToken, function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + // Pivot on '=' + if (ts.isBindingElement(node)) { + return splitChildren(node.getChildren(), function (_a) { + var kind = _a.kind; + return kind === 60 /* EqualsToken */; + }); + } + 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, groupOn) { + var result = []; + var group; + for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { + var child = children_1[_i]; + 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, pivotOn, separateTrailingSemicolon) { + if (separateTrailingSemicolon === void 0) { separateTrailingSemicolon = true; } + if (children.length < 2) { + return children; + } + var splitTokenIndex = ts.findIndex(children, pivotOn); + if (splitTokenIndex === -1) { + return children; + } + var leftChildren = children.slice(0, splitTokenIndex); + var splitToken = children[splitTokenIndex]; + var lastToken = ts.last(children); + var separateLastToken = separateTrailingSemicolon && lastToken.kind === 26 /* SemicolonToken */; + var rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); + var result = ts.compact([ + leftChildren.length ? createSyntaxList(leftChildren) : undefined, + splitToken, + rightChildren.length ? createSyntaxList(rightChildren) : undefined, + ]); + return separateLastToken ? result.concat(lastToken) : result; + } + function createSyntaxList(children) { + ts.Debug.assertGreaterThanOrEqual(children.length, 1); + var syntaxList = ts.createNode(312 /* SyntaxList */, children[0].pos, ts.last(children).end); + syntaxList._children = children; + return syntaxList; + } + function isListOpener(token) { + var kind = token && token.kind; + return kind === 18 /* OpenBraceToken */ + || kind === 22 /* OpenBracketToken */ + || kind === 20 /* OpenParenToken */ + || kind === 263 /* JsxOpeningElement */; + } + function isListCloser(token) { + var kind = token && token.kind; + return kind === 19 /* CloseBraceToken */ + || kind === 23 /* CloseBracketToken */ + || kind === 21 /* CloseParenToken */ + || kind === 264 /* JsxClosingElement */; + } + })(SmartSelectionRange = ts.SmartSelectionRange || (ts.SmartSelectionRange = {})); +})(ts || (ts = {})); +/* @internal */ +var ts; (function (ts) { var SignatureHelp; (function (SignatureHelp) { @@ -116561,8 +116830,8 @@ var ts; } } if (ts.isBlock(statement.parent)) { - var end_2 = start + length; - var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_2; })); + var end_3 = start + length; + var lastStatement = ts.Debug.assertDefined(lastWhere(ts.sliceAfter(statement.parent.statements, statement), function (s) { return s.pos < end_3; })); changes.deleteNodeRange(sourceFile, statement, lastStatement); } else { @@ -122501,6 +122770,9 @@ var ts; preferences: preferences, }; } + function getSmartSelectionRange(fileName, position) { + return ts.SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + } function getApplicableRefactors(fileName, positionOrRange, preferences) { if (preferences === void 0) { preferences = ts.emptyOptions; } synchronizeHostData(); @@ -122541,6 +122813,7 @@ var ts; getBreakpointStatementAtPosition: getBreakpointStatementAtPosition, getNavigateToItems: getNavigateToItems, getRenameInfo: getRenameInfo, + getSmartSelectionRange: getSmartSelectionRange, findRenameLocations: findRenameLocations, getNavigationBarItems: getNavigationBarItems, getNavigationTree: getNavigationTree, @@ -123748,6 +124021,10 @@ var ts; var _this = this; return this.forwardJSONCall("getRenameInfo('" + fileName + "', " + position + ")", function () { return _this.languageService.getRenameInfo(fileName, position, options); }); }; + LanguageServiceShimObject.prototype.getSmartSelectionRange = function (fileName, position) { + var _this = this; + return this.forwardJSONCall("getSmartSelectionRange('" + fileName + "', " + position + ")", function () { return _this.languageService.getSmartSelectionRange(fileName, position); }); + }; LanguageServiceShimObject.prototype.findRenameLocations = function (fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) { var _this = this; return this.forwardJSONCall("findRenameLocations('" + fileName + "', " + position + ", " + findInStrings + ", " + findInComments + ", " + providePrefixAndSuffixTextForRename + ")", function () { return _this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename); });