Skip to content

Commit 44491c9

Browse files
authored
Merge pull request #635 from camerondubas/feature/sort-imports
2 parents 4fcc033 + 52a51d6 commit 44491c9

File tree

6 files changed

+283
-3
lines changed

6 files changed

+283
-3
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { Project } from 'glint-monorepo-test-utils';
2+
import { describe, beforeEach, afterEach, test, expect } from 'vitest';
3+
import { stripIndent } from 'common-tags';
4+
import * as ts from 'typescript';
5+
6+
describe('Language Server: Organize Imports', () => {
7+
let project!: Project;
8+
9+
beforeEach(async () => {
10+
project = await Project.create();
11+
});
12+
13+
afterEach(async () => {
14+
await project.destroy();
15+
});
16+
17+
test('no imports', () => {
18+
project.write({
19+
'index.ts': stripIndent`
20+
21+
export default class Application extends Component {
22+
static template = hbs\`
23+
Hello, world!
24+
\`;
25+
}
26+
`,
27+
});
28+
29+
let server = project.startLanguageServer();
30+
let formatting = ts.getDefaultFormatCodeSettings();
31+
let preferences = {};
32+
let edits = server.organizeImports(project.fileURI('index.ts'), formatting, preferences);
33+
34+
expect(edits).toEqual([]);
35+
});
36+
37+
test('ts: handles sorting imports', () => {
38+
project.write({
39+
'index.ts': stripIndent`
40+
import './App.css';
41+
import EmberComponent from './ember-component';
42+
import Component from '@ember/component';
43+
44+
interface WrapperComponentSignature {
45+
Blocks: {
46+
default: [
47+
{
48+
InnerComponent: WithBoundArgs<typeof EmberComponent, 'required'>;
49+
MaybeComponent?: ComponentLike<{ Args: { key: string } }>;
50+
}
51+
];
52+
};
53+
}
54+
55+
// Second Import Block
56+
import logo from './logo.svg';
57+
import { ComponentLike, WithBoundArgs } from '@glint/template';
58+
59+
export default class WrapperComponent extends Component<WrapperComponentSignature> {
60+
logo = logo
61+
}
62+
`,
63+
});
64+
65+
let server = project.startLanguageServer();
66+
67+
let formatting = ts.getDefaultFormatCodeSettings();
68+
let preferences = {};
69+
let edits = server.organizeImports(project.fileURI('index.ts'), formatting, preferences);
70+
71+
expect(edits).toEqual([
72+
{
73+
newText:
74+
"import Component from '@ember/component';\nimport './App.css';\nimport EmberComponent from './ember-component';\n",
75+
range: {
76+
start: { character: 0, line: 0 },
77+
end: { character: 0, line: 1 },
78+
},
79+
},
80+
{
81+
newText: '',
82+
range: {
83+
start: { character: 0, line: 1 },
84+
end: { character: 0, line: 2 },
85+
},
86+
},
87+
{
88+
newText: '',
89+
range: {
90+
start: { character: 0, line: 2 },
91+
end: { character: 0, line: 3 },
92+
},
93+
},
94+
{
95+
newText:
96+
"import { ComponentLike, WithBoundArgs } from '@glint/template';\nimport logo from './logo.svg';\n",
97+
range: {
98+
start: { character: 0, line: 16 },
99+
end: { character: 0, line: 17 },
100+
},
101+
},
102+
{
103+
newText: '',
104+
range: {
105+
start: { character: 0, line: 17 },
106+
end: { character: 0, line: 18 },
107+
},
108+
},
109+
]);
110+
});
111+
112+
test('gts: handles sorting imports', () => {
113+
project.setGlintConfig({ environment: 'ember-template-imports' });
114+
project.write({
115+
'index.gts': stripIndent`
116+
import Component from '@glimmer/component';
117+
import { hash } from '@ember/helper';
118+
119+
class List<T> extends Component {
120+
<template>
121+
<MaybeComponent />
122+
<ol>
123+
{{#each-in (hash a=1 b='hi') as |key value|}}
124+
<li>{{key}}: {{value}}</li>
125+
{{/each-in}}
126+
</ol>
127+
</template>
128+
}
129+
130+
// Second Import Block
131+
import { ComponentLike, ModifierLike, HelperLike } from '@glint/template';
132+
import { TOC } from '@ember/component/template-only';
133+
134+
const MaybeComponent: undefined as TOC<{ Args: { arg: string } }> | undefined;
135+
declare const CanvasThing: ComponentLike<{ Args: { str: string }; Element: HTMLCanvasElement }>;
136+
`,
137+
});
138+
139+
let server = project.startLanguageServer();
140+
141+
let formatting = ts.getDefaultFormatCodeSettings();
142+
let preferences = {};
143+
let edits = server.organizeImports(project.fileURI('index.gts'), formatting, preferences);
144+
145+
expect(edits).toEqual([
146+
{
147+
newText:
148+
"import { hash } from '@ember/helper';\nimport Component from '@glimmer/component';\n",
149+
range: {
150+
start: { character: 0, line: 0 },
151+
end: { character: 0, line: 1 },
152+
},
153+
},
154+
{
155+
newText: '',
156+
range: {
157+
start: { character: 0, line: 1 },
158+
end: { character: 0, line: 2 },
159+
},
160+
},
161+
{
162+
newText:
163+
"import { TOC } from '@ember/component/template-only';\nimport { ComponentLike, HelperLike, ModifierLike } from '@glint/template';\n",
164+
range: {
165+
start: { character: 0, line: 15 },
166+
end: { character: 0, line: 16 },
167+
},
168+
},
169+
{
170+
newText: '',
171+
range: {
172+
start: { character: 0, line: 16 },
173+
end: { character: 0, line: 17 },
174+
},
175+
},
176+
]);
177+
});
178+
});

