Skip to content

Commit c7d08b7

Browse files
Merge branch 'main' into feat/better-theme-completions
2 parents 5e02dae + f91a3bc commit c7d08b7

File tree

7 files changed

+477
-61
lines changed

7 files changed

+477
-61
lines changed

packages/tailwindcss-language-server/src/tw.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { URI } from 'vscode-uri'
3939
import normalizePath from 'normalize-path'
4040
import * as path from 'node:path'
41+
import * as fs from 'node:fs/promises'
4142
import type * as chokidar from 'chokidar'
4243
import picomatch from 'picomatch'
4344
import * as parcel from './watcher/index.js'
@@ -174,6 +175,26 @@ export class TW {
174175
}
175176

176177
private async _initFolder(baseUri: URI): Promise<void> {
178+
// NOTE: We do this check because on Linux when using an LSP client that does
179+
// not support watching files on behalf of the server, we'll use Parcel
180+
// Watcher (if possible). If we start the watcher with a non-existent or
181+
// inaccessible directory, it will throw an error with a very unhelpful
182+
// message: "Bad file descriptor"
183+
//
184+
// The best thing we can do is an initial check for access to the directory
185+
// and log a more helpful error message if it fails.
186+
let base = baseUri.fsPath
187+
188+
try {
189+
await fs.access(base, fs.constants.F_OK | fs.constants.R_OK)
190+
} catch (err) {
191+
console.error(
192+
`Unable to access the workspace folder [${base}]. This may happen if the directory does not exist or the current user does not have the necessary permissions to access it.`,
193+
)
194+
console.error(err)
195+
return
196+
}
197+
177198
let initUserLanguages = this.initializeParams.initializationOptions?.userLanguages ?? {}
178199

179200
if (Object.keys(initUserLanguages).length > 0) {
@@ -182,7 +203,6 @@ export class TW {
182203
)
183204
}
184205

185-
let base = baseUri.fsPath
186206
let workspaceFolders: Array<ProjectConfig> = []
187207
let globalSettings = await this.settingsCache.get()
188208
let ignore = globalSettings.tailwindCSS.files.exclude

packages/tailwindcss-language-server/tests/env/v4.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,3 +791,51 @@ defineTest({
791791
})
792792
},
793793
})
794+
795+
defineTest({
796+
options: { only: true },
797+
name: 'regex literals do not break language boundaries',
798+
fs: {
799+
'app.css': css`
800+
@import 'tailwindcss';
801+
`,
802+
},
803+
prepare: async ({ root }) => ({ client: await createClient({ root }) }),
804+
handle: async ({ client }) => {
805+
let doc = await client.open({
806+
lang: 'javascriptreact',
807+
text: js`
808+
export default function Page() {
809+
let styles = "str".match(/<style>[\s\S]*?<\/style>/m)
810+
return <div className="bg-[#000]">{styles}</div>
811+
}
812+
`,
813+
})
814+
815+
expect(await client.project()).toMatchObject({
816+
tailwind: {
817+
version: '4.0.6',
818+
isDefaultVersion: true,
819+
},
820+
})
821+
822+
// return <div className="bg-[#000]">{styles}</div>
823+
// ^
824+
let hover = await doc.hover({ line: 2, character: 26 })
825+
826+
expect(hover).toEqual({
827+
contents: {
828+
language: 'css',
829+
value: dedent`
830+
.bg-\[\#000\] {
831+
background-color: #000;
832+
}
833+
`,
834+
},
835+
range: {
836+
start: { line: 2, character: 25 },
837+
end: { line: 2, character: 34 },
838+
},
839+
})
840+
},
841+
})

packages/tailwindcss-language-service/src/util/doc.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,155 @@ function getJsWithoutComments(text: string): string {
127127
}
128128
}
129129

