Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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,
});
}
}
40 changes: 37 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,41 @@ 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