packages/core/src/language-server/binding.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { TextDocument } from 'vscode-languageserver-textdocument';
1313
import { GlintCompletionItem } from './glint-language-server.js';
1414
import { LanguageServerPool } from './pool.js';
15-
import { GetIRRequest } from './messages.cjs';
15+
import { GetIRRequest, SortImportsRequest } from './messages.cjs';
1616
import { ConfigManager } from './config-manager.js';
1717
import type * as ts from 'typescript';
1818

@@ -219,6 +219,15 @@ export function bindLanguageServerPool({
219219
return pool.withServerForURI(uri, ({ server }) => server.getTransformedContents(uri));
220220
});
221221

222+
connection.onRequest(SortImportsRequest.type, ({ uri }) => {
223+
return pool.withServerForURI(uri, ({ server }) => {
224+
const language = server.getLanguageType(uri);
225+
const formatting = configManager.getFormatCodeSettingsFor(language);
226+
const preferences = configManager.getUserSettingsFor(language);
227+
return server.organizeImports(uri, formatting, preferences);
228+
});
229+
});
230+
222231
connection.onDidChangeWatchedFiles(({ changes }) => {
223232
pool.forEachServer(({ server, scheduleDiagnostics }) => {
224233
for (let change of changes) {

packages/core/src/language-server/glint-language-server.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,45 @@ export default class GlintLanguageServer {
416416
return this.glintConfig.environment.isTypedScript(file) ? 'typescript' : 'javascript';
417417
}
418418

419+
public organizeImports(
420+
uri: string,
421+
formatOptions: ts.FormatCodeSettings = {},
422+
preferences: ts.UserPreferences = {}
423+
): TextEdit[] {
424+
const transformInfo = this.transformManager.findTransformInfoForOriginalFile(
425+
uriToFilePath(uri)
426+
);
427+
428+
if (!transformInfo) {
429+
return [];
430+
}
431+
432+
const fileTextChanges = this.service.organizeImports(
433+
{
434+
type: 'file',
435+
fileName: transformInfo.transformedFileName,
436+
skipDestructiveCodeActions: true,
437+
},
438+
formatOptions,
439+
preferences
440+
);
441+
const edits: TextEdit[] = [];
442+
443+
for (const fileTextChange of fileTextChanges) {
444+
for (const textChange of fileTextChange.textChanges) {
445+
const location = this.textSpanToLocation(fileTextChange.fileName, textChange.span);
446+
if (location) {
447+
edits.push({
448+
range: location.range,
449+
newText: textChange.newText,
450+
});
451+
}
452+
}
453+
}
454+
455+
return edits;
456+
}
457+
419458
private applyCodeAction(
420459
uri: string,
421460
range: Range,

packages/core/src/language-server/messages.cts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ProtocolRequestType } from 'vscode-languageserver';
1+
import { ProtocolRequestType, TextEdit } from 'vscode-languageserver';
22

33
export type Request<Name extends string, T> = {
44
name: Name;
@@ -10,6 +10,11 @@ export const GetIRRequest = makeRequestType(
1010
ProtocolRequestType<GetIRParams, GetIRResult | null, void, void, void>
1111
);
1212

13+
export const SortImportsRequest = makeRequestType(
14+
'glint/sortImports',
15+
ProtocolRequestType<SortImportsParams, SortImportsResult | null, void, void, void>
16+
);
17+
1318
export interface GetIRParams {
1419
uri: string;
1520
}
@@ -19,6 +24,12 @@ export interface GetIRResult {
1924
uri: string;
2025
}
2126

27+
export interface SortImportsParams {
28+
uri: string;
29+
}
30+
31+
export type SortImportsResult = TextEdit[];
32+
2233
// This utility allows us to encode type information to enforce that we're using
2334
// a valid request name along with its associated param/response types without
2435
// actually requring the runtime code here to be imported elsewhere.

packages/vscode/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,20 @@
5050
"title": "Glint: Show IR for Debugging",
5151
"command": "glint.show-debug-ir",
5252
"enablement": "config.glint.debug == true"
53+
},
54+
{
55+
"title": "Glint: Sort Imports",
56+
"command": "glint.sort-imports"
5357
}
5458
],
59+
"menus": {
60+
"commandPalette": [
61+
{
62+
"command": "glint.sort-imports",
63+
"when": "editorLangId =~ /javascript|typescript|glimmer-js|glimmer-ts/"
64+
}
65+
]
66+
},
5567
"configuration": [
5668
{
5769
"title": "Glint",

packages/vscode/src/extension.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
commands,
1212
workspace,
1313
WorkspaceConfiguration,
14+
WorkspaceEdit,
1415
} from 'vscode';
1516
import { Disposable, LanguageClient, ServerOptions } from 'vscode-languageclient/node.js';
16-
import type { Request, GetIRRequest } from '@glint/core/lsp-messages';
17+
import type { Request, GetIRRequest, SortImportsRequest } from '@glint/core/lsp-messages';
1718

1819
///////////////////////////////////////////////////////////////////////////////
1920
// Setup and extension lifecycle
@@ -29,6 +30,7 @@ export function activate(context: ExtensionContext): void {
2930
context.subscriptions.push(fileWatcher, createConfigWatcher());
3031
context.subscriptions.push(
3132
commands.registerCommand('glint.restart-language-server', restartClients),
33+
commands.registerTextEditorCommand('glint.sort-imports', sortImports),
3234
commands.registerTextEditorCommand('glint.show-debug-ir', showDebugIR)
3335
);
3436

@@ -57,6 +59,35 @@ async function restartClients(): Promise<void> {
5759
await Promise.all([...clients.values()].map((client) => client.restart()));
5860
}
5961

62+
async function sortImports(editor: TextEditor): Promise<void> {
63+
const workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri);
64+
if (!workspaceFolder) {
65+
return;
66+
}
67+
68+
let client = clients.get(workspaceFolder.uri.fsPath);
69+
let request = requestKey<typeof SortImportsRequest>('glint/sortImports');
70+
const edits = await client?.sendRequest(request, { uri: editor.document.uri.toString() });
71+
72+
if (!edits) {
73+
return;
74+
}
75+
76+
const workspaceEdit = new WorkspaceEdit();
77+
78+
for (const edit of edits) {
79+
const range = new Range(
80+
edit.range.start.line,
81+
edit.range.start.character,
82+
edit.range.end.line,
83+
edit.range.end.character
84+
);
85+
workspaceEdit.replace(editor.document.uri, range, edit.newText);
86+
}
87+
88+
workspace.applyEdit(workspaceEdit);
89+
}
90+
6091
async function showDebugIR(editor: TextEditor): Promise<void> {
6192
let workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri);
6293
if (!workspaceFolder) {

0 commit comments

Comments
 (0)