From 6e1c7516c37fd7ab313743e7d2a10011934382af Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:32:17 -0700 Subject: [PATCH 1/6] Collate-free import sorting --- .../fourslash/_scripts/convertFourslash.mts | 26 ++ internal/fourslash/_scripts/manualTests.txt | 8 + .../organizeImportsPathsUnicode1_test.go | 17 +- .../organizeImportsPathsUnicode2_test.go | 13 +- .../organizeImportsPathsUnicode3_test.go | 17 +- .../organizeImportsPathsUnicode4_test.go | 19 +- .../organizeImportsUnicode1_test.go | 15 +- .../organizeImportsUnicode2_test.go | 13 +- .../organizeImportsUnicode3_test.go | 15 +- .../organizeImportsUnicode4_test.go | 17 +- .../organizeImports_coalesceExports_test.go | 19 +- .../organizeImports_coalesceImports_test.go | 35 ++- ...ganizeImports_sortModuleSpecifiers_test.go | 11 +- internal/ls/lsutil/organizeimports.go | 256 ++++++++++-------- internal/ls/lsutil/userpreferences.go | 79 +++++- internal/ls/lsutil/userpreferences_test.go | 99 +++++++ internal/ls/lsutil/utilities_test.go | 63 +++++ internal/ls/organizeimports.go | 9 +- internal/project/session_test.go | 8 +- .../state/codeLensAcrossProjects.baseline | 2 + 20 files changed, 467 insertions(+), 274 deletions(-) rename internal/fourslash/tests/{gen => manual}/organizeImportsPathsUnicode1_test.go (69%) rename internal/fourslash/tests/{gen => manual}/organizeImportsPathsUnicode2_test.go (65%) rename internal/fourslash/tests/{gen => manual}/organizeImportsPathsUnicode3_test.go (61%) rename internal/fourslash/tests/{gen => manual}/organizeImportsPathsUnicode4_test.go (63%) rename internal/fourslash/tests/{gen => manual}/organizeImportsUnicode1_test.go (69%) rename internal/fourslash/tests/{gen => manual}/organizeImportsUnicode2_test.go (63%) rename internal/fourslash/tests/{gen => manual}/organizeImportsUnicode3_test.go (63%) rename internal/fourslash/tests/{gen => manual}/organizeImportsUnicode4_test.go (63%) diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index 9c25c520f66..ab9d9596254 100755 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -2695,6 +2695,32 @@ function parseOrganizeImportsArgs(args: readonly ts.Expression[]): [VerifyOrgani throw new Error(`Unsupported value for organizeImportsIgnoreCase: ${propValue.getText()}`); } } + else if (propName === "organizeImportsSort") { + if (ts.isStringLiteral(propValue)) { + switch (propValue.text) { + case "auto": + prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortAuto`); + break; + case "ordinal": + prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortOrdinal`); + break; + case "ordinalIgnoreCase": + prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortOrdinalIgnoreCase`); + break; + case "natural": + prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNatural`); + break; + case "naturalIgnoreCase": + prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNaturalIgnoreCase`); + break; + default: + throw new Error(`Unsupported value for organizeImportsSort: ${propValue.text}`); + } + } + else { + throw new Error(`Expected string literal for organizeImportsSort, got ${propValue.getText()}`); + } + } else if (propName === "organizeImportsCollation") { if (ts.isStringLiteral(propValue)) { if (propValue.text === "unicode") { diff --git a/internal/fourslash/_scripts/manualTests.txt b/internal/fourslash/_scripts/manualTests.txt index 05618afd339..86d4b2e970a 100644 --- a/internal/fourslash/_scripts/manualTests.txt +++ b/internal/fourslash/_scripts/manualTests.txt @@ -110,6 +110,14 @@ navto_excludeLib3 navto_excludeLib4 navto_serverExcludeLib nodeModulesImportCompletions1 +organizeImportsPathsUnicode1 +organizeImportsPathsUnicode2 +organizeImportsPathsUnicode3 +organizeImportsPathsUnicode4 +organizeImportsUnicode1 +organizeImportsUnicode2 +organizeImportsUnicode3 +organizeImportsUnicode4 outliningForNonCompleteInterfaceDeclaration outliningHintSpansForFunction overloadOnConstCallSignature diff --git a/internal/fourslash/tests/gen/organizeImportsPathsUnicode1_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go similarity index 69% rename from internal/fourslash/tests/gen/organizeImportsPathsUnicode1_test.go rename to internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go index d16daa05a88..c3b71f39b01 100644 --- a/internal/fourslash/tests/gen/organizeImportsPathsUnicode1_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsPathsUnicode1" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsPathsUnicode1(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as Ab from "./Ab"; @@ -34,21 +29,19 @@ import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationOrdinal, + OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, }, ) f.VerifyOrganizeImports(t, - `import * as _aB from "./_aB"; -import * as _Ab from "./_Ab"; -import * as aB from "./aB"; + `import * as _Ab from "./_Ab"; +import * as _aB from "./_aB"; import * as Ab from "./Ab"; +import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/fourslash/tests/gen/organizeImportsPathsUnicode2_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go similarity index 65% rename from internal/fourslash/tests/gen/organizeImportsPathsUnicode2_test.go rename to internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go index 3d1002cd42a..ef57ce6b37f 100644 --- a/internal/fourslash/tests/gen/organizeImportsPathsUnicode2_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsPathsUnicode2" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsPathsUnicode2(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as a2 from "./a2"; @@ -32,9 +27,7 @@ import * as a2 from "./a2"; console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsNumericCollation: core.TSFalse, + OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, }, ) f.VerifyOrganizeImports(t, @@ -45,9 +38,7 @@ import * as a100 from "./a100"; console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsNumericCollation: core.TSTrue, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go similarity index 61% rename from internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go rename to internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go index 2f9260c263b..829a7e6c281 100644 --- a/internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsPathsUnicode3" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsPathsUnicode3(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as B from "./B"; @@ -25,16 +20,14 @@ console.log(A, À, B);` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.VerifyOrganizeImports(t, - `import * as À from "./À"; -import * as A from "./A"; + `import * as A from "./A"; import * as B from "./B"; +import * as À from "./À"; console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsAccentCollation: core.TSFalse, + OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, }, ) f.VerifyOrganizeImports(t, @@ -45,9 +38,7 @@ import * as B from "./B"; console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsAccentCollation: core.TSTrue, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/fourslash/tests/gen/organizeImportsPathsUnicode4_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go similarity index 63% rename from internal/fourslash/tests/gen/organizeImportsPathsUnicode4_test.go rename to internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go index a9cf7a9ad7b..ca1f869daa1 100644 --- a/internal/fourslash/tests/gen/organizeImportsPathsUnicode4_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsPathsUnicode4" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsPathsUnicode4(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as Ab from "./Ab"; @@ -34,23 +29,19 @@ import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstUpper, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) f.VerifyOrganizeImports(t, - `import * as _aB from "./_aB"; -import * as _Ab from "./_Ab"; -import * as aB from "./aB"; + `import * as _Ab from "./_Ab"; +import * as _aB from "./_aB"; import * as Ab from "./Ab"; +import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstLower, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/gen/organizeImportsUnicode1_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go similarity index 69% rename from internal/fourslash/tests/gen/organizeImportsUnicode1_test.go rename to internal/fourslash/tests/manual/organizeImportsUnicode1_test.go index f5502d932f7..aef695338bd 100644 --- a/internal/fourslash/tests/gen/organizeImportsUnicode1_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsUnicode1" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsUnicode1(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -38,23 +33,21 @@ console.log(_aB, _Ab, aB, Ab);` console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationOrdinal, + OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, }, ) f.VerifyOrganizeImports(t, `import { - _aB, _Ab, - aB, + _aB, Ab, + aB, } from './foo'; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/fourslash/tests/gen/organizeImportsUnicode2_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go similarity index 63% rename from internal/fourslash/tests/gen/organizeImportsUnicode2_test.go rename to internal/fourslash/tests/manual/organizeImportsUnicode2_test.go index 243353b0c46..380395c307d 100644 --- a/internal/fourslash/tests/gen/organizeImportsUnicode2_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsUnicode2" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsUnicode2(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -36,9 +31,7 @@ console.log(a1, a2, a100);` console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsNumericCollation: core.TSFalse, + OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, }, ) f.VerifyOrganizeImports(t, @@ -51,9 +44,7 @@ console.log(a1, a2, a100);`, console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsNumericCollation: core.TSTrue, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/fourslash/tests/gen/organizeImportsUnicode3_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go similarity index 63% rename from internal/fourslash/tests/gen/organizeImportsUnicode3_test.go rename to internal/fourslash/tests/manual/organizeImportsUnicode3_test.go index 97975f81edf..8ea458e5a62 100644 --- a/internal/fourslash/tests/gen/organizeImportsUnicode3_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsUnicode3" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsUnicode3(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -28,17 +23,15 @@ console.log(A, À, B);` defer done() f.VerifyOrganizeImports(t, `import { - À, A, B, + À, } from './foo'; console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsAccentCollation: core.TSFalse, + OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, }, ) f.VerifyOrganizeImports(t, @@ -51,9 +44,7 @@ console.log(A, À, B);`, console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsAccentCollation: core.TSTrue, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/fourslash/tests/gen/organizeImportsUnicode4_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go similarity index 63% rename from internal/fourslash/tests/gen/organizeImportsUnicode4_test.go rename to internal/fourslash/tests/manual/organizeImportsUnicode4_test.go index 02c4c495b81..69d07f54c96 100644 --- a/internal/fourslash/tests/gen/organizeImportsUnicode4_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go @@ -1,12 +1,8 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsUnicode4" - package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -14,7 +10,6 @@ import ( ) func TestOrganizeImportsUnicode4(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -38,25 +33,21 @@ console.log(_aB, _Ab, aB, Ab);` console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstUpper, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) f.VerifyOrganizeImports(t, `import { - _aB, _Ab, - aB, + _aB, Ab, + aB, } from './foo'; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSFalse, - OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, - OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstLower, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/organizeImports_coalesceExports_test.go b/internal/fourslash/tests/organizeImports_coalesceExports_test.go index e744b03aac3..c0dfddc9683 100644 --- a/internal/fourslash/tests/organizeImports_coalesceExports_test.go +++ b/internal/fourslash/tests/organizeImports_coalesceExports_test.go @@ -3,7 +3,6 @@ package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -22,7 +21,7 @@ void 0;` `export { B, default as M, a as n, Z as O, y } from "lib"; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -39,7 +38,7 @@ void 0;` `export * from "lib"; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -58,7 +57,7 @@ void 0;` export { x, z as y }; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -75,7 +74,7 @@ void 0;` `export { x, y as z } from "lib"; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -96,7 +95,7 @@ export * from "lib"; export { y } from "lib"; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -116,7 +115,7 @@ void 0;` export { z as default, q as w, x, w as y }; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -135,7 +134,7 @@ void 0;` export { x as a, z as b, y } from "lib"; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -160,7 +159,7 @@ export { x }; export type { y }; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -181,6 +180,6 @@ type y = number; export type { x, y }; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } diff --git a/internal/fourslash/tests/organizeImports_coalesceImports_test.go b/internal/fourslash/tests/organizeImports_coalesceImports_test.go index fe0af8a4e76..17ebba013f2 100644 --- a/internal/fourslash/tests/organizeImports_coalesceImports_test.go +++ b/internal/fourslash/tests/organizeImports_coalesceImports_test.go @@ -3,7 +3,6 @@ package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -22,7 +21,7 @@ M; n; B; y; O;` `import { B, default as M, a as n, Z as O, y } from "lib"; M; n; B; y; O;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -39,7 +38,7 @@ void 0;` `import "lib"; void 0;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -60,7 +59,7 @@ import * as x from "lib"; import * as y from "lib"; x; y; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -77,7 +76,7 @@ x; y;` `import { default as x, default as y } from "lib"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -94,7 +93,7 @@ x; z;` `import { x, y as z } from "lib"; x; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -115,7 +114,7 @@ import "lib"; import * as x from "lib"; x; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -136,7 +135,7 @@ import "lib"; import x from "lib"; x; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -157,7 +156,7 @@ import "lib"; import { x } from "lib"; x; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -175,7 +174,7 @@ x; y;` `import y, * as x from "lib"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -196,7 +195,7 @@ import * as x from "lib"; import { y } from "lib"; x; y; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -214,7 +213,7 @@ x; y;` `import x, { y } from "lib"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -240,7 +239,7 @@ import * as y from "lib"; import { a, b, default as w, default as z } from "lib"; w; x; y; z; a; b;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -263,7 +262,7 @@ import * as y from "lib"; import z from "lib"; x; y; z; w;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -283,7 +282,7 @@ x; y; z;` import { z } from "lib"; x; y; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -304,7 +303,7 @@ import type z from "lib"; import type { x } from "lib"; x; y; z;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -321,8 +320,8 @@ z; y; x; c; b; a;` z; y; x; c; b; a;`, lsproto.CodeActionKindSourceSortImports, &lsutil.UserPreferences{ - OrganizeImportsIgnoreCase: core.TSTrue, - OrganizeImportsTypeOrder: lsutil.OrganizeImportsTypeOrderInline, + OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase, + OrganizeImportsTypeOrder: lsutil.OrganizeImportsTypeOrderInline, }, ) } diff --git a/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go b/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go index 350c7186119..b3eff84f6d6 100644 --- a/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go +++ b/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go @@ -3,7 +3,6 @@ package fourslash_test import ( "testing" - "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -24,7 +23,7 @@ x; y;` import x from "lib2"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -42,7 +41,7 @@ x; y;` import x from "./lib2"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -60,7 +59,7 @@ x; y;` import x from "./lib"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -79,7 +78,7 @@ x; y;` import x from "Z"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } @@ -98,6 +97,6 @@ x; y;` import x from "z"; x; y;`, lsproto.CodeActionKindSourceSortImports, - &lsutil.UserPreferences{OrganizeImportsIgnoreCase: core.TSTrue}, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, ) } diff --git a/internal/ls/lsutil/organizeimports.go b/internal/ls/lsutil/organizeimports.go index 9ae778810aa..39c865e4c15 100644 --- a/internal/ls/lsutil/organizeimports.go +++ b/internal/ls/lsutil/organizeimports.go @@ -3,25 +3,21 @@ package lsutil import ( "cmp" "math" + "strings" "unicode" + "unicode/utf8" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" - "golang.org/x/text/collate" - "golang.org/x/text/language" + "golang.org/x/text/unicode/norm" ) -var ( - caseInsensitiveOrganizeImportsComparer = []func(a, b string) int{getOrganizeImportsOrdinalStringComparer(true)} - caseSensitiveOrganizeImportsComparer = []func(a, b string) int{getOrganizeImportsOrdinalStringComparer(false)} - organizeImportsComparers = []func(a, b string) int{ - caseInsensitiveOrganizeImportsComparer[0], - caseSensitiveOrganizeImportsComparer[0], - } -) +var organizeImportsAutoComparers = []func(a, b string) int{ + getOrganizeImportsOrdinalStringComparer(true), + getOrganizeImportsOrdinalStringComparer(false), +} // FilterImportDeclarations filters out non-import declarations from a list of statements. func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement { @@ -32,14 +28,11 @@ func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement { // GetDetectionLists returns the lists of comparers and type orders to test for organize imports detection. func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []OrganizeImportsTypeOrder) { - if !preferences.OrganizeImportsIgnoreCase.IsUnknown() { - ignoreCase := preferences.OrganizeImportsIgnoreCase.IsTrue() - comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(preferences, ignoreCase)} + sort := ResolveOrganizeImportsSort(preferences) + if sort != OrganizeImportsSortAuto { + comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(sort)} } else { - comparersToTest = []func(a, b string) int{ - getOrganizeImportsStringComparer(preferences, true), - getOrganizeImportsStringComparer(preferences, false), - } + comparersToTest = organizeImportsAutoComparers } if preferences.OrganizeImportsTypeOrder != OrganizeImportsTypeOrderAuto { @@ -55,6 +48,28 @@ func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b return comparersToTest, typeOrdersToTest } +func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort { + if preferences.OrganizeImportsSort != OrganizeImportsSortAuto { + return preferences.OrganizeImportsSort + } + + if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode { + if preferences.OrganizeImportsIgnoreCase.IsTrue() { + return OrganizeImportsSortNaturalIgnoreCase + } + return OrganizeImportsSortNatural + } + + switch preferences.OrganizeImportsIgnoreCase { + case core.TSTrue: + return OrganizeImportsSortOrdinalIgnoreCase + case core.TSFalse: + return OrganizeImportsSortOrdinal + default: + return OrganizeImportsSortAuto + } +} + func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int { if ignoreCase { return stringutil.CompareStringsCaseInsensitiveEslintCompatible @@ -62,125 +77,133 @@ func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) return stringutil.CompareStringsCaseSensitive } -func getOrganizeImportsUnicodeStringComparer(ignoreCase bool, preferences UserPreferences) func(a, b string) int { - resolvedLocale := getOrganizeImportsLocale(preferences) - - caseFirst := preferences.OrganizeImportsCaseFirst - numeric := preferences.OrganizeImportsNumericCollation.IsTrue() - accents := !preferences.OrganizeImportsAccentCollation.IsFalse() +func getOrganizeImportsNaturalStringComparer(ignoreCase bool) func(a, b string) int { + return func(a, b string) int { + return compareOrganizeImportsNaturalStrings(a, b, ignoreCase) + } +} - tag, _ := language.Parse(resolvedLocale) +func compareOrganizeImportsNaturalStrings(a string, b string, ignoreCase bool) int { + if cmp := compareStringsNumeric(naturalCollationPrimaryKey(a), naturalCollationPrimaryKey(b)); cmp != 0 { + return cmp + } - var opts []collate.Option + if cmp := compareStringsNumeric(naturalCollationAccentKey(a), naturalCollationAccentKey(b)); cmp != 0 { + return cmp + } - if numeric { - opts = append(opts, collate.Numeric) + if !ignoreCase { + if cmp := compareOrganizeImportsCaseUpperFirst(a, b); cmp != 0 { + return cmp + } } - looseOpts := append([]collate.Option{}, opts...) - looseOpts = append(looseOpts, collate.Loose) - looseCollator := collate.New(tag, looseOpts...) + return 0 +} - if !ignoreCase { - caseInsensitiveOpts := append([]collate.Option{}, opts...) - caseInsensitiveOpts = append(caseInsensitiveOpts, collate.IgnoreCase) - caseInsensitiveCollator := collate.New(tag, caseInsensitiveOpts...) +func naturalCollationPrimaryKey(s string) string { + return strings.ToLower(removeDiacritics(s)) +} - fullCollator := collate.New(tag, opts...) +func naturalCollationAccentKey(s string) string { + return strings.ToLower(s) +} - return func(a, b string) int { - var primaryCmp int - if !accents { - primaryCmp = looseCollator.CompareString(a, b) - } else { - primaryCmp = caseInsensitiveCollator.CompareString(a, b) - } - if primaryCmp != 0 { - return primaryCmp - } +func removeDiacritics(s string) string { + return strings.Map(func(r rune) rune { + if unicode.Is(unicode.Mn, r) { + return -1 + } + return r + }, norm.NFD.String(s)) +} - aRunes := []rune(a) - bRunes := []rune(b) - minLen := min(len(aRunes), len(bRunes)) - - for i := range minLen { - aUpper := unicode.IsUpper(aRunes[i]) - bUpper := unicode.IsUpper(bRunes[i]) - if aUpper != bUpper { - switch caseFirst { - case OrganizeImportsCaseFirstUpper: - if aUpper { - return -1 - } - return 1 - case OrganizeImportsCaseFirstLower: - if !aUpper { - return -1 - } - return 1 - default: - if aUpper { - return 1 - } - return -1 - } - } - } +func compareStringsNumeric(a string, b string) int { + for len(a) > 0 && len(b) > 0 { + if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) { + aRunEnd := asciiDigitRunEnd(a) + bRunEnd := asciiDigitRunEnd(b) - if !accents { - if len(aRunes) != len(bRunes) { - return len(aRunes) - len(bRunes) - } - return 0 + if cmp := compareNumericText(a[:aRunEnd], b[:bRunEnd]); cmp != 0 { + return cmp } - return fullCollator.CompareString(a, b) + a = a[aRunEnd:] + b = b[bRunEnd:] + continue } - } - if ignoreCase { - opts = append(opts, collate.IgnoreCase) - if !accents { - opts = append(opts, collate.Loose) + aRune, aSize := utf8.DecodeRuneInString(a) + bRune, bSize := utf8.DecodeRuneInString(b) + if aRune != bRune { + return cmp.Compare(aRune, bRune) } + + a = a[aSize:] + b = b[bSize:] } - collator := collate.New(tag, opts...) + return cmp.Compare(len(a), len(b)) +} - return func(a, b string) int { - return collator.CompareString(a, b) +func isASCIIDigit(ch byte) bool { + return ch >= '0' && ch <= '9' +} + +func asciiDigitRunEnd(s string) int { + i := 0 + for i < len(s) && isASCIIDigit(s[i]) { + i++ } + return i } -func getOrganizeImportsLocale(preferences UserPreferences) string { - localeStr := "en" - if preferences.OrganizeImportsLocale != "" { - localeStr = preferences.OrganizeImportsLocale +func compareNumericText(a string, b string) int { + aDigits := strings.TrimLeft(a, "0") + bDigits := strings.TrimLeft(b, "0") + if aDigits == "" { + aDigits = "0" + } + if bDigits == "" { + bDigits = "0" } - if localeStr == "auto" { - if locale.Default != (locale.Locale{}) { - tag := language.Tag(locale.Default) - return tag.String() - } - return "en" + if len(aDigits) != len(bDigits) { + return cmp.Compare(len(aDigits), len(bDigits)) } + return strings.Compare(aDigits, bDigits) +} - if locale, ok := locale.Parse(localeStr); ok { - tag := language.Tag(locale) - return tag.String() +func compareOrganizeImportsCaseUpperFirst(a string, b string) int { + aRunes := []rune(a) + bRunes := []rune(b) + minLen := min(len(aRunes), len(bRunes)) + + for i := range minLen { + aUpper := unicode.IsUpper(aRunes[i]) + bUpper := unicode.IsUpper(bRunes[i]) + if aUpper != bUpper { + if aUpper { + return -1 + } + return 1 + } } - return "en" + return cmp.Compare(len(aRunes), len(bRunes)) } -func getOrganizeImportsStringComparer(preferences UserPreferences, ignoreCase bool) func(a, b string) int { - collation := preferences.OrganizeImportsCollation - - if collation == OrganizeImportsCollationUnicode { - return getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) +func getOrganizeImportsStringComparer(sort OrganizeImportsSort) func(a, b string) int { + switch sort { + case OrganizeImportsSortOrdinalIgnoreCase: + return getOrganizeImportsOrdinalStringComparer(true) + case OrganizeImportsSortNatural: + return getOrganizeImportsNaturalStringComparer(false) + case OrganizeImportsSortNaturalIgnoreCase: + return getOrganizeImportsNaturalStringComparer(true) + default: + return getOrganizeImportsOrdinalStringComparer(false) } - return getOrganizeImportsOrdinalStringComparer(ignoreCase) } func getModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression { @@ -314,11 +337,7 @@ func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a // GetNamedImportSpecifierComparer returns a comparer function for sorting import specifiers. func GetNamedImportSpecifierComparer(preferences UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int { if comparer == nil { - ignoreCase := false - if !preferences.OrganizeImportsIgnoreCase.IsUnknown() { - ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue() - } - comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase) + comparer = getOrganizeImportsStringComparer(ResolveOrganizeImportsSort(preferences)) } return func(s1, s2 *ast.Node) int { return compareImportOrExportSpecifiers(s1, s2, comparer, preferences) @@ -346,14 +365,11 @@ func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.St } func getComparers(preferences UserPreferences) []func(a string, b string) int { - switch preferences.OrganizeImportsIgnoreCase { - case core.TSTrue: - return caseInsensitiveOrganizeImportsComparer - case core.TSFalse: - return caseSensitiveOrganizeImportsComparer + sort := ResolveOrganizeImportsSort(preferences) + if sort != OrganizeImportsSortAuto { + return []func(a, b string) int{getOrganizeImportsStringComparer(sort)} } - - return organizeImportsComparers + return organizeImportsAutoComparers } type namedImportSortResult struct { @@ -582,7 +598,7 @@ func GetNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFi specifierComparer = GetNamedImportSpecifierComparer(preferences, comparersToTest[0]) isSorted = core.TSUnknown - if (preferences.OrganizeImportsIgnoreCase.IsUnknown() || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderAuto) && importStmt != nil { + if (ResolveOrganizeImportsSort(preferences) == OrganizeImportsSortAuto || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderAuto) && importStmt != nil { detectFromDecl := detectNamedImportOrganizationBySort([]*ast.Statement{importStmt}, comparersToTest, typeOrdersToTest) if detectFromDecl != nil { isSorted = core.BoolToTristate(detectFromDecl.isSorted) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 2b54bda0158..54af678eb68 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -84,6 +84,9 @@ type UserPreferences struct { // ------- OrganizeImports ------- + // Indicates which deterministic preset should be used to sort imports. + // "auto" detects the existing ordinal case sensitivity where possible. + OrganizeImportsSort OrganizeImportsSort `raw:"organizeImportsSort" config:"preferences.organizeImports.sort"` // !!! // Indicates whether imports should be organized in a case-insensitive manner. // // Default: TSUnknown ("auto" in strada), will perform detection @@ -240,6 +243,16 @@ const ( IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals" ) +type OrganizeImportsSort int + +const ( + OrganizeImportsSortAuto OrganizeImportsSort = iota + OrganizeImportsSortOrdinal + OrganizeImportsSortOrdinalIgnoreCase + OrganizeImportsSortNatural + OrganizeImportsSortNaturalIgnoreCase +) + type OrganizeImportsCollation bool const ( @@ -318,6 +331,21 @@ var typeParsers = map[reflect.Type]func(any) any{ } return IncludeInlayParameterNameHintsNone }, + reflect.TypeFor[OrganizeImportsSort](): func(val any) any { + if s, ok := val.(string); ok { + switch strings.ToLower(s) { + case "ordinal": + return OrganizeImportsSortOrdinal + case "ordinalignorecase": + return OrganizeImportsSortOrdinalIgnoreCase + case "natural": + return OrganizeImportsSortNatural + case "naturalignorecase": + return OrganizeImportsSortNaturalIgnoreCase + } + } + return OrganizeImportsSortAuto + }, reflect.TypeFor[OrganizeImportsCollation](): func(val any) any { if s, ok := val.(string); ok && strings.ToLower(s) == "unicode" { return OrganizeImportsCollationUnicode @@ -389,6 +417,20 @@ var typeSerializers = map[reflect.Type]func(any) any{ return nil } }, + reflect.TypeFor[OrganizeImportsSort](): func(val any) any { + switch val.(OrganizeImportsSort) { + case OrganizeImportsSortOrdinal: + return "ordinal" + case OrganizeImportsSortOrdinalIgnoreCase: + return "ordinalIgnoreCase" + case OrganizeImportsSortNatural: + return "natural" + case OrganizeImportsSortNaturalIgnoreCase: + return "naturalIgnoreCase" + default: + return "auto" + } + }, reflect.TypeFor[OrganizeImportsCollation](): func(val any) any { if val.(OrganizeImportsCollation) == OrganizeImportsCollationUnicode { return "unicode" @@ -546,27 +588,34 @@ func setNestedValue(config map[string]any, path string, value any) { current[parts[len(parts)-1]] = value } +func setRawFieldsFromConfig(v reflect.Value, infos []fieldInfo, settings map[string]any) { + index := unstableNameIndex() + for name, value := range settings { + if idx, found := index[name]; found { + info := infos[idx] + field := getFieldByPath(v, info.fieldPath) + if info.rawInvert { + if b, ok := value.(bool); ok { + value = !b + } + } + setFieldFromValue(field, value) + } + } +} + func (p UserPreferences) withConfig(config map[string]any) UserPreferences { v := reflect.ValueOf(&p).Elem() infos := fieldInfoCache() + // Raw UserPreferences can be provided directly, notably via LSP initializationOptions. + setRawFieldsFromConfig(v, infos, config) + // Process "unstable" section first - allows any field to be set by raw name. // This mirrors VS Code's behavior: { ...config.get('unstable'), ...stableOptions } // where stable options are spread after and take precedence. if unstable, ok := config["unstable"].(map[string]any); ok { - index := unstableNameIndex() - for name, value := range unstable { - if idx, found := index[name]; found { - info := infos[idx] - field := getFieldByPath(v, info.fieldPath) - if info.rawInvert { - if b, ok := value.(bool); ok { - value = !b - } - } - setFieldFromValue(field, value) - } - } + setRawFieldsFromConfig(v, infos, unstable) } // Process path-based config (VS Code style nested paths). @@ -618,7 +667,7 @@ func setFieldFromValue(field reflect.Value, val any) { return } - // Check custom parsers first (for types like Tristate, OrganizeImportsCollation, etc.) + // Check custom parsers first (for types like Tristate, enums, etc.) if parser, ok := typeParsers[field.Type()]; ok { field.Set(reflect.ValueOf(parser(val))) return @@ -687,7 +736,7 @@ func (p *UserPreferences) MarshalJSONTo(enc *json.Encoder) error { } func serializeField(field reflect.Value) any { - // Check custom serializers first (for types like Tristate, OrganizeImportsCollation, etc.) + // Check custom serializers first (for types like Tristate, enums, etc.) if serializer, ok := typeSerializers[field.Type()]; ok { return serializer(field.Interface()) } diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go index d7ea9dcc6ea..13e2b57ce60 100644 --- a/internal/ls/lsutil/userpreferences_test.go +++ b/internal/ls/lsutil/userpreferences_test.go @@ -309,6 +309,105 @@ func TestUserPreferencesParseUnstable(t *testing.T) { }, }, }, + { + name: "old raw organize imports unicode preferences load as raw state", + json: `{ + "unstable": { + "organizeImportsCollation": "unicode", + "organizeImportsCaseFirst": "upper", + "organizeImportsIgnoreCase": false, + "organizeImportsNumericCollation": true + } + }`, + expected: UserPreferences{ + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsNumericCollation: core.TSTrue, + }, + }, + { + name: "old top-level raw organize imports unicode preferences load as raw state", + json: `{ + "organizeImportsCollation": "unicode", + "organizeImportsIgnoreCase": true + }`, + expected: UserPreferences{ + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsIgnoreCase: core.TSTrue, + }, + }, + { + name: "new top-level raw organize imports sort is accepted", + json: `{ + "organizeImportsSort": "natural" + }`, + expected: UserPreferences{ + OrganizeImportsSort: OrganizeImportsSortNatural, + }, + }, + { + name: "old raw organize imports ignore case loads as raw state", + json: `{ + "unstable": { + "organizeImportsIgnoreCase": true + } + }`, + expected: UserPreferences{ + OrganizeImportsIgnoreCase: core.TSTrue, + }, + }, + { + name: "new raw organize imports sort loads alongside old raw preferences", + json: `{ + "unstable": { + "organizeImportsSort": "ordinal", + "organizeImportsCollation": "unicode", + "organizeImportsIgnoreCase": true + } + }`, + expected: UserPreferences{ + OrganizeImportsSort: OrganizeImportsSortOrdinal, + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsIgnoreCase: core.TSTrue, + }, + }, + { + name: "old nested organize imports unicode preferences load as raw state", + json: `{ + "preferences": { + "organizeImports": { + "unicodeCollation": "unicode", + "caseSensitivity": "caseSensitive", + "numericCollation": true, + "caseFirst": "upper" + } + } + }`, + expected: UserPreferences{ + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsNumericCollation: core.TSTrue, + OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper, + }, + }, + { + name: "new nested organize imports sort loads alongside old nested preferences", + json: `{ + "preferences": { + "organizeImports": { + "sort": "ordinalIgnoreCase", + "unicodeCollation": "unicode", + "caseSensitivity": "caseSensitive" + } + } + }`, + expected: UserPreferences{ + OrganizeImportsSort: OrganizeImportsSortOrdinalIgnoreCase, + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsIgnoreCase: core.TSFalse, + }, + }, } for _, tt := range tests { diff --git a/internal/ls/lsutil/utilities_test.go b/internal/ls/lsutil/utilities_test.go index 51292adfbbb..3b31b848ebb 100644 --- a/internal/ls/lsutil/utilities_test.go +++ b/internal/ls/lsutil/utilities_test.go @@ -65,3 +65,66 @@ let c = 3; }) } } + +func TestResolveOrganizeImportsSort(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + preferences UserPreferences + want OrganizeImportsSort + }{ + { + name: "explicit sort wins", + preferences: UserPreferences{ + OrganizeImportsSort: OrganizeImportsSortOrdinal, + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsIgnoreCase: core.TSTrue, + }, + want: OrganizeImportsSortOrdinal, + }, + { + name: "unicode maps to natural", + preferences: UserPreferences{ + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsIgnoreCase: core.TSFalse, + }, + want: OrganizeImportsSortNatural, + }, + { + name: "unicode ignore case maps to natural ignore case", + preferences: UserPreferences{ + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + OrganizeImportsIgnoreCase: core.TSTrue, + }, + want: OrganizeImportsSortNaturalIgnoreCase, + }, + { + name: "ordinal ignore case maps to ordinal ignore case", + preferences: UserPreferences{ + OrganizeImportsIgnoreCase: core.TSTrue, + }, + want: OrganizeImportsSortOrdinalIgnoreCase, + }, + { + name: "ordinal case sensitive maps to ordinal", + preferences: UserPreferences{ + OrganizeImportsIgnoreCase: core.TSFalse, + }, + want: OrganizeImportsSortOrdinal, + }, + { + name: "unknown ordinal stays auto", + want: OrganizeImportsSortAuto, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := ResolveOrganizeImportsSort(tt.preferences); got != tt.want { + t.Fatalf("ResolveOrganizeImportsSort() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/ls/organizeimports.go b/internal/ls/organizeimports.go index ea558136d66..ddea88bffd1 100644 --- a/internal/ls/organizeimports.go +++ b/internal/ls/organizeimports.go @@ -37,24 +37,25 @@ func (l *LanguageService) OrganizeImports( preferences := l.UserPreferences() comparersToTest, typeOrdersToTest := lsutil.GetDetectionLists(preferences) defaultComparer := comparersToTest[0] + sort := lsutil.ResolveOrganizeImportsSort(preferences) var moduleSpecifierComparer func(a, b string) int var namedImportComparer func(a, b string) int - if !preferences.OrganizeImportsIgnoreCase.IsUnknown() { + if sort != lsutil.OrganizeImportsSortAuto { moduleSpecifierComparer = defaultComparer namedImportComparer = defaultComparer } typeOrder := preferences.OrganizeImportsTypeOrder - if preferences.OrganizeImportsIgnoreCase.IsUnknown() { + if sort == lsutil.OrganizeImportsSortAuto { result, _ := lsutil.DetectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest) moduleSpecifierComparer = result } - if typeOrder == lsutil.OrganizeImportsTypeOrderAuto || preferences.OrganizeImportsIgnoreCase.IsUnknown() { + if typeOrder == lsutil.OrganizeImportsTypeOrderAuto || sort == lsutil.OrganizeImportsSortAuto { namedImportComparer2, typeOrder2, found := lsutil.DetectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest) if found { - if namedImportComparer == nil || preferences.OrganizeImportsIgnoreCase.IsUnknown() { + if namedImportComparer == nil || sort == lsutil.OrganizeImportsSortAuto { namedImportComparer = namedImportComparer2 } if typeOrder == lsutil.OrganizeImportsTypeOrderAuto { diff --git a/internal/project/session_test.go b/internal/project/session_test.go index f33db22f9f7..955bea1a1d0 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -1423,7 +1423,7 @@ export const value = content;`, "quoteStyle": "single", }, "unstable": map[string]any{ - "organizeImportsIgnoreCase": true, + "organizeImportsSort": "ordinalIgnoreCase", }, } session.Configure(lsutil.ParseUserPreferences(map[string]any{"js/ts": configMap1})) @@ -1431,7 +1431,7 @@ export const value = content;`, expectedPrefs1 := lsutil.NewDefaultUserPreferences() expectedPrefs1.UseAliasesForRename = core.TSTrue expectedPrefs1.QuotePreference = lsutil.QuotePreferenceSingle - expectedPrefs1.OrganizeImportsIgnoreCase = core.TSTrue + expectedPrefs1.OrganizeImportsSort = lsutil.OrganizeImportsSortOrdinalIgnoreCase assert.DeepEqual(t, actualConfig1, expectedPrefs1) @@ -1441,7 +1441,7 @@ export const value = content;`, "quoteStyle": "double", }, "unstable": map[string]any{ - "organizeImportsIgnoreCase": false, + "organizeImportsSort": "ordinal", }, } session.Configure(lsutil.ParseUserPreferences(map[string]any{"js/ts": configMap2})) @@ -1449,7 +1449,7 @@ export const value = content;`, expectedPrefs2 := lsutil.NewDefaultUserPreferences() expectedPrefs2.UseAliasesForRename = core.TSFalse expectedPrefs2.QuotePreference = lsutil.QuotePreferenceDouble - expectedPrefs2.OrganizeImportsIgnoreCase = core.TSFalse + expectedPrefs2.OrganizeImportsSort = lsutil.OrganizeImportsSortOrdinal assert.DeepEqual(t, actualConfig2, expectedPrefs2) }) diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline index 529c89ae2cc..093bf66c7bc 100644 --- a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -190,6 +190,7 @@ Config File Names:: "organizeImports": { "caseFirst": "default", "locale": "", + "sort": "auto", "typeOrder": "auto", "unicodeCollation": "ordinal" }, @@ -642,6 +643,7 @@ Config:: "organizeImports": { "caseFirst": "default", "locale": "", + "sort": "auto", "typeOrder": "auto", "unicodeCollation": "ordinal" }, From 4c6af89f95362179028c3a6ffe5fedf547cd7c20 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:23:53 -0700 Subject: [PATCH 2/6] Tweak --- .../fourslash/_scripts/convertFourslash.mts | 4 +- .../organizeImportsPathsUnicode4_test.go | 2 +- .../manual/organizeImportsUnicode4_test.go | 2 +- internal/ls/lsutil/organizeimports.go | 28 +++----- internal/ls/lsutil/userpreferences.go | 10 +-- internal/ls/lsutil/utilities_test.go | 65 +++++++++++++++++-- 6 files changed, 80 insertions(+), 31 deletions(-) diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index ab9d9596254..bd91f4d848c 100755 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -2710,8 +2710,8 @@ function parseOrganizeImportsArgs(args: readonly ts.Expression[]): [VerifyOrgani case "natural": prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNatural`); break; - case "naturalIgnoreCase": - prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNaturalIgnoreCase`); + case "naturalCaseSensitive": + prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNaturalCaseSensitive`); break; default: throw new Error(`Unsupported value for organizeImportsSort: ${propValue.text}`); diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go index ca1f869daa1..951364739f7 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go @@ -41,7 +41,7 @@ import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go index 69d07f54c96..3ac31bd4bc1 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go @@ -47,7 +47,7 @@ console.log(_aB, _Ab, aB, Ab);`, console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, }, ) } diff --git a/internal/ls/lsutil/organizeimports.go b/internal/ls/lsutil/organizeimports.go index 39c865e4c15..de033a5645b 100644 --- a/internal/ls/lsutil/organizeimports.go +++ b/internal/ls/lsutil/organizeimports.go @@ -55,9 +55,9 @@ func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode { if preferences.OrganizeImportsIgnoreCase.IsTrue() { - return OrganizeImportsSortNaturalIgnoreCase + return OrganizeImportsSortNatural } - return OrganizeImportsSortNatural + return OrganizeImportsSortNaturalCaseSensitive } switch preferences.OrganizeImportsIgnoreCase { @@ -77,38 +77,30 @@ func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) return stringutil.CompareStringsCaseSensitive } -func getOrganizeImportsNaturalStringComparer(ignoreCase bool) func(a, b string) int { +func getOrganizeImportsNaturalStringComparer(caseSensitive bool) func(a, b string) int { return func(a, b string) int { - return compareOrganizeImportsNaturalStrings(a, b, ignoreCase) + return compareOrganizeImportsNaturalStrings(a, b, caseSensitive) } } -func compareOrganizeImportsNaturalStrings(a string, b string, ignoreCase bool) int { - if cmp := compareStringsNumeric(naturalCollationPrimaryKey(a), naturalCollationPrimaryKey(b)); cmp != 0 { +func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool) int { + if cmp := compareStringsNumeric(naturalCollationKey(a), naturalCollationKey(b)); cmp != 0 { return cmp } - if cmp := compareStringsNumeric(naturalCollationAccentKey(a), naturalCollationAccentKey(b)); cmp != 0 { - return cmp - } - - if !ignoreCase { + if caseSensitive { if cmp := compareOrganizeImportsCaseUpperFirst(a, b); cmp != 0 { return cmp } } - return 0 + return strings.Compare(a, b) } -func naturalCollationPrimaryKey(s string) string { +func naturalCollationKey(s string) string { return strings.ToLower(removeDiacritics(s)) } -func naturalCollationAccentKey(s string) string { - return strings.ToLower(s) -} - func removeDiacritics(s string) string { return strings.Map(func(r rune) rune { if unicode.Is(unicode.Mn, r) { @@ -199,7 +191,7 @@ func getOrganizeImportsStringComparer(sort OrganizeImportsSort) func(a, b string return getOrganizeImportsOrdinalStringComparer(true) case OrganizeImportsSortNatural: return getOrganizeImportsNaturalStringComparer(false) - case OrganizeImportsSortNaturalIgnoreCase: + case OrganizeImportsSortNaturalCaseSensitive: return getOrganizeImportsNaturalStringComparer(true) default: return getOrganizeImportsOrdinalStringComparer(false) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 54af678eb68..b881e795932 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -250,7 +250,7 @@ const ( OrganizeImportsSortOrdinal OrganizeImportsSortOrdinalIgnoreCase OrganizeImportsSortNatural - OrganizeImportsSortNaturalIgnoreCase + OrganizeImportsSortNaturalCaseSensitive ) type OrganizeImportsCollation bool @@ -340,8 +340,8 @@ var typeParsers = map[reflect.Type]func(any) any{ return OrganizeImportsSortOrdinalIgnoreCase case "natural": return OrganizeImportsSortNatural - case "naturalignorecase": - return OrganizeImportsSortNaturalIgnoreCase + case "naturalcasesensitive": + return OrganizeImportsSortNaturalCaseSensitive } } return OrganizeImportsSortAuto @@ -425,8 +425,8 @@ var typeSerializers = map[reflect.Type]func(any) any{ return "ordinalIgnoreCase" case OrganizeImportsSortNatural: return "natural" - case OrganizeImportsSortNaturalIgnoreCase: - return "naturalIgnoreCase" + case OrganizeImportsSortNaturalCaseSensitive: + return "naturalCaseSensitive" default: return "auto" } diff --git a/internal/ls/lsutil/utilities_test.go b/internal/ls/lsutil/utilities_test.go index 3b31b848ebb..9563feff76c 100644 --- a/internal/ls/lsutil/utilities_test.go +++ b/internal/ls/lsutil/utilities_test.go @@ -84,20 +84,20 @@ func TestResolveOrganizeImportsSort(t *testing.T) { want: OrganizeImportsSortOrdinal, }, { - name: "unicode maps to natural", + name: "unicode case-sensitive maps to natural case sensitive", preferences: UserPreferences{ OrganizeImportsCollation: OrganizeImportsCollationUnicode, OrganizeImportsIgnoreCase: core.TSFalse, }, - want: OrganizeImportsSortNatural, + want: OrganizeImportsSortNaturalCaseSensitive, }, { - name: "unicode ignore case maps to natural ignore case", + name: "unicode ignore case maps to natural", preferences: UserPreferences{ OrganizeImportsCollation: OrganizeImportsCollationUnicode, OrganizeImportsIgnoreCase: core.TSTrue, }, - want: OrganizeImportsSortNaturalIgnoreCase, + want: OrganizeImportsSortNatural, }, { name: "ordinal ignore case maps to ordinal ignore case", @@ -128,3 +128,60 @@ func TestResolveOrganizeImportsSort(t *testing.T) { }) } } + +func TestCompareOrganizeImportsNaturalStrings(t *testing.T) { + t.Parallel() + + comparer := getOrganizeImportsStringComparer(OrganizeImportsSortNatural) + tests := []struct { + name string + a string + b string + want int + }{ + { + name: "numeric runs sort by numeric value", + a: "a2", + b: "a100", + want: -1, + }, + { + name: "accents are folded for primary comparison", + a: "À", + b: "B", + want: -1, + }, + { + name: "raw comparison breaks accent ties", + a: "A", + b: "À", + want: -1, + }, + { + name: "hyphen sorts before slash like Intl.Collator fallback", + a: "app-init", + b: "app/app", + want: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := cmpSign(comparer(tt.a, tt.b)); got != tt.want { + t.Fatalf("comparer(%q, %q) = %v, want sign %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func cmpSign(value int) int { + switch { + case value < 0: + return -1 + case value > 0: + return 1 + default: + return 0 + } +} From d34ae8c1f57553628dbd43abfa52e7eeecf5e538 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:37:27 -0700 Subject: [PATCH 3/6] naming --- internal/fourslash/_scripts/convertFourslash.mts | 4 ++-- .../tests/manual/organizeImportsPathsUnicode1_test.go | 2 +- .../tests/manual/organizeImportsPathsUnicode2_test.go | 2 +- .../tests/manual/organizeImportsPathsUnicode3_test.go | 2 +- .../tests/manual/organizeImportsPathsUnicode4_test.go | 2 +- .../tests/manual/organizeImportsUnicode1_test.go | 2 +- .../tests/manual/organizeImportsUnicode2_test.go | 2 +- .../tests/manual/organizeImportsUnicode3_test.go | 2 +- .../tests/manual/organizeImportsUnicode4_test.go | 2 +- internal/ls/lsutil/organizeimports.go | 8 ++++---- internal/ls/lsutil/userpreferences.go | 10 +++++----- internal/ls/lsutil/utilities_test.go | 10 +++++----- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index bd91f4d848c..ab9d9596254 100755 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -2710,8 +2710,8 @@ function parseOrganizeImportsArgs(args: readonly ts.Expression[]): [VerifyOrgani case "natural": prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNatural`); break; - case "naturalCaseSensitive": - prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNaturalCaseSensitive`); + case "naturalIgnoreCase": + prefsFields.push(`${goFieldName}: lsutil.OrganizeImportsSortNaturalIgnoreCase`); break; default: throw new Error(`Unsupported value for organizeImportsSort: ${propValue.text}`); diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go index c3b71f39b01..12930cac53c 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go @@ -41,7 +41,7 @@ import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go index ef57ce6b37f..c93382f5356 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go @@ -38,7 +38,7 @@ import * as a100 from "./a100"; console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go index 829a7e6c281..144fe28dcaa 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go @@ -38,7 +38,7 @@ import * as B from "./B"; console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go index 951364739f7..ca1f869daa1 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go @@ -41,7 +41,7 @@ import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go index aef695338bd..d7521c9c8d4 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go @@ -47,7 +47,7 @@ console.log(_aB, _Ab, aB, Ab);`, console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go index 380395c307d..624b81bf1c6 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go @@ -44,7 +44,7 @@ console.log(a1, a2, a100);`, console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go index 8ea458e5a62..c28d6c4c48a 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go @@ -44,7 +44,7 @@ console.log(A, À, B);`, console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go index 3ac31bd4bc1..69d07f54c96 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go @@ -47,7 +47,7 @@ console.log(_aB, _Ab, aB, Ab);`, console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, }, ) } diff --git a/internal/ls/lsutil/organizeimports.go b/internal/ls/lsutil/organizeimports.go index de033a5645b..086eaaa86d2 100644 --- a/internal/ls/lsutil/organizeimports.go +++ b/internal/ls/lsutil/organizeimports.go @@ -55,9 +55,9 @@ func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode { if preferences.OrganizeImportsIgnoreCase.IsTrue() { - return OrganizeImportsSortNatural + return OrganizeImportsSortNaturalIgnoreCase } - return OrganizeImportsSortNaturalCaseSensitive + return OrganizeImportsSortNatural } switch preferences.OrganizeImportsIgnoreCase { @@ -190,9 +190,9 @@ func getOrganizeImportsStringComparer(sort OrganizeImportsSort) func(a, b string case OrganizeImportsSortOrdinalIgnoreCase: return getOrganizeImportsOrdinalStringComparer(true) case OrganizeImportsSortNatural: - return getOrganizeImportsNaturalStringComparer(false) - case OrganizeImportsSortNaturalCaseSensitive: return getOrganizeImportsNaturalStringComparer(true) + case OrganizeImportsSortNaturalIgnoreCase: + return getOrganizeImportsNaturalStringComparer(false) default: return getOrganizeImportsOrdinalStringComparer(false) } diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index b881e795932..54af678eb68 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -250,7 +250,7 @@ const ( OrganizeImportsSortOrdinal OrganizeImportsSortOrdinalIgnoreCase OrganizeImportsSortNatural - OrganizeImportsSortNaturalCaseSensitive + OrganizeImportsSortNaturalIgnoreCase ) type OrganizeImportsCollation bool @@ -340,8 +340,8 @@ var typeParsers = map[reflect.Type]func(any) any{ return OrganizeImportsSortOrdinalIgnoreCase case "natural": return OrganizeImportsSortNatural - case "naturalcasesensitive": - return OrganizeImportsSortNaturalCaseSensitive + case "naturalignorecase": + return OrganizeImportsSortNaturalIgnoreCase } } return OrganizeImportsSortAuto @@ -425,8 +425,8 @@ var typeSerializers = map[reflect.Type]func(any) any{ return "ordinalIgnoreCase" case OrganizeImportsSortNatural: return "natural" - case OrganizeImportsSortNaturalCaseSensitive: - return "naturalCaseSensitive" + case OrganizeImportsSortNaturalIgnoreCase: + return "naturalIgnoreCase" default: return "auto" } diff --git a/internal/ls/lsutil/utilities_test.go b/internal/ls/lsutil/utilities_test.go index 9563feff76c..86f91ffc727 100644 --- a/internal/ls/lsutil/utilities_test.go +++ b/internal/ls/lsutil/utilities_test.go @@ -84,20 +84,20 @@ func TestResolveOrganizeImportsSort(t *testing.T) { want: OrganizeImportsSortOrdinal, }, { - name: "unicode case-sensitive maps to natural case sensitive", + name: "unicode case-sensitive maps to natural", preferences: UserPreferences{ OrganizeImportsCollation: OrganizeImportsCollationUnicode, OrganizeImportsIgnoreCase: core.TSFalse, }, - want: OrganizeImportsSortNaturalCaseSensitive, + want: OrganizeImportsSortNatural, }, { - name: "unicode ignore case maps to natural", + name: "unicode ignore case maps to natural ignore case", preferences: UserPreferences{ OrganizeImportsCollation: OrganizeImportsCollationUnicode, OrganizeImportsIgnoreCase: core.TSTrue, }, - want: OrganizeImportsSortNatural, + want: OrganizeImportsSortNaturalIgnoreCase, }, { name: "ordinal ignore case maps to ordinal ignore case", @@ -132,7 +132,7 @@ func TestResolveOrganizeImportsSort(t *testing.T) { func TestCompareOrganizeImportsNaturalStrings(t *testing.T) { t.Parallel() - comparer := getOrganizeImportsStringComparer(OrganizeImportsSortNatural) + comparer := getOrganizeImportsStringComparer(OrganizeImportsSortNaturalIgnoreCase) tests := []struct { name string a string From b5b46d6ebab828db41f752c8110f2533d5134f7d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:42:15 -0700 Subject: [PATCH 4/6] Tweaks --- internal/fourslash/_scripts/manualTests.txt | 8 -- .../organizeImportsPathsUnicode1_test.go | 17 ++- .../organizeImportsPathsUnicode2_test.go | 13 ++- .../organizeImportsPathsUnicode3_test.go | 17 ++- .../organizeImportsPathsUnicode4_test.go | 19 +++- .../organizeImportsUnicode1_test.go | 15 ++- .../organizeImportsUnicode2_test.go | 13 ++- .../organizeImportsUnicode3_test.go | 15 ++- .../organizeImportsUnicode4_test.go | 17 ++- internal/ls/lsutil/organizeimports.go | 107 +++++++++++++++--- internal/ls/lsutil/utilities_test.go | 2 +- 11 files changed, 188 insertions(+), 55 deletions(-) rename internal/fourslash/tests/{manual => gen}/organizeImportsPathsUnicode1_test.go (69%) rename internal/fourslash/tests/{manual => gen}/organizeImportsPathsUnicode2_test.go (65%) rename internal/fourslash/tests/{manual => gen}/organizeImportsPathsUnicode3_test.go (61%) rename internal/fourslash/tests/{manual => gen}/organizeImportsPathsUnicode4_test.go (63%) rename internal/fourslash/tests/{manual => gen}/organizeImportsUnicode1_test.go (69%) rename internal/fourslash/tests/{manual => gen}/organizeImportsUnicode2_test.go (63%) rename internal/fourslash/tests/{manual => gen}/organizeImportsUnicode3_test.go (63%) rename internal/fourslash/tests/{manual => gen}/organizeImportsUnicode4_test.go (63%) diff --git a/internal/fourslash/_scripts/manualTests.txt b/internal/fourslash/_scripts/manualTests.txt index 86d4b2e970a..05618afd339 100644 --- a/internal/fourslash/_scripts/manualTests.txt +++ b/internal/fourslash/_scripts/manualTests.txt @@ -110,14 +110,6 @@ navto_excludeLib3 navto_excludeLib4 navto_serverExcludeLib nodeModulesImportCompletions1 -organizeImportsPathsUnicode1 -organizeImportsPathsUnicode2 -organizeImportsPathsUnicode3 -organizeImportsPathsUnicode4 -organizeImportsUnicode1 -organizeImportsUnicode2 -organizeImportsUnicode3 -organizeImportsUnicode4 outliningForNonCompleteInterfaceDeclaration outliningHintSpansForFunction overloadOnConstCallSignature diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go b/internal/fourslash/tests/gen/organizeImportsPathsUnicode1_test.go similarity index 69% rename from internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go rename to internal/fourslash/tests/gen/organizeImportsPathsUnicode1_test.go index 12930cac53c..d16daa05a88 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode1_test.go +++ b/internal/fourslash/tests/gen/organizeImportsPathsUnicode1_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsPathsUnicode1" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsPathsUnicode1(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as Ab from "./Ab"; @@ -29,19 +34,21 @@ import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationOrdinal, }, ) f.VerifyOrganizeImports(t, - `import * as _Ab from "./_Ab"; -import * as _aB from "./_aB"; -import * as Ab from "./Ab"; + `import * as _aB from "./_aB"; +import * as _Ab from "./_Ab"; import * as aB from "./aB"; +import * as Ab from "./Ab"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go b/internal/fourslash/tests/gen/organizeImportsPathsUnicode2_test.go similarity index 65% rename from internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go rename to internal/fourslash/tests/gen/organizeImportsPathsUnicode2_test.go index c93382f5356..3d1002cd42a 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode2_test.go +++ b/internal/fourslash/tests/gen/organizeImportsPathsUnicode2_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsPathsUnicode2" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsPathsUnicode2(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as a2 from "./a2"; @@ -27,7 +32,9 @@ import * as a2 from "./a2"; console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsNumericCollation: core.TSFalse, }, ) f.VerifyOrganizeImports(t, @@ -38,7 +45,9 @@ import * as a100 from "./a100"; console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsNumericCollation: core.TSTrue, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go b/internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go similarity index 61% rename from internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go rename to internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go index 144fe28dcaa..2f9260c263b 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go +++ b/internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsPathsUnicode3" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsPathsUnicode3(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as B from "./B"; @@ -20,14 +25,16 @@ console.log(A, À, B);` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.VerifyOrganizeImports(t, - `import * as A from "./A"; + `import * as À from "./À"; +import * as A from "./A"; import * as B from "./B"; -import * as À from "./À"; console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsAccentCollation: core.TSFalse, }, ) f.VerifyOrganizeImports(t, @@ -38,7 +45,9 @@ import * as B from "./B"; console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsAccentCollation: core.TSTrue, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go b/internal/fourslash/tests/gen/organizeImportsPathsUnicode4_test.go similarity index 63% rename from internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go rename to internal/fourslash/tests/gen/organizeImportsPathsUnicode4_test.go index ca1f869daa1..a9cf7a9ad7b 100644 --- a/internal/fourslash/tests/manual/organizeImportsPathsUnicode4_test.go +++ b/internal/fourslash/tests/gen/organizeImportsPathsUnicode4_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsPathsUnicode4" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsPathsUnicode4(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as Ab from "./Ab"; @@ -29,19 +34,23 @@ import * as aB from "./aB"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstUpper, }, ) f.VerifyOrganizeImports(t, - `import * as _Ab from "./_Ab"; -import * as _aB from "./_aB"; -import * as Ab from "./Ab"; + `import * as _aB from "./_aB"; +import * as _Ab from "./_Ab"; import * as aB from "./aB"; +import * as Ab from "./Ab"; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstLower, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go b/internal/fourslash/tests/gen/organizeImportsUnicode1_test.go similarity index 69% rename from internal/fourslash/tests/manual/organizeImportsUnicode1_test.go rename to internal/fourslash/tests/gen/organizeImportsUnicode1_test.go index d7521c9c8d4..f5502d932f7 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode1_test.go +++ b/internal/fourslash/tests/gen/organizeImportsUnicode1_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsUnicode1" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsUnicode1(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -33,21 +38,23 @@ console.log(_aB, _Ab, aB, Ab);` console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationOrdinal, }, ) f.VerifyOrganizeImports(t, `import { - _Ab, _aB, - Ab, + _Ab, aB, + Ab, } from './foo'; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go b/internal/fourslash/tests/gen/organizeImportsUnicode2_test.go similarity index 63% rename from internal/fourslash/tests/manual/organizeImportsUnicode2_test.go rename to internal/fourslash/tests/gen/organizeImportsUnicode2_test.go index 624b81bf1c6..243353b0c46 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode2_test.go +++ b/internal/fourslash/tests/gen/organizeImportsUnicode2_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsUnicode2" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsUnicode2(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -31,7 +36,9 @@ console.log(a1, a2, a100);` console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsNumericCollation: core.TSFalse, }, ) f.VerifyOrganizeImports(t, @@ -44,7 +51,9 @@ console.log(a1, a2, a100);`, console.log(a1, a2, a100);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsNumericCollation: core.TSTrue, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go b/internal/fourslash/tests/gen/organizeImportsUnicode3_test.go similarity index 63% rename from internal/fourslash/tests/manual/organizeImportsUnicode3_test.go rename to internal/fourslash/tests/gen/organizeImportsUnicode3_test.go index c28d6c4c48a..97975f81edf 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go +++ b/internal/fourslash/tests/gen/organizeImportsUnicode3_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsUnicode3" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsUnicode3(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -23,15 +28,17 @@ console.log(A, À, B);` defer done() f.VerifyOrganizeImports(t, `import { + À, A, B, - À, } from './foo'; console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinal, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsAccentCollation: core.TSFalse, }, ) f.VerifyOrganizeImports(t, @@ -44,7 +51,9 @@ console.log(A, À, B);`, console.log(A, À, B);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsAccentCollation: core.TSTrue, }, ) } diff --git a/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go b/internal/fourslash/tests/gen/organizeImportsUnicode4_test.go similarity index 63% rename from internal/fourslash/tests/manual/organizeImportsUnicode4_test.go rename to internal/fourslash/tests/gen/organizeImportsUnicode4_test.go index 69d07f54c96..02c4c495b81 100644 --- a/internal/fourslash/tests/manual/organizeImportsUnicode4_test.go +++ b/internal/fourslash/tests/gen/organizeImportsUnicode4_test.go @@ -1,8 +1,12 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual organizeImportsUnicode4" + package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -10,6 +14,7 @@ import ( ) func TestOrganizeImportsUnicode4(t *testing.T) { + fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -33,21 +38,25 @@ console.log(_aB, _Ab, aB, Ab);` console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNatural, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstUpper, }, ) f.VerifyOrganizeImports(t, `import { - _Ab, _aB, - Ab, + _Ab, aB, + Ab, } from './foo'; console.log(_aB, _Ab, aB, Ab);`, lsproto.CodeActionKindSourceOrganizeImports, &lsutil.UserPreferences{ - OrganizeImportsSort: lsutil.OrganizeImportsSortNaturalIgnoreCase, + OrganizeImportsIgnoreCase: core.TSFalse, + OrganizeImportsCollation: lsutil.OrganizeImportsCollationUnicode, + OrganizeImportsCaseFirst: lsutil.OrganizeImportsCaseFirstLower, }, ) } diff --git a/internal/ls/lsutil/organizeimports.go b/internal/ls/lsutil/organizeimports.go index 086eaaa86d2..1e5fb156519 100644 --- a/internal/ls/lsutil/organizeimports.go +++ b/internal/ls/lsutil/organizeimports.go @@ -14,11 +14,6 @@ import ( "golang.org/x/text/unicode/norm" ) -var organizeImportsAutoComparers = []func(a, b string) int{ - getOrganizeImportsOrdinalStringComparer(true), - getOrganizeImportsOrdinalStringComparer(false), -} - // FilterImportDeclarations filters out non-import declarations from a list of statements. func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement { return core.Filter(statements, func(stmt *ast.Statement) bool { @@ -28,11 +23,15 @@ func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement { // GetDetectionLists returns the lists of comparers and type orders to test for organize imports detection. func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []OrganizeImportsTypeOrder) { - sort := ResolveOrganizeImportsSort(preferences) - if sort != OrganizeImportsSortAuto { - comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(sort)} + if preferences.OrganizeImportsSort != OrganizeImportsSortAuto { + comparersToTest = []func(a, b string) int{getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)} + } else if !preferences.OrganizeImportsIgnoreCase.IsUnknown() { + comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(preferences, preferences.OrganizeImportsIgnoreCase.IsTrue())} } else { - comparersToTest = organizeImportsAutoComparers + comparersToTest = []func(a, b string) int{ + getOrganizeImportsStringComparer(preferences, true), + getOrganizeImportsStringComparer(preferences, false), + } } if preferences.OrganizeImportsTypeOrder != OrganizeImportsTypeOrderAuto { @@ -83,6 +82,16 @@ func getOrganizeImportsNaturalStringComparer(caseSensitive bool) func(a, b strin } } +func getOrganizeImportsUnicodeStringComparer(ignoreCase bool, preferences UserPreferences) func(a, b string) int { + caseFirst := preferences.OrganizeImportsCaseFirst + numeric := preferences.OrganizeImportsNumericCollation.IsTrue() + accents := !preferences.OrganizeImportsAccentCollation.IsFalse() + + return func(a, b string) int { + return compareOrganizeImportsUnicodeStrings(a, b, ignoreCase, caseFirst, numeric, accents) + } +} + func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool) int { if cmp := compareStringsNumeric(naturalCollationKey(a), naturalCollationKey(b)); cmp != 0 { return cmp @@ -97,6 +106,26 @@ func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool return strings.Compare(a, b) } +func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, caseFirst OrganizeImportsCaseFirst, numeric bool, accents bool) int { + if cmp := compareOrganizeImportsUnicodeKeys(naturalCollationKey(a), naturalCollationKey(b), numeric); cmp != 0 { + return cmp + } + + if accents { + if cmp := compareOrganizeImportsUnicodeKeys(strings.ToLower(a), strings.ToLower(b), numeric); cmp != 0 { + return cmp + } + } + + if !ignoreCase { + if cmp := compareOrganizeImportsCase(a, b, caseFirst); cmp != 0 { + return cmp + } + } + + return 0 +} + func naturalCollationKey(s string) string { return strings.ToLower(removeDiacritics(s)) } @@ -110,6 +139,13 @@ func removeDiacritics(s string) string { }, norm.NFD.String(s)) } +func compareOrganizeImportsUnicodeKeys(a string, b string, numeric bool) int { + if numeric { + return compareStringsNumeric(a, b) + } + return strings.Compare(a, b) +} + func compareStringsNumeric(a string, b string) int { for len(a) > 0 && len(b) > 0 { if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) { @@ -167,6 +203,10 @@ func compareNumericText(a string, b string) int { } func compareOrganizeImportsCaseUpperFirst(a string, b string) int { + return compareOrganizeImportsCase(a, b, OrganizeImportsCaseFirstUpper) +} + +func compareOrganizeImportsCase(a string, b string, caseFirst OrganizeImportsCaseFirst) int { aRunes := []rune(a) bRunes := []rune(b) minLen := min(len(aRunes), len(bRunes)) @@ -175,17 +215,30 @@ func compareOrganizeImportsCaseUpperFirst(a string, b string) int { aUpper := unicode.IsUpper(aRunes[i]) bUpper := unicode.IsUpper(bRunes[i]) if aUpper != bUpper { - if aUpper { + switch caseFirst { + case OrganizeImportsCaseFirstUpper: + if aUpper { + return -1 + } + return 1 + case OrganizeImportsCaseFirstLower: + if !aUpper { + return -1 + } + return 1 + default: + if aUpper { + return 1 + } return -1 } - return 1 } } return cmp.Compare(len(aRunes), len(bRunes)) } -func getOrganizeImportsStringComparer(sort OrganizeImportsSort) func(a, b string) int { +func getOrganizeImportsPresetStringComparer(sort OrganizeImportsSort) func(a, b string) int { switch sort { case OrganizeImportsSortOrdinalIgnoreCase: return getOrganizeImportsOrdinalStringComparer(true) @@ -198,6 +251,16 @@ func getOrganizeImportsStringComparer(sort OrganizeImportsSort) func(a, b string } } +func getOrganizeImportsStringComparer(preferences UserPreferences, ignoreCase bool) func(a, b string) int { + if preferences.OrganizeImportsSort != OrganizeImportsSortAuto { + return getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort) + } + if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode { + return getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) + } + return getOrganizeImportsOrdinalStringComparer(ignoreCase) +} + func getModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression { switch declaration.Kind { case ast.KindImportEqualsDeclaration: @@ -329,7 +392,11 @@ func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a // GetNamedImportSpecifierComparer returns a comparer function for sorting import specifiers. func GetNamedImportSpecifierComparer(preferences UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int { if comparer == nil { - comparer = getOrganizeImportsStringComparer(ResolveOrganizeImportsSort(preferences)) + ignoreCase := false + if !preferences.OrganizeImportsIgnoreCase.IsUnknown() { + ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue() + } + comparer = getOrganizeImportsStringComparer(preferences, ignoreCase) } return func(s1, s2 *ast.Node) int { return compareImportOrExportSpecifiers(s1, s2, comparer, preferences) @@ -357,11 +424,17 @@ func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.St } func getComparers(preferences UserPreferences) []func(a string, b string) int { - sort := ResolveOrganizeImportsSort(preferences) - if sort != OrganizeImportsSortAuto { - return []func(a, b string) int{getOrganizeImportsStringComparer(sort)} + if preferences.OrganizeImportsSort != OrganizeImportsSortAuto || !preferences.OrganizeImportsIgnoreCase.IsUnknown() { + ignoreCase := false + if !preferences.OrganizeImportsIgnoreCase.IsUnknown() { + ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue() + } + return []func(a, b string) int{getOrganizeImportsStringComparer(preferences, ignoreCase)} + } + return []func(a, b string) int{ + getOrganizeImportsStringComparer(preferences, true), + getOrganizeImportsStringComparer(preferences, false), } - return organizeImportsAutoComparers } type namedImportSortResult struct { diff --git a/internal/ls/lsutil/utilities_test.go b/internal/ls/lsutil/utilities_test.go index 86f91ffc727..f93df4c8186 100644 --- a/internal/ls/lsutil/utilities_test.go +++ b/internal/ls/lsutil/utilities_test.go @@ -132,7 +132,7 @@ func TestResolveOrganizeImportsSort(t *testing.T) { func TestCompareOrganizeImportsNaturalStrings(t *testing.T) { t.Parallel() - comparer := getOrganizeImportsStringComparer(OrganizeImportsSortNaturalIgnoreCase) + comparer := getOrganizeImportsPresetStringComparer(OrganizeImportsSortNaturalIgnoreCase) tests := []struct { name string a string From 97fec50110e57c859c9ff7e4883b2396adfe4f26 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:53:26 -0700 Subject: [PATCH 5/6] PR feedback --- internal/ls/lsutil/organizeimports.go | 18 ++++++++++++++---- internal/ls/lsutil/userpreferences.go | 16 ++++++---------- internal/ls/lsutil/utilities_test.go | 13 +++++++++++++ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/internal/ls/lsutil/organizeimports.go b/internal/ls/lsutil/organizeimports.go index 1e5fb156519..5ef99d9499d 100644 --- a/internal/ls/lsutil/organizeimports.go +++ b/internal/ls/lsutil/organizeimports.go @@ -53,10 +53,14 @@ func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort } if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode { - if preferences.OrganizeImportsIgnoreCase.IsTrue() { + switch preferences.OrganizeImportsIgnoreCase { + case core.TSTrue: return OrganizeImportsSortNaturalIgnoreCase + case core.TSFalse: + return OrganizeImportsSortNatural + default: + return OrganizeImportsSortAuto } - return OrganizeImportsSortNatural } switch preferences.OrganizeImportsIgnoreCase { @@ -123,7 +127,10 @@ func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, c } } - return 0 + if !accents { + return strings.Compare(b, a) + } + return strings.Compare(a, b) } func naturalCollationKey(s string) string { @@ -199,7 +206,10 @@ func compareNumericText(a string, b string) int { if len(aDigits) != len(bDigits) { return cmp.Compare(len(aDigits), len(bDigits)) } - return strings.Compare(aDigits, bDigits) + if cmp := strings.Compare(aDigits, bDigits); cmp != 0 { + return cmp + } + return strings.Compare(a, b) } func compareOrganizeImportsCaseUpperFirst(a string, b string) int { diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 54af678eb68..124f5f1b276 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -92,14 +92,13 @@ type UserPreferences struct { // Default: TSUnknown ("auto" in strada), will perform detection OrganizeImportsIgnoreCase core.Tristate `raw:"organizeImportsIgnoreCase" config:"preferences.organizeImports.caseSensitivity"` // !!! // Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their - // code points, or via "unicode" collation (via the Unicode Collation Algorithm (https://unicode.org/reports/tr10/#Scope)) - // - // using rules associated with the locale specified in organizeImportsCollationLocale. + // code points, or via "unicode" natural sorting. This implementation is locale-agnostic and approximates the practical + // import-sorting behavior rather than the full Unicode Collation Algorithm. // // Default: Ordinal OrganizeImportsCollation OrganizeImportsCollation `raw:"organizeImportsCollation" config:"preferences.organizeImports.unicodeCollation"` // !!! - // Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant - // for the sake of consistent sorting. Use `"auto"` to use the detected UI locale. + // Indicates the locale to use for "unicode" collation in legacy clients. This is accepted for compatibility, but + // currently ignored because organize-import sorting is deterministic and locale-agnostic. // // This preference is ignored if organizeImportsCollation is not `unicode`. // @@ -112,16 +111,13 @@ type UserPreferences struct { // // Default: `false` OrganizeImportsNumericCollation core.Tristate `raw:"organizeImportsNumericCollation" config:"preferences.organizeImports.numericCollation"` // !!! - // Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When - // `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified - // in organizeImportsCollationLocale. + // Indicates whether accents and other diacritic marks are considered unequal for the purpose of sorting. // // This preference is ignored if organizeImportsCollation is not `unicode`. // // Default: `true` OrganizeImportsAccentCollation core.Tristate `raw:"organizeImportsAccentCollation" config:"preferences.organizeImports.accentCollation"` // !!! - // Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale - // specified in organizeImportsCollationLocale is used. + // Indicates whether upper case or lower case should sort first. // // This permission is ignored if: // - organizeImportsCollation is not `unicode` diff --git a/internal/ls/lsutil/utilities_test.go b/internal/ls/lsutil/utilities_test.go index f93df4c8186..1f5b67426c3 100644 --- a/internal/ls/lsutil/utilities_test.go +++ b/internal/ls/lsutil/utilities_test.go @@ -99,6 +99,13 @@ func TestResolveOrganizeImportsSort(t *testing.T) { }, want: OrganizeImportsSortNaturalIgnoreCase, }, + { + name: "unicode unknown case sensitivity stays auto for detection", + preferences: UserPreferences{ + OrganizeImportsCollation: OrganizeImportsCollationUnicode, + }, + want: OrganizeImportsSortAuto, + }, { name: "ordinal ignore case maps to ordinal ignore case", preferences: UserPreferences{ @@ -145,6 +152,12 @@ func TestCompareOrganizeImportsNaturalStrings(t *testing.T) { b: "a100", want: -1, }, + { + name: "numeric runs with equal value use raw tie break", + a: "a02", + b: "a2", + want: -1, + }, { name: "accents are folded for primary comparison", a: "À", From 499fa23ecb7701b6038b8e055f863c933ce88326 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:41:17 -0700 Subject: [PATCH 6/6] Fix accent behavior, rip compat --- internal/fourslash/_scripts/manualTests.txt | 2 ++ .../{gen => manual}/organizeImportsPathsUnicode3_test.go | 8 ++------ .../tests/{gen => manual}/organizeImportsUnicode3_test.go | 6 +----- internal/ls/lsutil/organizeimports.go | 3 --- 4 files changed, 5 insertions(+), 14 deletions(-) rename internal/fourslash/tests/{gen => manual}/organizeImportsPathsUnicode3_test.go (86%) rename internal/fourslash/tests/{gen => manual}/organizeImportsUnicode3_test.go (89%) diff --git a/internal/fourslash/_scripts/manualTests.txt b/internal/fourslash/_scripts/manualTests.txt index 05618afd339..31d174381bb 100644 --- a/internal/fourslash/_scripts/manualTests.txt +++ b/internal/fourslash/_scripts/manualTests.txt @@ -110,6 +110,8 @@ navto_excludeLib3 navto_excludeLib4 navto_serverExcludeLib nodeModulesImportCompletions1 +organizeImportsPathsUnicode3 +organizeImportsUnicode3 outliningForNonCompleteInterfaceDeclaration outliningHintSpansForFunction overloadOnConstCallSignature diff --git a/internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go b/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go similarity index 86% rename from internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go rename to internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go index 2f9260c263b..41f38684401 100644 --- a/internal/fourslash/tests/gen/organizeImportsPathsUnicode3_test.go +++ b/internal/fourslash/tests/manual/organizeImportsPathsUnicode3_test.go @@ -1,6 +1,3 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsPathsUnicode3" - package fourslash_test import ( @@ -14,7 +11,6 @@ import ( ) func TestOrganizeImportsPathsUnicode3(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import * as B from "./B"; @@ -25,8 +21,8 @@ console.log(A, À, B);` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.VerifyOrganizeImports(t, - `import * as À from "./À"; -import * as A from "./A"; + `import * as A from "./A"; +import * as À from "./À"; import * as B from "./B"; console.log(A, À, B);`, diff --git a/internal/fourslash/tests/gen/organizeImportsUnicode3_test.go b/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go similarity index 89% rename from internal/fourslash/tests/gen/organizeImportsUnicode3_test.go rename to internal/fourslash/tests/manual/organizeImportsUnicode3_test.go index 97975f81edf..ae1ce80db39 100644 --- a/internal/fourslash/tests/gen/organizeImportsUnicode3_test.go +++ b/internal/fourslash/tests/manual/organizeImportsUnicode3_test.go @@ -1,6 +1,3 @@ -// Code generated by convertFourslash; DO NOT EDIT. -// To modify this test, run "npm run makemanual organizeImportsUnicode3" - package fourslash_test import ( @@ -14,7 +11,6 @@ import ( ) func TestOrganizeImportsUnicode3(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `import { @@ -28,8 +24,8 @@ console.log(A, À, B);` defer done() f.VerifyOrganizeImports(t, `import { - À, A, + À, B, } from './foo'; diff --git a/internal/ls/lsutil/organizeimports.go b/internal/ls/lsutil/organizeimports.go index 5ef99d9499d..08176faabd2 100644 --- a/internal/ls/lsutil/organizeimports.go +++ b/internal/ls/lsutil/organizeimports.go @@ -127,9 +127,6 @@ func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, c } } - if !accents { - return strings.Compare(b, a) - } return strings.Compare(a, b) }