Skip to content
5 changes: 5 additions & 0 deletions packages/tailwindcss-language-server/src/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,11 @@ export async function createProjectService(
let elapsed = process.hrtime.bigint() - start

console.log(`---- RELOADED IN ${(Number(elapsed) / 1e6).toFixed(2)}ms ----`)

let isTestMode = params.initializationOptions?.testMode ?? false
if (!isTestMode) return

connection.sendNotification('@/tailwindCSS/projectReloaded')
},

state,
Expand Down
42 changes: 30 additions & 12 deletions packages/tailwindcss-language-server/src/testing/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { onTestFinished, test, TestOptions } from 'vitest'
import { onTestFinished, test, TestContext, TestOptions } from 'vitest'
import * as os from 'node:os'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as proc from 'node:child_process'
import dedent from 'dedent'

export interface TestUtils {
export interface TestUtils<TestInput extends Record<string, any>> {
/** The "cwd" for this test */
root: string

/**
* The input for this test — taken from the `inputs` in the test config
*
* @see {TestConfig}
*/
input?: TestInput
}

export interface StorageSymlink {
Expand All @@ -21,29 +28,39 @@ export interface Storage {
[filePath: string]: string | Uint8Array | StorageSymlink
}

export interface TestConfig<Extras extends {}> {
export interface TestConfig<Extras extends {}, TestInput extends Record<string, any>> {
name: string
inputs?: TestInput[]

fs?: Storage
debug?: boolean
prepare?(utils: TestUtils): Promise<Extras>
handle(utils: TestUtils & Extras): void | Promise<void>
prepare?(utils: TestUtils<TestInput>): Promise<Extras>
handle(utils: TestUtils<TestInput> & Extras): void | Promise<void>

options?: TestOptions
}

export function defineTest<T>(config: TestConfig<T>) {
return test(config.name, config.options ?? {}, async ({ expect }) => {
let utils = await setup(config)
export function defineTest<T, I>(config: TestConfig<T, I>) {
async function runTest(ctx: TestContext, input?: I) {
let utils = await setup(config, input)
let extras = await config.prepare?.(utils)

await config.handle({
...utils,
...extras,
})
})
}

if (config.inputs) {
return test.for(config.inputs ?? [])(config.name, config.options ?? {}, (input, ctx) =>
runTest(ctx, input),
)
}

return test(config.name, config.options ?? {}, runTest)
}

async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {
async function setup<T, I>(config: TestConfig<T, I>, input: I): Promise<TestUtils<I>> {
let randomId = Math.random().toString(36).substring(7)

let baseDir = path.resolve(process.cwd(), `../../.debug/${randomId}`)
Expand All @@ -56,7 +73,7 @@ async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {
await installDependencies(baseDir, config.fs)
}

onTestFinished(async (result) => {
onTestFinished(async (ctx) => {
// Once done, move all the files to a new location
try {
await fs.rename(baseDir, doneDir)
Expand All @@ -66,7 +83,7 @@ async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {
console.error('Failed to move test files to done directory')
}

if (result.state === 'fail') return
if (ctx.task.result?.state === 'fail') return

if (path.sep === '\\') return

Expand All @@ -79,6 +96,7 @@ async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {

return {
root: baseDir,
input,
}
}

Expand Down
44 changes: 41 additions & 3 deletions packages/tailwindcss-language-server/src/tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import { readCssFile } from './util/css'
import { ProjectLocator, type ProjectConfig } from './project-locator'
import type { TailwindCssSettings } from '@tailwindcss/language-service/src/util/state'
import { createResolver, Resolver } from './resolver'
import { retry } from './util/retry'
import { analyzeStylesheet } from './version-guesser.js'

const TRIGGER_CHARACTERS = [
// class attributes
Expand Down Expand Up @@ -382,6 +382,13 @@ export class TW {
for (let [, project] of this.projects) {
if (!project.state.v4) continue

if (
change.type === FileChangeType.Deleted &&
changeAffectsFile(normalizedFilename, [project.projectConfig.configPath])
) {
continue
}

if (!changeAffectsFile(normalizedFilename, project.dependencies())) continue

needsSoftRestart = true
Expand All @@ -405,6 +412,31 @@ export class TW {
needsRestart = true
break
}

//
else {
// If the main CSS file in a project is deleted and then re-created
// the server won't restart because the project is gone by now and
// there's no concept of a "config file" for us to compare with
//
// So we'll check if the stylesheet could *potentially* create
// a new project but we'll only do so if no projects were found
//
// If we did this all the time we'd potentially restart the server
// unncessarily a lot while the user is editing their stylesheets
if (this.projects.size > 0) continue

let content = await readCssFile(change.file)
if (!content) continue

let stylesheet = analyzeStylesheet(content)
if (!stylesheet.root) continue

if (!stylesheet.versions.includes('4')) continue

needsRestart = true
break
}
}

let isConfigFile = isConfigMatcher(normalizedFilename)
Expand Down Expand Up @@ -1041,11 +1073,17 @@ export class TW {
this.watched.length = 0
}

restart(): void {
async restart(): void {
let isTestMode = this.initializeParams.initializationOptions?.testMode ?? false

console.log('----------\nRESTARTING\n----------')
this.dispose()
this.initPromise = undefined
this.init()
await this.init()

if (isTestMode) {
this.connection.sendNotification('@/tailwindCSS/serverRestarted')
}
}

async softRestart(): Promise<void> {
Expand Down
169 changes: 169 additions & 0 deletions packages/tailwindcss-language-server/tests/env/restart.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { expect } from 'vitest'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { css, defineTest } from '../../src/testing'
import dedent from 'dedent'
import { createClient } from '../utils/client'

defineTest({
name: 'The design system is reloaded when the CSS changes ($watcher)',
fs: {
'app.css': css`
@import 'tailwindcss';

@theme {
--color-primary: #c0ffee;
}
`,
},
prepare: async ({ root }) => ({
client: await createClient({
root,
capabilities(caps) {
caps.workspace!.didChangeWatchedFiles!.dynamicRegistration = false
},
}),
}),
handle: async ({ root, client }) => {
let doc = await client.open({
lang: 'html',
text: '<div class="text-primary">',
})

// <div class="text-primary">
// ^
let hover = await doc.hover({ line: 0, character: 13 })

expect(hover).toEqual({
contents: {
language: 'css',
value: dedent`
.text-primary {
color: var(--color-primary) /* #c0ffee */;
}
`,
},
range: {
start: { line: 0, character: 12 },
end: { line: 0, character: 24 },
},
})

let didReload = new Promise((resolve) => {
client.conn.onNotification('@/tailwindCSS/projectReloaded', resolve)
})

// Update the CSS
await fs.writeFile(
path.resolve(root, 'app.css'),
css`
@import 'tailwindcss';

@theme {
--color-primary: #bada55;
}
`,
)

await didReload

// <div class="text-primary">
// ^
let hover2 = await doc.hover({ line: 0, character: 13 })

expect(hover2).toEqual({
contents: {
language: 'css',
value: dedent`
.text-primary {
color: var(--color-primary) /* #bada55 */;
}
`,
},
range: {
start: { line: 0, character: 12 },
end: { line: 0, character: 24 },
},
})
},
})

defineTest({
options: {
retry: 3,
},
name: 'Server is "restarted" when a config file is removed',
fs: {
'app.css': css`
@import 'tailwindcss';

@theme {
--color-primary: #c0ffee;
}
`,
},
prepare: async ({ root }) => ({
client: await createClient({
root,
capabilities(caps) {
caps.workspace!.didChangeWatchedFiles!.dynamicRegistration = false
},
}),
}),
handle: async ({ root, client }) => {
let doc = await client.open({
lang: 'html',
text: '<div class="text-primary">',
})

// <div class="text-primary">
// ^
let hover = await doc.hover({ line: 0, character: 13 })

expect(hover).toEqual({
contents: {
language: 'css',
value: dedent`
.text-primary {
color: var(--color-primary) /* #c0ffee */;
}
`,
},
range: {
start: { line: 0, character: 12 },
end: { line: 0, character: 24 },
},
})

// Remove the CSS file
let didRestart = new Promise((resolve) => {
client.conn.onNotification('@/tailwindCSS/serverRestarted', resolve)
})
await fs.unlink(path.resolve(root, 'app.css'))
await didRestart

// <div class="text-primary">
// ^
let hover2 = await doc.hover({ line: 0, character: 13 })
expect(hover2).toEqual(null)

// Re-create the CSS file
let didRestartAgain = new Promise((resolve) => {
client.conn.onNotification('@/tailwindCSS/serverRestarted', resolve)
})
await fs.writeFile(
path.resolve(root, 'app.css'),
css`
@import 'tailwindcss';
`,
)
await didRestartAgain

await new Promise((resolve) => setTimeout(resolve, 500))

// <div class="text-primary">
// ^
let hover3 = await doc.hover({ line: 0, character: 13 })
expect(hover3).toEqual(null)
},
})
8 changes: 7 additions & 1 deletion packages/tailwindcss-language-server/tests/utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
CompletionList,
CompletionParams,
Diagnostic,
DidChangeWorkspaceFoldersNotification,
Disposable,
DocumentLink,
DocumentLinkRequest,
Expand Down Expand Up @@ -179,6 +178,11 @@ export interface ClientOptions extends ConnectOptions {
* and the Tailwind CSS version it detects
*/
features?: Feature[]

/**
* Tweak the client capabilities presented to the server
*/
capabilities?(caps: ClientCapabilities): ClientCapabilities | Promise<ClientCapabilities> | void
}

export interface Client extends ClientWorkspace {
Expand Down Expand Up @@ -394,6 +398,8 @@ export async function createClient(opts: ClientOptions): Promise<Client> {
},
}

capabilities = (await opts.capabilities?.(capabilities)) ?? capabilities

trace('Client initializing')
await conn.sendRequest(InitializeRequest.type, {
processId: process.pid,
Expand Down
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Only scan the file system once when needed ([#1287](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1287))
- Don't follow recursive symlinks when searching for projects ([#1270](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1270))
- Correctly re-create a project when its main config file is removed then re-created ([#1300](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1300))

# 0.14.13

Expand Down