Skip to content

Commit 70e9a5e

Browse files
authoredFeb 16, 2018
Merge pull request #21909 from amcasey/OrganizeImports
Introduce an organizeImports command
2 parents b70aa22 + 5c278ce commit 70e9a5e

20 files changed

+843
-0
lines changed
 

Diff for: ‎Jakefile.js

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ var harnessSources = harnessCoreSources.concat([
141141
"typingsInstaller.ts",
142142
"projectErrors.ts",
143143
"matchFiles.ts",
144+
"organizeImports.ts",
144145
"initializeTSConfig.ts",
145146
"extractConstants.ts",
146147
"extractFunctions.ts",

Diff for: ‎src/compiler/core.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1905,6 +1905,11 @@ namespace ts {
19051905
Comparison.EqualTo;
19061906
}
19071907

1908+
/** True is greater than false. */
1909+
export function compareBooleans(a: boolean, b: boolean): Comparison {
1910+
return compareValues(a ? 1 : 0, b ? 1 : 0);
1911+
}
1912+
19081913
function compareMessageText(text1: string | DiagnosticMessageChain, text2: string | DiagnosticMessageChain): Comparison {
19091914
while (text1 && text2) {
19101915
// We still have both chains.

Diff for: ‎src/harness/harnessLanguageService.ts

+3
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,9 @@ namespace Harness.LanguageService {
522522
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
523523
throw new Error("Not supported on the shim.");
524524
}
525+
organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): ReadonlyArray<ts.FileTextChanges> {
526+
throw new Error("Not supported on the shim.");
527+
}
525528
getEmitOutput(fileName: string): ts.EmitOutput {
526529
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
527530
}

Diff for: ‎src/harness/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"./unittests/tsserverProjectSystem.ts",
118118
"./unittests/tscWatchMode.ts",
119119
"./unittests/matchFiles.ts",
120+
"./unittests/organizeImports.ts",
120121
"./unittests/initializeTSConfig.ts",
121122
"./unittests/compileOnSave.ts",
122123
"./unittests/typingsInstaller.ts",

Diff for: ‎src/harness/unittests/organizeImports.ts

+426
Large diffs are not rendered by default.

Diff for: ‎src/harness/unittests/session.ts

+2
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ namespace ts.server {
262262
CommandNames.GetApplicableRefactors,
263263
CommandNames.GetEditsForRefactor,
264264
CommandNames.GetEditsForRefactorFull,
265+
CommandNames.OrganizeImports,
266+
CommandNames.OrganizeImportsFull,
265267
];
266268

267269
it("should not throw when commands are executed with invalid arguments", () => {

Diff for: ‎src/server/client.ts

+4
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,10 @@ namespace ts.server {
629629
};
630630
}
631631

632+
organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
633+
return notImplemented();
634+
}
635+
632636
private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] {
633637
return edits.map(edit => {
634638
const fileName = edit.fileName;

Diff for: ‎src/server/protocol.ts

+25
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ namespace ts.server.protocol {
113113
/* @internal */
114114
GetEditsForRefactorFull = "getEditsForRefactor-full",
115115

116+
OrganizeImports = "organizeImports",
117+
/* @internal */
118+
OrganizeImportsFull = "organizeImports-full",
119+
116120
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
117121
}
118122

@@ -547,6 +551,27 @@ namespace ts.server.protocol {
547551
renameFilename?: string;
548552
}
549553

554+
/**
555+
* Organize imports by:
556+
* 1) Removing unused imports
557+
* 2) Coalescing imports from the same module
558+
* 3) Sorting imports
559+
*/
560+
export interface OrganizeImportsRequest extends Request {
561+
command: CommandTypes.OrganizeImports;
562+
arguments: OrganizeImportsRequestArgs;
563+
}
564+
565+
export type OrganizeImportsScope = GetCombinedCodeFixScope;
566+
567+
export interface OrganizeImportsRequestArgs {
568+
scope: OrganizeImportsScope;
569+
}
570+
571+
export interface OrganizeImportsResponse extends Response {
572+
edits: ReadonlyArray<FileCodeEdits>;
573+
}
574+
550575
/**
551576
* Request for the available codefixes at a specific position.
552577
*/

Diff for: ‎src/server/session.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,19 @@ namespace ts.server {
15971597
}
15981598
}
15991599

1600+
private organizeImports({ scope }: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges> {
1601+
Debug.assert(scope.type === "file");
1602+
const { file, project } = this.getFileAndProject(scope.args);
1603+
const formatOptions = this.projectService.getFormatCodeOptions(file);
1604+
const changes = project.getLanguageService().organizeImports({ type: "file", fileName: file }, formatOptions);
1605+
if (simplifiedResult) {
1606+
return this.mapTextChangesToCodeEdits(project, changes);
1607+
}
1608+
else {
1609+
return changes;
1610+
}
1611+
}
1612+
16001613
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeAction> | ReadonlyArray<CodeAction> {
16011614
if (args.errorCodes.length === 0) {
16021615
return undefined;
@@ -2041,6 +2054,12 @@ namespace ts.server {
20412054
},
20422055
[CommandNames.GetEditsForRefactorFull]: (request: protocol.GetEditsForRefactorRequest) => {
20432056
return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ false));
2057+
},
2058+
[CommandNames.OrganizeImports]: (request: protocol.OrganizeImportsRequest) => {
2059+
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ true));
2060+
},
2061+
[CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => {
2062+
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false));
20442063
}
20452064
});
20462065

