Skip to content

Commit 4515659

Browse files
committed
Collect Scopes about at-rules
1 parent 86d580f commit 4515659

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

packages/tailwindcss-language-service/src/scopes/analyze.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument'
22
import type { DocumentClassList, Span, State } from '../util/state'
33
import type {
44
ScopeContext,
5+
ScopeAtRule,
56
ScopeClassList,
67
ScopeClassName,
78
} from './scope'
@@ -109,3 +110,108 @@ async function* analyzeClassLists(
109110
yield listScope
110111
}
111112
}
113+
114+
const AT_RULE = /(?<name>@[a-z-]+)\s*(?<params>[^;{]*)(?:[;{]|$)/dg
115+
116+
interface AtRuleDescriptor {
117+
name: Span
118+
params: Span
119+
body: Span | null
120+
}
121+
122+
/**
123+
* Find all class lists and classes within the given block of text
124+
*/
125+
function* analyzeAtRules(boundary: LanguageBoundary, slice: string): Iterable<ScopeAtRule> {
126+
if (boundary.type !== 'css') return
127+
128+
let rules: AtRuleDescriptor[] = []
129+
130+
// Look for at-rules like `@apply` and `@screen`
131+
// These _may not be complete_ but that's okay
132+
// We just want to know that they're there and where they are
133+
for (let match of slice.matchAll(AT_RULE)) {
134+
rules.push({
135+
name: match.indices!.groups!.name,
136+
params: match.indices!.groups!.params,
137+
body: null,
138+
})
139+
}
140+
141+
for (let rule of rules) {
142+
// Scan forward from each at-rule to find the body
143+
// This is a bit naive but it's good enough for now
144+
let depth = 0
145+
for (let i = rule.params[1]; i < slice.length; ++i) {
146+
if (depth === 0 && slice[i] === ';') {
147+
break
148+
}
149+
150+
//
151+
else if (slice[i] === '{') {
152+
depth += 1
153+
154+
if (depth === 1) {
155+
rule.body = [i, i]
156+
}
157+
}
158+
159+
//
160+
else if (slice[i] === '}') {
161+
depth -= 1
162+
163+
if (depth === 0) {
164+
rule.body![1] = i
165+
break
166+
}
167+
}
168+
}
169+
170+
for (let scope of analyzeAtRule(boundary, slice, rule)) {
171+
yield scope
172+
}
173+
}
174+
}
175+
176+
function* analyzeAtRule(
177+
boundary: LanguageBoundary,
178+
slice: string,
179+
desc: AtRuleDescriptor,
180+
): Iterable<ScopeAtRule> {
181+
let name = slice.slice(desc.name[0], desc.name[1])
182+
let params = slice.slice(desc.params[0], desc.params[1]).trim()
183+
let parts = segment(params, ' ')
184+
185+
// Offset spans so they're document-relative
186+
let offset = boundary.span[0]
187+
let overallSpan: Span = [desc.name[0], desc.body ? desc.body[1] : desc.params[1]]
188+
let nameSpan: Span = [desc.name[0], desc.name[1]]
189+
let paramsSpan: Span = [desc.params[0], desc.params[1]]
190+
let bodySpan: Span | null = desc.body ? [desc.body[0], desc.body[1]] : null
191+
192+
overallSpan[0] += offset
193+
overallSpan[1] += offset
194+
nameSpan[0] += offset
195+
nameSpan[1] += offset
196+
paramsSpan[0] += offset
197+
paramsSpan[1] += offset
198+
199+
if (bodySpan) {
200+
bodySpan[0] += offset
201+
bodySpan[1] += offset
202+
}
203+
204+
let scope: ScopeAtRule = {
205+
kind: 'css.at-rule',
206+
children: [],
207+
208+
source: {
209+
scope: overallSpan,
210+
name: nameSpan,
211+
params: paramsSpan,
212+
body: bodySpan,
213+
},
214+
}
215+
216+
yield scope
217+
}

packages/tailwindcss-language-service/src/scopes/scope.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,40 @@ export interface ScopeClassName {
167167
}
168168
}
169169

170+
/**
171+
* Represents an at-rule in CSS
172+
*
173+
* ```
174+
* @media (min-width: 600px) { ... }
175+
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
176+
* @apply bg-blue-500 text-white;
177+
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
178+
* ```
179+
*/
180+
export interface ScopeAtRule {
181+
kind: 'css.at-rule'
182+
children: AnyScope[]
183+
184+
source: {
185+
scope: Span
186+
187+
// Marks the name of an at-rule
188+
// @media (min-width: 600px) { ... }
189+
// ^^^^^^
190+
name: Span
191+
192+
// Marks the parameters of an at-rule
193+
// @media (min-width: 600px) { ... }
194+
// ^^^^^^^^^^^^^^^^^^
195+
params: Span
196+
197+
// Marks the body of an at-rule
198+
// @media (min-width: 600px) { ... }
199+
// ^^^^^
200+
body: Span | null
201+
}
202+
}
203+
170204
export type ScopeKind = keyof ScopeMap
171205
export type Scope<K extends ScopeKind> = ScopeMap[K]
172206
export type AnyScope = ScopeMap[ScopeKind]
@@ -177,4 +211,5 @@ type ScopeMap = {
177211
'class.attr': ScopeClassAttribute
178212
'class.list': ScopeClassList
179213
'class.name': ScopeClassName
214+
'css.at-rule': ScopeAtRule
180215
}

0 commit comments

Comments
 (0)