Skip to content

Hide completions from CSS language server inside @import "…" source(…) #1091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions packages/tailwindcss-language-server/src/language/css-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
CompletionItemKind,
Connection,
} from 'vscode-languageserver/node'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { Position, TextDocument } from 'vscode-languageserver-textdocument'
import { Utils, URI } from 'vscode-uri'
import { getLanguageModelCache } from './languageModelCache'
import { Stylesheet } from 'vscode-css-languageservice'
Expand Down Expand Up @@ -121,6 +121,7 @@ export class CssServer {
async function withDocumentAndSettings<T>(
uri: string,
callback: (result: {
original: TextDocument
document: TextDocument
settings: LanguageSettings | undefined
}) => T | Promise<T>,
Expand All @@ -130,13 +131,64 @@ export class CssServer {
return null
}
return await callback({
original: document,
document: createVirtualCssDocument(document),
settings: await getDocumentSettings(document),
})
}

function isInImportDirective(doc: TextDocument, pos: Position) {
let text = doc.getText({
start: { line: pos.line, character: 0 },
end: pos,
})

// Scan backwards to see if we're inside an `@import` directive
let foundImport = false
let foundDirective = false

for (let i = text.length - 1; i >= 0; i--) {
let char = text[i]
if (char === '\n') break

if (char === '(' && !foundDirective) {
if (text.startsWith(' source(', i - 7)) {
foundDirective = true
}

//
else if (text.startsWith(' theme(', i - 6)) {
foundDirective = true
}

//
else if (text.startsWith(' prefix(', i - 7)) {
foundDirective = true
}
}

//
else if (char === '@' && !foundImport) {
if (text.startsWith('@import ', i)) {
foundImport = true
}
}
}

return foundImport && foundDirective
}

connection.onCompletion(async ({ textDocument, position }, _token) =>
withDocumentAndSettings(textDocument.uri, async ({ document, settings }) => {
withDocumentAndSettings(textDocument.uri, async ({ original, document, settings }) => {
// If we're inside source(…), prefix(…), or theme(…), don't show
// completions from the CSS language server
if (isInImportDirective(original, position)) {
return {
isIncomplete: false,
items: [],
}
}

let result = await cssLanguageService.doComplete2(
document,
position,
Expand Down
46 changes: 46 additions & 0 deletions packages/tailwindcss-language-server/tests/css/css-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,49 @@ defineTest({
expect(await doc.diagnostics()).toEqual([])
},
})

defineTest({
name: 'completions are hidden inside @import source(…)/theme(…)/prefix(…) functions',
prepare: async ({ root }) => ({
client: await createClient({
server: 'css',
root,
}),
}),
handle: async ({ client }) => {
let doc = await client.open({
lang: 'tailwindcss',
name: 'file-1.css',
text: css`
@import './file.css' source(none);
@import './file.css' theme(inline);
@import './file.css' prefix(tw);
@import './file.css' source(none) theme(inline) prefix(tw);
`,
})

// @import './file.css' source(none)
// ^
// @import './file.css' theme(inline);
// ^
// @import './file.css' prefix(tw);
// ^
let completionsA = await doc.completions({ line: 0, character: 29 })
let completionsB = await doc.completions({ line: 1, character: 28 })
let completionsC = await doc.completions({ line: 2, character: 29 })

expect(completionsA).toEqual({ isIncomplete: false, items: [] })
expect(completionsB).toEqual({ isIncomplete: false, items: [] })
expect(completionsC).toEqual({ isIncomplete: false, items: [] })

// @import './file.css' source(none) theme(inline) prefix(tw);
// ^ ^ ^
let completionsD = await doc.completions({ line: 3, character: 29 })
let completionsE = await doc.completions({ line: 3, character: 41 })
let completionsF = await doc.completions({ line: 3, character: 56 })

expect(completionsD).toEqual({ isIncomplete: false, items: [] })
expect(completionsE).toEqual({ isIncomplete: false, items: [] })
expect(completionsF).toEqual({ isIncomplete: false, items: [] })
},
})