Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions _extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
"title": "Show LSP Trace",
"enablement": "typescript.native-preview.serverRunning",
"category": "TypeScript Native Preview"
},
{
"command": "typescript.native-preview.sortImports",
"title": "Sort Imports",
"enablement": "typescript.native-preview.serverRunning && editorLangId =~ /^(javascript|typescript|javascriptreact|typescriptreact)$/",
"category": "TypeScript Native Preview"
}
]
},
Expand Down
10 changes: 10 additions & 0 deletions _extension/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,14 @@ export class Client {
this.client.restart();
return new vscode.Disposable(() => {});
}

async executeCommand(command: string, ...args: any[]): Promise<any> {
if (!this.client) {
throw new Error("Language client is not initialized");
}
return this.client.sendRequest("workspace/executeCommand", {
command,
arguments: args,
});
}
}
41 changes: 38 additions & 3 deletions _extension/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export function registerLanguageCommands(context: vscode.ExtensionContext, clien

disposables.push(vscode.commands.registerCommand("typescript.native-preview.showMenu", showCommands));

disposables.push(vscode.commands.registerCommand("typescript.native-preview.sortImports", async () => {
return sortImports(client);
}));

return disposables;
}

Expand All @@ -53,11 +57,42 @@ async function updateUseTsgoSetting(enable: boolean): Promise<void> {
await vscode.commands.executeCommand("workbench.action.restartExtensionHost");
}

/**
* Shows the quick pick menu for TypeScript Native Preview commands
*/
async function sortImports(client: Client): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage("No active editor");
return;
}

const document = editor.document;
const languageId = document.languageId;

// Check if the file is TypeScript or JavaScript
if (!["typescript", "javascript", "typescriptreact", "javascriptreact"].includes(languageId)) {
vscode.window.showErrorMessage("Sort Imports is only available for TypeScript and JavaScript files");
return;
}

try {
// Execute the sort imports command on the server via LSP
await client.executeCommand(
"typescript-go.organizeImports",
document.uri.toString(),
);
vscode.window.showInformationMessage("Imports sorted successfully");
}
catch (error) {
vscode.window.showErrorMessage(`Failed to sort imports: ${error}`);
}
}

