1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts
index ac8951b8b58e4e439b17b523d1987768cf487bfb..9139983dfc2fd21e0ec9b93b9f4f48e826b6be79 100755
--- a/packages/vscode-tailwindcss/src/extension.ts
+++ b/packages/vscode-tailwindcss/src/extension.ts
@@ -27,6 +27,7 @@ ProviderResult,
SnippetString,
TextEdit,
TextEditorSelectionChangeKind,
+ Selection,
} from 'vscode'
import {
LanguageClient,
@@ -38,6 +39,7 @@ RevealOutputChannelOn,
Disposable,
} from 'vscode-languageclient/node'
import { languages as defaultLanguages } from 'tailwindcss-language-service/src/util/languages'
+import * as semver from 'tailwindcss-language-service/src/util/semver'
import isObject from 'tailwindcss-language-service/src/util/isObject'
import { dedupe, equal } from 'tailwindcss-language-service/src/util/array'
import namedColors from 'color-name'
@@ -121,6 +123,71 @@
async function fileContainsAtConfig(uri: Uri) {
let contents = (await Workspace.fs.readFile(uri)).toString()
return /@config\s*['"]/.test(contents)
+}
+
+function selectionsAreEqual(
+ aSelections: readonly Selection[],
+ bSelections: readonly Selection[]
+): boolean {
+ if (aSelections.length !== bSelections.length) {
+ return false
+ }
+ for (let i = 0; i < aSelections.length; i++) {
+ if (!aSelections[i].isEqual(bSelections[i])) {
+ return false
+ }
+ }
+ return true
+}
+
+async function getActiveTextEditorProject(): Promise<{ version: string } | null> {
+ if (clients.size === 0) {
+ return null
+ }
+ let editor = Window.activeTextEditor
+ if (!editor) {
+ return null
+ }
+ let uri = editor.document.uri
+ let folder = Workspace.getWorkspaceFolder(uri)
+ if (!folder) {
+ return null
+ }
+ let client = clients.get(folder.uri.toString())
+ if (!client) {
+ return null
+ }
+ if (isExcluded(uri.fsPath, folder)) {
+ return null
+ }
+ try {
+ let project = await client.sendRequest<{ version: string } | null>('@/tailwindCSS/getProject', {
+ uri: uri.toString(),
+ })
+ return project
+ } catch {
+ return null
+ }
+}
+
+async function activeTextEditorSupportsClassSorting(): Promise<boolean> {
+ let project = await getActiveTextEditorProject()
+ if (!project) {
+ return false
+ }
+ return semver.gte(project.version, '3.0.0')
+}
+
+async function updateActiveTextEditorContext(): Promise<void> {
+ commands.executeCommand(
+ 'setContext',
+ 'tailwindCSS.activeTextEditorSupportsClassSorting',
+ await activeTextEditorSupportsClassSorting()
+ )
+}
+
+function resetActiveTextEditorContext(): void {
+ commands.executeCommand('setContext', 'tailwindCSS.activeTextEditorSupportsClassSorting', false)
}
export async function activate(context: ExtensionContext) {
@@ -139,6 +206,72 @@ commands.registerCommand('tailwindCSS.showOutput', () => {
if (outputChannel) {
outputChannel.show()
}
+ })
+ )
+
+ async function sortSelection(): Promise<void> {
+ let { document, selections } = Window.activeTextEditor
+
+ if (selections.length === 0) {
+ return
+ }
+
+ let initialSelections = selections
+ let folder = Workspace.getWorkspaceFolder(document.uri)
+
+ if (clients.size === 0 || !folder || isExcluded(document.uri.fsPath, folder)) {
+ throw Error(`No active Tailwind project found for file ${document.uri.fsPath}`)
+ }
+
+ let client = clients.get(folder.uri.toString())
+ if (!client) {
+ throw Error(`No active Tailwind project found for file ${document.uri.fsPath}`)
+ }
+
+ let result = await client.sendRequest<{ error: string } | { classLists: string[] }>(
+ '@/tailwindCSS/sortSelection',
+ {
+ uri: document.uri.toString(),
+ classLists: selections.map((selection) => document.getText(selection)),
+ }
+ )
+
+ if (
+ Window.activeTextEditor.document !== document ||
+ !selectionsAreEqual(initialSelections, Window.activeTextEditor.selections)
+ ) {
+ return
+ }
+
+ if ('error' in result) {
+ throw Error(
+ {
+ 'no-project': `No active Tailwind project found for file ${document.uri.fsPath}`,
+ }[result.error] ?? 'An unknown error occurred.'
+ )
+ }
+
+ let sortedClassLists = result.classLists
+ Window.activeTextEditor.edit((builder) => {
+ for (let i = 0; i < selections.length; i++) {
+ builder.replace(selections[i], sortedClassLists[i])
+ }
+ })
+ }
+
+ context.subscriptions.push(
+ commands.registerCommand('tailwindCSS.sortSelection', async () => {
+ try {
+ await sortSelection()
+ } catch (error) {
+ Window.showWarningMessage(`Couldn’t sort Tailwind classes: ${error.message}`)
+ }
+ })
+ )
+
+ context.subscriptions.push(
+ Window.onDidChangeActiveTextEditor(async () => {
+ await updateActiveTextEditorContext()
})
)
@@ -619,6 +752,16 @@ }
})
client.onNotification('@/tailwindCSS/clearColors', () => clearColors())
+
+ client.onNotification('@/tailwindCSS/projectInitialized', async () => {
+ await updateActiveTextEditorContext()
+ })
+ client.onNotification('@/tailwindCSS/projectReset', async () => {
+ await updateActiveTextEditorContext()
+ })
+ client.onNotification('@/tailwindCSS/projectsDestroyed', () => {
+ resetActiveTextEditorContext()
+ })
client.onRequest('@/tailwindCSS/getDocumentSymbols', async ({ uri }) => {
return commands.executeCommand<SymbolInformation[]>(
|