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 excludedmaster
parent
2599da8541
commit
1cc8e62da3
|
@ -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 @@ interface ProjectService {
|
|||
onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
|
||||
onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
|
||||
onDocumentLinks(params: DocumentLinkParams): DocumentLink[]
|
||||
sortClassLists(classLists: string[]): string[]
|
||||
}
|
||||
|
||||
type ProjectConfig = {
|
||||
|
@ -533,6 +534,7 @@ async function createProjectService(
|
|||
state.enabled = false
|
||||
refreshDiagnostics()
|
||||
updateCapabilities()
|
||||
connection.sendNotification('@/tailwindCSS/projectReset')
|
||||
}
|
||||
|
||||
async function tryInit() {
|
||||
|
@ -541,6 +543,7 @@ async function createProjectService(
|
|||
}
|
||||
try {
|
||||
await init()
|
||||
connection.sendNotification('@/tailwindCSS/projectInitialized')
|
||||
} catch (error) {
|
||||
resetState()
|
||||
showError(connection, error)
|
||||
|
@ -1270,7 +1273,68 @@ async function createProjectService(
|
|||
.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 {
|
||||
|
@ -2150,6 +2214,39 @@ class TW {
|
|||
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 @@ class TW {
|
|||
}
|
||||
|
||||
dispose(): void {
|
||||
connection.sendNotification('@/tailwindCSS/projectsDestroyed')
|
||||
for (let [, project] of this.projects) {
|
||||
project.dispose()
|
||||
}
|
||||
|
|
|
@ -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'] })
|
||||
})
|
||||
})
|
|
@ -54,6 +54,16 @@ By default VS Code will not trigger completions when editing "string" content, f
|
|||
}
|
||||
```
|
||||
|
||||
## 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`
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
SnippetString,
|
||||
TextEdit,
|
||||
TextEditorSelectionChangeKind,
|
||||
Selection,
|
||||
} from 'vscode'
|
||||
import {
|
||||
LanguageClient,
|
||||
|
@ -38,6 +39,7 @@ import {
|
|||
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'
|
||||
|
@ -123,6 +125,71 @@ async function fileContainsAtConfig(uri: Uri) {
|
|||
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) {
|
||||
let module = context.asAbsolutePath(path.join('dist', 'server.js'))
|
||||
let prod = path.join('dist', 'tailwindServer.js')
|
||||
|
@ -142,6 +209,72 @@ export async function activate(context: ExtensionContext) {
|
|||
})
|
||||
)
|
||||
|
||||
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()
|
||||
})
|
||||
)
|
||||
|
||||
// context.subscriptions.push(
|
||||
// commands.registerCommand(
|
||||
// 'tailwindCSS.onInsertArbitraryVariantSnippet',
|
||||
|
@ -620,6 +753,16 @@ export async function activate(context: ExtensionContext) {
|
|||
|
||||
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[]>(
|
||||
'vscode.executeDocumentSymbolProvider',
|
||||
|
|
Loading…
Reference in New Issue