async function showCommands(): Promise<void> {
const commands: readonly { label: string; description: string; command: string; }[] = [
{
label: "$(symbol-namespace) Sort Imports",
description: "Sort imports in the current file",
command: "typescript.native-preview.sortImports",
},
{
label: "$(refresh) Restart Server",
description: "Restart the TypeScript Native Preview language server",
Expand Down
161 changes: 158 additions & 3 deletions internal/ls/organizeimports.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,165 @@ import (
"github.com/microsoft/typescript-go/internal/tspath"
)

// GetImportDeclarationInsertIndex determines where to insert a new import in a sorted list
// It uses binary search to find the appropriate insertion index
// statement = anyImportOrRequireStatement
func getImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
// !!!
return len(sortedImports)
func GetImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
n := len(sortedImports)
if n == 0 {
return 0
}

low, high := 0, n
for low < high {
mid := low + (high-low)/2
if comparer(sortedImports[mid], newImport) < 0 {
low = mid + 1
} else {
high = mid
}
}
return low
}

// CompareImportsOrRequireStatements compares two import statements for sorting purposes
// Returns:
// - negative if s1 should come before s2
// - 0 if they are equivalent
// - positive if s1 should come after s2
func CompareImportsOrRequireStatements(s1, s2 *ast.Statement, comparer func(a, b string) int) int {
// First compare module specifiers
comparison := compareModuleSpecifiersWorker(
GetModuleSpecifierExpression(s1),
GetModuleSpecifierExpression(s2),
comparer,
)
if comparison != 0 {
return comparison
}
// If module specifiers are equal, compare by import kind
return compareImportKind(s1, s2)
}

// GetModuleSpecifierExpression extracts the module specifier expression from an import/export statement
func GetModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression {
switch declaration.Kind {
case ast.KindImportDeclaration, ast.KindJSImportDeclaration:
return declaration.AsImportDeclaration().ModuleSpecifier
case ast.KindImportEqualsDeclaration:
moduleRef := declaration.AsImportEqualsDeclaration().ModuleReference
if moduleRef != nil && moduleRef.Kind == ast.KindExternalModuleReference {
return moduleRef.AsExternalModuleReference().Expression
}
return nil
case ast.KindVariableStatement:
// Handle require statements: const x = require("...")
declList := declaration.AsVariableStatement().DeclarationList
if declList != nil && declList.Kind == ast.KindVariableDeclarationList {
decls := declList.AsVariableDeclarationList().Declarations.Nodes
if len(decls) > 0 {
varDecl := decls[0].AsVariableDeclaration()
if varDecl.Initializer != nil && ast.IsCallExpression(varDecl.Initializer) {
call := varDecl.Initializer.AsCallExpression()
if len(call.Arguments.Nodes) > 0 {
return call.Arguments.Nodes[0]
}
}
}
}
return nil
}
return nil
}

// compareModuleSpecifiersWorker compares two module specifier expressions.
// Ordering:
// 1. undefined module specifiers come last
// 2. Relative imports come after absolute imports
// 3. Otherwise, compare by the comparer function
func compareModuleSpecifiersWorker(m1, m2 *ast.Expression, comparer func(a, b string) int) int {
name1 := getExternalModuleNameText(m1)
name2 := getExternalModuleNameText(m2)

// undefined names come last
if comparison := compareBooleans(name1 == "", name2 == ""); comparison != 0 {
return comparison
}

// If both are defined, absolute imports come before relative imports
if name1 != "" && name2 != "" {
isRelative1 := tspath.IsExternalModuleNameRelative(name1)
isRelative2 := tspath.IsExternalModuleNameRelative(name2)
// Reverse parameter order because we want absolute imports (isRelative=false) before relative imports (isRelative=true)
if comparison := compareBooleans(isRelative2, isRelative1); comparison != 0 {
Comment on lines +103 to +104
Copy link
Preview

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter reversal in compareBooleans(isRelative2, isRelative1) is confusing and error-prone. Consider using compareBooleans(isRelative1, isRelative2) and negating the result, or creating a clearer helper function like compareAbsoluteBeforeRelative.

Copilot uses AI. Check for mistakes.

return comparison
}

// Finally, compare by the provided comparer
return comparer(name1, name2)
}

return 0
}

// getExternalModuleNameText extracts the text of a module name from an expression
func getExternalModuleNameText(expr *ast.Expression) string {
if expr == nil {
return ""
}

if ast.IsStringLiteral(expr) {
return expr.AsStringLiteral().Text
}

return ""
}

// compareImportKind compares imports by their kind/type for sorting
// Import order:
// 1. Side-effect imports (import "foo")
// 2. Type-only imports (import type { Foo } from "foo")
// 3. Namespace imports (import * as foo from "foo")
// 4. Default imports (import foo from "foo")
// 5. Named imports (import { foo } from "foo")
// 6. ImportEquals declarations (import foo = require("foo"))
// 7. Require variable statements (const foo = require("foo"))
func compareImportKind(s1, s2 *ast.Statement) int {
order1 := getImportKindOrder(s1)
order2 := getImportKindOrder(s2)
return cmp.Compare(order1, order2)
}

// getImportKindOrder returns the sorting order for different import kinds
func getImportKindOrder(statement *ast.Statement) int {
switch statement.Kind {
case ast.KindImportDeclaration, ast.KindJSImportDeclaration:
importDecl := statement.AsImportDeclaration()
// Side-effect import (no import clause)
if importDecl.ImportClause == nil {
return 0
}
importClause := importDecl.ImportClause.AsImportClause()
// Type-only import
if importClause.IsTypeOnly {
return 1
}
// Namespace import (import * as foo)
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamespaceImport {
return 2
}
// Default import (import foo)
if importClause.Name() != nil {
return 3
}
// Named import (import { foo })
return 4
case ast.KindImportEqualsDeclaration:
return 5
case ast.KindVariableStatement:
return 6
}
return 999
}

// returns `-1` if `a` is better than `b`
Expand Down
Loading