130+
str = stripRegexLiterals(str)
131+
130132
return str
131133
}
134+
135+
function stripRegexLiterals(input: string) {
136+
const BACKSLASH = 0x5c // \
137+
const SLASH = 0x2f // /
138+
const LINE_BREAK = 0x0a // \n
139+
const COMMA = 0x2c // ,
140+
const COLON = 0x3a // :
141+
const EQUALS = 0x3d // =
142+
const SEMICOLON = 0x3b // ;
143+
const BRACKET_OPEN = 0x5b // [
144+
const BRACKET_CLOSE = 0x5d // ]
145+
const QUESTION_MARK = 0x3f // ?
146+
const PAREN_OPEN = 0x28 // (
147+
const CURLY_OPEN = 0x7b // {
148+
const DOUBLE_QUOTE = 0x22 // "
149+
const SINGLE_QUOTE = 0x27 // '
150+
const BACKTICK = 0x60 // `
151+
152+
let SPACE = 0x20 // " "
153+
let TAB = 0x09 // \t
154+
155+
// Top level; or
156+
// after comma
157+
// after colon
158+
// after equals
159+
// after semicolon
160+
// after square bracket (arrays, object property expressions)
161+
// after question mark
162+
// after open paren
163+
// after curly (jsx only)
164+
165+
let inRegex = false
166+
let inEscape = false
167+
let inCharacterClass = false
168+
169+
let regexStart = -1
170+
let regexEnd = -1
171+
172+
// Based on the oxc_parser crate
173+
// https://github.com/oxc-project/oxc/blob/5f97f28ddbd2cd303a306f7fb0092b0e54bda43c/crates/oxc_parser/src/lexer/regex.rs#L29
174+
let prev = null
175+
for (let i = 0; i < input.length; ++i) {
176+
let c = input.charCodeAt(i)
177+
178+
if (inRegex) {
179+
if (c === LINE_BREAK) {
180+
break
181+
} else if (inEscape) {
182+
inEscape = false
183+
} else if (c === SLASH && !inCharacterClass) {
184+
inRegex = false
185+
regexEnd = i
186+
break
187+
} else if (c === BRACKET_OPEN) {
188+
inCharacterClass = true
189+
} else if (c === BACKSLASH) {
190+
inEscape = true
191+
} else if (c === BRACKET_CLOSE) {
192+
inCharacterClass = false
193+
}
194+
195+
continue
196+
}
197+
198+
// Skip over strings
199+
if (c === SINGLE_QUOTE) {
200+
for (let j = i; j < input.length; ++j) {
201+
let peekChar = input.charCodeAt(j)
202+
203+
if (peekChar === BACKSLASH) {
204+
j += 1
205+
} else if (peekChar === SINGLE_QUOTE) {
206+
i = j
207+
break
208+
} else if (peekChar === LINE_BREAK) {
209+
i = j
210+
break
211+
}
212+
}
213+
}
214+
//
215+
else if (c === DOUBLE_QUOTE) {
216+
for (let j = i; j < input.length; ++j) {
217+
let peekChar = input.charCodeAt(j)
218+
219+
if (peekChar === BACKSLASH) {
220+
j += 1
221+
} else if (peekChar === DOUBLE_QUOTE) {
222+
i = j
223+
break
224+
} else if (peekChar === LINE_BREAK) {
225+
i = j
226+
break
227+
}
228+
}
229+
}
230+
//
231+
else if (c === BACKTICK) {
232+
for (let j = i; j < input.length; ++j) {
233+
let peekChar = input.charCodeAt(j)
234+
235+
if (peekChar === BACKSLASH) {
236+
j += 1
237+
} else if (peekChar === BACKTICK) {
238+
i = j
239+
break
240+
} else if (peekChar === LINE_BREAK) {
241+
i = j
242+
break
243+
}
244+
}
245+
}
246+
//
247+
else if (c === SPACE || c === TAB) {
248+
// do nothing
249+
}
250+
//
251+
else if (c === SLASH) {
252+
if (
253+
prev === COMMA ||
254+
prev === COLON ||
255+
prev === EQUALS ||
256+
prev === SEMICOLON ||
257+
prev === BRACKET_OPEN ||
258+
prev === QUESTION_MARK ||
259+
prev === PAREN_OPEN ||
260+
prev === CURLY_OPEN ||
261+
prev === LINE_BREAK
262+
) {
263+
inRegex = true
264+
regexStart = i
265+
}
266+
}
267+
//
268+
else {
269+
prev = c
270+
}
271+
}
272+
273+
// Unterminated regex literal
274+
if (inRegex) return input
275+
276+
if (regexStart === -1 || regexEnd === -1) return input
277+
278+
return (
279+
input.slice(0, regexStart) + ' '.repeat(regexEnd - regexStart + 1) + input.slice(regexEnd + 1)
280+
)
281+
}

packages/tailwindcss-language-service/src/util/find.test.ts

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import { createState, getDefaultTailwindSettings, Settings, type DocumentClassList } from './state'
21
import { test } from 'vitest'
3-
import { TextDocument } from 'vscode-languageserver-textdocument'
42
import { findClassListsInHtmlRange } from './find'
5-
import type { DeepPartial } from '../types'
6-
import dedent from 'dedent'
7-
8-
const js = dedent
9-
const html = dedent
3+
import { js, html, createDocument } from './test-utils'
104

115
test('class regex works in astro', async ({ expect }) => {
126
let file = createDocument({
@@ -688,56 +682,3 @@ test('classFunctions should only match in JS-like contexts', async ({ expect })
688682
},
689683
])
690684
})
691-
692-
function createDocument({
693-
name,
694-
lang,
695-
content,
696-
settings,
697-
}: {
698-
name: string
699-
lang: string
700-
content: string | string[]
701-
settings: DeepPartial<Settings>
702-
}) {
703-
let doc = TextDocument.create(
704-
`file://${name}`,
705-
lang,
706-
1,
707-
typeof content === 'string' ? content : content.join('\n'),
708-
)
709-
let defaults = getDefaultTailwindSettings()
710-
let state = createState({
711-
editor: {
712-
getConfiguration: async () => ({
713-
...defaults,
714-
...settings,
715-
tailwindCSS: {
716-
...defaults.tailwindCSS,
717-
...settings.tailwindCSS,
718-
lint: {
719-
...defaults.tailwindCSS.lint,
720-
...(settings.tailwindCSS?.lint ?? {}),
721-
},
722-
experimental: {
723-
...defaults.tailwindCSS.experimental,
724-
...(settings.tailwindCSS?.experimental ?? {}),
725-
},
726-
files: {
727-
...defaults.tailwindCSS.files,
728-
...(settings.tailwindCSS?.files ?? {}),
729-
},
730-
},
731-
editor: {
732-
...defaults.editor,
733-
...settings.editor,
734-
},
735-
}),
736-
},
737-
})
738-
739-
return {
740-
doc,
741-
state,
742-
}
743-
}

0 commit comments

Comments
 (0)