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 assert from 'assert'
|
||||||
// import postcssLoadConfig from 'postcss-load-config'
|
// import postcssLoadConfig from 'postcss-load-config'
|
||||||
import * as parcel from './watcher/index.js'
|
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 { getColor } from 'tailwindcss-language-service/src/util/color'
|
||||||
import * as culori from 'culori'
|
import * as culori from 'culori'
|
||||||
import namedColors from 'color-name'
|
import namedColors from 'color-name'
|
||||||
|
@ -195,6 +195,7 @@ interface ProjectService {
|
||||||
onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
|
onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
|
||||||
onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
|
onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
|
||||||
onDocumentLinks(params: DocumentLinkParams): DocumentLink[]
|
onDocumentLinks(params: DocumentLinkParams): DocumentLink[]
|
||||||
|
sortClassLists(classLists: string[]): string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectConfig = {
|
type ProjectConfig = {
|
||||||
|
@ -533,6 +534,7 @@ async function createProjectService(
|
||||||
state.enabled = false
|
state.enabled = false
|
||||||
refreshDiagnostics()
|
refreshDiagnostics()
|
||||||
updateCapabilities()
|
updateCapabilities()
|
||||||
|
connection.sendNotification('@/tailwindCSS/projectReset')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryInit() {
|
async function tryInit() {
|
||||||
|
@ -541,6 +543,7 @@ async function createProjectService(
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await init()
|
await init()
|
||||||
|
connection.sendNotification('@/tailwindCSS/projectInitialized')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resetState()
|
resetState()
|
||||||
showError(connection, error)
|
showError(connection, error)
|
||||||
|
@ -1270,7 +1273,68 @@ async function createProjectService(
|
||||||
.replace(/\d+\.\d+(%?)/g, (value, suffix) => `${Math.round(parseFloat(value))}${suffix}`),
|
.replace(/\d+\.\d+(%?)/g, (value, suffix) => `${Math.round(parseFloat(value))}${suffix}`),
|
||||||
].map((value) => ({ label: `${prefix}-[${value}]` }))
|
].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 {
|
function isObject(value: unknown): boolean {
|
||||||
|
@ -2150,6 +2214,39 @@ class TW {
|
||||||
this.connection.onColorPresentation(this.onColorPresentation.bind(this))
|
this.connection.onColorPresentation(this.onColorPresentation.bind(this))
|
||||||
this.connection.onCodeAction(this.onCodeAction.bind(this))
|
this.connection.onCodeAction(this.onCodeAction.bind(this))
|
||||||
this.connection.onDocumentLinks(this.onDocumentLinks.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() {
|
private updateCapabilities() {
|
||||||
|
@ -2270,6 +2367,7 @@ class TW {
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
|
connection.sendNotification('@/tailwindCSS/projectsDestroyed')
|
||||||
for (let [, project] of this.projects) {
|
for (let [, project] of this.projects) {
|
||||||
project.dispose()
|
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
|
## Extension Settings
|
||||||
|
|
||||||
### `tailwindCSS.includeLanguages`
|
### `tailwindCSS.includeLanguages`
|
||||||
|
|
|
@ -60,6 +60,11 @@
|
||||||
"command": "tailwindCSS.showOutput",
|
"command": "tailwindCSS.showOutput",
|
||||||
"title": "Tailwind CSS: Show Output",
|
"title": "Tailwind CSS: Show Output",
|
||||||
"enablement": "tailwindCSS.hasOutputChannel"
|
"enablement": "tailwindCSS.hasOutputChannel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "tailwindCSS.sortSelection",
|
||||||
|
"title": "Tailwind CSS: Sort Selection",
|
||||||
|
"enablement": "editorHasSelection && resourceScheme == file && tailwindCSS.activeTextEditorSupportsClassSorting"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grammars": [
|
"grammars": [
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
SnippetString,
|
SnippetString,
|
||||||
TextEdit,
|
TextEdit,
|
||||||
TextEditorSelectionChangeKind,
|
TextEditorSelectionChangeKind,
|
||||||
|
Selection,
|
||||||
} from 'vscode'
|
} from 'vscode'
|
||||||
import {
|
import {
|
||||||
LanguageClient,
|
LanguageClient,
|
||||||
|
@ -38,6 +39,7 @@ import {
|
||||||
Disposable,
|
Disposable,
|
||||||
} from 'vscode-languageclient/node'
|
} from 'vscode-languageclient/node'
|
||||||
import { languages as defaultLanguages } from 'tailwindcss-language-service/src/util/languages'
|
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 isObject from 'tailwindcss-language-service/src/util/isObject'
|
||||||
import { dedupe, equal } from 'tailwindcss-language-service/src/util/array'
|
import { dedupe, equal } from 'tailwindcss-language-service/src/util/array'
|
||||||
import namedColors from 'color-name'
|
import namedColors from 'color-name'
|
||||||
|
@ -123,6 +125,71 @@ async function fileContainsAtConfig(uri: Uri) {
|
||||||
return /@config\s*['"]/.test(contents)
|
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) {
|
export async function activate(context: ExtensionContext) {
|
||||||
let module = context.asAbsolutePath(path.join('dist', 'server.js'))
|
let module = context.asAbsolutePath(path.join('dist', 'server.js'))
|
||||||
let prod = path.join('dist', 'tailwindServer.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(
|
// context.subscriptions.push(
|
||||||
// commands.registerCommand(
|
// commands.registerCommand(
|
||||||
// 'tailwindCSS.onInsertArbitraryVariantSnippet',
|
// 'tailwindCSS.onInsertArbitraryVariantSnippet',
|
||||||
|
@ -620,6 +753,16 @@ export async function activate(context: ExtensionContext) {
|
||||||
|
|
||||||
client.onNotification('@/tailwindCSS/clearColors', () => clearColors())
|
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 }) => {
|
client.onRequest('@/tailwindCSS/getDocumentSymbols', async ({ uri }) => {
|
||||||
return commands.executeCommand<SymbolInformation[]>(
|
return commands.executeCommand<SymbolInformation[]>(
|
||||||
'vscode.executeDocumentSymbolProvider',
|
'vscode.executeDocumentSymbolProvider',
|
||||||
|
|
Loading…
Reference in New Issue