Diff for: ‎src/services/organizeImports.ts

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/* @internal */
2+
namespace ts.OrganizeImports {
3+
export function organizeImports(
4+
sourceFile: SourceFile,
5+
formatContext: formatting.FormatContext,
6+
host: LanguageServiceHost) {
7+
8+
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
9+
10+
// All of the old ImportDeclarations in the file, in syntactic order.
11+
const oldImportDecls = sourceFile.statements.filter(isImportDeclaration);
12+
13+
if (oldImportDecls.length === 0) {
14+
return [];
15+
}
16+
17+
const oldValidImportDecls = oldImportDecls.filter(importDecl => getExternalModuleName(importDecl.moduleSpecifier));
18+
const oldInvalidImportDecls = oldImportDecls.filter(importDecl => !getExternalModuleName(importDecl.moduleSpecifier));
19+
20+
// All of the new ImportDeclarations in the file, in sorted order.
21+
const newImportDecls = coalesceImports(sortImports(removeUnusedImports(oldValidImportDecls))).concat(oldInvalidImportDecls);
22+
23+
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext });
24+
25+
// Delete or replace the first import.
26+
if (newImportDecls.length === 0) {
27+
changeTracker.deleteNode(sourceFile, oldImportDecls[0]);
28+
}
29+
else {
30+
// Note: Delete the surrounding trivia because it will have been retained in newImportDecls.
31+
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, {
32+
useNonAdjustedStartPosition: false,
33+
useNonAdjustedEndPosition: false,
34+
suffix: getNewLineOrDefaultFromHost(host, formatContext.options),
35+
});
36+
}
37+
38+
// Delete any subsequent imports.
39+
for (let i = 1; i < oldImportDecls.length; i++) {
40+
changeTracker.deleteNode(sourceFile, oldImportDecls[i]);
41+
}
42+
43+
return changeTracker.getChanges();
44+
}
45+
46+
function removeUnusedImports(oldImports: ReadonlyArray<ImportDeclaration>) {
47+
return oldImports; // TODO (https://github.com/Microsoft/TypeScript/issues/10020)
48+
}
49+
50+
/* @internal */ // Internal for testing
51+
export function sortImports(oldImports: ReadonlyArray<ImportDeclaration>) {
52+
return stableSort(oldImports, (import1, import2) => {
53+
const name1 = getExternalModuleName(import1.moduleSpecifier);
54+
const name2 = getExternalModuleName(import2.moduleSpecifier);
55+
Debug.assert(name1 !== undefined);
56+
Debug.assert(name2 !== undefined);
57+
return compareBooleans(isExternalModuleNameRelative(name1), isExternalModuleNameRelative(name2)) ||
58+
compareStringsCaseSensitive(name1, name2);
59+
});
60+
}
61+
62+
function getExternalModuleName(specifier: Expression) {
63+
return isStringLiteral(specifier) || isNoSubstitutionTemplateLiteral(specifier)
64+
? specifier.text
65+
: undefined;
66+
}
67+
68+
/**
69+
* @param sortedImports a non-empty list of ImportDeclarations, sorted by module name.
70+
*/
71+
function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> {
72+
Debug.assert(length(sortedImports) > 0);
73+
74+
const groups: ImportDeclaration[][] = [];
75+
76+
let groupName: string | undefined = getExternalModuleName(sortedImports[0].moduleSpecifier);
77+
Debug.assert(groupName !== undefined);
78+
let group: ImportDeclaration[] = [];
79+
80+
for (const importDeclaration of sortedImports) {
81+
const moduleName = getExternalModuleName(importDeclaration.moduleSpecifier);
82+
Debug.assert(moduleName !== undefined);
83+
if (moduleName === groupName) {
84+
group.push(importDeclaration);
85+
}
86+
else if (group.length) {
87+
groups.push(group);
88+
89+
groupName = moduleName;
90+
group = [importDeclaration];
91+
}
92+
}
93+
94+
if (group.length) {
95+
groups.push(group);
96+
}
97+
98+
return groups;
99+
}
100+
101+
/* @internal */ // Internal for testing
102+
/**
103+
* @param sortedImports a list of ImportDeclarations, sorted by module name.
104+
*/
105+
export function coalesceImports(sortedImports: ReadonlyArray<ImportDeclaration>) {
106+
if (sortedImports.length === 0) {
107+
return sortedImports;
108+
}
109+
110+
const coalescedImports: ImportDeclaration[] = [];
111+
112+
const groupedImports = groupSortedImports(sortedImports);
113+
for (const importGroup of groupedImports) {
114+
115+
const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getImportParts(importGroup);
116+
117+
if (importWithoutClause) {
118+
coalescedImports.push(importWithoutClause);
119+
}
120+
121+
// Normally, we don't combine default and namespace imports, but it would be silly to
122+
// produce two import declarations in this special case.
123+
if (defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) {
124+
// Add the namespace import to the existing default ImportDeclaration.
125+
const defaultImportClause = defaultImports[0].parent as ImportClause;
126+
coalescedImports.push(
127+
updateImportDeclarationAndClause(defaultImportClause, defaultImportClause.name, namespaceImports[0]));
128+
129+
continue;
130+
}
131+
132+
const sortedNamespaceImports = stableSort(namespaceImports, (n1, n2) => compareIdentifiers(n1.name, n2.name));
133+
134+
for (const namespaceImport of sortedNamespaceImports) {
135+
// Drop the name, if any
136+
coalescedImports.push(
137+
updateImportDeclarationAndClause(namespaceImport.parent, /*name*/ undefined, namespaceImport));
138+
}
139+
140+
if (defaultImports.length === 0 && namedImports.length === 0) {
141+
continue;
142+
}
143+
144+
let newDefaultImport: Identifier | undefined;
145+
const newImportSpecifiers: ImportSpecifier[] = [];
146+
if (defaultImports.length === 1) {
147+
newDefaultImport = defaultImports[0];
148+
}
149+
else {
150+
for (const defaultImport of defaultImports) {
151+
newImportSpecifiers.push(
152+
createImportSpecifier(createIdentifier("default"), defaultImport));
153+
}
154+
}
155+
156+
newImportSpecifiers.push(...flatMap(namedImports, n => n.elements));
157+
158+
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) =>
159+
compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) ||
160+
compareIdentifiers(s1.name, s2.name));
161+
162+
const importClause = defaultImports.length > 0
163+
? defaultImports[0].parent as ImportClause
164+
: namedImports[0].parent;
165+
166+
const newNamedImports = sortedImportSpecifiers.length === 0
167+
? undefined
168+
: namedImports.length === 0
169+
? createNamedImports(sortedImportSpecifiers)
170+
: updateNamedImports(namedImports[0], sortedImportSpecifiers);
171+
172+
coalescedImports.push(
173+
updateImportDeclarationAndClause(importClause, newDefaultImport, newNamedImports));
174+
}
175+
176+
return coalescedImports;
177+
178+
function getImportParts(importGroup: ReadonlyArray<ImportDeclaration>) {
179+
let importWithoutClause: ImportDeclaration | undefined;
180+
const defaultImports: Identifier[] = [];
181+
const namespaceImports: NamespaceImport[] = [];
182+
const namedImports: NamedImports[] = [];
183+
184+
for (const importDeclaration of importGroup) {
185+
if (importDeclaration.importClause === undefined) {
186+
// Only the first such import is interesting - the others are redundant.
187+
// Note: Unfortunately, we will lose trivia that was on this node.
188+
importWithoutClause = importWithoutClause || importDeclaration;
189+
continue;
190+
}
191+
192+
const { name, namedBindings } = importDeclaration.importClause;
193+
194+
if (name) {
195+
defaultImports.push(name);
196+
}
197+
198+
if (namedBindings) {
199+
if (isNamespaceImport(namedBindings)) {
200+
namespaceImports.push(namedBindings);
201+
}
202+
else {
203+
namedImports.push(namedBindings);
204+
}
205+
}
206+
}
207+
208+
return {
209+
importWithoutClause,
210+
defaultImports,
211+
namespaceImports,
212+
namedImports,
213+
};
214+
}
215+
216+
function compareIdentifiers(s1: Identifier, s2: Identifier) {
217+
return compareStringsCaseSensitive(s1.text, s2.text);
218+
}
219+
220+
function updateImportDeclarationAndClause(
221+
importClause: ImportClause,
222+
name: Identifier | undefined,
223+
namedBindings: NamedImportBindings | undefined) {
224+
225+
const importDeclaration = importClause.parent;
226+
return updateImportDeclaration(
227+
importDeclaration,
228+
importDeclaration.decorators,
229+
importDeclaration.modifiers,
230+
updateImportClause(importClause, name, namedBindings),
231+
importDeclaration.moduleSpecifier);
232+
}
233+
}
234+
}

