Skip to content

feature: hide/show class attributes #658

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/media/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/vscode-tailwindcss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
"command": "tailwindCSS.showOutput",
"title": "Tailwind CSS: Show Output",
"enablement": "tailwindCSS.hasOutputChannel"
},
{
"command": "tailwindCSS.toggleFoldClassAttributes",
"title": "Tailwind CSS: Toggle Fold Class Attributes"
}
],
"grammars": [
Expand Down Expand Up @@ -274,6 +278,11 @@
"markdownDescription": "Class variants not in the recommended order (applies in [JIT mode](https://tailwindcss.com/docs/just-in-time-mode) only)",
"scope": "language-overridable"
},
"tailwindCSS.experimental.foldClassAttributes": {
"type": "boolean",
"default": true,
"markdownDescription": "Controls whether the editor should auto hide class attributes' values."
},
"tailwindCSS.experimental.classRegex": {
"type": "array",
"scope": "language-overridable"
Expand Down
218 changes: 218 additions & 0 deletions packages/vscode-tailwindcss/src/classFoldingDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import {
window as Window,
workspace as Workspace,
Range,
ExtensionContext,
TextEditorDecorationType,
commands,
languages as Languages,
TextDocument,
FoldingRange,
TextEditor,
ConfigurationTarget,
} from 'vscode'

const TailwindIconPath = 'media/icon.svg'
const FoldClassAttributesConfigId = 'experimental.foldClassAttributes'
const ClassAttributeConfigId = 'classAttributes'

let classRegex: RegExp | undefined

let classAttributeToDecType: Record<string, TextEditorDecorationType> = {}
const unFoldedDecType = Window.createTextEditorDecorationType({})
let multilineFoldDecType: TextEditorDecorationType | undefined

const multilineFoldRanges = new Set<Range>()

export function initClassFoldingDecorator(context: ExtensionContext) {
resetState()

const classAttributes = getConfigValue(ClassAttributeConfigId)
setClassAttributeToDecType(classAttributes, context)
setClassRegex(classAttributes)
}

function resetState() {
Object.values(classAttributeToDecType).forEach((decType) => decType.dispose())
multilineFoldDecType?.dispose()
multilineFoldRanges.forEach((r) => foldingCommand('unfold', r))
}

function setClassAttributeToDecType(classAttributes: string[], context: ExtensionContext) {
const fontSize = Workspace.getConfiguration('editor').fontSize
const margin = 2

classAttributes.forEach((classAttribute) => {
classAttributeToDecType[classAttribute] = Window.createTextEditorDecorationType({
before: {
contentText: classAttribute,
margin: `0 ${fontSize + margin}px 0 0`,
},
after: {
height: `${fontSize}px`,
width: `${fontSize}px`,
contentIconPath: context.asAbsolutePath(TailwindIconPath),
margin: `0 0 0 ${-fontSize}px`,
},
textDecoration: 'none; display:none;',
})
})

multilineFoldDecType = Window.createTextEditorDecorationType({
after: {
height: `${fontSize}px`,
width: `${fontSize}px`,
contentIconPath: context.asAbsolutePath(TailwindIconPath),
margin: `0 0 0 ${margin}px`,
},
textDecoration: 'none; display:none;',
})
}

function setClassRegex(classAttributes: string[]) {
const beforeEqualSign = '(' + classAttributes.join('|') + ')'
const afterEqualSign = '(({(`|))|([\'"`]))((.|\n)*?)(\\2|(\\4)})'
classRegex = new RegExp(beforeEqualSign + '=' + afterEqualSign, 'g')
}

let timeout: NodeJS.Timer | undefined = undefined
export function triggerUpdateDecorations(throttle = false) {
if (timeout) {
clearTimeout(timeout)
timeout = undefined
}
if (throttle) {
timeout = setTimeout(updateDecorations, 100)
} else {
updateDecorations()
}
}

function updateDecorations() {
const foldClassAttributes = getConfigValue(FoldClassAttributesConfigId)
let activeEditor = Window.activeTextEditor
if (!foldClassAttributes || !activeEditor || !classRegex) return

const matchesAndRanges = findMatchesAndRanges(activeEditor)

const unfoldRanges: Range[] = []
updateInlineFolding(activeEditor, matchesAndRanges, unfoldRanges)
updateMultilineFolding(activeEditor, matchesAndRanges, unfoldRanges)

activeEditor.setDecorations(unFoldedDecType, unfoldRanges)
}

function findMatchesAndRanges(activeEditor: TextEditor) {
const matchesAndRanges: [match: RegExpExecArray, range: Range][] = []

const text = activeEditor.document.getText()
let match
while ((match = classRegex.exec(text))) {
if (match && !match[0]) continue
const startPosition = activeEditor.document.positionAt(match.index)
const endPosition = activeEditor.document.positionAt(match.index + match[0].length)
const range = new Range(startPosition, endPosition)

matchesAndRanges.push([match, range])
}
return matchesAndRanges
}

function updateInlineFolding(
activeEditor: TextEditor,
matchesAndRanges: [match: RegExpExecArray, range: Range][],
unfoldRanges: Range[]
) {
const foldDecTypeToRanges = new Map<TextEditorDecorationType, Range[]>()
Object.values(classAttributeToDecType).forEach((decType) => foldDecTypeToRanges.set(decType, []))

for (let [match, range] of matchesAndRanges) {
if (!range.isSingleLine) continue

//Unfold if range is within user selection, accounting for both single or multiple cursors
if (activeEditor.selections.some((s) => range.intersection(s))) {
unfoldRanges.push(range)
} else {
const classAttribute = match[1]
foldDecTypeToRanges.get(classAttributeToDecType[classAttribute]).push(range)
}
}

foldDecTypeToRanges.forEach((ranges, decType) => activeEditor.setDecorations(decType, ranges))
}

function updateMultilineFolding(
activeEditor: TextEditor,
matchesAndRanges: [match: RegExpExecArray, range: Range][],
unfoldRanges: Range[]
) {
const foldRanges = []
for (let [match, range] of matchesAndRanges) {
if (range.isSingleLine) continue

const classAttribute = match[1]
const afterClassAttributePosition = range.start.translate(0, classAttribute.length)
const endOfLinePosition = activeEditor.document.lineAt(afterClassAttributePosition).range.end
const firstLineRange = new Range(afterClassAttributePosition, endOfLinePosition)

if (
activeEditor.selections.some((s) => s.start.line === range.end.line) ||
activeEditor.selections.some((s) => s.start.line === range.start.line) ||
activeEditor.selections.some((s) => range.intersection(s))
) {
unfoldRanges.push(firstLineRange)
foldingCommand('unfold', range)
} else {
foldRanges.push(firstLineRange)
foldingCommand('fold', range)
multilineFoldRanges.add(range)
}
}

activeEditor.setDecorations(multilineFoldDecType, foldRanges)
}

export function registerFoldingRangeProvider() {
return Languages.registerFoldingRangeProvider(
{ language: '*', scheme: 'file' },
{
provideFoldingRanges(document: TextDocument) {
const ranges = []

let match
while ((match = classRegex.exec(document.getText()))) {
if (match && !match[0]) continue

const startPosition = document.positionAt(match.index)
const endPosition = document.positionAt(match.index + match[0].length)

if (startPosition.line !== endPosition.line) {
ranges.push(new FoldingRange(startPosition.line, endPosition.line))
}
}
return ranges
},
}
)
}

export function toggleFoldClassAttributes() {
const foldClassAttributes = getConfigValue(FoldClassAttributesConfigId)
return Workspace.getConfiguration('tailwindCSS').update(
FoldClassAttributesConfigId,
!foldClassAttributes,
ConfigurationTarget.Global
)
}

function getConfigValue(configId: string) {
return Workspace.getConfiguration('tailwindCSS').get<string[]>(configId)
}

function foldingCommand(command: 'fold' | 'unfold', range: Range) {
return commands.executeCommand(`editor.${command}`, {
level: 1,
direction: 'down',
selectionLines: [range.end.line],
})
}
36 changes: 36 additions & 0 deletions packages/vscode-tailwindcss/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ import { dedupe, equal } from 'tailwindcss-language-service/src/util/array'
import namedColors from 'color-name'
import minimatch from 'minimatch'
import { CONFIG_GLOB, CSS_GLOB } from 'tailwindcss-language-server/src/lib/constants'
import {
registerFoldingRangeProvider,
toggleFoldClassAttributes,
triggerUpdateDecorations,
initClassFoldingDecorator,
} from './classFoldingDecorator'

const colorNames = Object.keys(namedColors)

Expand Down Expand Up @@ -269,6 +275,15 @@ export async function activate(context: ExtensionContext) {
clients.delete(folder.uri.toString())
bootClientForFolderIfNeeded(folder)
}

if (
event.affectsConfiguration('tailwindCSS.classAttributes') ||
event.affectsConfiguration('tailwindCSS.experimental.foldClassAttributes') ||
event.affectsConfiguration('editor.fontSize')
) {
initClassFoldingDecorator(context)
triggerUpdateDecorations()
}
})
)

Expand Down Expand Up @@ -709,6 +724,27 @@ export async function activate(context: ExtensionContext) {
}
})
)

initClassFoldingDecorator(context)
context.subscriptions.push(registerFoldingRangeProvider())

context.subscriptions.push(
commands.registerCommand('tailwindCSS.toggleFoldClassAttributes', toggleFoldClassAttributes)
)

context.subscriptions.push(
Window.onDidChangeActiveTextEditor((e) => {
if (e) triggerUpdateDecorations()
})
)

context.subscriptions.push(
Window.onDidChangeTextEditorSelection((e) => {
if (e) triggerUpdateDecorations(true)
})
)

triggerUpdateDecorations()
}

export function deactivate(): Thenable<void> {
Expand Down