Skip to content

Commit

Permalink
Merge pull request #635 from camerondubas/feature/sort-imports
Browse files Browse the repository at this point in the history
  • Loading branch information
dfreeman authored Nov 14, 2023
2 parents 4fcc033 + 52a51d6 commit 44491c9
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 3 deletions.
178 changes: 178 additions & 0 deletions packages/core/__tests__/language-server/organize-imports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Project } from 'glint-monorepo-test-utils';
import { describe, beforeEach, afterEach, test, expect } from 'vitest';
import { stripIndent } from 'common-tags';
import * as ts from 'typescript';

describe('Language Server: Organize Imports', () => {
let project!: Project;

beforeEach(async () => {
project = await Project.create();
});

afterEach(async () => {
await project.destroy();
});

test('no imports', () => {
project.write({
'index.ts': stripIndent`
export default class Application extends Component {
static template = hbs\`
Hello, world!
\`;
}
`,
});

let server = project.startLanguageServer();
let formatting = ts.getDefaultFormatCodeSettings();
let preferences = {};
let edits = server.organizeImports(project.fileURI('index.ts'), formatting, preferences);

expect(edits).toEqual([]);
});

test('ts: handles sorting imports', () => {
project.write({
'index.ts': stripIndent`
import './App.css';
import EmberComponent from './ember-component';
import Component from '@ember/component';
interface WrapperComponentSignature {
Blocks: {
default: [
{
InnerComponent: WithBoundArgs<typeof EmberComponent, 'required'>;
MaybeComponent?: ComponentLike<{ Args: { key: string } }>;
}
];
};
}
// Second Import Block
import logo from './logo.svg';
import { ComponentLike, WithBoundArgs } from '@glint/template';
export default class WrapperComponent extends Component<WrapperComponentSignature> {
logo = logo
}
`,
});

let server = project.startLanguageServer();

let formatting = ts.getDefaultFormatCodeSettings();
let preferences = {};
let edits = server.organizeImports(project.fileURI('index.ts'), formatting, preferences);

expect(edits).toEqual([
{
newText:
"import Component from '@ember/component';\nimport './App.css';\nimport EmberComponent from './ember-component';\n",
range: {
start: { character: 0, line: 0 },
end: { character: 0, line: 1 },
},
},
{
newText: '',
range: {
start: { character: 0, line: 1 },
end: { character: 0, line: 2 },
},
},
{
newText: '',
range: {
start: { character: 0, line: 2 },
end: { character: 0, line: 3 },
},
},
{
newText:
"import { ComponentLike, WithBoundArgs } from '@glint/template';\nimport logo from './logo.svg';\n",
range: {
start: { character: 0, line: 16 },
end: { character: 0, line: 17 },
},
},
{
newText: '',
range: {
start: { character: 0, line: 17 },
end: { character: 0, line: 18 },
},
},
]);
});

test('gts: handles sorting imports', () => {
project.setGlintConfig({ environment: 'ember-template-imports' });
project.write({
'index.gts': stripIndent`
import Component from '@glimmer/component';
import { hash } from '@ember/helper';
class List<T> extends Component {
<template>
<MaybeComponent />
<ol>
{{#each-in (hash a=1 b='hi') as |key value|}}
<li>{{key}}: {{value}}</li>
{{/each-in}}
</ol>
</template>
}
// Second Import Block
import { ComponentLike, ModifierLike, HelperLike } from '@glint/template';
import { TOC } from '@ember/component/template-only';
const MaybeComponent: undefined as TOC<{ Args: { arg: string } }> | undefined;
declare const CanvasThing: ComponentLike<{ Args: { str: string }; Element: HTMLCanvasElement }>;
`,
});

let server = project.startLanguageServer();

let formatting = ts.getDefaultFormatCodeSettings();
let preferences = {};
let edits = server.organizeImports(project.fileURI('index.gts'), formatting, preferences);

expect(edits).toEqual([
{
newText:
"import { hash } from '@ember/helper';\nimport Component from '@glimmer/component';\n",
range: {
start: { character: 0, line: 0 },
end: { character: 0, line: 1 },
},
},
{
newText: '',
range: {
start: { character: 0, line: 1 },
end: { character: 0, line: 2 },
},
},
{
newText:
"import { TOC } from '@ember/component/template-only';\nimport { ComponentLike, HelperLike, ModifierLike } from '@glint/template';\n",
range: {
start: { character: 0, line: 15 },
end: { character: 0, line: 16 },
},
},
{
newText: '',
range: {
start: { character: 0, line: 16 },
end: { character: 0, line: 17 },
},
},
]);
});
});
11 changes: 10 additions & 1 deletion packages/core/src/language-server/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { TextDocument } from 'vscode-languageserver-textdocument';
import { GlintCompletionItem } from './glint-language-server.js';
import { LanguageServerPool } from './pool.js';
import { GetIRRequest } from './messages.cjs';
import { GetIRRequest, SortImportsRequest } from './messages.cjs';
import { ConfigManager } from './config-manager.js';
import type * as ts from 'typescript';

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

