Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
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-----
Brad Cornes <hello@bradley.dev>
2 years ago
5 changed files, 271 additions(+), 1 deletions(-)
packages/tailwindcss-language-server/src/server.tspackages/tailwindcss-language-server/tests/commands/commands.test.jspackages/vscode-tailwindcss/README.mdpackages/vscode-tailwindcss/package.jsonpackages/vscode-tailwindcss/src/extension.ts
M packages/tailwindcss-language-server/src/server.tspackages/tailwindcss-language-server/src/server.ts
  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
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,7 +74,7 @@ import { getModuleDependencies } from './util/getModuleDependencies'
 import assert from 'assert'
 // import postcssLoadConfig from 'postcss-load-config'
 import * as parcel from './watcher/index.js'
-import { generateRules } from 'tailwindcss-language-service/src/util/jit'
+import { bigSign } from 'tailwindcss-language-service/src/util/jit'
 import { getColor } from 'tailwindcss-language-service/src/util/color'
 import * as culori from 'culori'
 import namedColors from 'color-name'
@@ -195,6 +195,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 = {
@@ -533,6 +534,7 @@     })
     state.enabled = false
     refreshDiagnostics()
     updateCapabilities()
+    connection.sendNotification('@/tailwindCSS/projectReset')
   }
 
   async function tryInit() {
@@ -541,6 +543,7 @@       return
     }
     try {
       await init()
+      connection.sendNotification('@/tailwindCSS/projectInitialized')
     } catch (error) {
       resetState()
       showError(connection, error)
@@ -1270,9 +1273,70 @@           // round numbers
           .replace(/\d+\.\d+(%?)/g, (value, suffix) => `${Math.round(parseFloat(value))}${suffix}`),
       ].map((value) => ({ label: `${prefix}-[${value}]` }))
     },
+    sortClassLists(classLists: string[]): string[] {
+      if (!state.jit) {
+        return classLists
+      }
+
+      return classLists.map((classList) => {
+        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()
+        }
+
+        let classNamesWithOrder = state.jitContext.getClassOrder
+          ? state.jitContext.getClassOrder(classes)
+          : 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
+}
+
 function isObject(value: unknown): boolean {
   return Object.prototype.toString.call(value) === '[object Object]'
 }
@@ -2150,6 +2214,39 @@     this.connection.onDocumentColor(this.onDocumentColor.bind(this))
     this.connection.onColorPresentation(this.onColorPresentation.bind(this))
     this.connection.onCodeAction(this.onCodeAction.bind(this))
     this.connection.onDocumentLinks(this.onDocumentLinks.bind(this))
+    this.connection.onRequest(this.onRequest.bind(this))
+  }
+
+  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,
+      }
+    }
   }
 
   private updateCapabilities() {
@@ -2270,6 +2367,7 @@     this.connection.listen()
   }
 
   dispose(): void {
+    connection.sendNotification('@/tailwindCSS/projectsDestroyed')
     for (let [, project] of this.projects) {
       project.dispose()
     }
I packages/tailwindcss-language-server/tests/commands/commands.test.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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'] })
+  })
+})
M packages/vscode-tailwindcss/README.mdpackages/vscode-tailwindcss/README.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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`
M packages/vscode-tailwindcss/package.jsonpackages/vscode-tailwindcss/package.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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": [
M packages/vscode-tailwindcss/src/extension.tspackages/vscode-tailwindcss/src/extension.ts
  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[]>(