Skip to content

Commit e79f72b

Browse files
committed
wip quick fix for invalid @apply
1 parent d76119c commit e79f72b

File tree

13 files changed

+455
-93
lines changed

13 files changed

+455
-93
lines changed

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@
166166
"chokidar": "^3.3.1",
167167
"concurrently": "^5.1.0",
168168
"css.escape": "^1.5.1",
169+
"detect-indent": "^6.0.0",
169170
"dlv": "^1.1.3",
170171
"dset": "^2.0.1",
171172
"esm": "^3.2.25",

src/class-names/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ export default async function getClassNames(
133133
postcss,
134134
browserslist,
135135
}),
136+
modules: {
137+
tailwindcss,
138+
postcss,
139+
},
136140
}
137141
}
138142

src/lsp/providers/codeActionProvider.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import {
2+
CodeAction,
3+
CodeActionParams,
4+
CodeActionKind,
5+
Range,
6+
TextEdit,
7+
Diagnostic,
8+
} from 'vscode-languageserver'
9+
import { State } from '../../util/state'
10+
import { findLast, findClassNamesInRange } from '../../util/find'
11+
import { isWithinRange } from '../../util/isWithinRange'
12+
import { getClassNameParts } from '../../util/getClassNameAtPosition'
13+
const dlv = require('dlv')
14+
import dset from 'dset'
15+
import { removeRangeFromString } from '../../util/removeRangeFromString'
16+
import detectIndent from 'detect-indent'
17+
import { cssObjToAst } from '../../util/cssObjToAst'
18+
import isObject from '../../../util/isObject'
19+
20+
export function provideCodeActions(
21+
state: State,
22+
params: CodeActionParams
23+
): Promise<CodeAction[]> {
24+
if (params.context.diagnostics.length === 0) {
25+
return null
26+
}
27+
28+
return Promise.all(
29+
params.context.diagnostics
30+
.map((diagnostic) => {
31+
if (diagnostic.code === 'invalidApply') {
32+
return provideInvalidApplyCodeAction(state, params, diagnostic)
33+
}
34+
35+
let match = findLast(
36+
/ Did you mean (?:something like )?'(?<replacement>[^']+)'\?$/g,
37+
diagnostic.message
38+
)
39+
40+
if (!match) {
41+
return null
42+
}
43+
44+
return {
45+
title: `Replace with '${match.groups.replacement}'`,
46+
kind: CodeActionKind.QuickFix,
47+
diagnostics: [diagnostic],
48+
edit: {
49+
changes: {
50+
[params.textDocument.uri]: [
51+
{
52+
range: diagnostic.range,
53+
newText: match.groups.replacement,
54+
},
55+
],
56+
},
57+
},
58+
}
59+
})
60+
.filter(Boolean)
61+
)
62+
}
63+
64+
function classNameToAst(
65+
state: State,
66+
className: string,
67+
selector: string = `.${className}`,
68+
important: boolean = false
69+
) {
70+
const parts = getClassNameParts(state, className)
71+
if (!parts) {
72+
return null
73+
}
74+
const baseClassName = dlv(
75+
state.classNames.classNames,
76+
parts[parts.length - 1]
77+
)
78+
if (!baseClassName) {
79+
return null
80+
}
81+
const info = dlv(state.classNames.classNames, parts)
82+
let context = info.__context || []
83+
let pseudo = info.__pseudo || []
84+
const globalContexts = state.classNames.context
85+
let screens = dlv(
86+
state.config,
87+
'theme.screens',
88+
dlv(state.config, 'screens', {})
89+
)
90+
if (!isObject(screens)) screens = {}
91+
screens = Object.keys(screens)
92+
const path = []
93+
94+
for (let i = 0; i < parts.length - 1; i++) {
95+
let part = parts[i]
96+
let common = globalContexts[part]
97+
if (!common) return null
98+
if (screens.includes(part)) {
99+
path.push(`@screen ${part}`)
100+
context = context.filter((con) => !common.includes(con))
101+
}
102+
}
103+
104+
path.push(...context)
105+
106+
let obj = {}
107+
for (let i = 1; i <= path.length; i++) {
108+
dset(obj, path.slice(0, i), {})
109+
}
110+
let rule = {
111+
// TODO: use proper selector parser
112+
[selector + pseudo.join('')]: {
113+
[`@apply ${parts[parts.length - 1]}${
114+
important ? ' !important' : ''
115+
}`]: '',
116+
},
117+
}
118+
if (path.length) {
119+
dset(obj, path, rule)
120+
} else {
121+
obj = rule
122+
}
123+
124+
return cssObjToAst(obj, state.modules.postcss)
125+
}
126+
127+
async function provideInvalidApplyCodeAction(
128+
state: State,
129+
params: CodeActionParams,
130+
diagnostic: Diagnostic
131+
): Promise<CodeAction> {
132+
let document = state.editor.documents.get(params.textDocument.uri)
133+
let documentText = document.getText()
134+
const { postcss } = state.modules
135+
let change: TextEdit
136+
137+
let documentClassNames = findClassNamesInRange(
138+
document,
139+
{
140+
start: {
141+
line: Math.max(0, diagnostic.range.start.line - 10),
142+
character: 0,
143+
},
144+
end: { line: diagnostic.range.start.line + 10, character: 0 },
145+
},
146+
'css'
147+
)
148+
let documentClassName = documentClassNames.find((className) =>
149+
isWithinRange(diagnostic.range.start, className.range)
150+
)
151+
if (!documentClassName) {
152+
return null
153+
}
154+
let totalClassNamesInClassList = documentClassName.classList.classList.split(
155+
/\s+/
156+
).length
157+
158+
await postcss([
159+
postcss.plugin('', (_options = {}) => {
160+
return (root) => {
161+
root.walkRules((rule) => {
162+
if (change) return false
163+
164+
rule.walkAtRules('apply', (atRule) => {
165+
let { start, end } = atRule.source
166+
let range: Range = {
167+
start: {
168+
line: start.line - 1,
169+
character: start.column - 1,
170+
},
171+
end: {
172+
line: end.line - 1,
173+
character: end.column - 1,
174+
},
175+
}
176+
177+
if (!isWithinRange(diagnostic.range.start, range)) {
178+
// keep looking
179+
return true
180+
}
181+
182+
let className = document.getText(diagnostic.range)
183+
let ast = classNameToAst(
184+
state,
185+
className,
186+
rule.selector,
187+
documentClassName.classList.important
188+
)
189+
190+
if (!ast) {
191+
return false
192+
}
193+
194+
rule.after(ast.nodes)
195+
let insertedRule = rule.next()
196+
197+
if (totalClassNamesInClassList === 1) {
198+
atRule.remove()
199+
}
200+
201+
let outputIndent: string
202+
let documentIndent = detectIndent(documentText)
203+
204+
change = {
205+
range: {
206+
start: {
207+
line: rule.source.start.line - 1,
208+
character: rule.source.start.column - 1,
209+
},
210+
end: {
211+
line: rule.source.end.line - 1,
212+
character: rule.source.end.column,
213+
},
214+
},
215+
newText:
216+
rule.toString() +
217+
(insertedRule.raws.before || '\n\n') +
218+
insertedRule
219+
.toString()
220+
.replace(/\n\s*\n/g, '\n')
221+
.replace(/(@apply [^;\n]+)$/gm, '$1;')
222+
.replace(/([^\s^]){$/gm, '$1 {')
223+
.replace(/^\s+/gm, (m: string) => {
224+
if (typeof outputIndent === 'undefined') outputIndent = m
225+
return m.replace(
226+
new RegExp(outputIndent, 'g'),
227+
documentIndent.indent
228+
)
229+
}),
230+
}
231+
232+
return false
233+
})
234+
})
235+
}
236+
}),
237+
]).process(documentText, { from: undefined })
238+
239+
if (!change) {
240+
return null
241+
}
242+
243+
return {
244+
title: 'Extract to new rule.',
245+
kind: CodeActionKind.QuickFix,
246+
diagnostics: [diagnostic],
247+
edit: {
248+
changes: {
249+
[params.textDocument.uri]: [
250+
...(totalClassNamesInClassList > 1
251+
? [
252+
{
253+
range: documentClassName.classList.range,
254+
newText: removeRangeFromString(
255+
documentClassName.classList.classList,
256+
documentClassName.relativeRange
257+
),
258+
},
259+
]
260+
: []),
261+
change,
262+
],
263+
},
264+
},
265+
}
266+
}

src/lsp/providers/diagnosticsProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function getInvalidApplyDiagnostics(
7373
: DiagnosticSeverity.Warning,
7474
range,
7575
message,
76+
code: 'invalidApply',
7677
}
7778
})
7879
.filter(Boolean)

src/lsp/server.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,11 @@ connection.onHover(
230230
}
231231
)
232232

233-
connection.onCodeAction((params: CodeActionParams): CodeAction[] => {
234-
if (!state.enabled) return null
235-
return provideCodeActions(state, params)
236-
})
233+
connection.onCodeAction(
234+
(params: CodeActionParams): Promise<CodeAction[]> => {
235+
if (!state.enabled) return null
236+
return provideCodeActions(state, params)
237+
}
238+
)
237239

238240
connection.listen()

0 commit comments

Comments
 (0)