tailwint 1.1.0 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +30 -21
- package/dist/lsp.d.ts +14 -4
- package/dist/lsp.js +237 -57
- package/dist/prescan.d.ts +27 -0
- package/dist/prescan.js +76 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import { resolve, relative } from "path";
|
|
12
12
|
import { readFileSync } from "fs";
|
|
13
13
|
import { glob } from "glob";
|
|
14
|
-
import { startServer, send, notify, shutdown, fileUri, langId, diagnosticsReceived,
|
|
14
|
+
import { startServer, send, notify, shutdown, fileUri, langId, diagnosticsReceived, settledProjects, brokenProjects, warnings, waitForAllProjects, resetState, } from "./lsp.js";
|
|
15
15
|
import { fixFile } from "./edits.js";
|
|
16
|
+
import { prescanCssFiles } from "./prescan.js";
|
|
16
17
|
import { c, setTitle, windTrail, braille, windWave, dots, tick, advanceTick, startSpinner, progressBar, banner, fileBadge, diagLine, rainbowText, celebrationAnimation, } from "./ui.js";
|
|
17
18
|
// Re-export for tests
|
|
18
19
|
export { applyEdits } from "./edits.js";
|
|
@@ -85,9 +86,15 @@ export async function run(options = {}) {
|
|
|
85
86
|
notify("initialized", {});
|
|
86
87
|
stopBoot();
|
|
87
88
|
console.error(` ${c.green}\u2714${c.reset} ${c.dim}language server ready${c.reset} ${windTrail(30)}`);
|
|
88
|
-
//
|
|
89
|
+
// Pre-scan CSS files to predict project count
|
|
90
|
+
const prescan = prescanCssFiles(files);
|
|
91
|
+
// Open found files — triggers the server's project discovery
|
|
89
92
|
const fileContents = new Map();
|
|
90
93
|
const fileVersions = new Map();
|
|
94
|
+
const found = files.length;
|
|
95
|
+
const foundText = `sent ${found} file${found === 1 ? "" : "s"} to lsp`;
|
|
96
|
+
const foundPad = 54 - 2 - foundText.length - 1;
|
|
97
|
+
console.error(` ${c.green}\u2714${c.reset} ${c.dim}${foundText}${c.reset} ${windTrail(foundPad)}`);
|
|
91
98
|
for (const filePath of files) {
|
|
92
99
|
let content;
|
|
93
100
|
try {
|
|
@@ -107,27 +114,29 @@ export async function run(options = {}) {
|
|
|
107
114
|
},
|
|
108
115
|
});
|
|
109
116
|
}
|
|
110
|
-
// Wait for
|
|
111
|
-
setTitle("tailwint ~
|
|
112
|
-
const
|
|
117
|
+
// Wait for all projects to be resolved (settled or broken)
|
|
118
|
+
setTitle("tailwint ~ scanning...");
|
|
119
|
+
const stopScan = startSpinner(() => {
|
|
113
120
|
const received = diagnosticsReceived.size;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
const resolved = settledProjects + brokenProjects;
|
|
122
|
+
const label = received > 0 ? "scanning" : "initializing";
|
|
123
|
+
setTitle(`tailwint ~ ${label} ${resolved}/${prescan.maxProjects}`);
|
|
124
|
+
const pct = found > 0 ? Math.round((received / found) * 100) : 0;
|
|
117
125
|
const bar = progressBar(pct, 18, true);
|
|
118
|
-
const totalStr = String(
|
|
126
|
+
const totalStr = String(found);
|
|
119
127
|
const recvStr = String(received).padStart(totalStr.length);
|
|
120
|
-
|
|
121
|
-
const usedCols = 2 + 1 + 1 + 20 + 1 + label.length + 3 + 1 + countText.length + 1;
|
|
122
|
-
const waveCols = Math.max(0, 56 - usedCols);
|
|
123
|
-
return ` ${braille()} ${bar} ${c.dim}${label}${dots()}${c.reset} ${c.bold}${recvStr}${c.reset}${c.dim}/${totalStr}${c.reset} ${windTrail(waveCols, tick)}`;
|
|
128
|
+
return ` ${braille()} ${bar} ${c.dim}${label}${dots()}${c.reset} ${c.bold}${recvStr}${c.reset}${c.dim}/${totalStr} scanned${c.reset} ${windTrail(12, tick)}`;
|
|
124
129
|
}, 80);
|
|
125
|
-
await
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
await waitForAllProjects(prescan.predictedRoots, prescan.maxProjects);
|
|
131
|
+
stopScan();
|
|
132
|
+
// Log warnings for broken projects
|
|
133
|
+
for (const w of warnings) {
|
|
134
|
+
console.error(` ${c.yellow}\u26A0${c.reset} ${c.dim}${w}${c.reset}`);
|
|
135
|
+
}
|
|
136
|
+
const scanned = diagnosticsReceived.size;
|
|
137
|
+
const scannedText = `${scanned}/${found} files received`;
|
|
138
|
+
const scannedPad = 54 - 2 - scannedText.length - 1;
|
|
139
|
+
console.error(` ${c.green}\u2714${c.reset} ${c.dim}${scannedText}${c.reset} ${windTrail(scannedPad)}`);
|
|
131
140
|
console.error("");
|
|
132
141
|
// Collect issues
|
|
133
142
|
let totalIssues = 0;
|
|
@@ -149,13 +158,13 @@ export async function run(options = {}) {
|
|
|
149
158
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
150
159
|
setTitle("tailwint \u2714 all clear");
|
|
151
160
|
await celebrationAnimation();
|
|
152
|
-
console.error(` ${c.green}\u2714${c.reset} ${c.bold}${
|
|
161
|
+
console.error(` ${c.green}\u2714${c.reset} ${c.bold}${scanned}${c.reset} files scanned ${c.dim}// ${rainbowText("all clear")} ${c.dim}${elapsed}s${c.reset}`);
|
|
153
162
|
console.error("");
|
|
154
163
|
await shutdown();
|
|
155
164
|
return 0;
|
|
156
165
|
}
|
|
157
166
|
// Summary
|
|
158
|
-
console.error(` ${c.bold}${c.white}${
|
|
167
|
+
console.error(` ${c.bold}${c.white}${scanned}${c.reset} files scanned ${c.dim}//${c.reset} ${c.orange}${c.bold}${conflicts}${c.reset}${c.orange} conflicts${c.reset} ${c.dim}\u2502${c.reset} ${c.yellow}${c.bold}${canonical}${c.reset}${c.yellow} canonical${c.reset}`);
|
|
159
168
|
console.error("");
|
|
160
169
|
// Report
|
|
161
170
|
let fileNum = 0;
|
package/dist/lsp.d.ts
CHANGED
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const diagnosticsReceived: Map<string, any[]>;
|
|
5
5
|
export declare let projectReady: boolean;
|
|
6
|
+
/** Tracking for projectInitialized events */
|
|
7
|
+
export declare let projectInitCount: number;
|
|
8
|
+
export declare let settledProjects: number;
|
|
9
|
+
export declare let brokenProjects: number;
|
|
10
|
+
export declare const warnings: string[];
|
|
6
11
|
/** Reset module state between runs (for programmatic multi-run usage). */
|
|
7
12
|
export declare function resetState(): void;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Wait for all expected projects to be resolved (settled or broken).
|
|
15
|
+
*
|
|
16
|
+
* @param predictedRoots - Number of CSS files predicted to be project roots
|
|
17
|
+
* @param maxProjects - Upper bound (predictedRoots + predictedNonRoots)
|
|
18
|
+
* @param initTimeoutMs - How long to wait for each projectInitialized event
|
|
19
|
+
* @param debounceMs - Silence window to consider diagnostics "settled"
|
|
20
|
+
*/
|
|
21
|
+
export declare function waitForAllProjects(predictedRoots: number, maxProjects: number, initTimeoutMs?: number, debounceMs?: number): Promise<void>;
|
|
12
22
|
/** Returns a promise that resolves when diagnostics are published for a specific URI. */
|
|
13
23
|
export declare function waitForDiagnostic(uri: string, timeoutMs?: number): Promise<any[]>;
|
|
14
24
|
export declare function startServer(root: string): void;
|
package/dist/lsp.js
CHANGED
|
@@ -3,8 +3,55 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { resolve } from "path";
|
|
6
|
-
import { existsSync } from "fs";
|
|
6
|
+
import { existsSync, readFileSync } from "fs";
|
|
7
7
|
const DEBUG = process.env.DEBUG === "1";
|
|
8
|
+
let workspaceRoot = "";
|
|
9
|
+
let vscodeSettings = null;
|
|
10
|
+
/** Load .vscode/settings.json once, cache the result. */
|
|
11
|
+
function loadVscodeSettings() {
|
|
12
|
+
if (vscodeSettings !== null)
|
|
13
|
+
return vscodeSettings;
|
|
14
|
+
const settingsPath = resolve(workspaceRoot, ".vscode/settings.json");
|
|
15
|
+
if (!existsSync(settingsPath)) {
|
|
16
|
+
vscodeSettings = {};
|
|
17
|
+
return vscodeSettings;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// Strip single-line comments (// ...) and trailing commas for JSON compat
|
|
21
|
+
const raw = readFileSync(settingsPath, "utf-8")
|
|
22
|
+
.replace(/\/\/[^\n]*/g, "")
|
|
23
|
+
.replace(/,\s*([\]}])/g, "$1");
|
|
24
|
+
vscodeSettings = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
vscodeSettings = {};
|
|
28
|
+
}
|
|
29
|
+
return vscodeSettings;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract a section from flat VS Code settings into a nested object.
|
|
33
|
+
* e.g. section "tailwindCSS" turns { "tailwindCSS.lint.cssConflict": "error" }
|
|
34
|
+
* into { lint: { cssConflict: "error" } }
|
|
35
|
+
*/
|
|
36
|
+
function getSettingsSection(section) {
|
|
37
|
+
const settings = loadVscodeSettings();
|
|
38
|
+
const prefix = section + ".";
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
41
|
+
if (!key.startsWith(prefix))
|
|
42
|
+
continue;
|
|
43
|
+
const path = key.slice(prefix.length).split(".");
|
|
44
|
+
let target = result;
|
|
45
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
46
|
+
if (!(path[i] in target) || typeof target[path[i]] !== "object") {
|
|
47
|
+
target[path[i]] = {};
|
|
48
|
+
}
|
|
49
|
+
target = target[path[i]];
|
|
50
|
+
}
|
|
51
|
+
target[path[path.length - 1]] = value;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
8
55
|
let server;
|
|
9
56
|
let serverDead = false;
|
|
10
57
|
let msgId = 0;
|
|
@@ -14,12 +61,25 @@ const pending = new Map();
|
|
|
14
61
|
export const diagnosticsReceived = new Map();
|
|
15
62
|
export let projectReady = false;
|
|
16
63
|
// ---------------------------------------------------------------------------
|
|
17
|
-
//
|
|
64
|
+
// Project-aware wait state
|
|
18
65
|
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
let
|
|
66
|
+
/** Tracking for projectInitialized events */
|
|
67
|
+
export let projectInitCount = 0;
|
|
68
|
+
export let settledProjects = 0;
|
|
69
|
+
export let brokenProjects = 0;
|
|
70
|
+
let lastInitMs = 0;
|
|
71
|
+
let inBrokenSequence = false;
|
|
72
|
+
let awaitingFirstDiag = false;
|
|
73
|
+
let currentProjectDiagCount = 0;
|
|
74
|
+
export const warnings = [];
|
|
75
|
+
/** Internal waiter state */
|
|
76
|
+
let projectWaitResolve = null;
|
|
77
|
+
let diagDebounceTimer = null;
|
|
78
|
+
let projectInitTimer = null;
|
|
79
|
+
let outerTimer = null;
|
|
22
80
|
const diagWaiters = new Map();
|
|
81
|
+
/** Config for the current wait */
|
|
82
|
+
let waitConfig = { predictedRoots: 0, maxProjects: 0, initTimeoutMs: 5000, debounceMs: 500 };
|
|
23
83
|
/** Reset module state between runs (for programmatic multi-run usage). */
|
|
24
84
|
export function resetState() {
|
|
25
85
|
msgId = 0;
|
|
@@ -29,37 +89,172 @@ export function resetState() {
|
|
|
29
89
|
pending.clear();
|
|
30
90
|
diagnosticsReceived.clear();
|
|
31
91
|
projectReady = false;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
92
|
+
projectInitCount = 0;
|
|
93
|
+
settledProjects = 0;
|
|
94
|
+
brokenProjects = 0;
|
|
95
|
+
lastInitMs = 0;
|
|
96
|
+
inBrokenSequence = false;
|
|
97
|
+
awaitingFirstDiag = false;
|
|
98
|
+
currentProjectDiagCount = 0;
|
|
99
|
+
warnings.length = 0;
|
|
100
|
+
projectWaitResolve = null;
|
|
101
|
+
if (diagDebounceTimer) {
|
|
102
|
+
clearTimeout(diagDebounceTimer);
|
|
103
|
+
diagDebounceTimer = null;
|
|
104
|
+
}
|
|
105
|
+
if (projectInitTimer) {
|
|
106
|
+
clearTimeout(projectInitTimer);
|
|
107
|
+
projectInitTimer = null;
|
|
108
|
+
}
|
|
109
|
+
if (outerTimer) {
|
|
110
|
+
clearTimeout(outerTimer);
|
|
111
|
+
outerTimer = null;
|
|
112
|
+
}
|
|
35
113
|
diagWaiters.clear();
|
|
114
|
+
vscodeSettings = null;
|
|
36
115
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
116
|
+
function cleanupWaitTimers() {
|
|
117
|
+
if (diagDebounceTimer) {
|
|
118
|
+
clearTimeout(diagDebounceTimer);
|
|
119
|
+
diagDebounceTimer = null;
|
|
120
|
+
}
|
|
121
|
+
if (projectInitTimer) {
|
|
122
|
+
clearTimeout(projectInitTimer);
|
|
123
|
+
projectInitTimer = null;
|
|
124
|
+
}
|
|
125
|
+
if (outerTimer) {
|
|
126
|
+
clearTimeout(outerTimer);
|
|
127
|
+
outerTimer = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function finishWait() {
|
|
131
|
+
if (!projectWaitResolve)
|
|
132
|
+
return;
|
|
133
|
+
const resolve = projectWaitResolve;
|
|
134
|
+
projectWaitResolve = null;
|
|
135
|
+
cleanupWaitTimers();
|
|
136
|
+
resolve();
|
|
51
137
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
138
|
+
function isAllResolved() {
|
|
139
|
+
const resolved = settledProjects + brokenProjects;
|
|
140
|
+
return resolved >= waitConfig.maxProjects;
|
|
141
|
+
}
|
|
142
|
+
function startProjectInitTimeout() {
|
|
143
|
+
if (projectInitTimer)
|
|
144
|
+
clearTimeout(projectInitTimer);
|
|
145
|
+
projectInitTimer = setTimeout(() => {
|
|
146
|
+
// Timer fired — either no project init came, or we were waiting for
|
|
147
|
+
// more diagnostics after a single early one. If we got any diagnostics
|
|
148
|
+
// for the current project, settle it before finishing.
|
|
149
|
+
if (currentProjectDiagCount > 0 && !awaitingFirstDiag) {
|
|
150
|
+
settleCurrentProject();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
finishWait();
|
|
154
|
+
}
|
|
155
|
+
}, waitConfig.initTimeoutMs);
|
|
156
|
+
}
|
|
157
|
+
function onProjectInitialized() {
|
|
158
|
+
projectInitCount++;
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
projectReady = true;
|
|
161
|
+
if (lastInitMs > 0 && (now - lastInitMs) < 500) {
|
|
162
|
+
// Rapid re-init — broken project
|
|
163
|
+
if (!inBrokenSequence) {
|
|
164
|
+
// First rapid init after a healthy one — the previous healthy init was actually broken
|
|
165
|
+
inBrokenSequence = true;
|
|
166
|
+
brokenProjects++;
|
|
167
|
+
warnings.push("A CSS file failed to initialize (likely an @apply referencing an unknown utility). " +
|
|
168
|
+
"That project's files will not receive diagnostics. " +
|
|
169
|
+
"See https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1121");
|
|
170
|
+
// The previous init was counted as starting a healthy project's diagnostic wait.
|
|
171
|
+
// Cancel that wait — this project won't produce diagnostics.
|
|
172
|
+
if (diagDebounceTimer) {
|
|
173
|
+
clearTimeout(diagDebounceTimer);
|
|
174
|
+
diagDebounceTimer = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Additional rapid inits for the same broken project — just update timestamp
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Healthy init — new project starting
|
|
181
|
+
inBrokenSequence = false;
|
|
182
|
+
awaitingFirstDiag = true;
|
|
183
|
+
currentProjectDiagCount = 0;
|
|
184
|
+
// Cancel any pending project-init timeout since we just got a new one
|
|
185
|
+
if (projectInitTimer) {
|
|
186
|
+
clearTimeout(projectInitTimer);
|
|
187
|
+
projectInitTimer = null;
|
|
188
|
+
}
|
|
189
|
+
if (diagDebounceTimer) {
|
|
190
|
+
clearTimeout(diagDebounceTimer);
|
|
191
|
+
diagDebounceTimer = null;
|
|
192
|
+
}
|
|
193
|
+
// Don't start the diagnostic debounce yet — wait for the first diagnostic to arrive.
|
|
194
|
+
// Use the init timeout as the safety net (if no diagnostics arrive at all,
|
|
195
|
+
// this project is effectively broken even though it didn't rapid-fire).
|
|
196
|
+
startProjectInitTimeout();
|
|
197
|
+
}
|
|
198
|
+
lastInitMs = now;
|
|
199
|
+
// Check if broken projects pushed us to completion
|
|
200
|
+
if (isAllResolved()) {
|
|
201
|
+
finishWait();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function settleCurrentProject() {
|
|
205
|
+
settledProjects++;
|
|
206
|
+
if (isAllResolved()) {
|
|
207
|
+
finishWait();
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
startProjectInitTimeout();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function startDiagDebounce() {
|
|
214
|
+
if (diagDebounceTimer)
|
|
215
|
+
clearTimeout(diagDebounceTimer);
|
|
216
|
+
// Cancel the init timeout — we're now in diagnostic-settling mode
|
|
217
|
+
if (projectInitTimer) {
|
|
218
|
+
clearTimeout(projectInitTimer);
|
|
219
|
+
projectInitTimer = null;
|
|
220
|
+
}
|
|
221
|
+
diagDebounceTimer = setTimeout(settleCurrentProject, waitConfig.debounceMs);
|
|
222
|
+
}
|
|
223
|
+
function onDiagnosticReceived() {
|
|
224
|
+
if (!projectWaitResolve)
|
|
225
|
+
return;
|
|
226
|
+
currentProjectDiagCount++;
|
|
227
|
+
if (awaitingFirstDiag) {
|
|
228
|
+
// First diagnostic after a healthy init — don't start the debounce yet.
|
|
229
|
+
// The first diagnostic is often just the CSS entry point, followed by a
|
|
230
|
+
// ~1s pause before the bulk TSX diagnostics arrive. Starting the debounce
|
|
231
|
+
// here would settle too early on large projects.
|
|
232
|
+
awaitingFirstDiag = false;
|
|
233
|
+
}
|
|
234
|
+
else if (currentProjectDiagCount >= 2) {
|
|
235
|
+
// Second diagnostic and beyond — the bulk is flowing, debounce is safe
|
|
236
|
+
startDiagDebounce();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Wait for all expected projects to be resolved (settled or broken).
|
|
241
|
+
*
|
|
242
|
+
* @param predictedRoots - Number of CSS files predicted to be project roots
|
|
243
|
+
* @param maxProjects - Upper bound (predictedRoots + predictedNonRoots)
|
|
244
|
+
* @param initTimeoutMs - How long to wait for each projectInitialized event
|
|
245
|
+
* @param debounceMs - Silence window to consider diagnostics "settled"
|
|
246
|
+
*/
|
|
247
|
+
export function waitForAllProjects(predictedRoots, maxProjects, initTimeoutMs = 5_000, debounceMs = 500) {
|
|
248
|
+
if (serverDead || maxProjects === 0)
|
|
55
249
|
return Promise.resolve();
|
|
250
|
+
waitConfig = { predictedRoots, maxProjects, initTimeoutMs, debounceMs };
|
|
56
251
|
return new Promise((res) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
252
|
+
projectWaitResolve = res;
|
|
253
|
+
// Start waiting for first project init
|
|
254
|
+
startProjectInitTimeout();
|
|
255
|
+
// Hard outer timeout — never wait longer than this
|
|
256
|
+
const outerMs = initTimeoutMs + (maxProjects * 3000) + 5000;
|
|
257
|
+
outerTimer = setTimeout(finishWait, Math.min(outerMs, 30_000));
|
|
63
258
|
});
|
|
64
259
|
}
|
|
65
260
|
/** Returns a promise that resolves when diagnostics are published for a specific URI. */
|
|
@@ -153,7 +348,7 @@ function processMessages() {
|
|
|
153
348
|
if (msg.id != null && msg.method) {
|
|
154
349
|
let result = null;
|
|
155
350
|
if (msg.method === "workspace/configuration") {
|
|
156
|
-
result = (msg.params?.items || []).map(() => ({})
|
|
351
|
+
result = (msg.params?.items || []).map((item) => item.section ? getSettingsSection(item.section) : {});
|
|
157
352
|
}
|
|
158
353
|
server.stdin.write(encode({ jsonrpc: "2.0", id: msg.id, result }));
|
|
159
354
|
continue;
|
|
@@ -169,21 +364,12 @@ function processMessages() {
|
|
|
169
364
|
diagWaiters.delete(uri);
|
|
170
365
|
resolve(diags);
|
|
171
366
|
}
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
const resolve = diagTargetResolve;
|
|
175
|
-
diagTargetResolve = null;
|
|
176
|
-
resolve();
|
|
177
|
-
}
|
|
367
|
+
// Notify the project-aware wait system
|
|
368
|
+
onDiagnosticReceived();
|
|
178
369
|
}
|
|
179
370
|
// Tailwind project initialized
|
|
180
371
|
if (msg.method === "@/tailwindCSS/projectInitialized") {
|
|
181
|
-
|
|
182
|
-
if (projectReadyResolve) {
|
|
183
|
-
const resolve = projectReadyResolve;
|
|
184
|
-
projectReadyResolve = null;
|
|
185
|
-
resolve();
|
|
186
|
-
}
|
|
372
|
+
onProjectInitialized();
|
|
187
373
|
}
|
|
188
374
|
}
|
|
189
375
|
}
|
|
@@ -201,18 +387,8 @@ function drainAll(reason) {
|
|
|
201
387
|
p.reject(reason);
|
|
202
388
|
pending.delete(id);
|
|
203
389
|
}
|
|
204
|
-
// Resolve project
|
|
205
|
-
|
|
206
|
-
const r = projectReadyResolve;
|
|
207
|
-
projectReadyResolve = null;
|
|
208
|
-
r();
|
|
209
|
-
}
|
|
210
|
-
// Resolve count-based waiter
|
|
211
|
-
if (diagTargetResolve) {
|
|
212
|
-
const r = diagTargetResolve;
|
|
213
|
-
diagTargetResolve = null;
|
|
214
|
-
r();
|
|
215
|
-
}
|
|
390
|
+
// Resolve project wait (so run() doesn't hang)
|
|
391
|
+
finishWait();
|
|
216
392
|
// Resolve all URI-specific waiters with empty arrays
|
|
217
393
|
for (const [uri, r] of diagWaiters) {
|
|
218
394
|
r([]);
|
|
@@ -220,6 +396,7 @@ function drainAll(reason) {
|
|
|
220
396
|
diagWaiters.clear();
|
|
221
397
|
}
|
|
222
398
|
export function startServer(root) {
|
|
399
|
+
workspaceRoot = root;
|
|
223
400
|
const bin = findLanguageServer(root);
|
|
224
401
|
server = spawn(bin, ["--stdio"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
225
402
|
server.on("error", (err) => {
|
|
@@ -274,7 +451,10 @@ export function notify(method, params) {
|
|
|
274
451
|
export async function shutdown() {
|
|
275
452
|
if (serverDead)
|
|
276
453
|
return;
|
|
277
|
-
await
|
|
454
|
+
await Promise.race([
|
|
455
|
+
send("shutdown", {}).catch(() => { }),
|
|
456
|
+
new Promise(r => setTimeout(r, 3000)),
|
|
457
|
+
]);
|
|
278
458
|
notify("exit", {});
|
|
279
459
|
serverDead = true;
|
|
280
460
|
try {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-scan CSS files to predict how many projects the language server will create.
|
|
3
|
+
*
|
|
4
|
+
* analyzeStylesheet() is adapted from tailwindlabs/tailwindcss-intellisense
|
|
5
|
+
* (packages/tailwindcss-language-server/src/version-guesser.ts, MIT licensed).
|
|
6
|
+
*/
|
|
7
|
+
type TailwindVersion = "3" | "4";
|
|
8
|
+
export interface TailwindStylesheet {
|
|
9
|
+
root: boolean;
|
|
10
|
+
versions: TailwindVersion[];
|
|
11
|
+
explicitImport: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function analyzeStylesheet(content: string): TailwindStylesheet;
|
|
14
|
+
export interface PrescanResult {
|
|
15
|
+
/** Total CSS files found */
|
|
16
|
+
totalCssFiles: number;
|
|
17
|
+
/** CSS files predicted to be project roots */
|
|
18
|
+
predictedRoots: number;
|
|
19
|
+
/** CSS files that are Tailwind-related but not roots (could be promoted) */
|
|
20
|
+
predictedNonRoots: number;
|
|
21
|
+
/** CSS files with no Tailwind signals */
|
|
22
|
+
predictedUnrelated: number;
|
|
23
|
+
/** Upper bound on projects the server might create */
|
|
24
|
+
maxProjects: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function prescanCssFiles(files: string[]): PrescanResult;
|
|
27
|
+
export {};
|
package/dist/prescan.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-scan CSS files to predict how many projects the language server will create.
|
|
3
|
+
*
|
|
4
|
+
* analyzeStylesheet() is adapted from tailwindlabs/tailwindcss-intellisense
|
|
5
|
+
* (packages/tailwindcss-language-server/src/version-guesser.ts, MIT licensed).
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
const HAS_V4_IMPORT = /@import\s*['"]tailwindcss(?:\/[^'"]+)?['"]/;
|
|
9
|
+
const HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant|reference)\s*[^;{]+[;{]/;
|
|
10
|
+
const HAS_V4_FN = /--(alpha|spacing|theme)\(/;
|
|
11
|
+
const HAS_LEGACY_TAILWIND = /@tailwind\s*(base|preflight|components|variants|screens)+;/;
|
|
12
|
+
const HAS_TAILWIND_UTILITIES = /@tailwind\s*utilities\s*[^;]*;/;
|
|
13
|
+
const HAS_TAILWIND = /@tailwind\s*[^;]+;/;
|
|
14
|
+
const HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/;
|
|
15
|
+
const HAS_NON_URL_IMPORT = /@import\s*['"](?!([a-z]+:|\/\/))/;
|
|
16
|
+
export function analyzeStylesheet(content) {
|
|
17
|
+
if (HAS_V4_IMPORT.test(content)) {
|
|
18
|
+
return { root: true, versions: ["4"], explicitImport: true };
|
|
19
|
+
}
|
|
20
|
+
if (HAS_V4_DIRECTIVE.test(content)) {
|
|
21
|
+
if (HAS_TAILWIND_UTILITIES.test(content)) {
|
|
22
|
+
return { root: true, versions: ["4"], explicitImport: false };
|
|
23
|
+
}
|
|
24
|
+
return { root: false, versions: ["4"], explicitImport: false };
|
|
25
|
+
}
|
|
26
|
+
if (HAS_V4_FN.test(content)) {
|
|
27
|
+
return { root: false, versions: ["4"], explicitImport: false };
|
|
28
|
+
}
|
|
29
|
+
if (HAS_LEGACY_TAILWIND.test(content)) {
|
|
30
|
+
return { root: false, versions: ["3"], explicitImport: false };
|
|
31
|
+
}
|
|
32
|
+
if (HAS_TAILWIND.test(content)) {
|
|
33
|
+
return { root: true, versions: ["4", "3"], explicitImport: false };
|
|
34
|
+
}
|
|
35
|
+
if (HAS_COMMON_DIRECTIVE.test(content)) {
|
|
36
|
+
return { root: false, versions: ["4", "3"], explicitImport: false };
|
|
37
|
+
}
|
|
38
|
+
if (HAS_NON_URL_IMPORT.test(content)) {
|
|
39
|
+
return { root: true, versions: ["4", "3"], explicitImport: false };
|
|
40
|
+
}
|
|
41
|
+
return { root: false, versions: [], explicitImport: false };
|
|
42
|
+
}
|
|
43
|
+
export function prescanCssFiles(files) {
|
|
44
|
+
let predictedRoots = 0;
|
|
45
|
+
let predictedNonRoots = 0;
|
|
46
|
+
let predictedUnrelated = 0;
|
|
47
|
+
for (const filePath of files) {
|
|
48
|
+
if (!filePath.endsWith(".css"))
|
|
49
|
+
continue;
|
|
50
|
+
let content;
|
|
51
|
+
try {
|
|
52
|
+
content = readFileSync(filePath, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const result = analyzeStylesheet(content);
|
|
58
|
+
if (result.versions.length === 0) {
|
|
59
|
+
predictedUnrelated++;
|
|
60
|
+
}
|
|
61
|
+
else if (result.root) {
|
|
62
|
+
predictedRoots++;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
predictedNonRoots++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const totalCssFiles = predictedRoots + predictedNonRoots + predictedUnrelated;
|
|
69
|
+
return {
|
|
70
|
+
totalCssFiles,
|
|
71
|
+
predictedRoots,
|
|
72
|
+
predictedNonRoots,
|
|
73
|
+
predictedUnrelated,
|
|
74
|
+
maxProjects: predictedRoots + predictedNonRoots,
|
|
75
|
+
};
|
|
76
|
+
}
|