connection.onRequest(SortImportsRequest.type, ({ uri }) => {
return pool.withServerForURI(uri, ({ server }) => {
const language = server.getLanguageType(uri);
const formatting = configManager.getFormatCodeSettingsFor(language);
const preferences = configManager.getUserSettingsFor(language);
return server.organizeImports(uri, formatting, preferences);
});
});

connection.onDidChangeWatchedFiles(({ changes }) => {
pool.forEachServer(({ server, scheduleDiagnostics }) => {
for (let change of changes) {
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/language-server/glint-language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,45 @@ export default class GlintLanguageServer {
return this.glintConfig.environment.isTypedScript(file) ? 'typescript' : 'javascript';
}

public organizeImports(
uri: string,
formatOptions: ts.FormatCodeSettings = {},
preferences: ts.UserPreferences = {}
): TextEdit[] {
const transformInfo = this.transformManager.findTransformInfoForOriginalFile(
uriToFilePath(uri)
);

if (!transformInfo) {
return [];
}

const fileTextChanges = this.service.organizeImports(
{
type: 'file',
fileName: transformInfo.transformedFileName,
skipDestructiveCodeActions: true,
},
formatOptions,
preferences
);
const edits: TextEdit[] = [];

for (const fileTextChange of fileTextChanges) {
for (const textChange of fileTextChange.textChanges) {
const location = this.textSpanToLocation(fileTextChange.fileName, textChange.span);
if (location) {
edits.push({
range: location.range,
newText: textChange.newText,
});
}
}
}

return edits;
}

private applyCodeAction(
uri: string,
range: Range,
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/language-server/messages.cts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProtocolRequestType } from 'vscode-languageserver';
import { ProtocolRequestType, TextEdit } from 'vscode-languageserver';

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

export const SortImportsRequest = makeRequestType(
'glint/sortImports',
ProtocolRequestType<SortImportsParams, SortImportsResult | null, void, void, void>
);

export interface GetIRParams {
uri: string;
}
Expand All @@ -19,6 +24,12 @@ export interface GetIRResult {
uri: string;
}

export interface SortImportsParams {
uri: string;
}

export type SortImportsResult = TextEdit[];

// This utility allows us to encode type information to enforce that we're using
// a valid request name along with its associated param/response types without
// actually requring the runtime code here to be imported elsewhere.
Expand Down
12 changes: 12 additions & 0 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,20 @@
"title": "Glint: Show IR for Debugging",
"command": "glint.show-debug-ir",
"enablement": "config.glint.debug == true"
},
{
"title": "Glint: Sort Imports",
"command": "glint.sort-imports"
}
],
"menus": {
"commandPalette": [
{
"command": "glint.sort-imports",
"when": "editorLangId =~ /javascript|typescript|glimmer-js|glimmer-ts/"
}
]
},
"configuration": [
{
"title": "Glint",
Expand Down
33 changes: 32 additions & 1 deletion packages/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
commands,
workspace,
WorkspaceConfiguration,
WorkspaceEdit,
} from 'vscode';
import { Disposable, LanguageClient, ServerOptions } from 'vscode-languageclient/node.js';
import type { Request, GetIRRequest } from '@glint/core/lsp-messages';
import type { Request, GetIRRequest, SortImportsRequest } from '@glint/core/lsp-messages';

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

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

async function sortImports(editor: TextEditor): Promise<void> {
const workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri);
if (!workspaceFolder) {
return;
}

let client = clients.get(workspaceFolder.uri.fsPath);
let request = requestKey<typeof SortImportsRequest>('glint/sortImports');
const edits = await client?.sendRequest(request, { uri: editor.document.uri.toString() });

if (!edits) {
return;
}

const workspaceEdit = new WorkspaceEdit();

for (const edit of edits) {
const range = new Range(
edit.range.start.line,
edit.range.start.character,
edit.range.end.line,
edit.range.end.character
);
workspaceEdit.replace(editor.document.uri, range, edit.newText);
}

workspace.applyEdit(workspaceEdit);
}

async function showDebugIR(editor: TextEditor): Promise<void> {
let workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri);
if (!workspaceFolder) {
Expand Down

0 comments on commit 44491c9

Please sign in to comment.