Merge pull request #630 from tailwindlabs/next
Theme helper improvements, support `@config`, adopt `getVariants`, bump bundled modulesmaster
commit
68750d859b
File diff suppressed because it is too large
Load Diff
|
@ -32,10 +32,10 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@parcel/watcher": "2.0.3",
|
||||
"@tailwindcss/aspect-ratio": "0.4.0",
|
||||
"@tailwindcss/forms": "0.4.0",
|
||||
"@tailwindcss/line-clamp": "0.3.0",
|
||||
"@tailwindcss/typography": "0.5.0",
|
||||
"@tailwindcss/aspect-ratio": "0.4.2",
|
||||
"@tailwindcss/forms": "0.5.3",
|
||||
"@tailwindcss/line-clamp": "0.4.2",
|
||||
"@tailwindcss/typography": "0.5.7",
|
||||
"@types/debounce": "1.2.0",
|
||||
"@types/node": "14.14.34",
|
||||
"@types/vscode": "1.65.0",
|
||||
|
@ -63,7 +63,7 @@
|
|||
"resolve": "1.20.0",
|
||||
"rimraf": "3.0.2",
|
||||
"stack-trace": "0.0.10",
|
||||
"tailwindcss": "3.0.11",
|
||||
"tailwindcss": "3.1.8",
|
||||
"typescript": "4.6.4",
|
||||
"vscode-css-languageservice": "5.4.1",
|
||||
"vscode-languageserver": "8.0.2",
|
||||
|
|
|
@ -162,11 +162,16 @@ connection.onCompletion(async ({ textDocument, position }, _token) =>
|
|||
{
|
||||
...item,
|
||||
label: 'theme()',
|
||||
filterText: 'theme',
|
||||
documentation: {
|
||||
kind: 'markdown',
|
||||
value:
|
||||
'Use the `theme()` function to access your Tailwind config values using dot notation.',
|
||||
},
|
||||
command: {
|
||||
title: '',
|
||||
command: 'editor.action.triggerSuggest',
|
||||
},
|
||||
textEdit: {
|
||||
...item.textEdit,
|
||||
newText: item.textEdit.newText.replace(/^calc\(/, 'theme('),
|
||||
|
@ -357,6 +362,7 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument {
|
|||
/@media(\s+screen\s*\([^)]+\))/g,
|
||||
(_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`
|
||||
)
|
||||
.replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_')
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -387,7 +393,7 @@ async function validateTextDocument(textDocument: TextDocument): Promise<void> {
|
|||
.filter((diagnostic) => {
|
||||
if (
|
||||
diagnostic.code === 'unknownAtRules' &&
|
||||
/Unknown at rule @(tailwind|apply)/.test(diagnostic.message)
|
||||
/Unknown at rule @(tailwind|apply|config)/.test(diagnostic.message)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@ import {
|
|||
FileChangeType,
|
||||
Disposable,
|
||||
TextDocumentIdentifier,
|
||||
DocumentLinkRequest,
|
||||
DocumentLinkParams,
|
||||
DocumentLink,
|
||||
} from 'vscode-languageserver/node'
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||
import { URI } from 'vscode-uri'
|
||||
|
@ -60,6 +63,7 @@ import {
|
|||
FeatureFlags,
|
||||
Settings,
|
||||
ClassNames,
|
||||
Variant,
|
||||
} from 'tailwindcss-language-service/src/util/state'
|
||||
import {
|
||||
provideDiagnostics,
|
||||
|
@ -68,6 +72,7 @@ import {
|
|||
} from './lsp/diagnosticsProvider'
|
||||
import { doCodeActions } from 'tailwindcss-language-service/src/codeActions/codeActionProvider'
|
||||
import { getDocumentColors } from 'tailwindcss-language-service/src/documentColorProvider'
|
||||
import { getDocumentLinks } from 'tailwindcss-language-service/src/documentLinksProvider'
|
||||
import { debounce } from 'debounce'
|
||||
import { getModuleDependencies } from './util/getModuleDependencies'
|
||||
import assert from 'assert'
|
||||
|
@ -112,6 +117,7 @@ const TRIGGER_CHARACTERS = [
|
|||
// @apply and emmet-style
|
||||
'.',
|
||||
// config/theme helper
|
||||
'(',
|
||||
'[',
|
||||
// JIT "important" prefix
|
||||
'!',
|
||||
|
@ -187,6 +193,7 @@ interface ProjectService {
|
|||
onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]>
|
||||
onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
|
||||
onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
|
||||
onDocumentLinks(params: DocumentLinkParams): DocumentLink[]
|
||||
}
|
||||
|
||||
type ProjectConfig = { folder: string; configPath?: string; documentSelector?: string[] }
|
||||
|
@ -298,6 +305,27 @@ async function createProjectService(
|
|||
getDocumentSymbols: (uri: string) => {
|
||||
return connection.sendRequest('@/tailwindCSS/getDocumentSymbols', { uri })
|
||||
},
|
||||
async readDirectory(document, directory) {
|
||||
try {
|
||||
directory = path.resolve(path.dirname(getFileFsPath(document.uri)), directory)
|
||||
let dirents = await fs.promises.readdir(directory, { withFileTypes: true })
|
||||
let result: Array<[string, { isDirectory: boolean }] | null> = await Promise.all(
|
||||
dirents.map(async (dirent) => {
|
||||
let isDirectory = dirent.isDirectory()
|
||||
return (await isExcluded(
|
||||
state,
|
||||
document,
|
||||
path.join(directory, dirent.name, isDirectory ? '/' : '')
|
||||
))
|
||||
? null
|
||||
: [dirent.name, { isDirectory }]
|
||||
})
|
||||
)
|
||||
return result.filter((item) => item !== null)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1027,6 +1055,14 @@ async function createProjectService(
|
|||
if (!settings.tailwindCSS.codeActions) return null
|
||||
return doCodeActions(state, params)
|
||||
},
|
||||
onDocumentLinks(params: DocumentLinkParams): DocumentLink[] {
|
||||
if (!state.enabled) return null
|
||||
let document = documentService.getDocument(params.textDocument.uri)
|
||||
if (!document) return null
|
||||
return getDocumentLinks(state, document, (linkPath) =>
|
||||
URI.file(path.resolve(path.dirname(URI.parse(document.uri).fsPath), linkPath)).toString()
|
||||
)
|
||||
},
|
||||
provideDiagnostics: debounce((document: TextDocument) => {
|
||||
if (!state.enabled) return
|
||||
provideDiagnostics(state, document)
|
||||
|
@ -1146,19 +1182,29 @@ function isAtRule(node: Node): node is AtRule {
|
|||
return node.type === 'atrule'
|
||||
}
|
||||
|
||||
function getVariants(state: State): Record<string, string> {
|
||||
function getVariants(state: State): Array<Variant> {
|
||||
if (state.jitContext?.getVariants) {
|
||||
return state.jitContext.getVariants()
|
||||
}
|
||||
|
||||
if (state.jit) {
|
||||
let result: Array<Variant> = []
|
||||
// [name, [sort, fn]]
|
||||
// [name, [[sort, fn]]]
|
||||
Array.from(state.jitContext.variantMap as Map<string, [any, any]>).forEach(
|
||||
([variantName, variantFnOrFns]) => {
|
||||
result.push({
|
||||
name: variantName,
|
||||
values: [],
|
||||
isArbitrary: false,
|
||||
hasDash: true,
|
||||
selectors: () => {
|
||||
function escape(className: string): string {
|
||||
let node = state.modules.postcssSelectorParser.module.className()
|
||||
node.value = className
|
||||
return dlv(node, 'raws.value', node.value)
|
||||
}
|
||||
|
||||
let result = {}
|
||||
// [name, [sort, fn]]
|
||||
// [name, [[sort, fn]]]
|
||||
Array.from(state.jitContext.variantMap as Map<string, [any, any]>).forEach(
|
||||
([variantName, variantFnOrFns]) => {
|
||||
let fns = (Array.isArray(variantFnOrFns[0]) ? variantFnOrFns : [variantFnOrFns]).map(
|
||||
([_sort, fn]) => fn
|
||||
)
|
||||
|
@ -1244,7 +1290,9 @@ function getVariants(state: State): Record<string, string> {
|
|||
}
|
||||
}
|
||||
|
||||
result[variantName] = definitions.join(', ') || null
|
||||
return definitions
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1276,7 +1324,13 @@ function getVariants(state: State): Record<string, string> {
|
|||
})
|
||||
})
|
||||
|
||||
return variants.reduce((obj, variant) => ({ ...obj, [variant]: null }), {})
|
||||
return variants.map((variant) => ({
|
||||
name: variant,
|
||||
values: [],
|
||||
isArbitrary: false,
|
||||
hasDash: true,
|
||||
selectors: () => [],
|
||||
}))
|
||||
}
|
||||
|
||||
async function getPlugins(config: any) {
|
||||
|
@ -1484,6 +1538,7 @@ class TW {
|
|||
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))
|
||||
}
|
||||
|
||||
private updateCapabilities() {
|
||||
|
@ -1498,6 +1553,7 @@ class TW {
|
|||
capabilities.add(HoverRequest.type, { documentSelector: null })
|
||||
capabilities.add(DocumentColorRequest.type, { documentSelector: null })
|
||||
capabilities.add(CodeActionRequest.type, { documentSelector: null })
|
||||
capabilities.add(DocumentLinkRequest.type, { documentSelector: null })
|
||||
|
||||
capabilities.add(CompletionRequest.type, {
|
||||
documentSelector: null,
|
||||
|
@ -1563,6 +1619,10 @@ class TW {
|
|||
return this.getProject(params.textDocument)?.onCodeAction(params) ?? null
|
||||
}
|
||||
|
||||
onDocumentLinks(params: DocumentLinkParams): DocumentLink[] {
|
||||
return this.getProject(params.textDocument)?.onDocumentLinks(params) ?? null
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.connection.listen()
|
||||
}
|
||||
|
@ -1604,7 +1664,8 @@ function supportsDynamicRegistration(connection: Connection, params: InitializeP
|
|||
params.capabilities.textDocument.hover?.dynamicRegistration &&
|
||||
params.capabilities.textDocument.colorProvider?.dynamicRegistration &&
|
||||
params.capabilities.textDocument.codeAction?.dynamicRegistration &&
|
||||
params.capabilities.textDocument.completion?.dynamicRegistration
|
||||
params.capabilities.textDocument.completion?.dynamicRegistration &&
|
||||
params.capabilities.textDocument.documentLink?.dynamicRegistration
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1629,6 +1690,7 @@ connection.onInitialize(async (params: InitializeParams): Promise<InitializeResu
|
|||
hoverProvider: true,
|
||||
colorProvider: true,
|
||||
codeActionProvider: true,
|
||||
documentLinkProvider: {},
|
||||
completionProvider: {
|
||||
resolveProvider: true,
|
||||
triggerCharacters: [...TRIGGER_CHARACTERS, ':'],
|
||||
|
|
|
@ -4,9 +4,12 @@ import { State } from 'tailwindcss-language-service/src/util/state'
|
|||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||
import { getFileFsPath } from './uri'
|
||||
|
||||
export default async function isExcluded(state: State, document: TextDocument): Promise<boolean> {
|
||||
export default async function isExcluded(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
file: string = getFileFsPath(document.uri)
|
||||
): Promise<boolean> {
|
||||
let settings = await state.editor.getConfiguration(document.uri)
|
||||
let file = getFileFsPath(document.uri)
|
||||
|
||||
for (let pattern of settings.tailwindCSS.files.exclude) {
|
||||
if (minimatch(file, path.join(state.editor.folder, pattern))) {
|
||||
|
|
|
@ -110,7 +110,6 @@ export function completionsFromClassList(
|
|||
}
|
||||
}
|
||||
|
||||
let allVariants = Object.keys(state.variants)
|
||||
let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName)
|
||||
|
||||
replacementRange.start.character += offset
|
||||
|
@ -123,40 +122,77 @@ export function completionsFromClassList(
|
|||
let items: CompletionItem[] = []
|
||||
|
||||
if (!important) {
|
||||
let shouldSortVariants = !semver.gte(state.version, '2.99.0')
|
||||
let variantOrder = 0
|
||||
|
||||
function variantItem(
|
||||
item: Omit<CompletionItem, 'kind' | 'data' | 'sortText' | 'textEdit'> & {
|
||||
textEdit?: { newText: string; range?: Range }
|
||||
}
|
||||
): CompletionItem {
|
||||
return {
|
||||
kind: 9,
|
||||
data: 'variant',
|
||||
command:
|
||||
item.insertTextFormat === 2 // Snippet
|
||||
? undefined
|
||||
: {
|
||||
title: '',
|
||||
command: 'editor.action.triggerSuggest',
|
||||
},
|
||||
sortText: '-' + naturalExpand(variantOrder++),
|
||||
...item,
|
||||
textEdit: {
|
||||
newText: item.label,
|
||||
range: replacementRange,
|
||||
...item.textEdit,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
items.push(
|
||||
...Object.entries(state.variants)
|
||||
.filter(([variant]) => !existingVariants.includes(variant))
|
||||
.map(([variant, definition], index) => {
|
||||
let resultingVariants = [...existingVariants, variant]
|
||||
...state.variants.flatMap((variant) => {
|
||||
let items: CompletionItem[] = []
|
||||
|
||||
if (variant.isArbitrary) {
|
||||
items.push(
|
||||
variantItem({
|
||||
label: `${variant.name}${variant.hasDash ? '-' : ''}[]${sep}`,
|
||||
insertTextFormat: 2,
|
||||
textEdit: {
|
||||
newText: `${variant.name}${variant.hasDash ? '-' : ''}[\${1}]${sep}\${0}`,
|
||||
},
|
||||
// command: {
|
||||
// title: '',
|
||||
// command: 'tailwindCSS.onInsertArbitraryVariantSnippet',
|
||||
// arguments: [variant.name, replacementRange],
|
||||
// },
|
||||
})
|
||||
)
|
||||
} else if (!existingVariants.includes(variant.name)) {
|
||||
let shouldSortVariants = !semver.gte(state.version, '2.99.0')
|
||||
let resultingVariants = [...existingVariants, variant.name]
|
||||
|
||||
if (shouldSortVariants) {
|
||||
let allVariants = state.variants.map(({ name }) => name)
|
||||
resultingVariants = resultingVariants.sort(
|
||||
(a, b) => allVariants.indexOf(b) - allVariants.indexOf(a)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
label: variant + sep,
|
||||
kind: 9,
|
||||
detail: definition,
|
||||
data: 'variant',
|
||||
command: {
|
||||
title: '',
|
||||
command: 'editor.action.triggerSuggest',
|
||||
},
|
||||
sortText: '-' + naturalExpand(index),
|
||||
items.push(
|
||||
variantItem({
|
||||
label: `${variant.name}${sep}`,
|
||||
detail: variant.selectors().join(', '),
|
||||
textEdit: {
|
||||
newText: resultingVariants[resultingVariants.length - 1] + sep,
|
||||
range: replacementRange,
|
||||
},
|
||||
additionalTextEdits:
|
||||
shouldSortVariants && resultingVariants.length > 1
|
||||
? [
|
||||
{
|
||||
newText:
|
||||
resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep,
|
||||
resultingVariants.slice(0, resultingVariants.length - 1).join(sep) +
|
||||
sep,
|
||||
range: {
|
||||
start: {
|
||||
...classListRange.start,
|
||||
|
@ -170,7 +206,24 @@ export function completionsFromClassList(
|
|||
},
|
||||
]
|
||||
: [],
|
||||
} as CompletionItem
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (variant.values.length) {
|
||||
items.push(
|
||||
...variant.values
|
||||
.filter((value) => !existingVariants.includes(`${variant.name}-${value}`))
|
||||
.map((value) =>
|
||||
variantItem({
|
||||
label: `${variant.name}${variant.hasDash ? '-' : ''}${value}${sep}`,
|
||||
detail: variant.selectors({ value }).join(', '),
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -503,6 +556,11 @@ function provideAtApplyCompletions(
|
|||
)
|
||||
}
|
||||
|
||||
const NUMBER_REGEX = /^(\d+\.?|\d*\.\d+)$/
|
||||
function isNumber(str: string): boolean {
|
||||
return NUMBER_REGEX.test(str)
|
||||
}
|
||||
|
||||
async function provideClassNameCompletions(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
|
@ -537,14 +595,26 @@ function provideCssHelperCompletions(
|
|||
|
||||
const match = text
|
||||
.substr(0, text.length - 1) // don't include that extra character from earlier
|
||||
.match(/\b(?<helper>config|theme)\(['"](?<keys>[^'"]*)$/)
|
||||
.match(/\b(?<helper>config|theme)\(\s*['"]?(?<path>[^)'"]*)$/)
|
||||
|
||||
if (match === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let alpha: string
|
||||
let path = match.groups.path.replace(/^['"]+/g, '')
|
||||
let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]*))$/)
|
||||
if (matches) {
|
||||
path = matches[1]
|
||||
alpha = matches[2]
|
||||
}
|
||||
|
||||
if (alpha !== undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
|
||||
let parts = match.groups.keys.split(/([\[\].]+)/)
|
||||
let parts = path.split(/([\[\].]+)/)
|
||||
let keys = parts.filter((_, i) => i % 2 === 0)
|
||||
let separators = parts.filter((_, i) => i % 2 !== 0)
|
||||
// let obj =
|
||||
|
@ -557,7 +627,7 @@ function provideCssHelperCompletions(
|
|||
}
|
||||
|
||||
let obj: any
|
||||
let offset: number = 0
|
||||
let offset: number = keys[keys.length - 1].length
|
||||
let separator: string = separators.length ? separators[separators.length - 1] : null
|
||||
|
||||
if (keys.length === 1) {
|
||||
|
@ -576,9 +646,32 @@ function provideCssHelperCompletions(
|
|||
|
||||
if (!obj) return null
|
||||
|
||||
let editRange = {
|
||||
start: {
|
||||
line: position.line,
|
||||
character: position.character - offset,
|
||||
},
|
||||
end: position,
|
||||
}
|
||||
|
||||
return {
|
||||
isIncomplete: false,
|
||||
items: Object.keys(obj).map((item, index) => {
|
||||
items: Object.keys(obj)
|
||||
.sort((a, z) => {
|
||||
let aIsNumber = isNumber(a)
|
||||
let zIsNumber = isNumber(z)
|
||||
if (aIsNumber && !zIsNumber) {
|
||||
return -1
|
||||
}
|
||||
if (!aIsNumber && zIsNumber) {
|
||||
return 1
|
||||
}
|
||||
if (aIsNumber && zIsNumber) {
|
||||
return parseFloat(a) - parseFloat(z)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.map((item, index) => {
|
||||
let color = getColorFromValue(obj[item])
|
||||
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
|
||||
const insertClosingBrace: boolean =
|
||||
|
@ -588,8 +681,10 @@ function provideCssHelperCompletions(
|
|||
|
||||
return {
|
||||
label: item,
|
||||
filterText: `${replaceDot ? '.' : ''}${item}`,
|
||||
sortText: naturalExpand(index),
|
||||
commitCharacters: [!item.includes('.') && '.', !item.includes('[') && '['].filter(
|
||||
Boolean
|
||||
),
|
||||
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
|
||||
// VS Code bug causes some values to not display in some cases
|
||||
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
|
||||
|
@ -598,16 +693,23 @@ function provideCssHelperCompletions(
|
|||
? culori.formatRgb(color)
|
||||
: null,
|
||||
textEdit: {
|
||||
newText: `${replaceDot ? '[' : ''}${item}${insertClosingBrace ? ']' : ''}`,
|
||||
newText: `${item}${insertClosingBrace ? ']' : ''}`,
|
||||
range: editRange,
|
||||
},
|
||||
additionalTextEdits: replaceDot
|
||||
? [
|
||||
{
|
||||
newText: '[',
|
||||
range: {
|
||||
start: {
|
||||
line: position.line,
|
||||
character:
|
||||
position.character - keys[keys.length - 1].length - (replaceDot ? 1 : 0) - offset,
|
||||
...editRange.start,
|
||||
character: editRange.start.character - 1,
|
||||
},
|
||||
end: position,
|
||||
end: editRange.start,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
data: 'helper',
|
||||
}
|
||||
}),
|
||||
|
@ -741,7 +843,12 @@ function provideVariantsDirectiveCompletions(
|
|||
|
||||
if (/\s+/.test(parts[parts.length - 1])) return null
|
||||
|
||||
let possibleVariants = Object.keys(state.variants)
|
||||
let possibleVariants = state.variants.flatMap((variant) => {
|
||||
if (variant.values.length) {
|
||||
return variant.values.map((value) => `${variant.name}${variant.hasDash ? '-' : ''}${value}`)
|
||||
}
|
||||
return [variant.name]
|
||||
})
|
||||
const existingVariants = parts.slice(0, parts.length - 1)
|
||||
|
||||
if (state.jit) {
|
||||
|
@ -945,6 +1052,20 @@ function provideCssDirectiveCompletions(
|
|||
},
|
||||
},
|
||||
]),
|
||||
...(semver.gte(state.version, '3.2.0')
|
||||
? [
|
||||
{
|
||||
label: '@config',
|
||||
documentation: {
|
||||
kind: 'markdown' as typeof MarkupKind.Markdown,
|
||||
value: `[Tailwind CSS Documentation](${docsUrl(
|
||||
state.version,
|
||||
'functions-and-directives/#config'
|
||||
)})`,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
return {
|
||||
|
@ -967,6 +1088,52 @@ function provideCssDirectiveCompletions(
|
|||
}
|
||||
}
|
||||
|
||||
async function provideConfigDirectiveCompletions(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
position: Position
|
||||
): Promise<CompletionList> {
|
||||
if (!isCssContext(state, document, position)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!semver.gte(state.version, '3.2.0')) {
|
||||
return null
|
||||
}
|
||||
|
||||
let text = document.getText({ start: { line: position.line, character: 0 }, end: position })
|
||||
let match = text.match(/@config\s*(?<partial>'[^']*|"[^"]*)$/)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
let partial = match.groups.partial.slice(1) // remove quote
|
||||
let valueBeforeLastSlash = partial.substring(0, partial.lastIndexOf('/'))
|
||||
let valueAfterLastSlash = partial.substring(partial.lastIndexOf('/') + 1)
|
||||
|
||||
return {
|
||||
isIncomplete: false,
|
||||
items: (await state.editor.readDirectory(document, valueBeforeLastSlash || '.'))
|
||||
.filter(([name, type]) => type.isDirectory || /\.c?js$/.test(name))
|
||||
.map(([name, type]) => ({
|
||||
label: type.isDirectory ? name + '/' : name,
|
||||
kind: type.isDirectory ? 19 : 17,
|
||||
textEdit: {
|
||||
newText: type.isDirectory ? name + '/' : name,
|
||||
range: {
|
||||
start: {
|
||||
line: position.line,
|
||||
character: position.character - valueAfterLastSlash.length,
|
||||
},
|
||||
end: position,
|
||||
},
|
||||
},
|
||||
command: type.isDirectory
|
||||
? { command: 'editor.action.triggerSuggest', title: '' }
|
||||
: undefined,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
async function provideEmmetCompletions(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
|
@ -1055,6 +1222,7 @@ export async function doComplete(
|
|||
provideVariantsDirectiveCompletions(state, document, position) ||
|
||||
provideTailwindDirectiveCompletions(state, document, position) ||
|
||||
provideLayerDirectiveCompletions(state, document, position) ||
|
||||
(await provideConfigDirectiveCompletions(state, document, position)) ||
|
||||
(await provideCustomClassNameCompletions(state, document, position))
|
||||
|
||||
if (result) return result
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { State, Settings } from '../util/state'
|
||||
import type { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
|
||||
import type { TextDocument } from 'vscode-languageserver'
|
||||
import { InvalidConfigPathDiagnostic, DiagnosticKind } from './types'
|
||||
import { isCssDoc } from '../util/css'
|
||||
import { getLanguageBoundaries } from '../util/getLanguageBoundaries'
|
||||
import { findAll, indexToPosition } from '../util/find'
|
||||
import { findHelperFunctionsInDocument } from '../util/find'
|
||||
import { stringToPath } from '../util/stringToPath'
|
||||
import isObject from '../util/isObject'
|
||||
import { closest } from '../util/closest'
|
||||
import { absoluteRange } from '../util/absoluteRange'
|
||||
import { combinations } from '../util/combinations'
|
||||
import dlv from 'dlv'
|
||||
import { getTextWithoutComments } from '../util/doc'
|
||||
|
||||
function pathToString(path: string | string[]): string {
|
||||
if (typeof path === 'string') return path
|
||||
|
@ -167,47 +163,18 @@ export function getInvalidConfigPathDiagnostics(
|
|||
if (severity === 'ignore') return []
|
||||
|
||||
let diagnostics: InvalidConfigPathDiagnostic[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
if (isCssDoc(state, document)) {
|
||||
ranges.push(undefined)
|
||||
} else {
|
||||
let boundaries = getLanguageBoundaries(state, document)
|
||||
if (!boundaries) return []
|
||||
ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range))
|
||||
}
|
||||
|
||||
ranges.forEach((range) => {
|
||||
let text = getTextWithoutComments(document, 'css', range)
|
||||
let matches = findAll(
|
||||
/(?<prefix>\s|^)(?<helper>config|theme)\((?<quote>['"])(?<key>[^)]+)\k<quote>[^)]*\)/g,
|
||||
text
|
||||
)
|
||||
|
||||
matches.forEach((match) => {
|
||||
let base = match.groups.helper === 'theme' ? ['theme'] : []
|
||||
let result = validateConfigPath(state, match.groups.key, base)
|
||||
findHelperFunctionsInDocument(state, document).forEach((helperFn) => {
|
||||
let base = helperFn.helper === 'theme' ? ['theme'] : []
|
||||
let result = validateConfigPath(state, helperFn.path, base)
|
||||
|
||||
if (result.isValid === true) {
|
||||
return null
|
||||
return
|
||||
}
|
||||
|
||||
let startIndex =
|
||||
match.index +
|
||||
match.groups.prefix.length +
|
||||
match.groups.helper.length +
|
||||
1 + // open paren
|
||||
match.groups.quote.length
|
||||
|
||||
diagnostics.push({
|
||||
code: DiagnosticKind.InvalidConfigPath,
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(text, startIndex),
|
||||
end: indexToPosition(text, startIndex + match.groups.key.length),
|
||||
},
|
||||
range
|
||||
),
|
||||
range: helperFn.ranges.path,
|
||||
severity:
|
||||
severity === 'error'
|
||||
? 1 /* DiagnosticSeverity.Error */
|
||||
|
@ -216,7 +183,6 @@ export function getInvalidConfigPathDiagnostics(
|
|||
suggestions: result.suggestions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
|
|
@ -32,7 +32,12 @@ export function getInvalidVariantDiagnostics(
|
|||
ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range))
|
||||
}
|
||||
|
||||
let possibleVariants = Object.keys(state.variants)
|
||||
let possibleVariants = state.variants.flatMap((variant) => {
|
||||
if (variant.values.length) {
|
||||
return variant.values.map((value) => `${variant.name}${variant.hasDash ? '-' : ''}${value}`)
|
||||
}
|
||||
return [variant.name]
|
||||
})
|
||||
if (state.jit) {
|
||||
possibleVariants.unshift('responsive')
|
||||
possibleVariants = possibleVariants.filter((v) => !state.screens.includes(v))
|
||||
|
|
|
@ -36,12 +36,12 @@ export async function getDocumentColors(
|
|||
|
||||
let helperFns = findHelperFunctionsInDocument(state, document)
|
||||
helperFns.forEach((fn) => {
|
||||
let keys = stringToPath(fn.value)
|
||||
let keys = stringToPath(fn.path)
|
||||
let base = fn.helper === 'theme' ? ['theme'] : []
|
||||
let value = dlv(state.config, [...base, ...keys])
|
||||
let color = getColorFromValue(value)
|
||||
if (color && typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
|
||||
colors.push({ range: fn.valueRange, color: culoriColorToVscodeColor(color) })
|
||||
colors.push({ range: fn.ranges.path, color: culoriColorToVscodeColor(color) })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { State } from './util/state'
|
||||
import type { DocumentLink, Range, TextDocument } from 'vscode-languageserver'
|
||||
import { isCssDoc } from './util/css'
|
||||
import { getLanguageBoundaries } from './util/getLanguageBoundaries'
|
||||
import { findAll, indexToPosition } from './util/find'
|
||||
import { getTextWithoutComments } from './util/doc'
|
||||
import { absoluteRange } from './util/absoluteRange'
|
||||
import * as semver from './util/semver'
|
||||
|
||||
export function getDocumentLinks(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
resolveTarget: (linkPath: string) => string
|
||||
): DocumentLink[] {
|
||||
return getConfigDirectiveLinks(state, document, resolveTarget)
|
||||
}
|
||||
|
||||
function getConfigDirectiveLinks(
|
||||
state: State,
|
||||
document: TextDocument,
|
||||
resolveTarget: (linkPath: string) => string
|
||||
): DocumentLink[] {
|
||||
if (!semver.gte(state.version, '3.2.0')) {
|
||||
return []
|
||||
}
|
||||
|
||||
let links: DocumentLink[] = []
|
||||
let ranges: Range[] = []
|
||||
|
||||
if (isCssDoc(state, document)) {
|
||||
ranges.push(undefined)
|
||||
} else {
|
||||
let boundaries = getLanguageBoundaries(state, document)
|
||||
if (!boundaries) return []
|
||||
ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range))
|
||||
}
|
||||
|
||||
for (let range of ranges) {
|
||||
let text = getTextWithoutComments(document, 'css', range)
|
||||
let matches = findAll(/@config\s*(?<path>'[^']+'|"[^"]+")/g, text)
|
||||
|
||||
for (let match of matches) {
|
||||
links.push({
|
||||
target: resolveTarget(match.groups.path.slice(1, -1)),
|
||||
range: absoluteRange(
|
||||
{
|
||||
start: indexToPosition(text, match.index + match[0].length - match.groups.path.length),
|
||||
end: indexToPosition(text, match.index + match[0].length),
|
||||
},
|
||||
range
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
|
@ -3,12 +3,12 @@ import type { Hover, TextDocument, Position } from 'vscode-languageserver'
|
|||
import { stringifyCss, stringifyConfigValue } from './util/stringify'
|
||||
import dlv from 'dlv'
|
||||
import { isCssContext } from './util/css'
|
||||
import { findClassNameAtPosition } from './util/find'
|
||||
import { findClassNameAtPosition, findHelperFunctionsInRange } from './util/find'
|
||||
import { validateApply } from './util/validateApply'
|
||||
import { getClassNameParts } from './util/getClassNameAtPosition'
|
||||
import * as jit from './util/jit'
|
||||
import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostics'
|
||||
import { getTextWithoutComments } from './util/doc'
|
||||
import { isWithinRange } from './util/isWithinRange'
|
||||
|
||||
export async function doHover(
|
||||
state: State,
|
||||
|
@ -22,50 +22,35 @@ export async function doHover(
|
|||
}
|
||||
|
||||
function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover {
|
||||
if (!isCssContext(state, document, position)) return null
|
||||
|
||||
const line = getTextWithoutComments(document, 'css').split('\n')[position.line]
|
||||
|
||||
const match = line.match(/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>[^)]*\)/)
|
||||
|
||||
if (match === null) return null
|
||||
|
||||
const startChar = match.index + match.groups.helper.length + 2
|
||||
const endChar = startChar + match.groups.key.length
|
||||
|
||||
if (position.character < startChar || position.character >= endChar) {
|
||||
if (!isCssContext(state, document, position)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let key = match.groups.key
|
||||
.split(/(\[[^\]]+\]|\.)/)
|
||||
.filter(Boolean)
|
||||
.filter((x) => x !== '.')
|
||||
.map((x) => x.replace(/^\[([^\]]+)\]$/, '$1'))
|
||||
let helperFns = findHelperFunctionsInRange(document, {
|
||||
start: { line: position.line, character: 0 },
|
||||
end: { line: position.line + 1, character: 0 },
|
||||
})
|
||||
|
||||
if (key.length === 0) return null
|
||||
|
||||
if (match.groups.helper === 'theme') {
|
||||
key = ['theme', ...key]
|
||||
for (let helperFn of helperFns) {
|
||||
if (isWithinRange(position, helperFn.ranges.path)) {
|
||||
let validated = validateConfigPath(
|
||||
state,
|
||||
helperFn.path,
|
||||
helperFn.helper === 'theme' ? ['theme'] : []
|
||||
)
|
||||
let value = validated.isValid ? stringifyConfigValue(validated.value) : null
|
||||
if (value === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const value = validateConfigPath(state, key).isValid
|
||||
? stringifyConfigValue(dlv(state.config, key))
|
||||
: null
|
||||
|
||||
if (value === null) return null
|
||||
|
||||
return {
|
||||
contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') },
|
||||
range: {
|
||||
start: { line: position.line, character: startChar },
|
||||
end: {
|
||||
line: position.line,
|
||||
character: endChar,
|
||||
},
|
||||
},
|
||||
range: helperFn.ranges.path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function provideClassNameHover(
|
||||
state: State,
|
||||
|
|
|
@ -359,36 +359,48 @@ export function findHelperFunctionsInRange(
|
|||
range?: Range
|
||||
): DocumentHelperFunction[] {
|
||||
const text = getTextWithoutComments(doc, 'css', range)
|
||||
const matches = findAll(
|
||||
/(?<before>^|\s)(?<helper>theme|config)\((?:(?<single>')([^']+)'|(?<double>")([^"]+)")[^)]*\)/gm,
|
||||
let matches = findAll(
|
||||
/(?<prefix>\s|^)(?<helper>config|theme)(?<innerPrefix>\(\s*)(?<path>[^)]*?)\s*\)/g,
|
||||
text
|
||||
)
|
||||
|
||||
return matches.map((match) => {
|
||||
let value = match[4] || match[6]
|
||||
let startIndex = match.index + match.groups.before.length
|
||||
let quotesBefore = ''
|
||||
let path = match.groups.path.replace(/['"]+$/, '').replace(/^['"]+/, (m) => {
|
||||
quotesBefore = m
|
||||
return ''
|
||||
})
|
||||
let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/)
|
||||
if (matches) {
|
||||
path = matches[1]
|
||||
}
|
||||
path = path.replace(/['"]*\s*$/, '')
|
||||
|
||||
let startIndex =
|
||||
match.index +
|
||||
match.groups.prefix.length +
|
||||
match.groups.helper.length +
|
||||
match.groups.innerPrefix.length
|
||||
|
||||
return {
|
||||
full: match[0].substr(match.groups.before.length),
|
||||
value,
|
||||
helper: match.groups.helper === 'theme' ? 'theme' : 'config',
|
||||
quotes: match.groups.single ? "'" : '"',
|
||||
range: resolveRange(
|
||||
path,
|
||||
ranges: {
|
||||
full: resolveRange(
|
||||
{
|
||||
start: indexToPosition(text, startIndex),
|
||||
end: indexToPosition(text, match.index + match[0].length),
|
||||
end: indexToPosition(text, startIndex + match.groups.path.length),
|
||||
},
|
||||
range
|
||||
),
|
||||
valueRange: resolveRange(
|
||||
path: resolveRange(
|
||||
{
|
||||
start: indexToPosition(text, startIndex + match.groups.helper.length + 1),
|
||||
end: indexToPosition(
|
||||
text,
|
||||
startIndex + match.groups.helper.length + 1 + 1 + value.length + 1
|
||||
),
|
||||
start: indexToPosition(text, startIndex + quotesBefore.length),
|
||||
end: indexToPosition(text, startIndex + quotesBefore.length + path.length),
|
||||
},
|
||||
range
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,17 +5,25 @@ export function getVariantsFromClassName(
|
|||
state: State,
|
||||
className: string
|
||||
): { variants: string[]; offset: number } {
|
||||
let allVariants = Object.keys(state.variants)
|
||||
let parts = splitAtTopLevelOnly(className, state.separator).filter(Boolean)
|
||||
let allVariants = state.variants.flatMap((variant) => {
|
||||
if (variant.values.length) {
|
||||
return variant.values.map((value) => `${variant.name}${variant.hasDash ? '-' : ''}${value}`)
|
||||
}
|
||||
return [variant.name]
|
||||
})
|
||||
let variants = new Set<string>()
|
||||
let offset = 0
|
||||
let parts = splitAtTopLevelOnly(className, state.separator)
|
||||
if (parts.length < 2) {
|
||||
return { variants: Array.from(variants), offset }
|
||||
}
|
||||
parts = parts.filter(Boolean)
|
||||
|
||||
for (let part of parts) {
|
||||
if (
|
||||
allVariants.includes(part) ||
|
||||
(state.jit &&
|
||||
((part.includes('[') && part.endsWith(']')) ||
|
||||
(part.includes('<') && part.includes('>'))) &&
|
||||
((part.includes('[') && part.endsWith(']')) || part.includes('/')) &&
|
||||
jit.generateRules(state, [`${part}${state.separator}[color:red]`]).rules.length > 0)
|
||||
) {
|
||||
variants.add(part)
|
||||
|
|
|
@ -29,6 +29,10 @@ export type EditorState = {
|
|||
}
|
||||
getConfiguration: (uri?: string) => Promise<Settings>
|
||||
getDocumentSymbols: (uri: string) => Promise<SymbolInformation[]>
|
||||
readDirectory: (
|
||||
document: TextDocument,
|
||||
directory: string
|
||||
) => Promise<Array<[string, { isDirectory: boolean }]>>
|
||||
}
|
||||
|
||||
type DiagnosticSeveritySetting = 'ignore' | 'warning' | 'error'
|
||||
|
@ -76,6 +80,14 @@ export interface FeatureFlags {
|
|||
experimental: string[]
|
||||
}
|
||||
|
||||
export interface Variant {
|
||||
name: string
|
||||
values: string[]
|
||||
isArbitrary: boolean
|
||||
hasDash: boolean
|
||||
selectors: (params?: { value?: string; label?: string }) => string[]
|
||||
}
|
||||
|
||||
export interface State {
|
||||
enabled: boolean
|
||||
configPath?: string
|
||||
|
@ -86,7 +98,7 @@ export interface State {
|
|||
dependencies?: string[]
|
||||
plugins?: any
|
||||
screens?: string[]
|
||||
variants?: Record<string, string | null>
|
||||
variants?: Variant[]
|
||||
corePlugins?: string[]
|
||||
modules?: {
|
||||
tailwindcss?: { version: string; module: any }
|
||||
|
@ -124,12 +136,12 @@ export type DocumentClassName = {
|
|||
}
|
||||
|
||||
export type DocumentHelperFunction = {
|
||||
full: string
|
||||
helper: 'theme' | 'config'
|
||||
value: string
|
||||
quotes: '"' | "'"
|
||||
range: Range
|
||||
valueRange: Range
|
||||
path: string
|
||||
ranges: {
|
||||
full: Range
|
||||
path: Range
|
||||
}
|
||||
}
|
||||
|
||||
export type ClassNameMeta = {
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
ProviderResult,
|
||||
SnippetString,
|
||||
TextEdit,
|
||||
TextEditorSelectionChangeKind,
|
||||
} from 'vscode'
|
||||
import {
|
||||
LanguageClient,
|
||||
|
@ -149,6 +150,62 @@ export async function activate(context: ExtensionContext) {
|
|||
})
|
||||
)
|
||||
|
||||
// context.subscriptions.push(
|
||||
// commands.registerCommand(
|
||||
// 'tailwindCSS.onInsertArbitraryVariantSnippet',
|
||||
// (
|
||||
// variantName: string,
|
||||
// range: {
|
||||
// start: { line: number; character: number }
|
||||
// end: { line: number; character: number }
|
||||
// }
|
||||
// ) => {
|
||||
// let listener = Window.onDidChangeTextEditorSelection((event) => {
|
||||
// if (event.selections.length !== 1) {
|
||||
// listener.dispose()
|
||||
// return
|
||||
// }
|
||||
|
||||
// let document = event.textEditor.document
|
||||
// let selection = event.selections[0]
|
||||
|
||||
// let line = document.lineAt(range.start.line)
|
||||
// let lineRangeFromCompletion = new Range(
|
||||
// range.start.line,
|
||||
// range.start.character,
|
||||
// line.range.end.line,
|
||||
// line.range.end.character
|
||||
// )
|
||||
// let lineText = document.getText(lineRangeFromCompletion)
|
||||
// let match = lineText.match(/^(\S+)]:/)
|
||||
|
||||
// if (!match) {
|
||||
// listener.dispose()
|
||||
// return
|
||||
// }
|
||||
|
||||
// let arbitraryValueRange = new Range(
|
||||
// lineRangeFromCompletion.start.translate(0, variantName.length + 2),
|
||||
// lineRangeFromCompletion.start.translate(0, match[1].length)
|
||||
// )
|
||||
|
||||
// if (!arbitraryValueRange.contains(selection)) {
|
||||
// listener.dispose()
|
||||
// }
|
||||
|
||||
// if (
|
||||
// event.kind === TextEditorSelectionChangeKind.Command &&
|
||||
// selection.isEmpty &&
|
||||
// selection.start.isEqual(arbitraryValueRange.end.translate(0, 2))
|
||||
// ) {
|
||||
// commands.executeCommand('editor.action.triggerSuggest')
|
||||
// }
|
||||
// })
|
||||
// context.subscriptions.push(listener)
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
|
||||
let watcher = Workspace.createFileSystemWatcher(`**/${CONFIG_FILE_GLOB}`, false, true, true)
|
||||
|
||||
watcher.onDidCreate((uri) => {
|
||||
|
@ -378,7 +435,11 @@ export async function activate(context: ExtensionContext) {
|
|||
async resolveCompletionItem(item, token, next) {
|
||||
let result = await next(item, token)
|
||||
let selections = Window.activeTextEditor.selections
|
||||
if (selections.length > 1 && result.additionalTextEdits?.length > 0) {
|
||||
if (
|
||||
result['data'] === 'variant' &&
|
||||
selections.length > 1 &&
|
||||
result.additionalTextEdits?.length > 0
|
||||
) {
|
||||
let length =
|
||||
selections[0].start.character - result.additionalTextEdits[0].range.start.character
|
||||
let prefixLength =
|
||||
|
|
Loading…
Reference in New Issue