Merge branch 'master' into cacheUnnormalizedIntersections

This commit is contained in:
Anders Hejlsberg
2019-05-20 07:04:33 -07:00
48 changed files with 1401 additions and 30 deletions
+22 -16
View File
@@ -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 {
+4
View File
@@ -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 ||
+44 -8
View File
@@ -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);
}
+3
View File
@@ -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));
}
+22 -1
View File
@@ -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.
*/
+31 -3
View File
@@ -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) {
+5
View File
@@ -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,
+8
View File
@@ -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})`,
+266
View File
@@ -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 VariableStatements 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) {
// Cant use 'ts.positionBelongsToNode()' here because it cleverly accounts
// for missing nodes, which cant 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 theyre just siblings of each
* other as well as of other top-level statements and declarations.
*/
function getSelectionChildren(node: Node): ReadonlyArray<Node> {
// Group top-level imports
if (isSourceFile(node)) {
return groupChildren(node.getChildAt(0).getChildren(), isImport);
}
// Mapped types _look_ like ObjectTypes with a single member,
// but in fact dont contain a SyntaxList or a node containing
// the “key/value” pair like ObjectTypes do, but it seems intuitive
// 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 theres 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;
}
}
+1
View File
@@ -28,6 +28,7 @@
"patternMatcher.ts",
"preProcess.ts",
"rename.ts",
"smartSelection.ts",
"signatureHelp.ts",
"sourcemaps.ts",
"suggestionDiagnostics.ts",
+7
View File
@@ -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<RenameLocation> | undefined;
getSmartSelectionRange(fileName: string, position: number): SelectionRange;
getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray<DefinitionInfo> | undefined;
getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined;
getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray<DefinitionInfo> | 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
+1
View File
@@ -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",
@@ -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", () => {
@@ -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<protocol.SelectionRangeRequest, protocol.SelectionRangeResponse>(
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 }, } } } } } } } } }]);
});
});
}
+23 -1
View File
@@ -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<RenameLocation> | undefined;
getSmartSelectionRange(fileName: string, position: number): SelectionRange;
getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray<DefinitionInfo> | undefined;
getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined;
getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray<DefinitionInfo> | 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;
+5
View File
@@ -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<RenameLocation> | undefined;
getSmartSelectionRange(fileName: string, position: number): SelectionRange;
getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray<DefinitionInfo> | undefined;
getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined;
getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray<DefinitionInfo> | 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
@@ -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))
@@ -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
@@ -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;
}
@@ -0,0 +1,4 @@
let/**/ x: string
let
let x: string
@@ -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 = {} } = {};
@@ -0,0 +1,12 @@
type X<T, P> = IsExactlyAny<P> extends true ? T : ({ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[/**/K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>)
K
P[K]
K extends keyof T ? T[K] : P[K]
IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]
[K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K];
{ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]; }
{ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>
({ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>)
IsExactlyAny<P> extends true ? T : ({ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>)
type X<T, P> = IsExactlyAny<P> extends true ? T : ({ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>)
@@ -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 '';
}
}
}
@@ -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[] = []) {}
@@ -0,0 +1,18 @@
function f(
a,
/**/b
) {}
b
••a,
b↲
function f(
a,
b
) {}
@@ -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);
@@ -0,0 +1,4 @@
const /**/x = 3;
x
const x = 3;
@@ -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 };
@@ -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
}
@@ -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;
}
}
@@ -0,0 +1,63 @@
export interface IService {
_serviceBrand: any;
open(ho/**/st: number, data: any): Promise<any>;
bar(): void
}
host
host: number
host: number, data: any
open(host: number, data: any): Promise<any>;
••_serviceBrand: any;
open(host: number, data: any): Promise<any>;
bar(): void↲
export interface IService {
_serviceBrand: any;
open(host: number, data: any): Promise<any>;
bar(): void
}
================================================================================
export interface IService {
_serviceBrand: any;
open(host: number, data: any): Promise<any>;
bar(): void/**/
}
void
bar(): void
••_serviceBrand: any;
open(host: number, data: any): Promise<any>;
bar(): void↲
export interface IService {
_serviceBrand: any;
open(host: number, data: any): Promise<any>;
bar(): void
}
@@ -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`
@@ -0,0 +1,8 @@
// @noEmit: true
// @allowJs: true
// @checkJs: true
// @Filename: loop.js
var loop1 = loop2;
var loop2 = loop1;
module.exports = loop2;
+2 -1
View File
@@ -244,6 +244,7 @@ declare namespace FourSlashInterface {
baselineGetEmitOutput(insertResultsIntoVfs?: boolean): void;
getEmitOutput(expectedOutputFiles: ReadonlyArray<string>): 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<string | Marker>,
readonly marker?: ArrayOrSingle<string | Marker>;
readonly isNewIdentifierLocation?: boolean;
readonly isGlobalCompletion?: boolean;
readonly exact?: ArrayOrSingle<ExpectedCompletionEntry>;
@@ -0,0 +1,11 @@
/// <reference path="fourslash.ts" />
////// Not a JSDoc comment
/////**
//// * @param {number} x The number to square
//// */
////function /**/square(x) {
//// return x * x;
////}
verify.baselineSmartSelection();
@@ -0,0 +1,6 @@
/// <reference path="fourslash.ts" />
////let/**/ x: string
// Verifies that the selection goes to 'let' first even though its behind the caret
verify.baselineSmartSelection();
@@ -0,0 +1,5 @@
/// <reference path="fourslash.ts" />
////const { /*1*/x, y: /*2*/a, .../*3*/zs = {} } = {};
verify.baselineSmartSelection();
@@ -0,0 +1,5 @@
/// <reference path="fourslash.ts" />
////type X<T, P> = IsExactlyAny<P> extends true ? T : ({ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[/**/K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>)
verify.baselineSmartSelection();
@@ -0,0 +1,11 @@
/// <reference path="fourslash.ts" />
////class HomePage {
//// componentDidMount(/*1*/) {
//// if (this.props.username/*2*/) {
//// return '/*3*/';
//// }
//// }
////}
verify.baselineSmartSelection();
@@ -0,0 +1,5 @@
/// <reference path="fourslash.ts" />
////function f(/*1*/p, /*2*/q?, /*3*/...r: any[] = []) {}
verify.baselineSmartSelection();
@@ -0,0 +1,8 @@
/// <reference path="fourslash.ts" />
////function f(
//// a,
//// /**/b
////) {}
verify.baselineSmartSelection();
@@ -0,0 +1,8 @@
/// <reference path="fourslash.ts" />
////import { /**/x as y, z } from './z';
////import { b } from './';
////
////console.log(1);
verify.baselineSmartSelection();
@@ -0,0 +1,5 @@
/// <reference path="fourslash.ts" />
////const /**/x = 3;
verify.baselineSmartSelection();
@@ -0,0 +1,5 @@
/// <reference path="fourslash.ts" />
////type M = { /*1*/-re/*2*/adonly /*3*/[K in ke/*4*/yof any]/*5*/-/*6*/?: any };
verify.baselineSmartSelection();
@@ -0,0 +1,9 @@
/// <reference path="fourslash.ts" />
////type X = {
//// /*1*/foo?: string;
//// /*2*/readonly /*3*/bar: { x: num/*4*/ber };
//// /*5*/meh
////}
verify.baselineSmartSelection();
@@ -0,0 +1,12 @@
/// <reference path="fourslash.ts" />
////class Foo {
//// bar(a, b) {
//// if (/*1*/a === b) {
//// return tr/*2*/ue;
//// }
//// return false;
//// }
////}
verify.baselineSmartSelection();
@@ -0,0 +1,10 @@
/// <reference path="fourslash.ts" />
////export interface IService {
//// _serviceBrand: any;
////
//// open(ho/*1*/st: number, data: any): Promise<any>;
//// bar(): void/*2*/
////}
verify.baselineSmartSelection();
@@ -0,0 +1,7 @@
/// <reference path="fourslash.ts" />
////`a /*1*/b ${
//// '/*2*/c'
////} d`
verify.baselineSmartSelection();