diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 96db98d337a73..cb6f67d66b1df 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2581,7 +2581,7 @@ namespace ts { // Fix up parent pointers since we're going to use these nodes before we bind into them node.left.parent = node; node.right.parent = node; - if (isIdentifier(lhs.expression) && container === file && isNameOfExportsOrModuleExportsAliasDeclaration(file, lhs.expression)) { + if (isIdentifier(lhs.expression) && container === file && isExportsOrModuleExportsOrAlias(file, lhs.expression)) { // This can be an alias for the 'exports' or 'module.exports' names, e.g. // var util = module.exports; // util.property = function ... @@ -2975,21 +2975,27 @@ namespace ts { } export function isExportsOrModuleExportsOrAlias(sourceFile: SourceFile, node: Expression): boolean { - return isExportsIdentifier(node) || - isModuleExportsPropertyAccessExpression(node) || - isIdentifier(node) && isNameOfExportsOrModuleExportsAliasDeclaration(sourceFile, node); - } - - function isNameOfExportsOrModuleExportsAliasDeclaration(sourceFile: SourceFile, node: Identifier): boolean { - const symbol = lookupSymbolForNameWorker(sourceFile, node.escapedText); - return !!symbol && !!symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && - !!symbol.valueDeclaration.initializer && isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, symbol.valueDeclaration.initializer); - } - - function isExportsOrModuleExportsOrAliasOrAssignment(sourceFile: SourceFile, node: Expression): boolean { - return isExportsOrModuleExportsOrAlias(sourceFile, node) || - (isAssignmentExpression(node, /*excludeCompoundAssignment*/ true) && ( - isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, node.left) || isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, node.right))); + let i = 0; + const q = [node]; + while (q.length && i < 100) { + i++; + node = q.shift()!; + if (isExportsIdentifier(node) || isModuleExportsPropertyAccessExpression(node)) { + return true; + } + else if (isIdentifier(node)) { + const symbol = lookupSymbolForNameWorker(sourceFile, node.escapedText); + if (!!symbol && !!symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && !!symbol.valueDeclaration.initializer) { + const init = symbol.valueDeclaration.initializer; + q.push(init); + if (isAssignmentExpression(init, /*excludeCompoundAssignment*/ true)) { + q.push(init.left); + q.push(init.right); + } + } + } + } + return false; } function lookupSymbolForNameWorker(container: Node, name: __String): Symbol | undefined { diff --git a/src/harness/client.ts b/src/harness/client.ts index 03d5334461688..d223f9f5fe3a5 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -424,6 +424,10 @@ namespace ts.server { return renameInfo; } + getSmartSelectionRange() { + return notImplemented(); + } + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { if (!this.lastRenameEntry || this.lastRenameEntry.inputs.fileName !== fileName || diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index e8c2912365f56..df7f0051f3f1f 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1417,12 +1417,7 @@ Actual: ${stringify(fullActual)}`); } public baselineCurrentFileBreakpointLocations() { - let baselineFile = this.testData.globalOptions[MetadataOptionNames.baselineFile]; - if (!baselineFile) { - baselineFile = this.activeFile.fileName.replace(this.basePath + "/breakpointValidation", "bpSpan"); - baselineFile = baselineFile.replace(ts.Extension.Ts, ".baseline"); - - } + const baselineFile = this.getBaselineFileName().replace("breakpointValidation", "bpSpan"); Harness.Baseline.runBaseline(baselineFile, this.baselineCurrentFileLocations(pos => this.getBreakpointStatementLocation(pos)!)); } @@ -1497,8 +1492,7 @@ Actual: ${stringify(fullActual)}`); } public baselineQuickInfo() { - const baselineFile = this.testData.globalOptions[MetadataOptionNames.baselineFile] || - ts.getBaseFileName(this.activeFile.fileName).replace(ts.Extension.Ts, ".baseline"); + const baselineFile = this.getBaselineFileName(); Harness.Baseline.runBaseline( baselineFile, stringify( @@ -1508,6 +1502,39 @@ Actual: ${stringify(fullActual)}`); })))); } + public baselineSmartSelection() { + const n = "\n"; + const baselineFile = this.getBaselineFileName(); + const markers = this.getMarkers(); + const fileContent = this.activeFile.content; + const text = markers.map(marker => { + const baselineContent = [fileContent.slice(0, marker.position) + "/**/" + fileContent.slice(marker.position) + n]; + let selectionRange: ts.SelectionRange | undefined = this.languageService.getSmartSelectionRange(this.activeFile.fileName, marker.position); + while (selectionRange) { + const { textSpan } = selectionRange; + let masked = Array.from(fileContent).map((char, index) => { + const charCode = char.charCodeAt(0); + if (index >= textSpan.start && index < ts.textSpanEnd(textSpan)) { + return char === " " ? "•" : ts.isLineBreak(charCode) ? `↲${n}` : char; + } + return ts.isLineBreak(charCode) ? char : " "; + }).join(""); + masked = masked.replace(/^\s*$\r?\n?/gm, ""); // Remove blank lines + const isRealCharacter = (char: string) => char !== "•" && char !== "↲" && !ts.isWhiteSpaceLike(char.charCodeAt(0)); + const leadingWidth = Array.from(masked).findIndex(isRealCharacter); + const trailingWidth = ts.findLastIndex(Array.from(masked), isRealCharacter); + masked = masked.slice(0, leadingWidth) + + masked.slice(leadingWidth, trailingWidth).replace(/•/g, " ").replace(/↲/g, "") + + masked.slice(trailingWidth); + baselineContent.push(masked); + selectionRange = selectionRange.parent; + } + return baselineContent.join(fileContent.includes("\n") ? n + n : n); + }).join(n.repeat(2) + "=".repeat(80) + n.repeat(2)); + + Harness.Baseline.runBaseline(baselineFile, text); + } + public printBreakpointLocation(pos: number) { Harness.IO.log("\n**Pos: " + pos + " " + this.spanInfoToString(this.getBreakpointStatementLocation(pos)!, " ")); } @@ -1562,6 +1589,11 @@ Actual: ${stringify(fullActual)}`); Harness.IO.log(stringify(help.items[help.selectedItemIndex])); } + private getBaselineFileName() { + return this.testData.globalOptions[MetadataOptionNames.baselineFile] || + ts.getBaseFileName(this.activeFile.fileName).replace(ts.Extension.Ts, ".baseline"); + } + private getSignatureHelp({ triggerReason }: FourSlashInterface.VerifySignatureHelpOptions): ts.SignatureHelpItems | undefined { return this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition, { triggerReason @@ -3960,6 +3992,10 @@ namespace FourSlashInterface { this.state.baselineQuickInfo(); } + public baselineSmartSelection() { + this.state.baselineSmartSelection(); + } + public nameOrDottedNameSpanTextIs(text: string) { this.state.verifyCurrentNameOrDottedNameSpanText(text); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index b70afecb7916d..03ccbb5d6de93 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -472,6 +472,9 @@ namespace Harness.LanguageService { getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); } + getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange { + return unwrapJSONCallResult(this.shim.getSmartSelectionRange(fileName, position)); + } findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] { return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3da890a766ff7..486208c2819d7 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -130,7 +130,10 @@ namespace ts.server.protocol { GetEditsForFileRename = "getEditsForFileRename", /* @internal */ GetEditsForFileRenameFull = "getEditsForFileRename-full", - ConfigurePlugin = "configurePlugin" + ConfigurePlugin = "configurePlugin", + SelectionRange = "selectionRange", + /* @internal */ + SelectionRangeFull = "selectionRange-full", // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. } @@ -1395,6 +1398,24 @@ namespace ts.server.protocol { export interface ConfigurePluginResponse extends Response { } + export interface SelectionRangeRequest extends FileRequest { + command: CommandTypes.SelectionRange; + arguments: SelectionRangeRequestArgs; + } + + export interface SelectionRangeRequestArgs extends FileRequestArgs { + locations: Location[]; + } + + export interface SelectionRangeResponse extends Response { + body?: SelectionRange[]; + } + + export interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } + /** * Information found in an "open" request. */ diff --git a/src/server/session.ts b/src/server/session.ts index 3c202c9aecd02..2e9d6948b1a5c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1318,11 +1318,11 @@ namespace ts.server { this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath); } - private getPosition(args: protocol.FileLocationRequestArgs, scriptInfo: ScriptInfo): number { + private getPosition(args: protocol.Location & { position?: number }, scriptInfo: ScriptInfo): number { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getPositionInFile(args: protocol.FileLocationRequestArgs, file: NormalizedPath): number { + private getPositionInFile(args: protocol.Location & { position?: number }, file: NormalizedPath): number { const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; return this.getPosition(args, scriptInfo); } @@ -2059,6 +2059,28 @@ namespace ts.server { this.projectService.configurePlugin(args); } + private getSmartSelectionRange(args: protocol.SelectionRangeRequestArgs, simplifiedResult: boolean) { + const { locations } = args; + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file)); + + return map(locations, location => { + const pos = this.getPosition(location, scriptInfo); + const selectionRange = languageService.getSmartSelectionRange(file, pos); + return simplifiedResult ? this.mapSelectionRange(selectionRange, scriptInfo) : selectionRange; + }); + } + + private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { + const result: protocol.SelectionRange = { + textSpan: this.toLocationTextSpan(selectionRange.textSpan, scriptInfo), + }; + if (selectionRange.parent) { + result.parent = this.mapSelectionRange(selectionRange.parent, scriptInfo); + } + return result; + } + getCanonicalFileName(fileName: string) { const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); return normalizePath(name); @@ -2414,7 +2436,13 @@ namespace ts.server { this.configurePlugin(request.arguments); this.doOutput(/*info*/ undefined, CommandNames.ConfigurePlugin, request.seq, /*success*/ true); return this.notRequired(); - } + }, + [CommandNames.SelectionRange]: (request: protocol.SelectionRangeRequest) => { + return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.SelectionRangeFull]: (request: protocol.SelectionRangeRequest) => { + return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ false)); + }, }); public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { diff --git a/src/services/services.ts b/src/services/services.ts index 628ec395437b6..9637b3c5321f6 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2080,6 +2080,10 @@ namespace ts { }; } + function getSmartSelectionRange(fileName: string, position: number): SelectionRange { + return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + } + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): ApplicableRefactorInfo[] { synchronizeHostData(); const file = getValidSourceFile(fileName); @@ -2127,6 +2131,7 @@ namespace ts { getBreakpointStatementAtPosition, getNavigateToItems, getRenameInfo, + getSmartSelectionRange, findRenameLocations, getNavigationBarItems, getNavigationTree, diff --git a/src/services/shims.ts b/src/services/shims.ts index 208cf3dbc3c01..e914a4acd424f 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -165,6 +165,7 @@ namespace ts { * { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } } */ getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string; + getSmartSelectionRange(fileName: string, position: number): string; /** * Returns a JSON-encoded value of the type: @@ -838,6 +839,13 @@ namespace ts { ); } + public getSmartSelectionRange(fileName: string, position: number): string { + return this.forwardJSONCall( + `getSmartSelectionRange('${fileName}', ${position})`, + () => this.languageService.getSmartSelectionRange(fileName, position) + ); + } + public findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string { return this.forwardJSONCall( `findRenameLocations('${fileName}', ${position}, ${findInStrings}, ${findInComments}, ${providePrefixAndSuffixTextForRename})`, diff --git a/src/services/smartSelection.ts b/src/services/smartSelection.ts new file mode 100644 index 0000000000000..ddd3c75c2c7de --- /dev/null +++ b/src/services/smartSelection.ts @@ -0,0 +1,266 @@ +/* @internal */ +namespace ts.SmartSelectionRange { + export function getSmartSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { + let selectionRange: SelectionRange = { + textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; + + let parentNode: Node = sourceFile; + outer: while (true) { + const children = getSelectionChildren(parentNode); + if (!children.length) break; + for (let i = 0; i < children.length; i++) { + const prevNode: Node | undefined = children[i - 1]; + const node: Node = children[i]; + const nextNode: Node | undefined = 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 (isBlock(node) + || isTemplateSpan(node) || isTemplateHead(node) + || prevNode && isTemplateHead(prevNode) + || isVariableDeclarationList(node) && isVariableStatement(parentNode) + || isSyntaxList(node) && isVariableDeclarationList(parentNode) + || isVariableDeclaration(node) && isSyntaxList(parentNode) && children.length === 1) { + parentNode = node; + break; + } + + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { + const start = node.getFullStart() - "${".length; + const end = nextNode.getStart() + "}".length; + pushSelectionRange(start, end); + } + + // 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. + const isBetweenMultiLineBookends = isSyntaxList(node) + && isListOpener(prevNode) + && isListCloser(nextNode) + && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + const jsDocCommentStart = hasJSDocNodes(node) && node.jsDoc![0].getStart(); + const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + const end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); + if (isNumber(jsDocCommentStart)) { + pushSelectionRange(jsDocCommentStart, end); + } + pushSelectionRange(start, end); + + // String literals should have a stop both inside and outside their quotes. + if (isStringLiteral(node) || isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + + parentNode = node; + break; + } + } + } + + return selectionRange; + + function pushSelectionRange(start: number, end: number): void { + // Skip empty ranges + if (start !== end) { + // Skip ranges that are identical to the parent + const textSpan = createTextSpanFromBounds(start, end); + if (!selectionRange || !textSpansEqual(textSpan, selectionRange.textSpan)) { + selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + } + } + } + } + + /** + * 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: number, node: Node, nextNode: Node | undefined) { + // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts + // for missing nodes, which can’t really be considered when deciding what + // to select. + Debug.assert(node.pos <= pos); + if (pos < node.end) { + return true; + } + const nodeEnd = node.getEnd(); + const nextNodeStart = nextNode && nextNode.getStart(); + if (nodeEnd === pos) { + return pos !== nextNodeStart; + } + return false; + } + + 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 { + // 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 + // that the selection would snap to those points. The philosophy + // of choosing a selection range is not so much about what the + // syntax currently _is_ as what the syntax might easily become + // if the user is making a selection; e.g., we synthesize a selection + // around the “key/value” pair not because there’s a node there, but + // because it allows the mapped type to become an object type with a + // few keystrokes. + if (isMappedTypeNode(node)) { + const [openBraceToken, ...children] = node.getChildren(); + const closeBraceToken = Debug.assertDefined(children.pop()); + Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); + Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); + // Group `-/+readonly` and `-/+?` + const groupedWithPlusMinusTokens = groupChildren(children, child => + child === node.readonlyToken || child.kind === SyntaxKind.ReadonlyKeyword || + child === node.questionToken || child.kind === SyntaxKind.QuestionToken); + // Group type parameter with surrounding brackets + const groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, ({ kind }) => + kind === SyntaxKind.OpenBracketToken || + kind === SyntaxKind.TypeParameter || + kind === SyntaxKind.CloseBracketToken + ); + return [ + openBraceToken, + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, ({ kind }) => kind === SyntaxKind.ColonToken)), + closeBraceToken, + ]; + } + + // Group modifiers and property name, then pivot on `:`. + if (isPropertySignature(node)) { + const children = groupChildren(node.getChildren(), child => + child === node.name || contains(node.modifiers, child)); + return splitChildren(children, ({ kind }) => kind === SyntaxKind.ColonToken); + } + + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (isParameter(node)) { + const groupedDotDotDotAndName = groupChildren(node.getChildren(), child => + child === node.dotDotDotToken || child === node.name); + const groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName, child => + child === groupedDotDotDotAndName[0] || child === node.questionToken); + return splitChildren(groupedWithQuestionToken, ({ kind }) => kind === SyntaxKind.EqualsToken); + } + + // Pivot on '=' + if (isBindingElement(node)) { + return splitChildren(node.getChildren(), ({ kind }) => kind === SyntaxKind.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: 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 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 isListOpener(token: Node | undefined): token is Node { + const kind = token && token.kind; + return kind === SyntaxKind.OpenBraceToken + || kind === SyntaxKind.OpenBracketToken + || kind === SyntaxKind.OpenParenToken + || kind === SyntaxKind.JsxOpeningElement; + } + + function isListCloser(token: Node | undefined): token is Node { + const kind = token && token.kind; + return kind === SyntaxKind.CloseBraceToken + || kind === SyntaxKind.CloseBracketToken + || kind === SyntaxKind.CloseParenToken + || kind === SyntaxKind.JsxClosingElement; + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 6b78d5c7604a8..681a9fc4b5eb7 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -28,6 +28,7 @@ "patternMatcher.ts", "preProcess.ts", "rename.ts", + "smartSelection.ts", "signatureHelp.ts", "sourcemaps.ts", "suggestionDiagnostics.ts", diff --git a/src/services/types.ts b/src/services/types.ts index d65a51d4dd342..8dee6c4f053c9 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -296,6 +296,8 @@ namespace ts { 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; @@ -848,6 +850,11 @@ namespace ts { isOptional: boolean; } + export 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/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index fb0280bbc667c..07499a998ba00 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -142,6 +142,7 @@ "unittests/tsserver/reload.ts", "unittests/tsserver/rename.ts", "unittests/tsserver/resolutionCache.ts", + "unittests/tsserver/smartSelection.ts", "unittests/tsserver/session.ts", "unittests/tsserver/skipLibCheck.ts", "unittests/tsserver/symLinks.ts", diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 715c0ab332408..cf84ffce6e8bc 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -264,6 +264,7 @@ namespace ts.server { CommandNames.OrganizeImportsFull, CommandNames.GetEditsForFileRename, CommandNames.GetEditsForFileRenameFull, + CommandNames.SelectionRange, ]; it("should not throw when commands are executed with invalid arguments", () => { diff --git a/src/testRunner/unittests/tsserver/smartSelection.ts b/src/testRunner/unittests/tsserver/smartSelection.ts new file mode 100644 index 0000000000000..3c903d7da675f --- /dev/null +++ b/src/testRunner/unittests/tsserver/smartSelection.ts @@ -0,0 +1,66 @@ +namespace ts.projectSystem { + function setup(fileName: string, content: string) { + const file: File = { path: fileName, content }; + const host = createServerHost([file, libFile]); + const session = createSession(host); + openFilesForSession([file], session); + return function getSmartSelectionRange(locations: protocol.SelectionRangeRequestArgs["locations"]) { + return executeSessionRequest( + session, + CommandNames.SelectionRange, + { file: fileName, locations }); + }; + } + + // More tests in fourslash/smartSelection_* + describe("unittests:: tsserver:: smartSelection", () => { + it("works for simple JavaScript", () => { + const getSmartSelectionRange = setup("/file.js", ` +class Foo { + bar(a, b) { + if (a === b) { + return true; + } + return false; + } +}`); + + const locations = getSmartSelectionRange([ + { line: 4, offset: 13 }, // a === b + ]); + + assert.deepEqual(locations, [{ + textSpan: { // a + start: { line: 4, offset: 13 }, + end: { line: 4, offset: 14 } }, + parent: { + textSpan: { // a === b + start: { line: 4, offset: 13 }, + end: { line: 4, offset: 20 } }, + parent: { + textSpan: { // IfStatement + start: { line: 4, offset: 9 }, + end: { line: 6, offset: 10 } }, + parent: { + textSpan: { // SyntaxList + whitespace (body of method) + start: { line: 3, offset: 16 }, + end: { line: 8, offset: 5 } }, + parent: { + textSpan: { // MethodDeclaration + start: { line: 3, offset: 5 }, + end: { line: 8, offset: 6 } }, + parent: { + textSpan: { // SyntaxList + whitespace (body of class) + start: { line: 2, offset: 12 }, + end: { line: 9, offset: 1 } }, + parent: { + textSpan: { // ClassDeclaration + start: { line: 2, offset: 1 }, + end: { line: 9, offset: 2 } }, + parent: { + textSpan: { // SourceFile (all text) + start: { line: 1, offset: 1 }, + end: { line: 9, offset: 2 }, } } } } } } } } }]); + }); + }); +} diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ca97395891a31..4b06bc9e8392a 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/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/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index e136f2c96e4df..f70916a882be6 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/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/tests/baselines/reference/binderUninitializedModuleExportsAssignment.symbols b/tests/baselines/reference/binderUninitializedModuleExportsAssignment.symbols new file mode 100644 index 0000000000000..73777fb2299df --- /dev/null +++ b/tests/baselines/reference/binderUninitializedModuleExportsAssignment.symbols @@ -0,0 +1,15 @@ +=== tests/cases/conformance/salsa/loop.js === +var loop1 = loop2; +>loop1 : Symbol(loop1, Decl(loop.js, 0, 3)) +>loop2 : Symbol(loop2, Decl(loop.js, 1, 3)) + +var loop2 = loop1; +>loop2 : Symbol(loop2, Decl(loop.js, 1, 3)) +>loop1 : Symbol(loop1, Decl(loop.js, 0, 3)) + +module.exports = loop2; +>module.exports : Symbol("tests/cases/conformance/salsa/loop", Decl(loop.js, 0, 0)) +>module : Symbol(export=, Decl(loop.js, 1, 18)) +>exports : Symbol(export=, Decl(loop.js, 1, 18)) +>loop2 : Symbol(loop2, Decl(loop.js, 1, 3)) + diff --git a/tests/baselines/reference/binderUninitializedModuleExportsAssignment.types b/tests/baselines/reference/binderUninitializedModuleExportsAssignment.types new file mode 100644 index 0000000000000..c8b8540cc562c --- /dev/null +++ b/tests/baselines/reference/binderUninitializedModuleExportsAssignment.types @@ -0,0 +1,16 @@ +=== tests/cases/conformance/salsa/loop.js === +var loop1 = loop2; +>loop1 : any +>loop2 : any + +var loop2 = loop1; +>loop2 : any +>loop1 : any + +module.exports = loop2; +>module.exports = loop2 : any +>module.exports : any +>module : { "tests/cases/conformance/salsa/loop": any; } +>exports : any +>loop2 : any + diff --git a/tests/baselines/reference/smartSelection_JSDoc.baseline b/tests/baselines/reference/smartSelection_JSDoc.baseline new file mode 100644 index 0000000000000..6f223644e4366 --- /dev/null +++ b/tests/baselines/reference/smartSelection_JSDoc.baseline @@ -0,0 +1,30 @@ +// Not a JSDoc comment +/** + * @param {number} x The number to square + */ +function /**/square(x) { + return x * x; +} + + + square + + +function square(x) { + return x * x; +} + +/** + * @param {number} x The number to square + */ +function square(x) { + return x * x; +} + +// Not a JSDoc comment +/** + * @param {number} x The number to square + */ +function square(x) { + return x * x; +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_behindCaret.baseline b/tests/baselines/reference/smartSelection_behindCaret.baseline new file mode 100644 index 0000000000000..6b7b393f7ea8d --- /dev/null +++ b/tests/baselines/reference/smartSelection_behindCaret.baseline @@ -0,0 +1,4 @@ +let/**/ x: string + +let +let x: string \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_bindingPatterns.baseline b/tests/baselines/reference/smartSelection_bindingPatterns.baseline new file mode 100644 index 0000000000000..78f3e4a10b74e --- /dev/null +++ b/tests/baselines/reference/smartSelection_bindingPatterns.baseline @@ -0,0 +1,27 @@ +const { /**/x, y: a, ...zs = {} } = {}; + + x + x, y: a, ...zs = {} + { x, y: a, ...zs = {} } +const { x, y: a, ...zs = {} } = {}; + +================================================================================ + +const { x, y: /**/a, ...zs = {} } = {}; + + a + y: a + x, y: a, ...zs = {} + { x, y: a, ...zs = {} } +const { x, y: a, ...zs = {} } = {}; + +================================================================================ + +const { x, y: a, .../**/zs = {} } = {}; + + zs + ...zs + ...zs = {} + x, y: a, ...zs = {} + { x, y: a, ...zs = {} } +const { x, y: a, ...zs = {} } = {}; \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_complex.baseline b/tests/baselines/reference/smartSelection_complex.baseline new file mode 100644 index 0000000000000..c16b1912ae78c --- /dev/null +++ b/tests/baselines/reference/smartSelection_complex.baseline @@ -0,0 +1,12 @@ +type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[/**/K] : P[K]; } & Pick>) + + K + P[K] + K extends keyof T ? T[K] : P[K] + IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K] + [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; + { [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } + { [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick> + ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) + IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) +type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_emptyRanges.baseline b/tests/baselines/reference/smartSelection_emptyRanges.baseline new file mode 100644 index 0000000000000..8ec58d839abbb --- /dev/null +++ b/tests/baselines/reference/smartSelection_emptyRanges.baseline @@ -0,0 +1,140 @@ +class HomePage { + componentDidMount(/**/) { + if (this.props.username) { + return ''; + } + } +} + + + ) + + + componentDidMount() { + if (this.props.username) { + return ''; + } + } + + + ↲ +••componentDidMount() { + if (this.props.username) { + return ''; + } + }↲ + + +class HomePage { + componentDidMount() { + if (this.props.username) { + return ''; + } + } +} + +================================================================================ + +class HomePage { + componentDidMount() { + if (this.props.username/**/) { + return ''; + } + } +} + + + ) + + + if (this.props.username) { + return ''; + } + + + ↲ +••••if (this.props.username) { + return ''; + }↲ +•• + + + componentDidMount() { + if (this.props.username) { + return ''; + } + } + + + ↲ +••componentDidMount() { + if (this.props.username) { + return ''; + } + }↲ + + +class HomePage { + componentDidMount() { + if (this.props.username) { + return ''; + } + } +} + +================================================================================ + +class HomePage { + componentDidMount() { + if (this.props.username) { + return '/**/'; + } + } +} + + + '' + + + return ''; + + + ↲ +••••••return '';↲ +•••• + + + if (this.props.username) { + return ''; + } + + + ↲ +••••if (this.props.username) { + return ''; + }↲ +•• + + + componentDidMount() { + if (this.props.username) { + return ''; + } + } + + + ↲ +••componentDidMount() { + if (this.props.username) { + return ''; + } + }↲ + + +class HomePage { + componentDidMount() { + if (this.props.username) { + return ''; + } + } +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_functionParams1.baseline b/tests/baselines/reference/smartSelection_functionParams1.baseline new file mode 100644 index 0000000000000..1162ceb917a40 --- /dev/null +++ b/tests/baselines/reference/smartSelection_functionParams1.baseline @@ -0,0 +1,25 @@ +function f(/**/p, q?, ...r: any[] = []) {} + + p + p, q?, ...r: any[] = [] +function f(p, q?, ...r: any[] = []) {} + +================================================================================ + +function f(p, /**/q?, ...r: any[] = []) {} + + q + q? + p, q?, ...r: any[] = [] +function f(p, q?, ...r: any[] = []) {} + +================================================================================ + +function f(p, q?, /**/...r: any[] = []) {} + + ... + ...r + ...r: any[] + ...r: any[] = [] + p, q?, ...r: any[] = [] +function f(p, q?, ...r: any[] = []) {} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_functionParams2.baseline b/tests/baselines/reference/smartSelection_functionParams2.baseline new file mode 100644 index 0000000000000..5f355ff0f0e88 --- /dev/null +++ b/tests/baselines/reference/smartSelection_functionParams2.baseline @@ -0,0 +1,18 @@ +function f( + a, + /**/b +) {} + + + b + + + ↲ +••a, + b↲ + + +function f( + a, + b +) {} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_imports.baseline b/tests/baselines/reference/smartSelection_imports.baseline new file mode 100644 index 0000000000000..f3a449ee13e58 --- /dev/null +++ b/tests/baselines/reference/smartSelection_imports.baseline @@ -0,0 +1,29 @@ +import { /**/x as y, z } from './z'; +import { b } from './'; + +console.log(1); + + + x + + + x as y + + + x as y, z + + + { x as y, z } + + +import { x as y, z } from './z'; + + +import { x as y, z } from './z'; +import { b } from './'; + + +import { x as y, z } from './z'; +import { b } from './'; + +console.log(1); \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_loneVariableDeclaration.baseline b/tests/baselines/reference/smartSelection_loneVariableDeclaration.baseline new file mode 100644 index 0000000000000..df2345bd20f12 --- /dev/null +++ b/tests/baselines/reference/smartSelection_loneVariableDeclaration.baseline @@ -0,0 +1,4 @@ +const /**/x = 3; + + x +const x = 3; \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_mappedTypes.baseline b/tests/baselines/reference/smartSelection_mappedTypes.baseline new file mode 100644 index 0000000000000..5dd6b37d7feef --- /dev/null +++ b/tests/baselines/reference/smartSelection_mappedTypes.baseline @@ -0,0 +1,65 @@ +type M = { /**/-readonly [K in keyof any]-?: any }; + + - + -readonly + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -re/**/adonly [K in keyof any]-?: any }; + + readonly + -readonly + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly /**/[K in keyof any]-?: any }; + + [ + [K in keyof any] + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly [K in ke/**/yof any]-?: any }; + + keyof + keyof any + K in keyof any + [K in keyof any] + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly [K in keyof any]/**/-?: any }; + + - + -? + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly [K in keyof any]-/**/?: any }; + + ? + -? + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_objectTypes.baseline b/tests/baselines/reference/smartSelection_objectTypes.baseline new file mode 100644 index 0000000000000..612d5df2faae0 --- /dev/null +++ b/tests/baselines/reference/smartSelection_objectTypes.baseline @@ -0,0 +1,174 @@ +type X = { + /**/foo?: string; + readonly bar: { x: number }; + meh +} + + + foo + + + foo? + + + foo?: string; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + /**/readonly bar: { x: number }; + meh +} + + + readonly + + + readonly bar + + + readonly bar: { x: number }; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + readonly /**/bar: { x: number }; + meh +} + + + bar + + + readonly bar + + + readonly bar: { x: number }; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + readonly bar: { x: num/**/ber }; + meh +} + + + number + + + x: number + + + { x: number } + + + readonly bar: { x: number }; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + readonly bar: { x: number }; + /**/meh +} + + + meh + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_simple1.baseline b/tests/baselines/reference/smartSelection_simple1.baseline new file mode 100644 index 0000000000000..f28c607c524ba --- /dev/null +++ b/tests/baselines/reference/smartSelection_simple1.baseline @@ -0,0 +1,116 @@ +class Foo { + bar(a, b) { + if (/**/a === b) { + return true; + } + return false; + } +} + + + a + + + a === b + + + if (a === b) { + return true; + } + + + ↲ +••••••if (a === b) { + return true; + } + return false;↲ +•• + + + bar(a, b) { + if (a === b) { + return true; + } + return false; + } + + + ↲ +••bar(a, b) { + if (a === b) { + return true; + } + return false; + }↲ + + +class Foo { + bar(a, b) { + if (a === b) { + return true; + } + return false; + } +} + +================================================================================ + +class Foo { + bar(a, b) { + if (a === b) { + return tr/**/ue; + } + return false; + } +} + + + true + + + return true; + + + ↲ +••••••••••return true;↲ +•••••• + + + if (a === b) { + return true; + } + + + ↲ +••••••if (a === b) { + return true; + } + return false;↲ +•• + + + bar(a, b) { + if (a === b) { + return true; + } + return false; + } + + + ↲ +••bar(a, b) { + if (a === b) { + return true; + } + return false; + }↲ + + +class Foo { + bar(a, b) { + if (a === b) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_simple2.baseline b/tests/baselines/reference/smartSelection_simple2.baseline new file mode 100644 index 0000000000000..2c4b520d449f6 --- /dev/null +++ b/tests/baselines/reference/smartSelection_simple2.baseline @@ -0,0 +1,63 @@ +export interface IService { + _serviceBrand: any; + + open(ho/**/st: number, data: any): Promise; + bar(): void +} + + + host + + + host: number + + + host: number, data: any + + + open(host: number, data: any): Promise; + + + ↲ +••_serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void↲ + + +export interface IService { + _serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void +} + +================================================================================ + +export interface IService { + _serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void/**/ +} + + + void + + + bar(): void + + + ↲ +••_serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void↲ + + +export interface IService { + _serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_templateStrings.baseline b/tests/baselines/reference/smartSelection_templateStrings.baseline new file mode 100644 index 0000000000000..91fec157842a2 --- /dev/null +++ b/tests/baselines/reference/smartSelection_templateStrings.baseline @@ -0,0 +1,37 @@ +`a /**/b ${ + 'c' +} d` + + + a b ${ + 'c' +} d + +`a b ${ + 'c' +} d` + +================================================================================ + +`a b ${ + '/**/c' +} d` + + + c + + + 'c' + + + ${ + 'c' +} + + a b ${ + 'c' +} d + +`a b ${ + 'c' +} d` \ No newline at end of file diff --git a/tests/cases/conformance/salsa/binderUninitializedModuleExportsAssignment.ts b/tests/cases/conformance/salsa/binderUninitializedModuleExportsAssignment.ts new file mode 100644 index 0000000000000..ef3ef213d3608 --- /dev/null +++ b/tests/cases/conformance/salsa/binderUninitializedModuleExportsAssignment.ts @@ -0,0 +1,8 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @Filename: loop.js +var loop1 = loop2; +var loop2 = loop1; + +module.exports = loop2; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 6488748b7286e..4252296d5da48 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -244,6 +244,7 @@ declare namespace FourSlashInterface { baselineGetEmitOutput(insertResultsIntoVfs?: boolean): void; getEmitOutput(expectedOutputFiles: ReadonlyArray): void; baselineQuickInfo(): void; + baselineSmartSelection(): void; nameOrDottedNameSpanTextIs(text: string): void; outliningSpansInCurrentFile(spans: Range[]): void; todoCommentsInCurrentFile(descriptors: string[]): void; @@ -508,7 +509,7 @@ declare namespace FourSlashInterface { readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; } interface CompletionsOptions { - readonly marker?: ArrayOrSingle, + readonly marker?: ArrayOrSingle; readonly isNewIdentifierLocation?: boolean; readonly isGlobalCompletion?: boolean; readonly exact?: ArrayOrSingle; diff --git a/tests/cases/fourslash/smartSelection_JSDoc.ts b/tests/cases/fourslash/smartSelection_JSDoc.ts new file mode 100644 index 0000000000000..2d50810a11e8d --- /dev/null +++ b/tests/cases/fourslash/smartSelection_JSDoc.ts @@ -0,0 +1,11 @@ +/// + +////// Not a JSDoc comment +/////** +//// * @param {number} x The number to square +//// */ +////function /**/square(x) { +//// return x * x; +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_behindCaret.ts b/tests/cases/fourslash/smartSelection_behindCaret.ts new file mode 100644 index 0000000000000..f7bb0ec169def --- /dev/null +++ b/tests/cases/fourslash/smartSelection_behindCaret.ts @@ -0,0 +1,6 @@ +/// + +////let/**/ x: string + +// Verifies that the selection goes to 'let' first even though it’s behind the caret +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_bindingPatterns.ts b/tests/cases/fourslash/smartSelection_bindingPatterns.ts new file mode 100644 index 0000000000000..350f72bac255f --- /dev/null +++ b/tests/cases/fourslash/smartSelection_bindingPatterns.ts @@ -0,0 +1,5 @@ +/// + +////const { /*1*/x, y: /*2*/a, .../*3*/zs = {} } = {}; + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_complex.ts b/tests/cases/fourslash/smartSelection_complex.ts new file mode 100644 index 0000000000000..28797e4833ebe --- /dev/null +++ b/tests/cases/fourslash/smartSelection_complex.ts @@ -0,0 +1,5 @@ +/// + +////type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[/**/K] : P[K]; } & Pick>) + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_emptyRanges.ts b/tests/cases/fourslash/smartSelection_emptyRanges.ts new file mode 100644 index 0000000000000..6f2bfe7a2c000 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_emptyRanges.ts @@ -0,0 +1,11 @@ +/// + +////class HomePage { +//// componentDidMount(/*1*/) { +//// if (this.props.username/*2*/) { +//// return '/*3*/'; +//// } +//// } +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_functionParams1.ts b/tests/cases/fourslash/smartSelection_functionParams1.ts new file mode 100644 index 0000000000000..d2e0350d73abd --- /dev/null +++ b/tests/cases/fourslash/smartSelection_functionParams1.ts @@ -0,0 +1,5 @@ +/// + +////function f(/*1*/p, /*2*/q?, /*3*/...r: any[] = []) {} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_functionParams2.ts b/tests/cases/fourslash/smartSelection_functionParams2.ts new file mode 100644 index 0000000000000..0172763a70ec7 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_functionParams2.ts @@ -0,0 +1,8 @@ +/// + +////function f( +//// a, +//// /**/b +////) {} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_imports.ts b/tests/cases/fourslash/smartSelection_imports.ts new file mode 100644 index 0000000000000..4e800f6973b44 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_imports.ts @@ -0,0 +1,8 @@ +/// + +////import { /**/x as y, z } from './z'; +////import { b } from './'; +//// +////console.log(1); + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_loneVariableDeclaration.ts b/tests/cases/fourslash/smartSelection_loneVariableDeclaration.ts new file mode 100644 index 0000000000000..ae50d094ee77f --- /dev/null +++ b/tests/cases/fourslash/smartSelection_loneVariableDeclaration.ts @@ -0,0 +1,5 @@ +/// + +////const /**/x = 3; + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_mappedTypes.ts b/tests/cases/fourslash/smartSelection_mappedTypes.ts new file mode 100644 index 0000000000000..0f76491a59212 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_mappedTypes.ts @@ -0,0 +1,5 @@ +/// + +////type M = { /*1*/-re/*2*/adonly /*3*/[K in ke/*4*/yof any]/*5*/-/*6*/?: any }; + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_objectTypes.ts b/tests/cases/fourslash/smartSelection_objectTypes.ts new file mode 100644 index 0000000000000..3d458c43b4cc7 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_objectTypes.ts @@ -0,0 +1,9 @@ +/// + +////type X = { +//// /*1*/foo?: string; +//// /*2*/readonly /*3*/bar: { x: num/*4*/ber }; +//// /*5*/meh +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_simple1.ts b/tests/cases/fourslash/smartSelection_simple1.ts new file mode 100644 index 0000000000000..97d7249138b8e --- /dev/null +++ b/tests/cases/fourslash/smartSelection_simple1.ts @@ -0,0 +1,12 @@ +/// + +////class Foo { +//// bar(a, b) { +//// if (/*1*/a === b) { +//// return tr/*2*/ue; +//// } +//// return false; +//// } +////} + +verify.baselineSmartSelection(); diff --git a/tests/cases/fourslash/smartSelection_simple2.ts b/tests/cases/fourslash/smartSelection_simple2.ts new file mode 100644 index 0000000000000..85d3252bca0fa --- /dev/null +++ b/tests/cases/fourslash/smartSelection_simple2.ts @@ -0,0 +1,10 @@ +/// + +////export interface IService { +//// _serviceBrand: any; +//// +//// open(ho/*1*/st: number, data: any): Promise; +//// bar(): void/*2*/ +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_templateStrings.ts b/tests/cases/fourslash/smartSelection_templateStrings.ts new file mode 100644 index 0000000000000..e3b311d20d992 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_templateStrings.ts @@ -0,0 +1,7 @@ +/// + +////`a /*1*/b ${ +//// '/*2*/c' +////} d` + +verify.baselineSmartSelection(); \ No newline at end of file