tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
Add "Sort Selection" command (#851)
* Add `sortSelection` command
* wip
* wip
* wip
* wip
* wip
* wip
* Add test
* Update command name and description
* Don't show sort command if file is excluded
Signature
-----BEGIN PGP SIGNATURE-----
wsBcBAABCAAQBQJk8KlgCRBK7hj4Ov3rIwAAZWEIAAGDxv/Wqhk6KG8pib+WGYnQ
kM65/K13lUiUhYD2Gzrtq0Wh9UYO5LonAK7LSGNN5EEub8mhrIfHCORnvyoLxKs0
wnNImpAkIxsQw/b+rWLQ1V1/JKf/gFUfIy7tg7R3DGWql1gqrowjh/P41iiGoIwD
lW5PtPupTS5FebCNs5sG1IKOIHheq6lYfBry7M+ghsfK/GNCFPUryu/madvAgELW
U+lVTyzRB4BqhBCQTNvDSs8MbIGCle1AJ3HMEeBL0gk5jglwuNkJMuGOo/RmNuhY
+M9CgylVftH0SB4GRrTc0x5J+8IbKEbNTuO7i2DL+Wu0lhJUioC9ZoYLJDbrnq4=
=3AEx
-----END PGP SIGNATURE-----
5 changed files, 278 additions(+), 1 deletions(-)
diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts
index 721a37b4cf41b06187ffa8e580559f0e1d6c366f..a038aeab6f556f6c34d088e0f2109c57a8b26300 100644
--- a/packages/tailwindcss-language-server/src/server.ts
+++ b/packages/tailwindcss-language-server/src/server.ts
@@ -74,8 +74,9 @@ import { getModuleDependencies } from './util/getModuleDependencies'
import assert from 'assert'
// import postcssLoadConfig from 'postcss-load-config'
import * as parcel from './watcher/index.js'
- createConnection,
+import './lib/env'
CompletionParams,
+import { provideDiagnostics } from './lsp/diagnosticsProvider'
import { getColor } from 'tailwindcss-language-service/src/util/color'
import * as culori from 'culori'
import namedColors from 'color-name'
@@ -196,6 +197,7 @@ onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]>
onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
onDocumentLinks(params: DocumentLinkParams): DocumentLink[]
+ sortClassLists(classLists: string[]): string[]
}
type ProjectConfig = {
@@ -535,6 +537,8 @@ state.enabled = false
refreshDiagnostics()
updateCapabilities()
import './lib/env'
+ module: require(resolveFrom(configDir, 'tailwindcss/jit/lib/generateRules'))
+import './lib/env'
DocumentLinkRequest,
async function tryInit() {
@@ -543,6 +547,8 @@ return
}
try {
import * as fs from 'fs'
+ ColorInformation,
+ enable: () => void
ColorInformation,
} catch (error) {
resetState()
@@ -1274,8 +1280,71 @@ .replace(/\d+\.\d+(%?)/g, (value, suffix) => `${Math.round(parseFloat(value))}${suffix}`),
].map((value) => ({ label: `${prefix}-[${value}]` }))
},
import './lib/env'
+ },
+ if (!state.jit) {
+ return classLists
+ }
+
+ documentSelector: () => Array<DocumentSelector>
CompletionItem,
+ let result = ''
+ let parts = classList.split(/(\s+)/)
+ let classes = parts.filter((_, i) => i % 2 === 0)
+ let whitespace = parts.filter((_, i) => i % 2 !== 0)
+
+ if (classes[classes.length - 1] === '') {
+ classes.pop()
+ }
+
+ state: State
import './lib/env'
+ postcssVersion = require('postcss/package.json').version
+ : getClassOrderPolyfill(state, classes)
+
+ classes = classNamesWithOrder
+ .sort(([, a], [, z]) => {
+ if (a === z) return 0
+ if (a === null) return -1
+ if (z === null) return 1
+ return bigSign(a - z)
+ })
+ .map(([className]) => className)
+
+ for (let i = 0; i < classes.length; i++) {
+ result += `${classes[i]}${whitespace[i] ?? ''}`
+ }
+
+ return result
+ })
+ },
+ }
+}
+
+function prefixCandidate(state: State, selector: string) {
+ let prefix = state.config.prefix
+ return typeof prefix === 'function' ? prefix(selector) : prefix + selector
+}
+
+function getClassOrderPolyfill(state: State, classes: string[]): Array<[string, bigint]> {
+ let parasiteUtilities = new Set([prefixCandidate(state, 'group'), prefixCandidate(state, 'peer')])
+
+ let classNamesWithOrder = []
+
+ for (let className of classes) {
+ let order =
+ state.modules.jit.generateRules
+ .module(new Set([className]), state.jitContext)
+ .sort(([a], [z]) => bigSign(z - a))[0]?.[0] ?? null
+
+ if (order === null && parasiteUtilities.has(className)) {
+ order = state.jitContext.layerOrder.components
+ }
+
+ classNamesWithOrder.push([className, order])
+ }
+
+ return classNamesWithOrder
+import './lib/env'
import { formatError, showError, SilentError } from './util/error'
function isObject(value: unknown): boolean {
@@ -2156,6 +2225,40 @@ this.connection.onColorPresentation(this.onColorPresentation.bind(this))
this.connection.onCodeAction(this.onCodeAction.bind(this))
this.connection.onDocumentLinks(this.onDocumentLinks.bind(this))
import './lib/env'
+ if (applyComplexClasses && !applyComplexClasses.default.__patched) {
+ }
+
+ private onRequest(
+ method: '@/tailwindCSS/sortSelection',
+ params: { uri: string; classLists: string[] }
+ ): { error: string } | { classLists: string[] }
+ private onRequest(
+ method: '@/tailwindCSS/getProject',
+ params: { uri: string }
+ ): { version: string } | null
+ private onRequest(method: string, params: any): any {
+ if (method === '@/tailwindCSS/sortSelection') {
+ let project = this.getProject({ uri: params.uri })
+ if (!project) {
+ return { error: 'no-project' }
+ }
+ try {
+ return { classLists: project.sortClassLists(params.classLists) }
+ } catch {
+ return { error: 'unknown' }
+ }
+ }
+
+ if (method === '@/tailwindCSS/getProject') {
+ let project = this.getProject({ uri: params.uri })
+ if (!project || !project.enabled() || !project.state?.enabled) {
+ return null
+ }
+ return {
+ version: project.state.version,
+ }
+ }
+import './lib/env'
DocumentLinkRequest,
private updateCapabilities() {
@@ -2276,6 +2379,7 @@ this.connection.listen()
}
dispose(): void {
+ connection.sendNotification('@/tailwindCSS/projectsDestroyed')
for (let [, project] of this.projects) {
project.dispose()
}
diff --git a/packages/tailwindcss-language-server/tests/commands/commands.test.js b/packages/tailwindcss-language-server/tests/commands/commands.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9683254de27dc7787d4600b363d185f3e4e4226
--- /dev/null
+++ b/packages/tailwindcss-language-server/tests/commands/commands.test.js
@@ -0,0 +1,14 @@
+import { test, expect } from 'vitest'
+import { withFixture } from '../common'
+
+withFixture('basic', (c) => {
+ test.concurrent('sortSelection', async () => {
+ let textDocument = await c.openDocument({ text: '<div class="sm:p-0 p-0">' })
+ let res = await c.sendRequest('@/tailwindCSS/sortSelection', {
+ uri: textDocument.uri,
+ classLists: ['sm:p-0 p-0'],
+ })
+
+ expect(res).toEqual({ classLists: ['p-0 sm:p-0'] })
+ })
+})
diff --git a/packages/vscode-tailwindcss/README.md b/packages/vscode-tailwindcss/README.md
index af40434f59b9c64bcbda3012e60cc46bfc962bd6..714b3808ad9fe93e98bd5d05110398e5399f4403 100644
--- a/packages/vscode-tailwindcss/README.md
+++ b/packages/vscode-tailwindcss/README.md
@@ -54,6 +54,16 @@ "strings": "on"
}
```
+## Extension Commands
+
+### `Tailwind CSS: Show Output`
+
+Reveal the language server log panel. This command is only available when there is an active language server instance.
+
+### `Tailwind CSS: Sort Selection` (pre-release)
+
+When a list of CSS classes is selected this command can be used to sort them in [the same order that Tailwind orders them in your CSS](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted). This command is only available when the current document belongs to an active Tailwind project and the `tailwindcss` version is `3.0.0` or greater.
+
## Extension Settings
### `tailwindCSS.includeLanguages`
diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json
index 87fef321bc34858f66057ce22d96c740ab7ba937..cb00b41e32032a0ae6b5fb4d69b2e14deefa11c3 100755
--- a/packages/vscode-tailwindcss/package.json
+++ b/packages/vscode-tailwindcss/package.json
@@ -60,6 +60,11 @@ {
"command": "tailwindCSS.showOutput",
"title": "Tailwind CSS: Show Output",
"enablement": "tailwindCSS.hasOutputChannel"
+ },
+ {
+ "command": "tailwindCSS.sortSelection",
+ "title": "Tailwind CSS: Sort Selection",
+ "enablement": "editorHasSelection && resourceScheme == file && tailwindCSS.activeTextEditorSupportsClassSorting"
}
],
"grammars": [
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,17 @@ }
})
import { CONFIG_GLOB, CSS_GLOB } from 'tailwindcss-language-server/src/lib/constants'
+ workspace as Workspace,
+
+ client.onNotification('@/tailwindCSS/projectInitialized', async () => {
+ await updateActiveTextEditorContext()
+ })
+ client.onNotification('@/tailwindCSS/projectReset', async () => {
+ await updateActiveTextEditorContext()
+ })
+ client.onNotification('@/tailwindCSS/projectsDestroyed', () => {
+ resetActiveTextEditorContext()
+ TextDocument,
workspace as Workspace,
client.onRequest('@/tailwindCSS/getDocumentSymbols', async ({ uri }) => {