mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Start smart select API
This commit is contained in:
+20
-1
@@ -130,7 +130,8 @@ namespace ts.server.protocol {
|
||||
GetEditsForFileRename = "getEditsForFileRename",
|
||||
/* @internal */
|
||||
GetEditsForFileRenameFull = "getEditsForFileRename-full",
|
||||
ConfigurePlugin = "configurePlugin"
|
||||
ConfigurePlugin = "configurePlugin",
|
||||
SelectionRange = "selectionRange",
|
||||
|
||||
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
|
||||
}
|
||||
@@ -1395,6 +1396,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.
|
||||
*/
|
||||
|
||||
+61
-2
@@ -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,62 @@ namespace ts.server {
|
||||
this.projectService.configurePlugin(args);
|
||||
}
|
||||
|
||||
private getSelectionRange(args: protocol.SelectionRangeRequestArgs): protocol.SelectionRange[] {
|
||||
const { locations } = args;
|
||||
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
|
||||
|
||||
const sourceFile = languageService.getNonBoundSourceFile(file);
|
||||
const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file));
|
||||
const fullTextSpan = this.toLocationTextSpan(
|
||||
createTextSpan(sourceFile.getFullStart(), sourceFile.getEnd() - sourceFile.getFullStart()),
|
||||
scriptInfo);
|
||||
|
||||
return map(locations, location => {
|
||||
const pos = this.getPosition(location, scriptInfo);
|
||||
let selectionRange: protocol.SelectionRange = { textSpan: fullTextSpan };
|
||||
// Skip top-level SyntaxList
|
||||
let current: Node | undefined = sourceFile.getChildAt(0);
|
||||
while (true) {
|
||||
const children = current && current.getChildren(sourceFile);
|
||||
if (!children || !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) {
|
||||
current = undefined;
|
||||
break;
|
||||
}
|
||||
// Blocks are effectively redundant with SyntaxLists; dive in without adding to the list
|
||||
if (isBlock(node)) {
|
||||
current = node;
|
||||
break;
|
||||
}
|
||||
if (positionBelongsToNode(node, pos, sourceFile)) {
|
||||
// Blocks with braces should be selected from brace to brace, non-inclusive
|
||||
const isBetweenBraces = isSyntaxList(node)
|
||||
&& prevNode && prevNode.kind === SyntaxKind.OpenBraceToken
|
||||
&& nextNode && nextNode.kind === SyntaxKind.CloseBraceToken;
|
||||
const start = isBetweenBraces ? prevNode.getEnd() : node.getStart();
|
||||
const end = isBetweenBraces ? nextNode.getStart() : node.getEnd();
|
||||
const textSpan = this.toLocationTextSpan(createTextSpan(start, end - start), scriptInfo);
|
||||
current = node;
|
||||
// Skip ranges that are identical to the parent
|
||||
if (selectionRange.textSpan.start !== textSpan.start || selectionRange.textSpan.end !== textSpan.end) {
|
||||
selectionRange = {
|
||||
textSpan,
|
||||
parent: selectionRange,
|
||||
};
|
||||
Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(node.kind) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectionRange;
|
||||
});
|
||||
}
|
||||
|
||||
getCanonicalFileName(fileName: string) {
|
||||
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
|
||||
return normalizePath(name);
|
||||
@@ -2414,6 +2470,9 @@ 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.getSelectionRange(request.arguments));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"unittests/tsserver/reload.ts",
|
||||
"unittests/tsserver/rename.ts",
|
||||
"unittests/tsserver/resolutionCache.ts",
|
||||
"unittests/tsserver/selectionRange.ts",
|
||||
"unittests/tsserver/session.ts",
|
||||
"unittests/tsserver/skipLibCheck.ts",
|
||||
"unittests/tsserver/symLinks.ts",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
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 getSelectionRange(locations: protocol.SelectionRangeRequestArgs["locations"]) {
|
||||
return executeSessionRequest<protocol.SelectionRangeRequest, protocol.SelectionRangeResponse>(
|
||||
session,
|
||||
CommandNames.SelectionRange,
|
||||
{ file: fileName, locations });
|
||||
};
|
||||
}
|
||||
|
||||
describe("unittests:: tsserver:: selectionRange", () => {
|
||||
it("works for simple JavaScript", () => {
|
||||
const getSelectionRange = setup("/file.js", `
|
||||
class Foo {
|
||||
bar(a, b) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}`);
|
||||
|
||||
const locations = getSelectionRange([{
|
||||
line: 4,
|
||||
offset: 13
|
||||
}]);
|
||||
|
||||
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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user