Skip to content

Commit

Permalink
Improve support for source(…) feature in v4 (#1083)
Browse files Browse the repository at this point in the history
This PR mades a fair number of changes to improve the developer
experience in v4. Most of it is related to `@source` and `source(…)` but
there are some additional tweaks:

- [x] Make sure `@import "…" source(…)` does not issue syntax warnings
- [x] Make sure `@import "…" theme(…)` does not issue syntax warnings
- [x] Make sure `@import "…" prefix(…)` does not issue syntax warnings
- [x] Make sure `@tailwind utilities source(…)` isn't diagnosed as
invalid
- [x] Add suggestions for `@theme` options
- [x] Add suggestions for `@import "…" theme(…)` options
- [x] Directory auto-completion for `source(…)`
- [x] Directory auto-completion for `@source "…"`
- [x] Don’t suggest TypeScript declaration files for `@config`,
`@plugin`, and `@source`
- [x] In a new workspace creating/editing a CSS file should check if
it's the language server needs to start.
- [x] Show brace expansion when hovering `@source`
- [x] Highlight `@import "…" source(…)` properly
- [x] Highlight `@import "…" theme(…)` properly
- [x] Highlight `@import "…" prefix(…)` properly
- [x] Highlight `@tailwind utilities source(…)` properly
- [x] Highlight uses of `source(none)` explicitly
- [x] Link paths in valid uses of `source("…")`
- [x] Don't link Windows-style paths in `@source`, `@config`, and
`@plugin`
- [x] Warn that `@source none` is invalid
- [x] Highlight `@source none` as invalid (theme-dependent)
- [x] Warn when `source(…)` is not passed anything
- [x] Warn when `source(none)` is mispelled
- [x] Warn when a v4 project uses `@tailwind base` or `@tailwind
components`
- [x] Warn when non-POSIX paths are passed to `@source` and `source(…)`
- [x] Warn when `@tailwind base` is used in a v4 project
- [x] Warn when `@tailwind preflight` is used in a v4 project
- [x] Warn when `@tailwind components` is used in a v4 project
- [x] Warn when `@tailwind screens` is used in a v4 project
- [x] Warn when `@tailwind variants` is used in a v4 project


I had some stretch goals but I don't think I'll get to them in this PR
unless we think they're important enough to hold up the PR:

- [ ] Warn when braces surround a single item in `@source` globs
- [ ] Warn when unsupported glob syntax is used in `@source`
- [ ] Warn when a v4 project uses `@import "tailwindcss/tailwind"`
- [ ] Warn when a v4 project uses `@import "tailwindcss/tailwind.css"`
- [ ] Highlight glob parts in `@source "…"` strings
- [ ] Auto-complete `source(none)` when typing `source(…)`

---------

Co-authored-by: Robin Malfait <[email protected]>
  • Loading branch information
thecrypticace and RobinMalfait authored Nov 15, 2024
1 parent 55050a4 commit 20e1448
Show file tree
Hide file tree
Showing 28 changed files with 1,276 additions and 109 deletions.
1 change: 1 addition & 0 deletions packages/tailwindcss-language-server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function getDefaultSettings(): Settings {
invalidVariant: 'error',
invalidConfigPath: 'error',
invalidTailwindDirective: 'error',
invalidSourceDirective: 'error',
recommendedVariantOrder: 'warning',
},
showPixelEquivalents: true,
Expand Down
20 changes: 14 additions & 6 deletions packages/tailwindcss-language-server/src/language/cssServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,7 @@ function replace(delta = 0) {
}

function createVirtualCssDocument(textDocument: TextDocument): TextDocument {
return TextDocument.create(
textDocument.uri,
textDocument.languageId,
textDocument.version,
textDocument
let content = textDocument
.getText()
.replace(/@screen(\s+[^{]+){/g, replace(-2))
.replace(/@variants(\s+[^{]+){/g, replace())
Expand All @@ -350,7 +346,19 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument {
/@media(\s+screen\s*\([^)]+\))/g,
(_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`,
)
.replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_'),
// Remove`source(…)`, `theme(…)`, and `prefix(…)` from `@import`s
// otherwise we'll show syntax-error diagnostics which we don't want
.replace(
/@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*((source|theme|prefix)\([^)]+\)\s*)+/g,
(_match, url) => `@import "${url.slice(1, -1)}"`,
)
.replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_')

return TextDocument.create(
textDocument.uri,
textDocument.languageId,
textDocument.version,
content,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,90 @@ withFixture('v4/dependencies', (c) => {
],
})
})

test.concurrent('@import "…" source(…)', async ({ expect }) => {
let result = await completion({
text: '@import "tailwindcss" source("',
lang: 'css',
position: {
line: 0,
character: 30,
},
})

expect(result).toEqual({
isIncomplete: false,
items: [
{
label: 'sub-dir/',
kind: 19,
command: { command: 'editor.action.triggerSuggest', title: '' },
data: expect.anything(),
textEdit: {
newText: 'sub-dir/',
range: { start: { line: 0, character: 30 }, end: { line: 0, character: 30 } },
},
},
],
})
})

test.concurrent('@tailwind utilities source(…)', async ({ expect }) => {
let result = await completion({
text: '@tailwind utilities source("',
lang: 'css',
position: {
line: 0,
character: 28,
},
})

expect(result).toEqual({
isIncomplete: false,
items: [
{
label: 'sub-dir/',
kind: 19,
command: { command: 'editor.action.triggerSuggest', title: '' },
data: expect.anything(),
textEdit: {
newText: 'sub-dir/',
range: { start: { line: 0, character: 28 }, end: { line: 0, character: 28 } },
},
},
],
})
})

test.concurrent('@import "…" source(…) directory', async ({ expect }) => {
let result = await completion({
text: '@import "tailwindcss" source("sub-dir/',
lang: 'css',
position: {
line: 0,
character: 38,
},
})

expect(result).toEqual({
isIncomplete: false,
items: [],
})
})

test.concurrent('@tailwind utilities source(…) directory', async ({ expect }) => {
let result = await completion({
text: '@tailwind utilities source("sub-dir/',
lang: 'css',
position: {
line: 0,
character: 36,
},
})

expect(result).toEqual({
isIncomplete: false,
items: [],
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,40 @@ withFixture('v4/basic', (c) => {
)
})

test.concurrent('@theme suggests options', async ({ expect }) => {
let result = await completion({
lang: 'css',
text: '@theme ',
position: { line: 0, character: 7 },
})

expect(result.items.length).toBe(3)
expect(result.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ label: 'reference' }),
expect.objectContaining({ label: 'inline' }),
expect.objectContaining({ label: 'default' }),
]),
)
})

test.concurrent('@import "…" theme(…) suggests options', async ({ expect }) => {
let result = await completion({
lang: 'css',
text: '@import "tailwindcss/theme" theme()',
position: { line: 0, character: 34 },
})

expect(result.items.length).toBe(3)
expect(result.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ label: 'reference' }),
expect.objectContaining({ label: 'inline' }),
expect.objectContaining({ label: 'default' }),
]),
)
})

test.concurrent('resolve', async ({ expect }) => {
let result = await completion({
text: '<div class="">',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,72 @@ withFixture('v4/basic', (c) => {
},
],
})

testMatch('Old Tailwind directives warn when used in a v4 project', {
language: 'css',
code: `
@tailwind base;
@tailwind preflight;
@tailwind components;
@tailwind screens;
@tailwind variants;
`,
expected: [
{
code: 'invalidTailwindDirective',
message:
"'@tailwind base' is no longer available in v4. Use '@import \"tailwindcss/preflight\"' instead.",
suggestions: [],
range: {
start: { line: 1, character: 16 },
end: { line: 1, character: 20 },
},
severity: 1,
},
{
code: 'invalidTailwindDirective',
message:
"'@tailwind preflight' is no longer available in v4. Use '@import \"tailwindcss/preflight\"' instead.",
suggestions: [],
range: {
start: { line: 2, character: 16 },
end: { line: 2, character: 25 },
},
severity: 1,
},
{
code: 'invalidTailwindDirective',
message:
"'@tailwind components' is no longer available in v4. Use '@tailwind utilities' instead.",
suggestions: ['utilities'],
range: {
start: { line: 3, character: 16 },
end: { line: 3, character: 26 },
},
severity: 1,
},
{
code: 'invalidTailwindDirective',
message:
"'@tailwind screens' is no longer available in v4. Use '@tailwind utilities' instead.",
suggestions: ['utilities'],
range: {
start: { line: 4, character: 16 },
end: { line: 4, character: 23 },
},
severity: 1,
},
{
code: 'invalidTailwindDirective',
message:
"'@tailwind variants' is no longer available in v4. Use '@tailwind utilities' instead.",
suggestions: ['utilities'],
range: {
start: { line: 5, character: 16 },
end: { line: 5, character: 24 },
},
severity: 1,
},
],
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { expect, test } from 'vitest'
import { withFixture } from '../common'

withFixture('v4/basic', (c) => {
function runTest(name, { code, expected, language }) {
test(name, async () => {
let promise = new Promise((resolve) => {
c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => {
resolve(diagnostics)
})
})

let doc = await c.openDocument({ text: code, lang: language })
let diagnostics = await promise

expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', doc.uri))

expect(diagnostics).toMatchObject(expected)
})
}

runTest('Source directives require paths', {
language: 'css',
code: `
@import 'tailwindcss' source();
@import 'tailwindcss' source('');
@import 'tailwindcss' source("");
@tailwind utilities source();
@tailwind utilities source('');
@tailwind utilities source("");
`,
expected: [
{
code: 'invalidSourceDirective',
message: 'The source directive requires a path to a directory.',
range: {
start: { line: 1, character: 35 },
end: { line: 1, character: 35 },
},
},
{
code: 'invalidSourceDirective',
message: 'The source directive requires a path to a directory.',
range: {
start: { line: 2, character: 35 },
end: { line: 2, character: 37 },
},
},
{
code: 'invalidSourceDirective',
message: 'The source directive requires a path to a directory.',
range: {
start: { line: 3, character: 35 },
end: { line: 3, character: 37 },
},
},
{
code: 'invalidSourceDirective',
message: 'The source directive requires a path to a directory.',
range: {
start: { line: 4, character: 33 },
end: { line: 4, character: 33 },
},
},
{
code: 'invalidSourceDirective',
message: 'The source directive requires a path to a directory.',
range: {
start: { line: 5, character: 33 },
end: { line: 5, character: 35 },
},
},
{
code: 'invalidSourceDirective',
message: 'The source directive requires a path to a directory.',
range: {
start: { line: 6, character: 33 },
end: { line: 6, character: 35 },
},
},
],
})

runTest('source(none) must not be misspelled', {
language: 'css',
code: `
@import 'tailwindcss' source(no);
@tailwind utilities source(no);
`,
expected: [
{
code: 'invalidSourceDirective',
message: '`source(no)` is invalid. Did you mean `source(none)`?',
range: {
start: { line: 1, character: 35 },
end: { line: 1, character: 37 },
},
},
{
code: 'invalidSourceDirective',
message: '`source(no)` is invalid. Did you mean `source(none)`?',
range: {
start: { line: 2, character: 33 },
end: { line: 2, character: 35 },
},
},
],
})

runTest('source("…") does not produce diagnostics', {
language: 'css',
code: `
@import 'tailwindcss' source('../app');
@tailwind utilities source('../app');
@import 'tailwindcss' source("../app");
@tailwind utilities source("../app");
`,
expected: [],
})

runTest('paths given to source("…") must error when not POSIX', {
language: 'css',
code: String.raw`
@import 'tailwindcss' source('C:\\absolute\\path');
@import 'tailwindcss' source('C:relative.txt');
`,
expected: [
{
code: 'invalidSourceDirective',
message:
'POSIX-style paths are required with `source(…)` but `C:\\absolute\\path` is a Windows-style path.',
range: {
start: { line: 1, character: 35 },
end: { line: 1, character: 55 },
},
},
{
code: 'invalidSourceDirective',
message:
'POSIX-style paths are required with `source(…)` but `C:relative.txt` is a Windows-style path.',
range: {
start: { line: 2, character: 35 },
end: { line: 2, character: 51 },
},
},
],
})
})
Loading

0 comments on commit 20e1448

Please sign in to comment.