Diff for: ‎src/services/services.ts

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
/// <reference path='jsTyping.ts' />
1515
/// <reference path='navigateTo.ts' />
1616
/// <reference path='navigationBar.ts' />
17+
/// <reference path='organizeImports.ts' />
1718
/// <reference path='outliningElementsCollector.ts' />
1819
/// <reference path='patternMatcher.ts' />
1920
/// <reference path='preProcess.ts' />
@@ -1848,6 +1849,15 @@ namespace ts {
18481849
return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext });
18491850
}
18501851

1852+
function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
1853+
synchronizeHostData();
1854+
Debug.assert(scope.type === "file");
1855+
const sourceFile = getValidSourceFile(scope.fileName);
1856+
const formatContext = formatting.getFormatContext(formatOptions);
1857+
1858+
return OrganizeImports.organizeImports(sourceFile, formatContext, host);
1859+
}
1860+
18511861
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
18521862
function applyCodeActionCommand(action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
18531863
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
@@ -2143,6 +2153,7 @@ namespace ts {
21432153
getCodeFixesAtPosition,
21442154
getCombinedCodeFix,
21452155
applyCodeActionCommand,
2156+
organizeImports,
21462157
getEmitOutput,
21472158
getNonBoundSourceFile,
21482159
getSourceFile,

0 commit comments

Comments
 (0)
Please sign in to comment.