├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml └── workflows │ ├── bump-version.mjs │ ├── ci.yml │ ├── manual-release.yml │ └── pre-release.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── esbuild.mjs ├── package.json ├── packages ├── tailwindcss-language-server │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── ThirdPartyNotices.txt │ ├── package.json │ ├── scripts │ │ ├── createNoticesFile.mjs │ │ └── hashbang.mjs │ ├── src │ │ ├── cache-map.ts │ │ ├── config.ts │ │ ├── css │ │ │ ├── extract-source-directives.ts │ │ │ ├── fix-relative-paths.ts │ │ │ ├── index.ts │ │ │ └── resolve-css-imports.ts │ │ ├── documents.ts │ │ ├── graph.test.ts │ │ ├── graph.ts │ │ ├── language │ │ │ ├── css-server.ts │ │ │ ├── css.ts │ │ │ ├── languageModelCache.ts │ │ │ ├── rewriting.test.ts │ │ │ └── rewriting.ts │ │ ├── lib │ │ │ ├── constants.ts │ │ │ ├── env.ts │ │ │ ├── extract-class-names.test.ts │ │ │ ├── extractClassNames.ts │ │ │ ├── hook.ts │ │ │ ├── plugins.ts │ │ │ └── preflight.ts │ │ ├── lsp │ │ │ └── diagnosticsProvider.ts │ │ ├── matching.ts │ │ ├── oxide.ts │ │ ├── project-locator.test.ts │ │ ├── project-locator.ts │ │ ├── projects.ts │ │ ├── resolver │ │ │ ├── index.ts │ │ │ ├── pnp.ts │ │ │ └── tsconfig.ts │ │ ├── server.ts │ │ ├── testing │ │ │ └── index.ts │ │ ├── tw.ts │ │ ├── util │ │ │ ├── css.ts │ │ │ ├── default-map.ts │ │ │ ├── error.ts │ │ │ ├── get-package-root.ts │ │ │ ├── getModuleDependencies.ts │ │ │ ├── isExcluded.ts │ │ │ ├── logs.ts │ │ │ ├── resolveFrom.ts │ │ │ ├── retry.ts │ │ │ ├── uri.ts │ │ │ └── v4 │ │ │ │ ├── assets.ts │ │ │ │ ├── design-system.ts │ │ │ │ ├── index.ts │ │ │ │ └── plugins.ts │ │ ├── utils.ts │ │ ├── version-guesser.ts │ │ └── watcher │ │ │ ├── index.js │ │ │ └── licenses │ │ │ ├── @parcel │ │ │ └── watcher │ │ │ └── node-gyp-build │ ├── tests │ │ ├── code-actions │ │ │ ├── code-actions.test.js │ │ │ ├── code-actions.v2-jit.test.js │ │ │ ├── code-actions.v2.test.js │ │ │ ├── code-actions.v4.test.js │ │ │ ├── conflict.json │ │ │ ├── invalid-screen.json │ │ │ ├── invalid-theme.json │ │ │ ├── invalid-variant.json │ │ │ └── variant-order.json │ │ ├── code-lens │ │ │ └── source-inline.test.ts │ │ ├── colors │ │ │ ├── colors.test.js │ │ │ └── presentation.test.js │ │ ├── commands │ │ │ └── commands.test.js │ │ ├── common.ts │ │ ├── completions │ │ │ ├── at-config.test.js │ │ │ └── completions.test.js │ │ ├── css │ │ │ └── css-server.test.ts │ │ ├── diagnostics │ │ │ ├── css-conflict │ │ │ │ ├── css-multi-prop.json │ │ │ │ ├── css-multi-rule.json │ │ │ │ ├── css.json │ │ │ │ ├── jsx-concat-negative.json │ │ │ │ ├── jsx-concat-positive.json │ │ │ │ ├── simple.json │ │ │ │ ├── variants-negative.json │ │ │ │ ├── variants-positive.json │ │ │ │ └── vue-style-lang-sass.json │ │ │ ├── diagnostics.test.js │ │ │ ├── invalid-screen │ │ │ │ └── simple.json │ │ │ ├── invalid-theme │ │ │ │ └── simple.json │ │ │ └── source-diagnostics.test.js │ │ ├── document-links │ │ │ └── document-links.test.js │ │ ├── env │ │ │ ├── capabilities.test.ts │ │ │ ├── custom-languages.test.js │ │ │ ├── document-selection.test.js │ │ │ ├── ignored.test.ts │ │ │ ├── multi-config-content.test.js │ │ │ ├── multi-config.test.js │ │ │ ├── restart.test.ts │ │ │ ├── v4.test.js │ │ │ └── workspace-folders.test.ts │ │ ├── fixtures │ │ │ ├── basic │ │ │ │ └── tailwind.config.js │ │ │ ├── dependencies │ │ │ │ ├── sub-dir │ │ │ │ │ └── colors.js │ │ │ │ └── tailwind.config.js │ │ │ ├── document-selection │ │ │ │ ├── (parens) │ │ │ │ │ ├── file.html │ │ │ │ │ └── tailwind.config.js │ │ │ │ ├── [brackets] │ │ │ │ │ ├── file.html │ │ │ │ │ └── tailwind.config.js │ │ │ │ ├── basic │ │ │ │ │ ├── file.html │ │ │ │ │ └── tailwind.config.js │ │ │ │ └── {curlies} │ │ │ │ │ ├── file.html │ │ │ │ │ └── tailwind.config.js │ │ │ ├── multi-config-content │ │ │ │ ├── tailwind.config.one.js │ │ │ │ └── tailwind.config.two.js │ │ │ ├── multi-config │ │ │ │ ├── one │ │ │ │ │ └── tailwind.config.js │ │ │ │ └── two │ │ │ │ │ └── tailwind.config.js │ │ │ ├── overrides-variants │ │ │ │ └── tailwind.config.js │ │ │ ├── v1 │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── tailwind.config.js │ │ │ ├── v2-jit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── tailwind.config.js │ │ │ ├── v2 │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── tailwind.config.js │ │ │ ├── v3 │ │ │ │ ├── cts-config │ │ │ │ │ └── tailwind.config.cts │ │ │ │ ├── esm-config │ │ │ │ │ └── tailwind.config.mjs │ │ │ │ ├── mts-config │ │ │ │ │ └── tailwind.config.mts │ │ │ │ └── ts-config │ │ │ │ │ └── tailwind.config.ts │ │ │ └── v4 │ │ │ │ ├── basic │ │ │ │ ├── app.css │ │ │ │ ├── package-lock.json │ │ │ │ └── package.json │ │ │ │ ├── css-loading-js │ │ │ │ ├── app.css │ │ │ │ ├── cjs │ │ │ │ │ ├── my-config.cjs │ │ │ │ │ └── my-plugin.cjs │ │ │ │ ├── esm │ │ │ │ │ ├── my-config.mjs │ │ │ │ │ └── my-plugin.mjs │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── ts │ │ │ │ │ ├── my-config.ts │ │ │ │ │ └── my-plugin.ts │ │ │ │ ├── dependencies │ │ │ │ ├── app.css │ │ │ │ ├── file.d.ts │ │ │ │ ├── index.html │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ ├── sub-dir │ │ │ │ │ └── colors.js │ │ │ │ └── tailwind.config.js │ │ │ │ ├── invalid-import-order │ │ │ │ ├── a.css │ │ │ │ ├── b.css │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── tailwind.css │ │ │ │ ├── missing-files │ │ │ │ ├── app.css │ │ │ │ ├── i-exist.css │ │ │ │ ├── package-lock.json │ │ │ │ └── package.json │ │ │ │ ├── multi-config │ │ │ │ ├── admin │ │ │ │ │ ├── app.css │ │ │ │ │ ├── tw.css │ │ │ │ │ └── ui.css │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── web │ │ │ │ │ └── app.css │ │ │ │ ├── path-mappings │ │ │ │ ├── app.css │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── a │ │ │ │ │ │ ├── file.css │ │ │ │ │ │ ├── my-config.ts │ │ │ │ │ │ └── my-plugin.ts │ │ │ │ └── tsconfig.json │ │ │ │ ├── with-prefix │ │ │ │ ├── app.css │ │ │ │ ├── package-lock.json │ │ │ │ └── package.json │ │ │ │ └── workspaces │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── packages │ │ │ │ ├── admin │ │ │ │ ├── app.css │ │ │ │ ├── package.json │ │ │ │ └── tw.css │ │ │ │ ├── shared │ │ │ │ ├── package.json │ │ │ │ └── ui.css │ │ │ │ ├── style-export │ │ │ │ ├── lib.css │ │ │ │ ├── package.json │ │ │ │ └── theme.css │ │ │ │ ├── style-main-field │ │ │ │ ├── lib.css │ │ │ │ └── package.json │ │ │ │ └── web │ │ │ │ ├── app.css │ │ │ │ └── package.json │ │ ├── hover │ │ │ └── hover.test.js │ │ ├── prepare.mjs │ │ └── utils │ │ │ ├── client.ts │ │ │ ├── configuration.ts │ │ │ ├── connection.ts │ │ │ └── messages.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── tailwindcss-language-service │ ├── .browserslistrc │ ├── .gitignore │ ├── LICENSE │ ├── package.json │ ├── scripts │ │ ├── build.mjs │ │ └── tsconfig.build.json │ ├── src │ │ ├── codeActions │ │ │ ├── codeActionProvider.ts │ │ │ ├── provideCssConflictCodeActions.ts │ │ │ ├── provideInvalidApplyCodeActions.ts │ │ │ └── provideSuggestionCodeActions.ts │ │ ├── codeLensProvider.ts │ │ ├── completionProvider.ts │ │ ├── completions │ │ │ ├── file-paths.test.ts │ │ │ └── file-paths.ts │ │ ├── diagnostics │ │ │ ├── diagnosticsProvider.ts │ │ │ ├── getCssConflictDiagnostics.ts │ │ │ ├── getInvalidApplyDiagnostics.ts │ │ │ ├── getInvalidConfigPathDiagnostics.ts │ │ │ ├── getInvalidScreenDiagnostics.ts │ │ │ ├── getInvalidSourceDiagnostics.ts │ │ │ ├── getInvalidTailwindDirectiveDiagnostics.ts │ │ │ ├── getInvalidVariantDiagnostics.ts │ │ │ ├── getRecommendedVariantOrderDiagnostics.ts │ │ │ ├── getUsedBlocklistedClassDiagnostics.ts │ │ │ └── types.ts │ │ ├── documentColorProvider.ts │ │ ├── documentLinksProvider.ts │ │ ├── features.ts │ │ ├── hoverProvider.ts │ │ ├── index.ts │ │ ├── metadata │ │ │ └── extensions.ts │ │ ├── types.ts │ │ └── util │ │ │ ├── absoluteRange.ts │ │ │ ├── array.ts │ │ │ ├── braceLevel.ts │ │ │ ├── classes.test.ts │ │ │ ├── classes.ts │ │ │ ├── closest.ts │ │ │ ├── color.ts │ │ │ ├── colorEquivalents.ts │ │ │ ├── combinations.ts │ │ │ ├── comments.ts │ │ │ ├── constants.ts │ │ │ ├── css.ts │ │ │ ├── cssObjToAst.ts │ │ │ ├── doc.ts │ │ │ ├── docsUrl.ts │ │ │ ├── equivalents.ts │ │ │ ├── estimated-class-size.ts │ │ │ ├── find.test.ts │ │ │ ├── find.ts │ │ │ ├── flagEnabled.ts │ │ │ ├── format-bytes.ts │ │ │ ├── getClassNameAtPosition.ts │ │ │ ├── getClassNameDecls.ts │ │ │ ├── getClassNameMeta.ts │ │ │ ├── getLanguageBoundaries.ts │ │ │ ├── getVariantsFromClassName.ts │ │ │ ├── html.ts │ │ │ ├── isObject.ts │ │ │ ├── isValidLocationForEmmetAbbreviation.ts │ │ │ ├── isWithinRange.ts │ │ │ ├── jit.ts │ │ │ ├── joinWithAnd.ts │ │ │ ├── js.ts │ │ │ ├── language-blocks.ts │ │ │ ├── language-boundaries.test.ts │ │ │ ├── languages.ts │ │ │ ├── lazy.ts │ │ │ ├── lexers.ts │ │ │ ├── naturalExpand.ts │ │ │ ├── pixelEquivalents.ts │ │ │ ├── rangesEqual.ts │ │ │ ├── removeMeta.ts │ │ │ ├── removeRangesFromString.ts │ │ │ ├── rewriting │ │ │ ├── add-theme-values.ts │ │ │ ├── calc.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── inline-theme-values.ts │ │ │ ├── lookup.ts │ │ │ ├── replacements.ts │ │ │ └── var-fallbacks.ts │ │ │ ├── screens.ts │ │ │ ├── segment.ts │ │ │ ├── semver.ts │ │ │ ├── splice-changes-into-string.ts │ │ │ ├── state.ts │ │ │ ├── stringToPath.ts │ │ │ ├── stringify.ts │ │ │ ├── test-utils.ts │ │ │ ├── v4 │ │ │ ├── ast.ts │ │ │ ├── candidate.ts │ │ │ ├── design-system.ts │ │ │ ├── index.ts │ │ │ └── theme-keys.ts │ │ │ └── validateApply.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── tailwindcss-language-syntax │ ├── package.json │ ├── syntaxes │ │ └── css.json │ ├── tests │ │ ├── __snapshots__ │ │ │ └── syntax.test.ts.snap │ │ ├── default-map.ts │ │ ├── scopes.ts │ │ ├── syntax.test.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── vitest.config.ts └── vscode-tailwindcss │ ├── .github │ ├── autocomplete.png │ ├── banner.png │ ├── hover.png │ └── linting.png │ ├── .gitignore │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── media │ └── icon.png │ ├── package.json │ ├── src │ ├── analyze.ts │ ├── api.ts │ ├── cssServer.ts │ ├── exclusions.ts │ ├── extension.ts │ ├── server.ts │ └── servers │ │ ├── css.ts │ │ └── index.ts │ ├── syntaxes │ ├── at-apply.tmLanguage.json │ ├── at-rules.postcss.tmLanguage.json │ ├── at-rules.scss.tmLanguage.json │ ├── at-rules.tmLanguage.json │ ├── screen-fn.tmLanguage.json │ ├── source.css.tailwind.tmLanguage.json │ └── theme-fn.tmLanguage.json │ ├── tailwindcss.language.configuration.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── types ├── culori.d.ts └── global.d.ts /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to Tailwind CSS IntelliSense! Please take a moment to review this document **before submitting a pull request**. 4 | 5 | ## Pull requests 6 | 7 | **Please ask first before starting work on any significant new features.** 8 | 9 | It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create [a feature request](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) to first discuss any significant new ideas. 10 | 11 | ## Building the Extension 12 | 13 | You can build the VSIX package by running these commands in the project root. 14 | 15 | ```bash 16 | pnpm install 17 | pnpx run package --workspace=packages/vscode-tailwindcss 18 | ``` 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: If you think something is broken with Tailwind CSS IntelliSense itself, create a bug report. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **What version of VS Code are you using?** 10 | 11 | For example: v1.78.2 12 | 13 | **What version of Tailwind CSS IntelliSense are you using?** 14 | 15 | For example: v0.7.0 16 | 17 | **What version of Tailwind CSS are you using?** 18 | 19 | For example: v2.0.4 20 | 21 | **What package manager are you using?** 22 | 23 | For example: npm, yarn 24 | 25 | **What operating system are you using?** 26 | 27 | For example: macOS, Windows 28 | 29 | **Tailwind config** 30 | 31 | ```js 32 | // Paste the contents of your Tailwind config file here 33 | ``` 34 | 35 | **VS Code settings** 36 | 37 | ```json 38 | // Paste your VS Code settings in JSON format here 39 | ``` 40 | 41 | **Reproduction URL** 42 | 43 | A public GitHub repo that includes a minimal reproduction of the bug. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn't matter if your real project is private/confidential, since we want a link to a separate, isolated reproduction anyways. 44 | 45 | **Describe your issue** 46 | 47 | Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Request 4 | url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas&title=%5BIntelliSense%5D%20 5 | about: 'Suggest any ideas you have using our discussion forums.' 6 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.mjs: -------------------------------------------------------------------------------- 1 | import PackageJson from '@npmcli/package-json' 2 | import assert from 'node:assert' 3 | import * as path from 'node:path' 4 | import { spawnSync } from 'node:child_process' 5 | import { fileURLToPath } from 'node:url' 6 | import semver from 'semver' 7 | 8 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 9 | 10 | // Let `vsce` get the metadata for the extension 11 | // Querying the marketplace API directly is not supported or recommended 12 | let result = spawnSync( 13 | path.resolve(__dirname, '../../packages/vscode-tailwindcss/node_modules/.bin/vsce'), 14 | ['show', 'bradlc.vscode-tailwindcss', '--json'], 15 | { encoding: 'utf8' }, 16 | ) 17 | 18 | let metadata = JSON.parse(result.stdout) 19 | 20 | if (!metadata) { 21 | console.error(result.error) 22 | throw new Error('Failed to get extension metadata') 23 | } 24 | 25 | /** @type {string[]} */ 26 | let versions = metadata.versions.map(({ version }) => version) 27 | 28 | // Determine the latest version of the extension 29 | let latest = versions 30 | .map((v) => semver.parse(v, { includePrerelease: true, loose: false })) 31 | .filter((v) => v !== null) 32 | .filter((v) => v.prerelease.length === 0) 33 | .sort((a, b) => b.compare(a) || b.compareBuild(a)) 34 | .at(0) 35 | 36 | // Require the minor version to be odd. This is done because 37 | // the VSCode Marketplace suggests using odd numbers for 38 | // pre-release builds and even ones for release builds 39 | assert(latest && latest.minor % 2 === 1) 40 | 41 | // Bump the patch version in `package.json` 42 | let nextVersion = latest.inc('patch').format() 43 | let pkg = await PackageJson.load('packages/vscode-tailwindcss') 44 | await pkg.update({ version: nextVersion }).save() 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | tests: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node: [18, 20, 22, 24] 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | 15 | runs-on: ${{ matrix.os }} 16 | name: Run Tests - Node v${{ matrix.node }} / ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: pnpm/action-setup@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | cache: 'pnpm' 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install dependencies 27 | run: pnpm install 28 | 29 | - name: Run syntax tests 30 | run: | 31 | cd packages/tailwindcss-language-syntax && 32 | pnpm run build && 33 | pnpm run test 34 | 35 | - name: Run service tests 36 | run: | 37 | cd packages/tailwindcss-language-service && 38 | pnpm run build && 39 | pnpm run test 40 | 41 | - name: Run tests 42 | run: | 43 | cd packages/tailwindcss-language-server && 44 | pnpm run build && 45 | pnpm run test 46 | -------------------------------------------------------------------------------- /.github/workflows/manual-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | concurrency: publish 3 | on: 4 | workflow_dispatch: {} 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: pnpm/action-setup@v3 12 | with: 13 | version: ^9.6.0 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | registry-url: 'https://registry.npmjs.org' 18 | cache: 'pnpm' 19 | - name: Install dependencies 20 | run: pnpm install 21 | - name: Run tests 22 | run: > 23 | cd packages/tailwindcss-language-server && 24 | pnpm run build && 25 | pnpm run test 26 | - name: Publish IntelliSense 27 | env: 28 | VSCODE_TOKEN: ${{ secrets.VSCODE_TOKEN }} 29 | run: > 30 | cd packages/vscode-tailwindcss && 31 | pnpm run publish -p $VSCODE_TOKEN 32 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish pre-release 2 | concurrency: publish 3 | on: 4 | push: 5 | branches: [main] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: pnpm/action-setup@v3 12 | with: 13 | version: ^9.6.0 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | registry-url: 'https://registry.npmjs.org' 18 | cache: 'pnpm' 19 | - name: Install dependencies 20 | run: pnpm install 21 | - name: Build everything 22 | run: pnpm -r run build 23 | - name: Run tests 24 | run: pnpm --filter ./packages/tailwindcss-language-server run test 25 | - name: Bump IntelliSense version 26 | run: > 27 | node .github/workflows/bump-version.mjs && 28 | cat packages/vscode-tailwindcss/package.json 29 | - name: Resolve LSP version 30 | run: | 31 | echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 32 | - name: 'Version LSP based on commit: 0.0.0-insiders.${{ env.SHA_SHORT }}' 33 | run: > 34 | cd packages/tailwindcss-language-server && 35 | pnpm version 0.0.0-insiders.${{ env.SHA_SHORT }} --force --no-git-tag-version 36 | - name: Publish IntelliSense 37 | env: 38 | VSCODE_TOKEN: ${{ secrets.VSCODE_TOKEN }} 39 | run: > 40 | cd packages/vscode-tailwindcss && 41 | pnpm run publish --pre-release -p $VSCODE_TOKEN 42 | - name: Publish LSP 43 | run: > 44 | cd packages/tailwindcss-language-server && 45 | pnpm publish --tag insiders --access public --no-git-checks 46 | env: 47 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /*.vsix 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/*/dist 2 | packages/*/bin 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | // List of configurations. Add new configurations or edit existing ones. 4 | "configurations": [ 5 | { 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "name": "Launch Client", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | // enable this flag if you want to activate the extension only when you are debugging the extension 12 | // "--disable-extensions", 13 | "--disable-updates", 14 | "--disable-workspace-trust", 15 | "--skip-release-notes", 16 | "--skip-welcome", 17 | "--extensionDevelopmentPath=${workspaceRoot}/packages/vscode-tailwindcss" 18 | ], 19 | "stopOnEntry": false, 20 | "sourceMaps": true, 21 | "outFiles": ["${workspaceRoot}/packages/vscode-tailwindcss/dist/**/*.js"], 22 | "preLaunchTask": "npm: dev" 23 | }, 24 | { 25 | "type": "node", 26 | "request": "attach", 27 | "name": "Attach to Server 6011", 28 | "address": "localhost", 29 | "protocol": "inspector", 30 | "port": 6011, 31 | "sourceMaps": true, 32 | "outFiles": ["${workspaceRoot}/packages/vscode-tailwindcss/dist/**/*.js"] 33 | }, 34 | { 35 | "type": "node", 36 | "request": "attach", 37 | "name": "Attach to Server 6012", 38 | "address": "localhost", 39 | "protocol": "inspector", 40 | "port": 6012, 41 | "sourceMaps": true, 42 | "outFiles": ["${workspaceRoot}/packages/vscode-tailwindcss/dist/**/*.js"] 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": ["$tsc"] 13 | }, 14 | { 15 | "type": "npm", 16 | "script": "dev", 17 | "isBackground": true, 18 | "group": { 19 | "kind": "build", 20 | "isDefault": true 21 | }, 22 | "presentation": { 23 | "panel": "dedicated", 24 | "reveal": "never" 25 | }, 26 | "options": { 27 | "cwd": "${workspaceFolder}/packages/vscode-tailwindcss" 28 | }, 29 | "problemMatcher": ["$tsc-watch"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/vscode-tailwindcss/README.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-intellisense", 3 | "private": true, 4 | "devDependencies": { 5 | "@npmcli/package-json": "^5.0.0", 6 | "@types/culori": "^2.1.0", 7 | "culori": "^4.0.1", 8 | "esbuild": "^0.25.0", 9 | "minimist": "^1.2.8", 10 | "prettier": "^3.2.5", 11 | "semver": "^7.7.1" 12 | }, 13 | "prettier": { 14 | "semi": false, 15 | "singleQuote": true, 16 | "printWidth": 100 17 | }, 18 | "packageManager": "pnpm@9.6.0" 19 | } 20 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tailwind Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/README.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS Language Server 2 | 3 | [Language Server Protocol](https://github.com/Microsoft/language-server-protocol) implementation for [Tailwind CSS](https://tailwindcss.com), used by [Tailwind CSS IntelliSense for VS Code](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss). 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm install -g @tailwindcss/language-server 9 | ``` 10 | 11 | ## Run 12 | 13 | ```bash 14 | tailwindcss-language-server --stdio 15 | ``` 16 | 17 | ``` 18 | Usage: tailwindcss-language-server [options] 19 | 20 | Options: 21 | 22 | --stdio use stdio 23 | --node-ipc use node-ipc 24 | --socket= use socket 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/scripts/createNoticesFile.mjs: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs' 2 | import checker from 'license-checker' 3 | import { readFileSync } from 'fs' 4 | import { dirname, resolve } from 'path' 5 | import { fileURLToPath } from 'url' 6 | 7 | const exclude = [/^@types\//, 'esbuild', 'rimraf', 'prettier', 'typescript', 'license-checker'] 8 | 9 | function isExcluded(name) { 10 | for (let pattern of exclude) { 11 | if (typeof pattern === 'string') { 12 | if (name === pattern) { 13 | return true 14 | } 15 | } else if (pattern.test(name)) { 16 | return true 17 | } 18 | } 19 | return false 20 | } 21 | 22 | function getDeps(dir, dev = false) { 23 | return Object.entries( 24 | JSON.parse(readFileSync(resolve(dir, 'package.json'), 'utf-8'))[ 25 | dev ? 'devDependencies' : 'dependencies' 26 | ], 27 | ).map(([name, version]) => `${name}@${version}`) 28 | } 29 | 30 | function getLicenses(dir) { 31 | return new Promise((resolve, reject) => { 32 | checker.init({ start: dir }, (err, packages) => { 33 | if (err) { 34 | reject(err) 35 | } else { 36 | resolve(packages) 37 | } 38 | }) 39 | }) 40 | } 41 | 42 | ;(async function () { 43 | const __dirname = dirname(fileURLToPath(import.meta.url)) 44 | let contents = [] 45 | 46 | let serverDeps = getDeps(resolve(__dirname, '..'), true) 47 | let serviceDeps = getDeps(resolve(__dirname, '../../tailwindcss-language-service')) 48 | let allDeps = [...serverDeps, ...serviceDeps] 49 | 50 | let serverLicenses = await getLicenses(resolve(__dirname, '../')) 51 | let serviceLicenses = await getLicenses(resolve(__dirname, '../../tailwindcss-language-service')) 52 | let allLicenses = { ...serverLicenses, ...serviceLicenses } 53 | 54 | for (let pkg in allLicenses) { 55 | let parts = pkg.split('@') 56 | let name = parts.slice(0, parts.length - 1).join('@') 57 | if (allDeps.includes(pkg) && !isExcluded(name)) { 58 | let license = allLicenses[pkg].licenseFile 59 | ? readFileSync(allLicenses[pkg].licenseFile, 'utf-8').trim() 60 | : undefined 61 | if (license) { 62 | contents.push(`${pkg}\n\n${license}`) 63 | } 64 | } 65 | } 66 | 67 | writeFileSync( 68 | resolve(__dirname, '../ThirdPartyNotices.txt'), 69 | contents.join(`\n\n${'='.repeat(80)}\n\n`), 70 | 'utf-8', 71 | ) 72 | })() 73 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/scripts/hashbang.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs' 2 | import { dirname, resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | let __dirname = dirname(fileURLToPath(import.meta.url)) 6 | let file = resolve(__dirname, '../bin/tailwindcss-language-server') 7 | 8 | writeFileSync(file, '#!/usr/bin/env node\n' + readFileSync(file, 'utf-8'), 'utf-8') 9 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/cache-map.ts: -------------------------------------------------------------------------------- 1 | export class CacheMap extends Map { 2 | remember(key: TKey, factory: (key: TKey) => TValue): TValue { 3 | let value = super.get(key) 4 | if (!value) { 5 | value = factory(key) 6 | this.set(key, value) 7 | } 8 | return value! 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/config.ts: -------------------------------------------------------------------------------- 1 | import merge from 'deepmerge' 2 | import { isObject } from './utils' 3 | import { 4 | getDefaultTailwindSettings, 5 | type Settings, 6 | } from '@tailwindcss/language-service/src/util/state' 7 | import type { Connection } from 'vscode-languageserver' 8 | 9 | export interface SettingsCache { 10 | get(uri?: string): Promise 11 | clear(): void 12 | } 13 | 14 | export function createSettingsCache(connection: Connection): SettingsCache { 15 | const cache: Map = new Map() 16 | 17 | async function get(uri?: string) { 18 | let config = cache.get(uri) 19 | 20 | if (!config) { 21 | config = await load(uri) 22 | cache.set(uri, config) 23 | } 24 | 25 | return config 26 | } 27 | 28 | async function load(uri?: string) { 29 | let [editor, tailwindCSS] = await Promise.all([ 30 | connection.workspace.getConfiguration({ 31 | section: 'editor', 32 | scopeUri: uri, 33 | }), 34 | 35 | connection.workspace.getConfiguration({ 36 | section: 'tailwindCSS', 37 | scopeUri: uri, 38 | }), 39 | ]) 40 | 41 | editor = isObject(editor) ? editor : {} 42 | tailwindCSS = isObject(tailwindCSS) ? tailwindCSS : {} 43 | 44 | return merge( 45 | getDefaultTailwindSettings(), 46 | { editor, tailwindCSS }, 47 | { arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray }, 48 | ) 49 | } 50 | 51 | return { 52 | get, 53 | clear() { 54 | cache.clear() 55 | }, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/css/extract-source-directives.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'postcss' 2 | import type { SourcePattern } from '../project-locator' 3 | 4 | export function extractSourceDirectives(sources: SourcePattern[]): Plugin { 5 | return { 6 | postcssPlugin: 'extract-at-rules', 7 | AtRule: { 8 | source: ({ params }) => { 9 | let negated = /^not\s+/.test(params) 10 | 11 | if (negated) params = params.slice(4).trimStart() 12 | 13 | if (params[0] !== '"' && params[0] !== "'") return 14 | 15 | sources.push({ 16 | pattern: params.slice(1, -1), 17 | negated, 18 | }) 19 | }, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/css/fix-relative-paths.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import type { AtRule, Plugin } from 'postcss' 3 | import { normalizePath } from '../utils' 4 | 5 | const SINGLE_QUOTE = "'" 6 | const DOUBLE_QUOTE = '"' 7 | 8 | export function fixRelativePaths(): Plugin { 9 | // Retain a list of touched at-rules to avoid infinite loops 10 | let touched: WeakSet = new WeakSet() 11 | 12 | function fixRelativePath(atRule: AtRule) { 13 | if (touched.has(atRule)) return 14 | 15 | let rootPath = atRule.root().source?.input.file 16 | if (!rootPath) return 17 | 18 | let inputFilePath = atRule.source?.input.file 19 | if (!inputFilePath) return 20 | 21 | let value = atRule.params[0] 22 | 23 | let quote = 24 | value[0] === DOUBLE_QUOTE && value[value.length - 1] === DOUBLE_QUOTE 25 | ? DOUBLE_QUOTE 26 | : value[0] === SINGLE_QUOTE && value[value.length - 1] === SINGLE_QUOTE 27 | ? SINGLE_QUOTE 28 | : null 29 | 30 | if (!quote) return 31 | 32 | let glob = atRule.params.slice(1, -1) 33 | 34 | // Handle eventual negative rules. We only support one level of negation. 35 | let negativePrefix = '' 36 | if (glob.startsWith('!')) { 37 | glob = glob.slice(1) 38 | negativePrefix = '!' 39 | } 40 | 41 | // We only want to rewrite relative paths. 42 | if (!glob.startsWith('./') && !glob.startsWith('../')) { 43 | return 44 | } 45 | 46 | let absoluteGlob = path.posix.join(normalizePath(path.dirname(inputFilePath)), glob) 47 | let absoluteRootPosixPath = path.posix.dirname(normalizePath(rootPath)) 48 | 49 | let relative = path.posix.relative(absoluteRootPosixPath, absoluteGlob) 50 | 51 | // If the path points to a file in the same directory, `path.relative` will 52 | // remove the leading `./` and we need to add it back in order to still 53 | // consider the path relative 54 | if (!relative.startsWith('.')) { 55 | relative = './' + relative 56 | } 57 | 58 | atRule.params = quote + negativePrefix + relative + quote 59 | touched.add(atRule) 60 | } 61 | 62 | return { 63 | postcssPlugin: 'tailwindcss-postcss-fix-relative-paths', 64 | AtRule: { 65 | source: fixRelativePath, 66 | plugin: fixRelativePath, 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/css/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resolve-css-imports' 2 | export * from './extract-source-directives' 3 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/css/resolve-css-imports.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises' 2 | import postcss from 'postcss' 3 | import postcssImport from 'postcss-import' 4 | import { fixRelativePaths } from './fix-relative-paths' 5 | import { Resolver } from '../resolver' 6 | 7 | export function resolveCssImports({ 8 | resolver, 9 | loose = false, 10 | }: { 11 | resolver: Resolver 12 | loose?: boolean 13 | }) { 14 | return postcss([ 15 | // Replace `@reference "…"` with `@import "…" reference` 16 | { 17 | postcssPlugin: 'replace-at-reference', 18 | Once(root) { 19 | root.walkAtRules('reference', (atRule) => { 20 | atRule.name = 'import' 21 | atRule.params += ' reference' 22 | }) 23 | }, 24 | }, 25 | 26 | // Hoist imports to the top of the file 27 | { 28 | postcssPlugin: 'hoist-at-import', 29 | Once(root, { result }) { 30 | if (!loose) return 31 | 32 | let hoist: postcss.AtRule[] = [] 33 | let seenOtherNodes = false 34 | let seenImportsAfterOtherNodes = false 35 | 36 | for (let node of root.nodes) { 37 | if (node.type === 'atrule' && (node.name === 'import' || node.name === 'charset')) { 38 | hoist.push(node) 39 | 40 | if (seenOtherNodes) { 41 | seenImportsAfterOtherNodes = true 42 | } 43 | } else if (node.type === 'atrule') { 44 | if (node.name === 'layer') { 45 | if (!node.nodes || node.nodes.length > 0) { 46 | continue 47 | } 48 | } 49 | 50 | seenOtherNodes = true 51 | } else if (node.type === 'rule') { 52 | seenOtherNodes = true 53 | } 54 | } 55 | 56 | root.prepend(hoist) 57 | 58 | if (!seenImportsAfterOtherNodes) return 59 | 60 | console.log( 61 | `hoist-at-import: The file '${result.opts.from}' contains @import rules after other at rules. This is invalid CSS and may cause problems with your build.`, 62 | ) 63 | }, 64 | }, 65 | 66 | postcssImport({ 67 | async resolve(id, base) { 68 | try { 69 | return await resolver.resolveCssId(id, base) 70 | } catch (e) { 71 | // TODO: Need to test this on windows 72 | return `/virtual:missing/${id}` 73 | } 74 | }, 75 | 76 | load(filepath) { 77 | if (filepath.startsWith('/virtual:missing/')) { 78 | return Promise.resolve('') 79 | } 80 | 81 | return fs.readFile(filepath, 'utf-8') 82 | }, 83 | }), 84 | fixRelativePaths(), 85 | ]) 86 | } 87 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/documents.ts: -------------------------------------------------------------------------------- 1 | import { type Connection, TextDocuments } from 'vscode-languageserver/node' 2 | import { TextDocument } from 'vscode-languageserver-textdocument' 3 | 4 | export class DocumentService { 5 | public documents: TextDocuments 6 | 7 | constructor(conn: Connection) { 8 | this.documents = new TextDocuments(TextDocument) 9 | this.documents.listen(conn) 10 | } 11 | 12 | getDocument(uri: string) { 13 | return this.documents.get(uri) 14 | } 15 | 16 | getAllDocuments() { 17 | return this.documents.all() 18 | } 19 | 20 | get onDidChangeContent() { 21 | return this.documents.onDidChangeContent 22 | } 23 | get onDidClose() { 24 | return this.documents.onDidClose 25 | } 26 | get onDidOpen() { 27 | return this.documents.onDidOpen 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/graph.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { Graph } from './graph' 3 | 4 | function buildGraph() { 5 | let graph = new Graph() 6 | graph.add('A', 'a') 7 | graph.add('B', 'b') 8 | graph.add('C', 'c') 9 | graph.add('D', 'd') 10 | graph.add('E', 'e') 11 | graph.add('F', 'f') 12 | graph.add('G', 'g') 13 | 14 | // A -> B -> {D,E} 15 | graph.connect('A', 'B') 16 | graph.connect('B', 'D') 17 | graph.connect('B', 'E') 18 | 19 | // A -> C -> F 20 | graph.connect('A', 'C') 21 | graph.connect('C', 'F') 22 | 23 | // B -> C (-> F, implied) 24 | graph.connect('B', 'C') 25 | 26 | return graph 27 | } 28 | 29 | test('graph#add returns existing nodes', () => { 30 | let a1 = { foo: 'bar' } 31 | let a2 = { foo: 'baz' } 32 | 33 | let graph1 = new Graph() 34 | expect(graph1.add('A', a1)).toBe(a1) 35 | expect(graph1.add('A', a2)).toBe(a1) 36 | 37 | let graph2 = new Graph() 38 | expect(graph2.add('A', a2)).toBe(a2) 39 | expect(graph2.add('A', a1)).toBe(a2) 40 | }) 41 | 42 | test('graph#connect with nodes that do not exist', () => { 43 | expect(() => { 44 | let graph = new Graph() 45 | graph.connect('A', 'B') 46 | }).toThrowErrorMatchingInlineSnapshot(`[Error: Node A does not exist]`) 47 | 48 | expect(() => { 49 | let graph = new Graph() 50 | graph.add('A', 'a') 51 | graph.connect('A', 'B') 52 | }).toThrowErrorMatchingInlineSnapshot(`[Error: Node B does not exist]`) 53 | 54 | expect(() => { 55 | let graph = new Graph() 56 | graph.add('A', 'a') 57 | graph.add('B', 'b') 58 | graph.connect('A', 'B') 59 | }).not.toThrow() 60 | }) 61 | 62 | test('graph#roots', () => { 63 | let result = Array.from(buildGraph().roots()) 64 | 65 | expect(result).toMatchInlineSnapshot(` 66 | [ 67 | "a", 68 | "g", 69 | ] 70 | `) 71 | }) 72 | 73 | test('graph#leaves', () => { 74 | let result = Array.from(buildGraph().leaves()) 75 | 76 | expect(result).toMatchInlineSnapshot(` 77 | [ 78 | "d", 79 | "e", 80 | "f", 81 | "g", 82 | ] 83 | `) 84 | }) 85 | 86 | test('graph#descendants', () => { 87 | let result = [ 88 | Array.from(buildGraph().descendants('A')), 89 | Array.from(buildGraph().descendants('B')), 90 | Array.from(buildGraph().descendants('C')), 91 | Array.from(buildGraph().descendants('D')), 92 | ] 93 | 94 | expect(result).toMatchInlineSnapshot(` 95 | [ 96 | [ 97 | "b", 98 | "c", 99 | "d", 100 | "e", 101 | "f", 102 | ], 103 | [ 104 | "d", 105 | "e", 106 | "c", 107 | "f", 108 | ], 109 | [ 110 | "f", 111 | ], 112 | [], 113 | ] 114 | `) 115 | }) 116 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/graph.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a dependency graph 3 | */ 4 | export class Graph { 5 | /** 6 | * A list of all direct ancestors for each node 7 | */ 8 | private parents: Map> = new Map() 9 | 10 | /** 11 | * A list of all direct descendants for each node 12 | */ 13 | private children: Map> = new Map() 14 | 15 | /** 16 | * A list of all direct descendants for each node 17 | */ 18 | private nodes: Map = new Map() 19 | 20 | add(id: string, value: T): T { 21 | if (this.nodes.has(id)) { 22 | return this.nodes.get(id) 23 | } 24 | 25 | this.nodes.set(id, value) 26 | this.parents.set(id, new Set()) 27 | this.children.set(id, new Set()) 28 | 29 | return value 30 | } 31 | 32 | /** 33 | * Connect two nodes to each other 34 | */ 35 | connect(from: string, to: string) { 36 | let children = this.children.get(from) 37 | if (!children) throw new Error(`Node ${from} does not exist`) 38 | 39 | let parents = this.parents.get(to) 40 | if (!parents) throw new Error(`Node ${to} does not exist`) 41 | 42 | parents.add(from) 43 | children.add(to) 44 | } 45 | 46 | *descendants(id: string): Iterable { 47 | let q: string[] = [] 48 | let seen: Set = new Set() 49 | 50 | for (let child of this.children.get(id)) { 51 | q.push(child) 52 | } 53 | 54 | while (q.length > 0) { 55 | let current = q.shift()! 56 | if (seen.has(current)) continue 57 | yield this.nodes.get(current) 58 | seen.add(current) 59 | for (let child of this.children.get(current)) { 60 | q.push(child) 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * A list of all root nodes in the graph 67 | * (meaning they have no parents) 68 | */ 69 | *roots(): Iterable { 70 | for (let [id, parents] of this.parents) { 71 | if (parents.size !== 0) continue 72 | yield this.nodes.get(id) 73 | } 74 | } 75 | 76 | /** 77 | * A list of all leaf nodes in the graph 78 | * (meaning they have no children) 79 | */ 80 | *leaves(): Iterable { 81 | for (let [id, children] of this.children) { 82 | if (children.size !== 0) continue 83 | yield this.nodes.get(id) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/language/css.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { createConnection, ProposedFeatures } from 'vscode-languageserver/node' 3 | import { interceptLogs } from '../util/logs' 4 | import { CssServer } from './css-server' 5 | 6 | let connection = 7 | process.argv.length <= 2 8 | ? createConnection(ProposedFeatures.all, process.stdin, process.stdout) 9 | : createConnection(ProposedFeatures.all) 10 | 11 | interceptLogs(console, connection) 12 | 13 | process.on('unhandledRejection', (e: any) => { 14 | console.error('Unhandled exception', e) 15 | }) 16 | 17 | let server = new CssServer(connection) 18 | server.setup() 19 | server.listen() 20 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const CONFIG_GLOB = 2 | '{tailwind,tailwind.config,tailwind.*.config,tailwind.config.*}.{js,cjs,ts,mjs,mts,cts}' 3 | export const PACKAGE_LOCK_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}' 4 | export const CSS_GLOB = '*.{css,scss,sass,less,pcss}' 5 | export const TSCONFIG_GLOB = '{tsconfig,tsconfig.*,jsconfig,jsconfig.*}.json' 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/lib/env.ts: -------------------------------------------------------------------------------- 1 | import Module from 'node:module' 2 | import * as path from 'node:path' 3 | import { resolveFrom } from '../util/resolveFrom' 4 | 5 | process.env.TAILWIND_MODE = 'build' 6 | process.env.TAILWIND_DISABLE_TOUCH = 'true' 7 | 8 | let oldResolveFilename = (Module as any)._resolveFilename 9 | 10 | function isBuiltin(id: string) { 11 | // Node 16.17+, v18.6.0+, >= v20 12 | // VSCode >= 1.78 13 | if ('isBuiltin' in Module) { 14 | return Module.isBuiltin(id) 15 | } 16 | 17 | // Older versions of Node and VSCode 18 | // @ts-ignore 19 | return Module.builtinModules.includes(id.replace(/^node:/, '')) 20 | } 21 | 22 | ;(Module as any)._resolveFilename = (id: any, parent: any) => { 23 | if (typeof id === 'string' && isBuiltin(id)) { 24 | return oldResolveFilename(id, parent) 25 | } 26 | 27 | if (parent) { 28 | return resolveFrom(path.dirname(parent.id), id) 29 | } 30 | 31 | // console.log(`Resolving w/o parent ${id}`) 32 | 33 | return resolveFrom(process.cwd(), id) 34 | } 35 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/lib/extract-class-names.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { parse } from 'postcss' 3 | import extractClassNames from './extractClassNames' 4 | 5 | test('ex: 1', async () => { 6 | let result = await extractClassNames(parse('.foo {}')) 7 | 8 | expect(result.classNames).toHaveProperty('foo') 9 | expect(result.classNames['foo']).toEqual({ 10 | __info: { 11 | __rule: true, 12 | __source: undefined, 13 | __pseudo: [], 14 | __scope: null, 15 | __context: [], 16 | }, 17 | }) 18 | }) 19 | 20 | test('ex: 2', async () => { 21 | let result = await extractClassNames(parse('.foo.bar {}')) 22 | 23 | expect(result.classNames).toHaveProperty('foo') 24 | expect(result.classNames).toHaveProperty('bar') 25 | expect(result.classNames['foo']).toEqual({ 26 | __info: { 27 | __source: undefined, 28 | __pseudo: [], 29 | __scope: null, 30 | __context: [], 31 | }, 32 | }) 33 | expect(result.classNames['bar']).toEqual({ 34 | __info: { 35 | __rule: true, 36 | __source: undefined, 37 | __pseudo: [], 38 | __scope: '.foo', 39 | __context: [], 40 | }, 41 | }) 42 | }) 43 | 44 | test('ex: 3', async () => { 45 | let result = await extractClassNames(parse('.foo:where(.bar:is(.baz:has(> .klass))) {}')) 46 | 47 | expect(result.classNames).toHaveProperty('foo') 48 | expect(result.classNames).not.toHaveProperty('bar') 49 | expect(result.classNames).not.toHaveProperty('baz') 50 | expect(result.classNames).not.toHaveProperty('klass') 51 | expect(result.classNames['foo']).toEqual({ 52 | __info: { 53 | __rule: true, 54 | __source: undefined, 55 | __pseudo: [':where(.bar:is(.baz:has(> .klass)))'], 56 | __scope: null, 57 | __context: [], 58 | }, 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/lib/plugins.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '@tailwindcss/aspect-ratio': { 3 | module: require('@tailwindcss/aspect-ratio'), 4 | version: require('@tailwindcss/aspect-ratio/package.json').version, 5 | }, 6 | '@tailwindcss/container-queries': { 7 | module: require('@tailwindcss/container-queries'), 8 | version: require('@tailwindcss/container-queries/package.json').version, 9 | }, 10 | '@tailwindcss/forms': { 11 | module: require('@tailwindcss/forms'), 12 | version: require('@tailwindcss/forms/package.json').version, 13 | }, 14 | '@tailwindcss/line-clamp': { 15 | module: require('@tailwindcss/line-clamp'), 16 | version: require('@tailwindcss/line-clamp/package.json').version, 17 | }, 18 | '@tailwindcss/typography': { 19 | module: require('@tailwindcss/typography'), 20 | version: require('@tailwindcss/typography/package.json').version, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument } from 'vscode-languageserver-textdocument' 2 | import type { State } from '@tailwindcss/language-service/src/util/state' 3 | import { doValidate } from '@tailwindcss/language-service/src/diagnostics/diagnosticsProvider' 4 | import isExcluded from '../util/isExcluded' 5 | 6 | export async function provideDiagnostics(state: State, document: TextDocument) { 7 | if (await isExcluded(state, document)) { 8 | clearDiagnostics(state, document) 9 | } else { 10 | state.editor?.connection.sendDiagnostics({ 11 | uri: document.uri, 12 | diagnostics: await doValidate(state, document), 13 | }) 14 | } 15 | } 16 | 17 | export function clearDiagnostics(state: State, document: TextDocument): void { 18 | state.editor?.connection.sendDiagnostics({ 19 | uri: document.uri, 20 | diagnostics: [], 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/matching.ts: -------------------------------------------------------------------------------- 1 | import picomatch from 'picomatch' 2 | import { DefaultMap } from './util/default-map' 3 | 4 | export interface PathMatcher { 5 | anyMatches(pattern: string, paths: string[]): boolean 6 | clear(): void 7 | } 8 | 9 | export function createPathMatcher(): PathMatcher { 10 | let matchers = new DefaultMap((pattern) => { 11 | // Escape picomatch special characters so they're matched literally 12 | pattern = pattern.replace(/[\[\]{}()]/g, (m) => `\\${m}`) 13 | 14 | return picomatch(pattern, { dot: true }) 15 | }) 16 | 17 | return { 18 | anyMatches: (pattern, paths) => { 19 | let check = matchers.get(pattern) 20 | return paths.some((path) => check(path)) 21 | }, 22 | clear: () => matchers.clear(), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/resolver/pnp.ts: -------------------------------------------------------------------------------- 1 | import findUp from 'find-up' 2 | import * as path from 'node:path' 3 | import { pathToFileURL } from '../utils' 4 | 5 | export interface PnpApi { 6 | resolveToUnqualified: (arg0: string, arg1: string, arg2: object) => null | string 7 | } 8 | 9 | const cache = new Map() 10 | 11 | /** 12 | * Loads the PnP API from the given directory if found. 13 | * We intentionally do not call `setup` to monkey patch global APIs 14 | * TODO: Verify that we can get by without doing this 15 | */ 16 | export async function loadPnPApi(root: string): Promise { 17 | let existing = cache.get(root) 18 | if (existing !== undefined) { 19 | return existing 20 | } 21 | 22 | let pnpPath = await findPnPApi(path.normalize(root)) 23 | if (!pnpPath) { 24 | cache.set(root, null) 25 | return null 26 | } 27 | 28 | let pnpUrl = pathToFileURL(pnpPath).href 29 | let mod = await import(pnpUrl) 30 | let api = mod.default 31 | api.setup() 32 | cache.set(root, api) 33 | return api 34 | } 35 | 36 | /** 37 | * Locates the PnP API file for a given directory 38 | */ 39 | async function findPnPApi(root: string): Promise { 40 | let names = ['.pnp.js', '.pnp.cjs'] 41 | 42 | for (let name of names) { 43 | let filepath = path.join(root, name) 44 | 45 | if (await findUp.exists(filepath)) { 46 | return filepath 47 | } 48 | } 49 | 50 | return null 51 | } 52 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/server.ts: -------------------------------------------------------------------------------- 1 | import './lib/env' 2 | import { createConnection } from 'vscode-languageserver/node' 3 | // @ts-ignore 4 | import preflight from 'tailwindcss/lib/css/preflight.css' 5 | import { TW } from './tw' 6 | import { interceptLogs } from './util/logs' 7 | 8 | // @ts-ignore 9 | // new Function(…) is used to work around issues with esbuild 10 | global.__preflight = preflight 11 | new Function( 12 | 'require', 13 | '__dirname', 14 | ` 15 | let oldReadFileSync = require('fs').readFileSync 16 | require('fs').readFileSync = function (filename, ...args) { 17 | if (filename === require('path').join(__dirname, 'css/preflight.css')) { 18 | return global.__preflight 19 | } 20 | return oldReadFileSync(filename, ...args) 21 | } 22 | `, 23 | )(require, __dirname) 24 | 25 | const connection = 26 | process.argv.length <= 2 ? createConnection(process.stdin, process.stdout) : createConnection() 27 | 28 | interceptLogs(console, connection) 29 | 30 | process.on('unhandledRejection', (e: any) => { 31 | console.error(`Unhandled rejection`, e) 32 | }) 33 | 34 | const tw = new TW(connection) 35 | 36 | console.log('Setting up server…') 37 | tw.setup() 38 | 39 | console.log('Listening for messages…') 40 | tw.listen() 41 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/css.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | import { getTextWithoutComments } from '@tailwindcss/language-service/src/util/doc' 3 | 4 | export async function readCssFile(filepath: string): Promise { 5 | try { 6 | let contents = await readFile(filepath, 'utf8') 7 | return getTextWithoutComments(contents, 'css') 8 | } catch { 9 | return null 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/default-map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Map that can generate default values for keys that don't exist. 3 | * Generated default values are added to the map to avoid recomputation. 4 | */ 5 | export class DefaultMap extends Map { 6 | constructor(private factory: (key: T, self: DefaultMap) => V) { 7 | super() 8 | } 9 | 10 | get(key: T): V { 11 | let value = super.get(key) 12 | 13 | if (value === undefined) { 14 | value = this.factory(key, this) 15 | this.set(key, value) 16 | } 17 | 18 | return value 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/error.ts: -------------------------------------------------------------------------------- 1 | import type { Connection } from 'vscode-languageserver/node' 2 | 3 | function toString(err: any, includeStack: boolean = true): string { 4 | if (err instanceof Error) { 5 | let error = err 6 | return `${error.message}${includeStack ? `\n${error.stack}` : ''}` 7 | } else if (typeof err === 'string') { 8 | return err 9 | } else { 10 | return err.toString() 11 | } 12 | } 13 | 14 | // https://github.com/vscode-langservers/vscode-json-languageserver/blob/master/src/utils/runner.ts 15 | export function formatError(message: string, err: any, includeStack: boolean = true): string { 16 | if (err) { 17 | return `${message}: ${toString(err, includeStack)}` 18 | } 19 | return message 20 | } 21 | 22 | export function showError( 23 | connection: Connection, 24 | err: any, 25 | message: string = 'Tailwind CSS', 26 | ): void { 27 | console.error(formatError(message, err)) 28 | // if (!(err instanceof SilentError)) { 29 | // connection.sendNotification('@/tailwindCSS/error', { 30 | // message: formatError(message, err, false), 31 | // }) 32 | // } 33 | } 34 | 35 | export function showWarning( 36 | connection: Connection, 37 | message: string = 'Tailwind CSS', 38 | err: any, 39 | ): void { 40 | connection.sendNotification('@/tailwindCSS/warn', { 41 | message: formatError(message, err, false), 42 | }) 43 | } 44 | 45 | export function SilentError(message: string) { 46 | this.name = 'SilentError' 47 | this.message = message 48 | this.stack = new Error().stack 49 | } 50 | SilentError.prototype = new Error() 51 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/get-package-root.ts: -------------------------------------------------------------------------------- 1 | import findUp from 'find-up' 2 | import * as path from 'node:path' 3 | 4 | export async function getPackageRoot(cwd: string, rootDir: string) { 5 | async function check(dir: string) { 6 | let pkgJson = path.join(dir, 'package.json') 7 | if (await findUp.exists(pkgJson)) { 8 | return pkgJson 9 | } 10 | 11 | if (dir === path.normalize(rootDir)) { 12 | return findUp.stop 13 | } 14 | } 15 | 16 | try { 17 | let pkgJsonPath = await findUp(check, { cwd }) 18 | return pkgJsonPath ? path.dirname(pkgJsonPath) : rootDir 19 | } catch { 20 | return rootDir 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/isExcluded.ts: -------------------------------------------------------------------------------- 1 | import picomatch from 'picomatch' 2 | import * as path from 'node:path' 3 | import type { TextDocument } from 'vscode-languageserver-textdocument' 4 | import type { State } from '@tailwindcss/language-service/src/util/state' 5 | import { getFileFsPath } from './uri' 6 | import { normalizePath, normalizeDriveLetter } from '../utils' 7 | 8 | export default async function isExcluded( 9 | state: State, 10 | document: TextDocument, 11 | file: string = getFileFsPath(document.uri), 12 | ): Promise { 13 | let settings = await state.editor.getConfiguration(document.uri) 14 | 15 | file = normalizePath(file) 16 | file = normalizeDriveLetter(file) 17 | 18 | for (let pattern of settings.tailwindCSS.files.exclude) { 19 | pattern = path.join(state.editor.folder, pattern) 20 | pattern = normalizePath(pattern) 21 | pattern = normalizeDriveLetter(pattern) 22 | 23 | // TODO: This should use the server-level path matcher 24 | if (picomatch(pattern)(file)) { 25 | return true 26 | } 27 | } 28 | 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/logs.ts: -------------------------------------------------------------------------------- 1 | import type { Connection } from 'vscode-languageserver' 2 | import { format } from 'node:util' 3 | 4 | function formatForLogging(params: any[]): string { 5 | return params.map((item) => format(item)).join(' ') 6 | } 7 | 8 | export function interceptLogs(console: Console, connection: Connection) { 9 | console.debug = (...params: any[]) => connection.console.info(formatForLogging(params)) 10 | console.error = (...params: any[]) => connection.console.error(formatForLogging(params)) 11 | console.warn = (...params: any[]) => connection.console.warn(formatForLogging(params)) 12 | console.info = (...params: any[]) => connection.console.info(formatForLogging(params)) 13 | console.log = (...params: any[]) => connection.console.log(formatForLogging(params)) 14 | } 15 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/resolveFrom.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs' 2 | import * as path from 'node:path' 3 | import { equal } from '@tailwindcss/language-service/src/util/array' 4 | import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve' 5 | 6 | let pnpApi: any 7 | let extensions = Object.keys(require.extensions) 8 | 9 | function recreateResolver() { 10 | let fileSystem = new CachedInputFileSystem(fs, 4000) 11 | 12 | return ResolverFactory.createResolver({ 13 | fileSystem, 14 | useSyncFileSystemCalls: true, 15 | conditionNames: ['node', 'require'], 16 | extensions, 17 | pnpApi, 18 | }) 19 | } 20 | 21 | let resolver = recreateResolver() 22 | 23 | /** 24 | * @deprecated Use `createResolver()` instead. 25 | */ 26 | export function setPnpApi(newPnpApi: any): void { 27 | pnpApi = newPnpApi 28 | resolver = recreateResolver() 29 | } 30 | 31 | /** 32 | * Resolve a module id from a given path synchronously. 33 | * 34 | * This is a legacy API and should be avoided in favor of the async version as 35 | * it does not support TypeScript path mapping. 36 | * 37 | * @deprecated Use `createResolver().resolveJsId(…)` instead. 38 | */ 39 | export function resolveFrom(from?: string, id?: string): string { 40 | // Network share path on Windows 41 | if (id.startsWith('\\\\')) return id 42 | 43 | // Normalized network share path on Windows 44 | if (id.startsWith('//') && path.sep === '\\') return id 45 | 46 | // Normalized network share path on Windows 47 | if (from.startsWith('//') && path.sep === '\\') { 48 | from = '\\\\' + from.slice(2) 49 | } 50 | 51 | let newExtensions = Object.keys(require.extensions) 52 | if (!equal(newExtensions, extensions)) { 53 | extensions = newExtensions 54 | resolver = recreateResolver() 55 | } 56 | 57 | let result = resolver.resolveSync({}, from, id) 58 | if (result === false) throw Error() 59 | 60 | // The `enhanced-resolve` package supports resolving paths with fragment 61 | // identifiers. For example, it can resolve `foo/bar#baz` to `foo/bar.js` 62 | // However, it's also possible that a path contains a `#` character as part 63 | // of the path itself. For example, `foo#bar` might point to a file named 64 | // `foo#bar.js`. The resolver distinguishes between these two cases by 65 | // escaping the `#` character with a NUL byte when it's part of the path. 66 | // 67 | // Since the real path doesn't actually contain NUL bytes, we need to remove 68 | // them to get the correct path otherwise readFileSync will throw an error. 69 | result = result.replace(/\0(.)/g, '$1') 70 | 71 | return result 72 | } 73 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/retry.ts: -------------------------------------------------------------------------------- 1 | export interface RetryOptions { 2 | tries: number 3 | delay: number 4 | callback: () => Promise 5 | } 6 | 7 | export async function retry({ tries, delay, callback }) { 8 | retry: try { 9 | return await callback() 10 | } catch (err) { 11 | if (tries-- === 0) throw err 12 | 13 | // Wait a bit before trying again _ this exists for projects like 14 | // Nuxt that create a several tsconfig files at once 15 | await new Promise((resolve) => setTimeout(resolve, delay)) 16 | 17 | break retry 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/uri.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri' 2 | 3 | export function normalizeFileNameToFsPath(fileName: string) { 4 | return URI.file(fileName).fsPath 5 | } 6 | 7 | export function getFileFsPath(documentUri: string): string { 8 | return URI.parse(documentUri).fsPath 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/v4/assets.ts: -------------------------------------------------------------------------------- 1 | import index from 'tailwindcss-v4/index.css' 2 | import preflight from 'tailwindcss-v4/preflight.css' 3 | import theme from 'tailwindcss-v4/theme.css' 4 | import utilities from 'tailwindcss-v4/utilities.css' 5 | 6 | export const assets = { 7 | tailwindcss: index, 8 | 'tailwindcss/index': index, 9 | 'tailwindcss/index.css': index, 10 | 11 | 'tailwindcss/preflight': preflight, 12 | 'tailwindcss/preflight.css': preflight, 13 | 14 | 'tailwindcss/theme': theme, 15 | 'tailwindcss/theme.css': theme, 16 | 17 | 'tailwindcss/utilities': utilities, 18 | 'tailwindcss/utilities.css': utilities, 19 | } 20 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/v4/index.ts: -------------------------------------------------------------------------------- 1 | export * from './design-system' 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/util/v4/plugins.ts: -------------------------------------------------------------------------------- 1 | export const plugins = { 2 | '@tailwindcss/forms': () => import('@tailwindcss/forms').then((m) => m.default), 3 | '@tailwindcss/aspect-ratio': () => import('@tailwindcss/aspect-ratio').then((m) => m.default), 4 | '@tailwindcss/typography': () => import('@tailwindcss/typography').then((m) => m.default), 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/watcher/licenses/@parcel/watcher: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Devon Govett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/src/watcher/licenses/node-gyp-build: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/code-actions.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import * as fs from 'node:fs/promises' 3 | import { withFixture } from '../common' 4 | 5 | withFixture('basic', (c) => { 6 | function testFixture(fixture) { 7 | test(fixture, async () => { 8 | fixture = await fs.readFile(`tests/code-actions/${fixture}.json`, 'utf8') 9 | 10 | let { code, expected, language = 'html' } = JSON.parse(fixture) 11 | 12 | let promise = new Promise((resolve) => { 13 | c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => { 14 | resolve(diagnostics) 15 | }) 16 | }) 17 | 18 | let textDocument = await c.openDocument({ text: code, lang: language }) 19 | let diagnostics = await promise 20 | 21 | let res = await c.sendRequest('textDocument/codeAction', { 22 | textDocument, 23 | context: { 24 | diagnostics, 25 | }, 26 | }) 27 | // console.log(JSON.stringify(res)) 28 | 29 | expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', textDocument.uri)) 30 | 31 | expect(res).toEqual(expected) 32 | }) 33 | } 34 | 35 | testFixture('conflict') 36 | testFixture('invalid-theme') 37 | testFixture('invalid-screen') 38 | }) 39 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/code-actions.v2-jit.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { withFixture } from '../common' 3 | import * as fs from 'node:fs/promises' 4 | 5 | withFixture('v2-jit', (c) => { 6 | function testFixture(fixture) { 7 | test(fixture, async () => { 8 | fixture = await fs.readFile(`tests/code-actions/${fixture}.json`, 'utf8') 9 | 10 | let { code, expected, language = 'html' } = JSON.parse(fixture) 11 | 12 | let promise = new Promise((resolve) => { 13 | c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => { 14 | resolve(diagnostics) 15 | }) 16 | }) 17 | 18 | let textDocument = await c.openDocument({ text: code, lang: language }) 19 | let diagnostics = await promise 20 | 21 | let res = await c.sendRequest('textDocument/codeAction', { 22 | textDocument, 23 | context: { 24 | diagnostics, 25 | }, 26 | }) 27 | // console.log(JSON.stringify(res)) 28 | 29 | expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', textDocument.uri)) 30 | 31 | expect(res).toEqual(expected) 32 | }) 33 | } 34 | 35 | testFixture('variant-order') 36 | }) 37 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/code-actions.v2.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { withFixture } from '../common' 3 | import * as fs from 'node:fs/promises' 4 | 5 | withFixture('v2', (c) => { 6 | function testFixture(fixture) { 7 | test(fixture, async () => { 8 | fixture = await fs.readFile(`tests/code-actions/${fixture}.json`, 'utf8') 9 | 10 | let { code, expected, language = 'html' } = JSON.parse(fixture) 11 | 12 | let promise = new Promise((resolve) => { 13 | c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => { 14 | resolve(diagnostics) 15 | }) 16 | }) 17 | 18 | let textDocument = await c.openDocument({ text: code, lang: language }) 19 | let diagnostics = await promise 20 | 21 | let res = await c.sendRequest('textDocument/codeAction', { 22 | textDocument, 23 | context: { 24 | diagnostics, 25 | }, 26 | }) 27 | 28 | expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', textDocument.uri)) 29 | 30 | expect(res).toEqual(expected) 31 | }) 32 | } 33 | 34 | // testFixture('conflict') 35 | testFixture('invalid-theme') 36 | testFixture('invalid-screen') 37 | testFixture('invalid-variant') 38 | }) 39 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/code-actions.v4.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import * as fs from 'node:fs/promises' 3 | import { withFixture } from '../common' 4 | 5 | withFixture('v4/basic', (c) => { 6 | function testFixture(fixture) { 7 | test(fixture, async () => { 8 | fixture = await fs.readFile(`tests/code-actions/${fixture}.json`, 'utf8') 9 | 10 | let { code, expected, language = 'html' } = JSON.parse(fixture) 11 | 12 | let promise = new Promise((resolve) => { 13 | c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => { 14 | resolve(diagnostics) 15 | }) 16 | }) 17 | 18 | let textDocument = await c.openDocument({ text: code, lang: language }) 19 | let diagnostics = await promise 20 | 21 | let res = await c.sendRequest('textDocument/codeAction', { 22 | textDocument, 23 | context: { 24 | diagnostics, 25 | }, 26 | }) 27 | // console.log(JSON.stringify(res)) 28 | 29 | expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', textDocument.uri)) 30 | 31 | expect(res).toEqual(expected) 32 | }) 33 | } 34 | 35 | testFixture('conflict') 36 | // testFixture('invalid-theme') 37 | // testFixture('invalid-screen') 38 | }) 39 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/invalid-screen.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "@screen small", 3 | "language": "css", 4 | "expected": [ 5 | { 6 | "title": "Replace with 'sm'", 7 | "kind": "quickfix", 8 | "diagnostics": [ 9 | { 10 | "code": "invalidScreen", 11 | "range": { 12 | "start": { "line": 0, "character": 8 }, 13 | "end": { "line": 0, "character": 13 } 14 | }, 15 | "severity": 1, 16 | "message": "The screen 'small' does not exist in your theme config. Did you mean 'sm'?", 17 | "suggestions": ["sm"] 18 | } 19 | ], 20 | "edit": { 21 | "changes": { 22 | "{{URI}}": [ 23 | { 24 | "range": { 25 | "start": { "line": 0, "character": 8 }, 26 | "end": { "line": 0, "character": 13 } 27 | }, 28 | "newText": "sm" 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/invalid-theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": ".test { color: theme(colors.red.901) }", 3 | "language": "css", 4 | "expected": [ 5 | { 6 | "title": "Replace with 'colors.red.900'", 7 | "kind": "quickfix", 8 | "diagnostics": [ 9 | { 10 | "code": "invalidConfigPath", 11 | "range": { 12 | "start": { "line": 0, "character": 21 }, 13 | "end": { "line": 0, "character": 35 } 14 | }, 15 | "severity": 1, 16 | "message": "'colors.red.901' does not exist in your theme config. Did you mean 'colors.red.900'?", 17 | "suggestions": ["colors.red.900"] 18 | } 19 | ], 20 | "edit": { 21 | "changes": { 22 | "{{URI}}": [ 23 | { 24 | "range": { 25 | "start": { "line": 0, "character": 21 }, 26 | "end": { "line": 0, "character": 35 } 27 | }, 28 | "newText": "colors.red.900" 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/invalid-variant.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "@variants hoover", 3 | "language": "css", 4 | "expected": [ 5 | { 6 | "title": "Replace with 'hover'", 7 | "kind": "quickfix", 8 | "diagnostics": [ 9 | { 10 | "code": "invalidVariant", 11 | "range": { 12 | "start": { "line": 0, "character": 10 }, 13 | "end": { "line": 0, "character": 16 } 14 | }, 15 | "severity": 1, 16 | "message": "The variant 'hoover' does not exist. Did you mean 'hover'?", 17 | "suggestions": ["hover"] 18 | } 19 | ], 20 | "edit": { 21 | "changes": { 22 | "{{URI}}": [ 23 | { 24 | "range": { 25 | "start": { "line": 0, "character": 10 }, 26 | "end": { "line": 0, "character": 16 } 27 | }, 28 | "newText": "hover" 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/code-actions/variant-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "
", 3 | "expected": [ 4 | { 5 | "title": "Replace with 'focus:hover:uppercase'", 6 | "kind": "quickfix", 7 | "diagnostics": [ 8 | { 9 | "code": "recommendedVariantOrder", 10 | "suggestions": ["focus:hover:uppercase"], 11 | "range": { 12 | "start": { "line": 0, "character": 12 }, 13 | "end": { "line": 0, "character": 33 } 14 | }, 15 | "severity": 2, 16 | "message": "Variants are not in the recommended order, which may cause unexpected CSS output." 17 | } 18 | ], 19 | "edit": { 20 | "changes": { 21 | "{{URI}}": [ 22 | { 23 | "range": { 24 | "start": { "line": 0, "character": 12 }, 25 | "end": { "line": 0, "character": 33 } 26 | }, 27 | "newText": "focus:hover:uppercase" 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/commands/commands.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { withFixture } from '../common' 3 | 4 | withFixture('basic', (c) => { 5 | test.concurrent('sortSelection', async ({ expect }) => { 6 | let textDocument = await c.openDocument({ text: '
' }) 7 | let res = await c.sendRequest('@/tailwindCSS/sortSelection', { 8 | uri: textDocument.uri, 9 | classLists: ['sm:p-0 p-0'], 10 | }) 11 | 12 | expect(res).toEqual({ classLists: ['p-0 sm:p-0'] }) 13 | }) 14 | }) 15 | 16 | withFixture('v4/basic', (c) => { 17 | test.concurrent('sortSelection', async ({ expect }) => { 18 | let textDocument = await c.openDocument({ text: '
' }) 19 | let res = await c.sendRequest('@/tailwindCSS/sortSelection', { 20 | uri: textDocument.uri, 21 | classLists: ['sm:p-0 p-0'], 22 | }) 23 | 24 | expect(res).toEqual({ classLists: ['p-0 sm:p-0'] }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/diagnostics/css-conflict/css-multi-prop.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": ".test { @apply uppercase; color: red; @apply lowercase }", 3 | "language": "css", 4 | "expected": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/diagnostics/css-conflict/css-multi-rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": ".test { @apply uppercase }\n.test { @apply lowercase }", 3 | "language": "css", 4 | "expected": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/diagnostics/css-conflict/jsx-concat-negative.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "
", 3 | "language": "javascriptreact", 4 | "expected": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/diagnostics/css-conflict/variants-negative.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "
", 3 | "expected": [] 4 | } 5 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/diagnostics/invalid-screen/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "@screen small", 3 | "language": "css", 4 | "expected": [ 5 | { 6 | "code": "invalidScreen", 7 | "range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 } }, 8 | "severity": 1, 9 | "message": "The screen 'small' does not exist in your theme config. Did you mean 'sm'?", 10 | "suggestions": ["sm"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/diagnostics/invalid-theme/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": ".test { color: theme(colors.red.901) }", 3 | "language": "css", 4 | "expected": [ 5 | { 6 | "code": "invalidConfigPath", 7 | "range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 35 } }, 8 | "severity": 1, 9 | "message": "'colors.red.901' does not exist in your theme config. Did you mean 'colors.red.900'?", 10 | "suggestions": ["colors.red.900"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/env/multi-config-content.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest' 2 | import { css, defineTest, html, js, json, symlinkTo } from '../../src/testing' 3 | import dedent from 'dedent' 4 | import { createClient } from '../utils/client' 5 | 6 | defineTest({ 7 | name: 'multi-config with content config', 8 | fs: { 9 | 'tailwind.config.one.js': js` 10 | module.exports = { 11 | content: ['./one/**/*'], 12 | theme: { 13 | extend: { 14 | colors: { 15 | foo: 'red', 16 | }, 17 | }, 18 | }, 19 | } 20 | `, 21 | 'tailwind.config.two.js': js` 22 | module.exports = { 23 | content: ['./two/**/*'], 24 | theme: { 25 | extend: { 26 | colors: { 27 | foo: 'blue', 28 | }, 29 | }, 30 | }, 31 | } 32 | `, 33 | }, 34 | prepare: async ({ root }) => ({ client: await createClient({ root }) }), 35 | handle: async ({ client }) => { 36 | let one = await client.open({ 37 | lang: 'html', 38 | name: 'one/index.html', 39 | text: '
', 40 | }) 41 | 42 | let two = await client.open({ 43 | lang: 'html', 44 | name: 'two/index.html', 45 | text: '
', 46 | }) 47 | 48 | //
49 | // ^ 50 | let hoverOne = await one.hover({ line: 0, character: 13 }) 51 | let hoverTwo = await two.hover({ line: 0, character: 13 }) 52 | 53 | expect(hoverOne).toEqual({ 54 | contents: { 55 | language: 'css', 56 | value: dedent` 57 | .bg-foo { 58 | --tw-bg-opacity: 1; 59 | background-color: rgb(255 0 0 / var(--tw-bg-opacity, 1)) /* #ff0000 */; 60 | } 61 | `, 62 | }, 63 | range: { 64 | start: { line: 0, character: 12 }, 65 | end: { line: 0, character: 18 }, 66 | }, 67 | }) 68 | 69 | expect(hoverTwo).toEqual({ 70 | contents: { 71 | language: 'css', 72 | value: dedent` 73 | .bg-foo { 74 | --tw-bg-opacity: 1; 75 | background-color: rgb(0 0 255 / var(--tw-bg-opacity, 1)) /* #0000ff */; 76 | } 77 | `, 78 | }, 79 | range: { 80 | start: { line: 0, character: 12 }, 81 | end: { line: 0, character: 18 }, 82 | }, 83 | }) 84 | }, 85 | }) 86 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/env/multi-config.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'vitest' 2 | import { withFixture } from '../common' 3 | 4 | withFixture('multi-config', (c) => { 5 | test.concurrent('multi-config 1', async ({ expect }) => { 6 | let textDocument = await c.openDocument({ text: '
', dir: 'one' }) 7 | let res = await c.sendRequest('textDocument/hover', { 8 | textDocument, 9 | position: { line: 0, character: 13 }, 10 | }) 11 | 12 | expect(res).toEqual({ 13 | contents: { 14 | language: 'css', 15 | value: 16 | '.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity, 1)) /* #ff0000 */;\n}', 17 | }, 18 | range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } }, 19 | }) 20 | }) 21 | 22 | test.concurrent('multi-config 2', async ({ expect }) => { 23 | let textDocument = await c.openDocument({ text: '
', dir: 'two' }) 24 | let res = await c.sendRequest('textDocument/hover', { 25 | textDocument, 26 | position: { line: 0, character: 13 }, 27 | }) 28 | 29 | expect(res).toEqual({ 30 | contents: { 31 | language: 'css', 32 | value: 33 | '.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity, 1)) /* #0000ff */;\n}', 34 | }, 35 | range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } }, 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/env/workspace-folders.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'vitest' 2 | import { withWorkspace } from '../common' 3 | import { DidChangeWorkspaceFoldersNotification, HoverRequest } from 'vscode-languageserver' 4 | 5 | withWorkspace({ 6 | fixtures: ['basic', 'v4/basic'], 7 | run(c) { 8 | test('basic: should provide hovers', async ({ expect }) => { 9 | let textDocument = await c.openDocument({ 10 | dir: 'basic', 11 | text: '
', 12 | }) 13 | 14 | let res = await c.sendRequest(HoverRequest.type, { 15 | textDocument, 16 | position: { line: 0, character: 13 }, 17 | }) 18 | 19 | expect(res).toEqual({ 20 | contents: { 21 | language: 'css', 22 | value: 23 | '.bg-\\[\\#000\\] {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1)) /* #000000 */;\n}', 24 | }, 25 | range: { start: { line: 0, character: 12 }, end: { line: 0, character: 21 } }, 26 | }) 27 | }) 28 | 29 | test('v4/basic: should provide hovers', async ({ expect }) => { 30 | let textDocument = await c.openDocument({ 31 | dir: 'v4/basic', 32 | text: '
', 33 | }) 34 | 35 | let res = await c.sendRequest(HoverRequest.type, { 36 | textDocument, 37 | position: { line: 0, character: 13 }, 38 | }) 39 | 40 | expect(res).toEqual({ 41 | contents: { 42 | language: 'css', 43 | value: '.bg-\\[\\#000\\] {\n background-color: #000;\n}', 44 | }, 45 | range: { start: { line: 0, character: 12 }, end: { line: 0, character: 21 } }, 46 | }) 47 | }) 48 | 49 | test('added workspaces can provide hovers', async ({ expect }) => { 50 | // Add a new workspace folder 51 | await c.sendNotification(DidChangeWorkspaceFoldersNotification.type, { 52 | event: { 53 | added: [ 54 | { 55 | uri: c.fixtureUri('v3/ts-config'), 56 | name: 'added-workspace', 57 | }, 58 | ], 59 | removed: [], 60 | }, 61 | }) 62 | 63 | // Hover a document in the new workspace 64 | let textDocument = await c.openDocument({ 65 | dir: 'v3/ts-config', 66 | text: '
', 67 | }) 68 | 69 | let res = await c.sendRequest(HoverRequest.type, { 70 | textDocument, 71 | position: { line: 0, character: 13 }, 72 | }) 73 | 74 | expect(res).toEqual({ 75 | contents: { 76 | language: 'css', 77 | value: 78 | '.bg-cool {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity, 1)) /* #0000ff */;\n}', 79 | }, 80 | range: { start: { line: 0, character: 12 }, end: { line: 0, character: 19 } }, 81 | }) 82 | }) 83 | }, 84 | }) 85 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/basic/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/dependencies/sub-dir/colors.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | foo: 'red', 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/dependencies/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('./sub-dir/colors') 2 | 3 | module.exports = { 4 | theme: { 5 | extend: { 6 | colors, 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/(parens)/file.html: -------------------------------------------------------------------------------- 1 |
test
2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/(parens)/tailwind.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/[brackets]/file.html: -------------------------------------------------------------------------------- 1 |
test
2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/[brackets]/tailwind.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/basic/file.html: -------------------------------------------------------------------------------- 1 |
test
2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/basic/tailwind.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/{curlies}/file.html: -------------------------------------------------------------------------------- 1 |
test
2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/document-selection/{curlies}/tailwind.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/multi-config-content/tailwind.config.one.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./one/**/*'], 3 | theme: { 4 | extend: { 5 | colors: { 6 | foo: 'red', 7 | }, 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/multi-config-content/tailwind.config.two.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./two/**/*'], 3 | theme: { 4 | extend: { 5 | colors: { 6 | foo: 'blue', 7 | }, 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/multi-config/one/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | foo: 'red', 6 | }, 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/multi-config/two/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | foo: 'blue', 6 | }, 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/overrides-variants/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | function ({ addVariant, matchVariant }) { 4 | matchVariant('custom', (value) => `.custom:${value} &`, { values: { hover: 'hover' } }) 5 | addVariant('custom-hover', `.custom:hover &:hover`) 6 | }, 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "1.9.6" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v1/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v2-jit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "2.2.19" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v2-jit/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "2.2.19" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v2/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v3/cts-config/tailwind.config.cts: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: { 3 | colors: { cool: 'blue' }, 4 | }, 5 | } satisfies { 6 | theme: Record 7 | } 8 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v3/esm-config/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: { 3 | colors: { cool: 'blue' }, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v3/mts-config/tailwind.config.mts: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: { 3 | colors: { cool: 'blue' }, 4 | }, 5 | } satisfies { 6 | theme: Record 7 | } 8 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v3/ts-config/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: { 3 | colors: { cool: 'blue' }, 4 | }, 5 | } satisfies { 6 | theme: Record 7 | } 8 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/basic/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @theme { 4 | --color-potato: #907a70; 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/basic/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* Load ESM versions */ 4 | @config './esm/my-config.mjs'; 5 | @plugin './esm/my-plugin.mjs'; 6 | 7 | /* Load Common JS versions */ 8 | @config './cjs/my-config.cjs'; 9 | @plugin './cjs/my-plugin.cjs'; 10 | 11 | /* Load TypeScript versions */ 12 | @config './ts/my-config.ts'; 13 | @plugin './ts/my-plugin.ts'; 14 | 15 | /* Attempt to load files that do not exist */ 16 | @config './missing-confg.mjs'; 17 | @plugin './missing-plugin.mjs'; 18 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | 'cjs-from-config': 'black', 6 | }, 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-plugin.cjs: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = plugin( 4 | () => { 5 | // 6 | }, 7 | { 8 | theme: { 9 | extend: { 10 | colors: { 11 | 'cjs-from-plugin': 'black', 12 | }, 13 | }, 14 | }, 15 | }, 16 | ) 17 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: { 3 | extend: { 4 | colors: { 5 | 'esm-from-config': 'black', 6 | }, 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-plugin.mjs: -------------------------------------------------------------------------------- 1 | import plugin from 'tailwindcss/plugin' 2 | 3 | export default plugin( 4 | () => { 5 | // 6 | }, 7 | { 8 | theme: { 9 | extend: { 10 | colors: { 11 | 'esm-from-plugin': 'black', 12 | }, 13 | }, 14 | }, 15 | }, 16 | ) 17 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-loading-js", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | export default { 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'ts-from-config': 'black', 8 | }, 9 | }, 10 | }, 11 | } satisfies Config 12 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { PluginAPI } from 'tailwindcss' 2 | import plugin from 'tailwindcss/plugin' 3 | 4 | export default plugin( 5 | (api: PluginAPI) => { 6 | // 7 | }, 8 | { 9 | theme: { 10 | extend: { 11 | colors: { 12 | 'ts-from-plugin': 'black', 13 | }, 14 | }, 15 | }, 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/file.d.ts: -------------------------------------------------------------------------------- 1 | export type ColorSpace = 'srgb' | 'display-p3' 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/index.html: -------------------------------------------------------------------------------- 1 |
foo
2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dependencies", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/sub-dir/colors.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | foo: 'red', 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('./sub-dir/colors') 2 | 3 | module.exports = { 4 | theme: { 5 | extend: { 6 | colors, 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/a.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | :root { 3 | font-family: sans-serif; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/b.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | :root { 3 | --foo: red; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invalid-import-order", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* 4 | * This is invalid in this position because some `@import`s are not at the top of the file. 5 | * We don't want project discovery to fail so we hoist them up and then warn in the console. 6 | */ 7 | @custom-variant dark (&:where(.dark, .dark *)); 8 | 9 | @import './a.css'; 10 | @import './b.css'; 11 | 12 | @theme { 13 | --color-primary: #c0ffee; 14 | } 15 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @import './i-do-not-exist.css'; 4 | @import './i-exist.css'; 5 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/i-exist.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "missing-files", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/admin/app.css: -------------------------------------------------------------------------------- 1 | @import './tw.css'; 2 | @import './ui.css'; 3 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/admin/tw.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/admin/ui.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --color-potato: #907a70; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-config", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/web/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @theme { 4 | --color-potato: #907a70; 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @import '#a/file.css'; 4 | @config '#a/my-config.ts'; 5 | @plugin '#a/my-plugin.ts'; 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "path-mappings", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/file.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --color-map-a-css: black; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | export default { 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'map-a-config': 'black', 8 | }, 9 | }, 10 | }, 11 | } satisfies Config 12 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { PluginAPI } from 'tailwindcss' 2 | import plugin from 'tailwindcss/plugin' 3 | 4 | export default plugin( 5 | (api: PluginAPI) => { 6 | // 7 | }, 8 | { 9 | theme: { 10 | extend: { 11 | colors: { 12 | 'map-a-plugin': 'black', 13 | }, 14 | }, 15 | }, 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "#a/*": ["./src/a/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @theme prefix(tw) { 4 | --color-potato: #907a70; 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-prefix", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "tailwindcss": "4.1.1" 9 | } 10 | }, 11 | "node_modules/tailwindcss": { 12 | "version": "4.1.1", 13 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 14 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 15 | "license": "MIT" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "tailwindcss": "4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspaces", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "dependencies": { 11 | "tailwindcss": "4.1.1" 12 | } 13 | }, 14 | "node_modules/@private/admin": { 15 | "resolved": "packages/admin", 16 | "link": true 17 | }, 18 | "node_modules/@private/shared": { 19 | "resolved": "packages/shared", 20 | "link": true 21 | }, 22 | "node_modules/@private/style-export": { 23 | "resolved": "packages/style-export", 24 | "link": true 25 | }, 26 | "node_modules/@private/style-main-field": { 27 | "resolved": "packages/style-main-field", 28 | "link": true 29 | }, 30 | "node_modules/@private/web": { 31 | "resolved": "packages/web", 32 | "link": true 33 | }, 34 | "node_modules/tailwindcss": { 35 | "version": "4.1.1", 36 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", 37 | "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", 38 | "license": "MIT" 39 | }, 40 | "packages/admin": { 41 | "name": "@private/admin" 42 | }, 43 | "packages/shared": { 44 | "name": "@private/shared" 45 | }, 46 | "packages/style-export": { 47 | "name": "@private/style-export" 48 | }, 49 | "packages/style-main-field": { 50 | "name": "@private/style-main-field" 51 | }, 52 | "packages/web": { 53 | "name": "@private/web" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaces": [ 3 | "packages/*" 4 | ], 5 | "dependencies": { 6 | "tailwindcss": "4.1.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/admin/app.css: -------------------------------------------------------------------------------- 1 | @import './tw.css'; 2 | @import '@private/shared/ui.css'; 3 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@private/admin" 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/admin/tw.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@private/shared" 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/shared/ui.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --color-potato: #907a70; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/lib.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --color-beet: #8e3b46; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@private/style-export", 3 | "exports": { 4 | ".": { 5 | "style": "./lib.css" 6 | }, 7 | "./theme": "./theme.css" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/theme.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --color-orangepeel: #ff9f00; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/lib.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --color-style-main: #8e3b46; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@private/style-main-field", 3 | "style": "./lib.css" 4 | } 5 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/web/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import '@private/shared/ui.css'; 3 | @import '@private/style-export'; 4 | @import '@private/style-export/theme'; 5 | @import '@private/style-main-field'; 6 | 7 | @theme { 8 | --color-potato: #907a70; 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@private/web" 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/prepare.mjs: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process' 2 | import * as path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { promisify } from 'node:util' 5 | import { glob } from 'tinyglobby' 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 8 | 9 | const root = path.resolve(__dirname, '..') 10 | 11 | const fixtures = await glob({ 12 | cwd: root, 13 | patterns: ['tests/fixtures/*/package.json', 'tests/fixtures/v4/*/package.json'], 14 | absolute: true, 15 | }) 16 | 17 | const execAsync = promisify(exec) 18 | 19 | await Promise.all( 20 | fixtures.map(async (fixture) => { 21 | console.log(`Installing dependencies for ${path.relative(root, fixture)}`) 22 | 23 | await execAsync('npm install', { cwd: path.dirname(fixture) }) 24 | }), 25 | ) 26 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/utils/configuration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getDefaultTailwindSettings, 3 | type Settings, 4 | } from '@tailwindcss/language-service/src/util/state' 5 | import { URI } from 'vscode-uri' 6 | import type { DeepPartial } from './types' 7 | import { CacheMap } from '../../src/cache-map' 8 | import deepmerge from 'deepmerge' 9 | 10 | export interface Configuration { 11 | get(uri: string | null): Settings 12 | set(uri: string | null, value: DeepPartial): void 13 | } 14 | 15 | export function createConfiguration(): Configuration { 16 | let defaults = getDefaultTailwindSettings() 17 | 18 | /** 19 | * Settings per file or directory URI 20 | */ 21 | let cache = new CacheMap() 22 | 23 | function compute(uri: URI | null) { 24 | let groups: Partial[] = [ 25 | // 1. Extension defaults 26 | structuredClone(defaults), 27 | 28 | // 2. "Global" settings 29 | cache.get(null) ?? {}, 30 | ] 31 | 32 | // 3. Workspace and per-file settings 33 | let components = uri ? uri.path.split('/') : [] 34 | 35 | for (let i = 0; i <= components.length; i++) { 36 | let parts = components.slice(0, i) 37 | if (parts.length === 0) continue 38 | let path = parts.join('/') 39 | let cached = cache.get(uri!.with({ path }).toString()) 40 | if (!cached) continue 41 | groups.push(cached) 42 | } 43 | 44 | // Merge all the settings together 45 | return deepmerge.all(groups, { 46 | arrayMerge: (_target, source) => source, 47 | }) 48 | } 49 | 50 | function get(uri: string | null) { 51 | return compute(uri ? URI.parse(uri) : null) 52 | } 53 | 54 | function set(uri: string | null, value: Settings) { 55 | cache.set(uri ? URI.parse(uri).toString() : null, value) 56 | } 57 | 58 | return { get, set } 59 | } 60 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/utils/connection.ts: -------------------------------------------------------------------------------- 1 | import { fork } from 'node:child_process' 2 | import { createConnection } from 'vscode-languageserver/node' 3 | import type { ProtocolConnection } from 'vscode-languageclient/node' 4 | import { Duplex, type Readable, type Writable } from 'node:stream' 5 | import { TW } from '../../src/tw' 6 | import { CssServer } from '../../src/language/css-server' 7 | 8 | class TestStream extends Duplex { 9 | _write(chunk: string, _encoding: string, done: () => void) { 10 | this.emit('data', chunk) 11 | done() 12 | } 13 | 14 | _read(_size: number) {} 15 | } 16 | 17 | const SERVERS = { 18 | tailwindcss: { 19 | ServerClass: TW, 20 | binaryPath: './bin/tailwindcss-language-server', 21 | }, 22 | css: { 23 | ServerClass: CssServer, 24 | binaryPath: './bin/css-language-server', 25 | }, 26 | } 27 | 28 | export interface ConnectOptions { 29 | /** 30 | * How to connect to the LSP: 31 | * - `in-band` runs the server in the same process (default) 32 | * - `spawn` launches the binary as a separate process, connects via stdio, 33 | * and requires a rebuild of the server after making changes. 34 | */ 35 | mode?: 'in-band' | 'spawn' 36 | 37 | /** 38 | * The server to connect to 39 | */ 40 | server?: keyof typeof SERVERS 41 | } 42 | 43 | export function connect(opts: ConnectOptions) { 44 | let server = opts.server ?? 'tailwindcss' 45 | let mode = opts.mode ?? 'in-band' 46 | 47 | let details = SERVERS[server] 48 | if (!details) { 49 | throw new Error(`Unsupported connection: ${server} / ${mode}`) 50 | } 51 | 52 | if (mode === 'in-band') { 53 | let input = new TestStream() 54 | let output = new TestStream() 55 | 56 | let server = new details.ServerClass(createConnection(input, output)) 57 | server.setup() 58 | server.listen() 59 | 60 | return connectStreams(output, input) 61 | } else if (mode === 'spawn') { 62 | let server = fork(details.binaryPath, { silent: true }) 63 | 64 | return connectStreams(server.stdout!, server.stdin!) 65 | } 66 | 67 | throw new Error(`Unsupported connection: ${server} / ${mode}`) 68 | } 69 | 70 | function connectStreams(input: Readable, output: Writable) { 71 | let clientConn = createConnection(input, output) as unknown as ProtocolConnection 72 | clientConn.listen() 73 | return clientConn 74 | } 75 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tests/utils/messages.ts: -------------------------------------------------------------------------------- 1 | import { Feature } from '@tailwindcss/language-service/src/features' 2 | import { MessageDirection, ProtocolNotificationType } from 'vscode-languageserver' 3 | import type { DocumentUri } from 'vscode-languageserver-textdocument' 4 | 5 | export interface DocumentReady { 6 | uri: DocumentUri 7 | } 8 | 9 | export namespace DocumentReadyNotification { 10 | export const method: '@/tailwindCSS/documentReady' = '@/tailwindCSS/documentReady' 11 | export const messageDirection: MessageDirection = MessageDirection.clientToServer 12 | export const type = new ProtocolNotificationType(method) 13 | } 14 | 15 | export interface ProjectDetails { 16 | uri: string 17 | config: string 18 | tailwind: { 19 | version: string 20 | features: Feature[] 21 | isDefaultVersion: boolean 22 | } 23 | } 24 | 25 | export namespace ProjectDetailsNotification { 26 | export const method: '@/tailwindCSS/projectDetails' = '@/tailwindCSS/projectDetails' 27 | export const messageDirection: MessageDirection = MessageDirection.clientToServer 28 | export const type = new ProtocolNotificationType(method) 29 | } 30 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2018", 5 | "lib": ["ES2022"], 6 | "rootDir": "..", 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "allowJs": true, 11 | "resolveJsonModule": true, 12 | "baseUrl": "..", 13 | "paths": { 14 | "@tailwindcss/language-service/*": ["../packages/tailwindcss-language-service/*"] 15 | } 16 | }, 17 | "include": ["src", "../packages/tailwindcss-language-service", "../../types"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import tsconfigPaths from 'vite-tsconfig-paths' 3 | 4 | export default defineConfig({ 5 | test: { 6 | testTimeout: 15000, 7 | css: true, 8 | }, 9 | 10 | plugins: [tsconfigPaths()], 11 | }) 12 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Firefox versions 3 | last 2 Safari versions 4 | node 12 5 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tailwind Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tailwindcss/language-service", 3 | "version": "0.14.20", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "start": "node ./scripts/build.mjs --watch", 11 | "build": "node ./scripts/build.mjs", 12 | "prepublishOnly": "pnpm run build", 13 | "test": "vitest" 14 | }, 15 | "dependencies": { 16 | "@csstools/css-calc": "2.1.2", 17 | "@csstools/css-parser-algorithms": "3.0.4", 18 | "@csstools/css-tokenizer": "3.0.3", 19 | "@csstools/media-query-list-parser": "2.0.4", 20 | "@types/culori": "^2.1.0", 21 | "@types/moo": "0.5.3", 22 | "@types/semver": "7.3.10", 23 | "braces": "3.0.3", 24 | "color-name": "1.1.4", 25 | "css.escape": "1.5.1", 26 | "culori": "^4.0.1", 27 | "detect-indent": "6.0.0", 28 | "dlv": "1.1.3", 29 | "dset": "3.1.4", 30 | "line-column": "1.0.2", 31 | "moo": "0.5.1", 32 | "postcss": "8.4.31", 33 | "postcss-selector-parser": "6.0.2", 34 | "postcss-value-parser": "4.2.0", 35 | "semver": "7.7.1", 36 | "sift-string": "0.0.2", 37 | "stringify-object": "3.3.0", 38 | "tmp-cache": "1.1.0", 39 | "vscode-emmet-helper-bundled": "0.0.1", 40 | "vscode-languageserver": "8.1.0", 41 | "vscode-languageserver-textdocument": "1.0.12" 42 | }, 43 | "devDependencies": { 44 | "@types/braces": "3.0.1", 45 | "@types/css.escape": "^1.5.2", 46 | "@types/dedent": "^0.7.2", 47 | "@types/line-column": "^1.0.2", 48 | "@types/node": "^18.19.33", 49 | "@types/stringify-object": "^4.0.5", 50 | "dedent": "^1.5.3", 51 | "esbuild": "^0.25.0", 52 | "esbuild-node-externals": "^1.9.0", 53 | "minimist": "^1.2.8", 54 | "tslib": "2.2.0", 55 | "typescript": "^5.8.3", 56 | "vitest": "^3.0.9" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { spawnSync } from 'node:child_process' 3 | import esbuild from 'esbuild' 4 | import minimist from 'minimist' 5 | import { nodeExternalsPlugin } from 'esbuild-node-externals' 6 | import { fileURLToPath } from 'node:url' 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 9 | 10 | const args = minimist(process.argv.slice(2), { 11 | boolean: ['watch', 'minify'], 12 | }) 13 | 14 | console.log('- Preparing') 15 | let build = await esbuild.context({ 16 | entryPoints: [path.resolve(__dirname, '../src/index.ts')], 17 | bundle: true, 18 | platform: 'node', 19 | external: [], 20 | outdir: 'dist', 21 | minify: args.minify, 22 | 23 | format: 'esm', 24 | 25 | plugins: [ 26 | nodeExternalsPlugin(), 27 | { 28 | name: 'generate-types', 29 | async setup(build) { 30 | build.onEnd(async (result) => { 31 | // Call the tsc command to generate the types 32 | spawnSync( 33 | 'tsc', 34 | [ 35 | '-p', 36 | path.resolve(__dirname, './tsconfig.build.json'), 37 | '--emitDeclarationOnly', 38 | '--outDir', 39 | path.resolve(__dirname, '../dist'), 40 | ], 41 | { 42 | stdio: 'inherit', 43 | }, 44 | ) 45 | }) 46 | }, 47 | }, 48 | ], 49 | }) 50 | 51 | console.log('- Building') 52 | await build.rebuild() 53 | 54 | if (args.watch) { 55 | console.log('- Watching') 56 | await build.watch() 57 | } else { 58 | console.log('- Cleaning up') 59 | await build.dispose() 60 | } 61 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/scripts/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": ["../src/**/*.test.ts"] 4 | } -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/codeActions/provideCssConflictCodeActions.ts: -------------------------------------------------------------------------------- 1 | import type { State } from '../util/state' 2 | import type { CodeActionParams, CodeAction } from 'vscode-languageserver' 3 | import type { CssConflictDiagnostic } from '../diagnostics/types' 4 | import { joinWithAnd } from '../util/joinWithAnd' 5 | import { removeRangesFromString } from '../util/removeRangesFromString' 6 | 7 | export async function provideCssConflictCodeActions( 8 | _state: State, 9 | params: CodeActionParams, 10 | diagnostic: CssConflictDiagnostic, 11 | ): Promise { 12 | return [ 13 | { 14 | title: `Delete ${joinWithAnd( 15 | diagnostic.otherClassNames.map((otherClassName) => `'${otherClassName.className}'`), 16 | )}`, 17 | kind: 'quickfix', // CodeActionKind.QuickFix, 18 | diagnostics: [diagnostic], 19 | edit: { 20 | changes: { 21 | [params.textDocument.uri]: [ 22 | { 23 | range: diagnostic.className.classList.range, 24 | newText: removeRangesFromString( 25 | diagnostic.className.classList.classList, 26 | diagnostic.otherClassNames.map((otherClassName) => otherClassName.relativeRange), 27 | ), 28 | }, 29 | ], 30 | }, 31 | }, 32 | }, 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/codeActions/provideSuggestionCodeActions.ts: -------------------------------------------------------------------------------- 1 | import type { State } from '../util/state' 2 | import type { CodeActionParams, CodeAction } from 'vscode-languageserver' 3 | import type { 4 | InvalidConfigPathDiagnostic, 5 | InvalidTailwindDirectiveDiagnostic, 6 | InvalidScreenDiagnostic, 7 | InvalidVariantDiagnostic, 8 | RecommendedVariantOrderDiagnostic, 9 | } from '../diagnostics/types' 10 | 11 | export function provideSuggestionCodeActions( 12 | _state: State, 13 | params: CodeActionParams, 14 | diagnostic: 15 | | InvalidConfigPathDiagnostic 16 | | InvalidTailwindDirectiveDiagnostic 17 | | InvalidScreenDiagnostic 18 | | InvalidVariantDiagnostic 19 | | RecommendedVariantOrderDiagnostic, 20 | ): CodeAction[] { 21 | return diagnostic.suggestions.map((suggestion) => ({ 22 | title: `Replace with '${suggestion}'`, 23 | kind: 'quickfix', // CodeActionKind.QuickFix, 24 | diagnostics: [diagnostic], 25 | edit: { 26 | changes: { 27 | [params.textDocument.uri]: [ 28 | { 29 | range: diagnostic.range, 30 | newText: suggestion, 31 | }, 32 | ], 33 | }, 34 | }, 35 | })) 36 | } 37 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/codeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import type { Range, TextDocument } from 'vscode-languageserver-textdocument' 2 | import type { State } from './util/state' 3 | import type { CodeLens } from 'vscode-languageserver' 4 | import braces from 'braces' 5 | import { findAll, indexToPosition } from './util/find' 6 | import { absoluteRange } from './util/absoluteRange' 7 | import { formatBytes } from './util/format-bytes' 8 | import { estimatedClassSize } from './util/estimated-class-size' 9 | 10 | export async function getCodeLens(state: State, doc: TextDocument): Promise { 11 | if (!state.enabled) return [] 12 | 13 | let groups: CodeLens[][] = await Promise.all([ 14 | // 15 | sourceInlineCodeLens(state, doc), 16 | ]) 17 | 18 | return groups.flat() 19 | } 20 | 21 | const SOURCE_INLINE_PATTERN = /@source(?:\s+not)?\s*inline\((?'[^']+'|"[^"]+")/dg 22 | async function sourceInlineCodeLens(state: State, doc: TextDocument): Promise { 23 | if (!state.features.includes('source-inline')) return [] 24 | 25 | let text = doc.getText() 26 | 27 | let countFormatter = new Intl.NumberFormat('en', { 28 | maximumFractionDigits: 2, 29 | }) 30 | 31 | let lenses: CodeLens[] = [] 32 | 33 | for (let match of findAll(SOURCE_INLINE_PATTERN, text)) { 34 | let glob = match.groups.glob.slice(1, -1) 35 | 36 | // Perform brace expansion 37 | let expanded = new Set(braces.expand(glob)) 38 | if (expanded.size < 2) continue 39 | 40 | let slice: Range = absoluteRange({ 41 | start: indexToPosition(text, match.indices.groups.glob[0]), 42 | end: indexToPosition(text, match.indices.groups.glob[1]), 43 | }) 44 | 45 | let size = 0 46 | for (let className of expanded) { 47 | size += estimatedClassSize(className) 48 | } 49 | 50 | lenses.push({ 51 | range: slice, 52 | command: { 53 | title: `Generates ${countFormatter.format(expanded.size)} classes`, 54 | command: '', 55 | }, 56 | }) 57 | 58 | if (size >= 1_000_000) { 59 | lenses.push({ 60 | range: slice, 61 | command: { 62 | title: `At least ${formatBytes(size)} of CSS`, 63 | command: '', 64 | }, 65 | }) 66 | 67 | lenses.push({ 68 | range: slice, 69 | command: { 70 | title: `This may slow down your bundler/browser`, 71 | command: '', 72 | }, 73 | }) 74 | } 75 | } 76 | 77 | return lenses 78 | } 79 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/completions/file-paths.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { findFileDirective } from './file-paths' 3 | 4 | test('Detecting v3 directives that point to files', async () => { 5 | function find(text: string) { 6 | return findFileDirective({ enabled: true, v4: false }, text) 7 | } 8 | 9 | await expect(find('@config "./')).resolves.toEqual({ 10 | directive: 'config', 11 | partial: './', 12 | suggest: 'script', 13 | }) 14 | 15 | // The following are not supported in v3 16 | await expect(find('@plugin "./')).resolves.toEqual(null) 17 | await expect(find('@source "./')).resolves.toEqual(null) 18 | await expect(find('@source not "./')).resolves.toEqual(null) 19 | await expect(find('@import "tailwindcss" source("./')).resolves.toEqual(null) 20 | await expect(find('@tailwind utilities source("./')).resolves.toEqual(null) 21 | }) 22 | 23 | test('Detecting v4 directives that point to files', async () => { 24 | function find(text: string) { 25 | return findFileDirective({ enabled: true, v4: true }, text) 26 | } 27 | 28 | await expect(find('@config "./')).resolves.toEqual({ 29 | directive: 'config', 30 | partial: './', 31 | suggest: 'script', 32 | }) 33 | 34 | await expect(find('@plugin "./')).resolves.toEqual({ 35 | directive: 'plugin', 36 | partial: './', 37 | suggest: 'script', 38 | }) 39 | 40 | await expect(find('@source "./')).resolves.toEqual({ 41 | directive: 'source', 42 | partial: './', 43 | suggest: 'source', 44 | }) 45 | 46 | await expect(find('@source not "./')).resolves.toEqual({ 47 | directive: 'source', 48 | partial: './', 49 | suggest: 'source', 50 | }) 51 | 52 | await expect(find('@import "tailwindcss" source("./')).resolves.toEqual({ 53 | directive: 'import', 54 | partial: './', 55 | suggest: 'directory', 56 | }) 57 | 58 | await expect(find('@tailwind utilities source("./')).resolves.toEqual({ 59 | directive: 'tailwind', 60 | partial: './', 61 | suggest: 'directory', 62 | }) 63 | }) 64 | 65 | test('@source inline is ignored', async () => { 66 | function find(text: string) { 67 | return findFileDirective({ enabled: true, v4: true }, text) 68 | } 69 | 70 | await expect(find('@source inline("')).resolves.toEqual(null) 71 | await expect(find('@source not inline("')).resolves.toEqual(null) 72 | }) 73 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/completions/file-paths.ts: -------------------------------------------------------------------------------- 1 | import type { State } from '../util/state' 2 | 3 | // @config, @plugin, @source 4 | // - @source inline("…") is *not* a file directive 5 | // - @source not inline("…") is *not* a file directive 6 | const PATTERN_CUSTOM_V4 = 7 | /@(?config|plugin|source)(?\s+not)?\s*(?'[^']*|"[^"]*)$/ 8 | const PATTERN_CUSTOM_V3 = /@(?config)\s*(?'[^']*|"[^"]*)$/ 9 | 10 | // @import … source('…') 11 | // @tailwind utilities source('…') 12 | const PATTERN_IMPORT_SOURCE = 13 | /@(?(?:import|reference))\s*(?'[^']*'|"[^"]*")\s*(layer\([^)]+\)\s*)?source\((?'[^']*|"[^"]*)$/ 14 | const PATTERN_UTIL_SOURCE = 15 | /@(?tailwind)\s+utilities\s+source\((?'[^']*|"[^"]*)?$/ 16 | 17 | export type FileDirective = { 18 | directive: string 19 | partial: string 20 | suggest: 'script' | 'source' | 'directory' 21 | } 22 | 23 | export async function findFileDirective(state: State, text: string): Promise { 24 | if (state.v4) { 25 | let match = 26 | text.match(PATTERN_CUSTOM_V4) ?? 27 | text.match(PATTERN_IMPORT_SOURCE) ?? 28 | text.match(PATTERN_UTIL_SOURCE) 29 | 30 | if (!match) return null 31 | 32 | let isNot = match.groups.not !== undefined && match.groups.not.length > 0 33 | let directive = match.groups.directive 34 | let partial = match.groups.partial?.slice(1) ?? '' // remove leading quote 35 | 36 | // Most suggestions are for JS files so we'll default to that 37 | let suggest: FileDirective['suggest'] = 'script' 38 | 39 | // If we're looking at @source then it's for a template file 40 | if (directive === 'source') { 41 | suggest = 'source' 42 | } 43 | 44 | // If we're looking at @import … source('…') or @tailwind … source('…') then 45 | // we want to list directories instead of files 46 | else if (directive === 'import' || directive === 'tailwind') { 47 | if (isNot) return null 48 | suggest = 'directory' 49 | } 50 | 51 | return { directive, partial, suggest } 52 | } 53 | 54 | let match = text.match(PATTERN_CUSTOM_V3) 55 | if (!match) return null 56 | 57 | let isNot = match.groups.not !== undefined && match.groups.not.length > 0 58 | if (isNot) return null 59 | 60 | let directive = match.groups.directive 61 | let partial = match.groups.partial.slice(1) // remove leading quote 62 | 63 | return { directive, partial, suggest: 'script' } 64 | } 65 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument } from 'vscode-languageserver-textdocument' 2 | import { findClassNamesInRange } from '../util/find' 3 | import { type InvalidApplyDiagnostic, DiagnosticKind } from './types' 4 | import type { Settings, State } from '../util/state' 5 | import { validateApply } from '../util/validateApply' 6 | 7 | export async function getInvalidApplyDiagnostics( 8 | state: State, 9 | document: TextDocument, 10 | settings: Settings, 11 | ): Promise { 12 | let severity = settings.tailwindCSS.lint.invalidApply 13 | if (severity === 'ignore') return [] 14 | 15 | const classNames = await findClassNamesInRange(state, document, undefined, 'css', false) 16 | 17 | let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => { 18 | let result = validateApply(state, className.className) 19 | 20 | if (result === null || result.isApplyable === true) { 21 | return null 22 | } 23 | 24 | return { 25 | code: DiagnosticKind.InvalidApply, 26 | severity: 27 | severity === 'error' 28 | ? 1 /* DiagnosticSeverity.Error */ 29 | : 2 /* DiagnosticSeverity.Warning */, 30 | range: className.range, 31 | message: result.reason, 32 | className, 33 | } 34 | }) 35 | 36 | return diagnostics.filter(Boolean) 37 | } 38 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/diagnostics/getInvalidScreenDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import type { State, Settings } from '../util/state' 2 | import type { Range } from 'vscode-languageserver' 3 | import type { TextDocument } from 'vscode-languageserver-textdocument' 4 | import { type InvalidScreenDiagnostic, DiagnosticKind } from './types' 5 | import { isCssDoc } from '../util/css' 6 | import { getLanguageBoundaries } from '../util/getLanguageBoundaries' 7 | import { findAll, indexToPosition } from '../util/find' 8 | import { closest } from '../util/closest' 9 | import { absoluteRange } from '../util/absoluteRange' 10 | import { getTextWithoutComments } from '../util/doc' 11 | 12 | export function getInvalidScreenDiagnostics( 13 | state: State, 14 | document: TextDocument, 15 | settings: Settings, 16 | ): InvalidScreenDiagnostic[] { 17 | let severity = settings.tailwindCSS.lint.invalidScreen 18 | if (severity === 'ignore') return [] 19 | 20 | let diagnostics: InvalidScreenDiagnostic[] = [] 21 | let ranges: Range[] = [] 22 | 23 | if (isCssDoc(state, document)) { 24 | ranges.push(undefined) 25 | } else { 26 | let boundaries = getLanguageBoundaries(state, document) 27 | if (!boundaries) return [] 28 | ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range)) 29 | } 30 | 31 | ranges.forEach((range) => { 32 | let text = getTextWithoutComments(document, 'css', range) 33 | let matches = findAll(/(?:\s|^)@screen\s+(?[^\s{]+)/g, text) 34 | 35 | matches.forEach((match) => { 36 | if (state.screens.includes(match.groups.screen)) { 37 | return null 38 | } 39 | 40 | let message = `The screen '${match.groups.screen}' does not exist in your theme config.` 41 | let suggestions: string[] = [] 42 | let suggestion = closest(match.groups.screen, state.screens) 43 | 44 | if (suggestion) { 45 | suggestions.push(suggestion) 46 | message += ` Did you mean '${suggestion}'?` 47 | } 48 | 49 | diagnostics.push({ 50 | code: DiagnosticKind.InvalidScreen, 51 | range: absoluteRange( 52 | { 53 | start: indexToPosition( 54 | text, 55 | match.index + match[0].length - match.groups.screen.length, 56 | ), 57 | end: indexToPosition(text, match.index + match[0].length), 58 | }, 59 | range, 60 | ), 61 | severity: 62 | severity === 'error' 63 | ? 1 /* DiagnosticSeverity.Error */ 64 | : 2 /* DiagnosticSeverity.Warning */, 65 | message, 66 | suggestions, 67 | }) 68 | }) 69 | }) 70 | 71 | return diagnostics 72 | } 73 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/diagnostics/getRecommendedVariantOrderDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument } from 'vscode-languageserver-textdocument' 2 | import type { State, Settings } from '../util/state' 3 | import { type RecommendedVariantOrderDiagnostic, DiagnosticKind } from './types' 4 | import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' 5 | import * as jit from '../util/jit' 6 | import { getVariantsFromClassName } from '../util/getVariantsFromClassName' 7 | import { equalExact } from '../util/array' 8 | import * as semver from '../util/semver' 9 | 10 | export async function getRecommendedVariantOrderDiagnostics( 11 | state: State, 12 | document: TextDocument, 13 | settings: Settings, 14 | ): Promise { 15 | if (state.v4) return [] 16 | if (!state.jit) return [] 17 | 18 | if (semver.gte(state.version, '2.99.0')) return [] 19 | 20 | let severity = settings.tailwindCSS.lint.recommendedVariantOrder 21 | if (severity === 'ignore') return [] 22 | 23 | let diagnostics: RecommendedVariantOrderDiagnostic[] = [] 24 | const classLists = await findClassListsInDocument(state, document) 25 | 26 | classLists.forEach((classList) => { 27 | const classNames = getClassNamesInClassList(classList, state.blocklist) 28 | classNames.forEach((className) => { 29 | let { rules } = jit.generateRules(state, [className.className]) 30 | if (rules.length === 0) { 31 | return 32 | } 33 | 34 | let order = state.jitContext.variantOrder ?? state.jitContext.offsets.variantOffsets 35 | let { variants, offset } = getVariantsFromClassName(state, className.className) 36 | let sortedVariants = [...variants].sort((a, b) => jit.bigSign(order.get(b) - order.get(a))) 37 | 38 | if (!equalExact(variants, sortedVariants)) { 39 | diagnostics.push({ 40 | code: DiagnosticKind.RecommendedVariantOrder, 41 | suggestions: [ 42 | [...sortedVariants, className.className.substr(offset)].join(state.separator), 43 | ], 44 | range: className.range, 45 | severity: 46 | severity === 'error' 47 | ? 1 /* DiagnosticSeverity.Error */ 48 | : 2 /* DiagnosticSeverity.Warning */, 49 | message: 50 | 'Variants are not in the recommended order, which may cause unexpected CSS output.', 51 | }) 52 | } 53 | }) 54 | }) 55 | 56 | return diagnostics 57 | } 58 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/diagnostics/getUsedBlocklistedClassDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument } from 'vscode-languageserver-textdocument' 2 | import type { State, Settings } from '../util/state' 3 | import { type UsedBlocklistedClassDiagnostic, DiagnosticKind } from './types' 4 | import { findClassListsInDocument, getClassNamesInClassList } from '../util/find' 5 | 6 | export async function getUsedBlocklistedClassDiagnostics( 7 | state: State, 8 | document: TextDocument, 9 | settings: Settings, 10 | ): Promise { 11 | if (!state.v4) return [] 12 | if (!state.blocklist?.length) return [] 13 | 14 | let severity = settings.tailwindCSS.lint.usedBlocklistedClass 15 | if (severity === 'ignore') return [] 16 | 17 | let blocklist = new Set(state.blocklist ?? []) 18 | let diagnostics: UsedBlocklistedClassDiagnostic[] = [] 19 | 20 | let classLists = await findClassListsInDocument(state, document) 21 | 22 | for (let classList of classLists) { 23 | let classNames = getClassNamesInClassList(classList, []) 24 | 25 | for (let className of classNames) { 26 | if (!blocklist.has(className.className)) continue 27 | 28 | diagnostics.push({ 29 | code: DiagnosticKind.UsedBlocklistedClass, 30 | range: className.range, 31 | severity: 32 | severity === 'error' 33 | ? 1 /* DiagnosticSeverity.Error */ 34 | : 2 /* DiagnosticSeverity.Warning */, 35 | message: `The class "${className.className}" will not be generated as it has been blocklisted`, 36 | }) 37 | } 38 | } 39 | 40 | return diagnostics 41 | } 42 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/documentColorProvider.ts: -------------------------------------------------------------------------------- 1 | import type { State } from './util/state' 2 | import { 3 | findClassListsInDocument, 4 | getClassNamesInClassList, 5 | findHelperFunctionsInDocument, 6 | } from './util/find' 7 | import { getColor, getColorFromValue, culoriColorToVscodeColor } from './util/color' 8 | import { stringToPath } from './util/stringToPath' 9 | import type { ColorInformation } from 'vscode-languageserver' 10 | import type { TextDocument } from 'vscode-languageserver-textdocument' 11 | import dlv from 'dlv' 12 | import { dedupeByRange } from './util/array' 13 | 14 | export async function getDocumentColors( 15 | state: State, 16 | document: TextDocument, 17 | ): Promise { 18 | let colors: ColorInformation[] = [] 19 | if (!state.enabled) return colors 20 | 21 | let settings = await state.editor.getConfiguration(document.uri) 22 | if (settings.tailwindCSS.colorDecorators === false) return colors 23 | 24 | let classLists = await findClassListsInDocument(state, document) 25 | classLists.forEach((classList) => { 26 | let classNames = getClassNamesInClassList(classList, state.blocklist) 27 | classNames.forEach((className) => { 28 | let color = getColor(state, className.className) 29 | if (color === null || typeof color === 'string' || (color.alpha ?? 1) === 0) { 30 | return 31 | } 32 | colors.push({ 33 | range: className.range, 34 | color: culoriColorToVscodeColor(color), 35 | }) 36 | }) 37 | }) 38 | 39 | let helperFns = findHelperFunctionsInDocument(state, document) 40 | helperFns.forEach((fn) => { 41 | let keys = stringToPath(fn.path) 42 | let base = fn.helper === 'theme' ? ['theme'] : [] 43 | let value = dlv(state.config, [...base, ...keys]) 44 | let color = getColorFromValue(value) 45 | if (color && typeof color !== 'string' && (color.alpha ?? 1) !== 0) { 46 | colors.push({ range: fn.ranges.path, color: culoriColorToVscodeColor(color) }) 47 | } 48 | }) 49 | 50 | return dedupeByRange(colors) 51 | } 52 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/documentLinksProvider.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument } from 'vscode-languageserver-textdocument' 2 | import type { State } from './util/state' 3 | import type { DocumentLink, Range } from 'vscode-languageserver' 4 | import { findAll, indexToPosition } from './util/find' 5 | import { absoluteRange } from './util/absoluteRange' 6 | import * as semver from './util/semver' 7 | import { getCssBlocks } from './util/language-blocks' 8 | 9 | const HAS_DRIVE_LETTER = /^[A-Z]:/ 10 | 11 | export function getDocumentLinks( 12 | state: State, 13 | document: TextDocument, 14 | resolveTarget: (linkPath: string) => Promise, 15 | ): Promise { 16 | let patterns = [/@config\s*(?'[^']+'|"[^"]+")/g] 17 | 18 | if (state.v4) { 19 | patterns.push( 20 | /@plugin\s*(?'[^']+'|"[^"]+")/g, 21 | /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/g, 22 | /@import\s*('[^']*'|"[^"]*")\s*(layer\([^)]+\)\s*)?source\((?'[^']*'?|"[^"]*"?)/g, 23 | /@reference\s*('[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?)/g, 24 | /@tailwind\s*utilities\s*source\((?'[^']*'?|"[^"]*"?)/g, 25 | ) 26 | } 27 | 28 | return getDirectiveLinks(state, document, patterns, resolveTarget) 29 | } 30 | 31 | async function getDirectiveLinks( 32 | state: State, 33 | document: TextDocument, 34 | patterns: RegExp[], 35 | resolveTarget: (linkPath: string) => Promise, 36 | ): Promise { 37 | if (!semver.gte(state.version, '3.2.0')) { 38 | return [] 39 | } 40 | 41 | let links: DocumentLink[] = [] 42 | 43 | for (let block of getCssBlocks(state, document)) { 44 | let text = block.text 45 | 46 | let matches: RegExpMatchArray[] = [] 47 | 48 | for (let pattern of patterns) { 49 | matches.push(...findAll(pattern, text)) 50 | } 51 | 52 | for (let match of matches) { 53 | let path = match.groups.path.slice(1, -1) 54 | 55 | // Ignore glob-like paths 56 | if (path.includes('*') || path.includes('{') || path.includes('}')) { 57 | continue 58 | } 59 | 60 | // Ignore Windows-style paths 61 | if (path.includes('\\') || HAS_DRIVE_LETTER.test(path)) { 62 | continue 63 | } 64 | 65 | let range = { 66 | start: indexToPosition(text, match.index + match[0].length - match.groups.path.length), 67 | end: indexToPosition(text, match.index + match[0].length), 68 | } 69 | 70 | links.push({ 71 | target: await resolveTarget(path), 72 | range: absoluteRange(range, block.range), 73 | }) 74 | } 75 | } 76 | 77 | return links 78 | } 79 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/index.ts: -------------------------------------------------------------------------------- 1 | export { doComplete, resolveCompletionItem, completionsFromClassList } from './completionProvider' 2 | export { doValidate } from './diagnostics/diagnosticsProvider' 3 | export { doHover } from './hoverProvider' 4 | export { doCodeActions } from './codeActions/codeActionProvider' 5 | export { getDocumentColors } from './documentColorProvider' 6 | export { getDocumentLinks } from './documentLinksProvider' 7 | export * from './util/state' 8 | export * from './diagnostics/types' 9 | export * from './util/color' 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/metadata/extensions.ts: -------------------------------------------------------------------------------- 1 | let scriptExtensions = [ 2 | // JS 3 | 'js', 4 | 'cjs', 5 | 'mjs', 6 | '(? = { 2 | [P in keyof T]?: T[P] extends ((...args: any) => any) | ReadonlyArray | Date 3 | ? T[P] 4 | : T[P] extends (infer U)[] 5 | ? U[] 6 | : T[P] extends object 7 | ? DeepPartial 8 | : T[P] 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/absoluteRange.ts: -------------------------------------------------------------------------------- 1 | import type { Range } from 'vscode-languageserver' 2 | 3 | export function absoluteRange(range: Range, reference?: Range): Range { 4 | return { 5 | start: { 6 | line: (reference?.start.line || 0) + range.start.line, 7 | character: 8 | (range.end.line === 0 ? reference?.start.character || 0 : 0) + range.start.character, 9 | }, 10 | end: { 11 | line: (reference?.start.line || 0) + range.end.line, 12 | character: (range.end.line === 0 ? reference?.start.character || 0 : 0) + range.end.character, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/array.ts: -------------------------------------------------------------------------------- 1 | import type { Range } from 'vscode-languageserver' 2 | import { rangesEqual } from './rangesEqual' 3 | 4 | export function dedupe(arr: Array): Array { 5 | return arr.filter((value, index, self) => self.indexOf(value) === index) 6 | } 7 | 8 | export function dedupeBy(arr: Array, transform: (item: T) => any): Array { 9 | return arr.filter((value, index, self) => self.map(transform).indexOf(transform(value)) === index) 10 | } 11 | 12 | export function dedupeByRange(arr: Array): Array { 13 | return arr.filter( 14 | (classList, classListIndex) => 15 | classListIndex === arr.findIndex((c) => rangesEqual(c.range, classList.range)), 16 | ) 17 | } 18 | 19 | export function ensureArray(value: T | T[]): T[] { 20 | return Array.isArray(value) ? value : [value] 21 | } 22 | 23 | export function flatten(arrays: T[][]): T[] { 24 | return [].concat.apply([], arrays) 25 | } 26 | 27 | export function equal(a: any[], b: any[]): boolean { 28 | if (a === b) return true 29 | if (a.length !== b.length) return false 30 | 31 | let aSorted = a.concat().sort() 32 | let bSorted = b.concat().sort() 33 | 34 | for (let i = 0; i < aSorted.length; ++i) { 35 | if (aSorted[i] !== bSorted[i]) return false 36 | } 37 | 38 | return true 39 | } 40 | 41 | export function equalExact(a: any[], b: any[]): boolean { 42 | if (a === b) return true 43 | if (a.length !== b.length) return false 44 | 45 | for (let i = 0; i < a.length; ++i) { 46 | if (a[i] !== b[i]) return false 47 | } 48 | 49 | return true 50 | } 51 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/braceLevel.ts: -------------------------------------------------------------------------------- 1 | export function braceLevel(text: string): number { 2 | let count = 0 3 | 4 | for (let i = text.length - 1; i >= 0; i--) { 5 | let char = text.charCodeAt(i) 6 | 7 | count += Number(char === 0x7b /* { */) - Number(char === 0x7d /* } */) 8 | } 9 | 10 | return count 11 | } 12 | 13 | export function parenLevel(text: string): number { 14 | let count = 0 15 | 16 | for (let i = text.length - 1; i >= 0; i--) { 17 | let char = text.charCodeAt(i) 18 | 19 | count += Number(char === 0x28 /* ( */) - Number(char === 0x29 /* ) */) 20 | } 21 | 22 | return count 23 | } 24 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/classes.ts: -------------------------------------------------------------------------------- 1 | export type ClassRegexFilter = string | [string] | [string, string] 2 | export interface ClassMatch { 3 | classList: string 4 | range: [start: number, end: number] 5 | } 6 | 7 | export function* customClassesIn({ 8 | text, 9 | filters, 10 | cursor = null, 11 | }: { 12 | text: string 13 | filters: ClassRegexFilter[] 14 | cursor?: number | null 15 | }): Iterable { 16 | for (let filter of filters) { 17 | let [containerPattern, classPattern] = Array.isArray(filter) ? filter : [filter] 18 | 19 | let containerRegex = new RegExp(containerPattern, 'gd') 20 | let classRegex = classPattern ? new RegExp(classPattern, 'gd') : undefined 21 | 22 | for (let match of matchesIn(text, containerRegex, classRegex, cursor)) { 23 | yield match 24 | } 25 | } 26 | } 27 | 28 | function* matchesIn( 29 | text: string, 30 | containerRegex: RegExp, 31 | classRegex: RegExp | undefined, 32 | cursor: number | null, 33 | ): Iterable { 34 | for (let containerMatch of text.matchAll(containerRegex)) { 35 | // Don't crash when there's no capture group 36 | if (containerMatch[1] === undefined) { 37 | console.warn(`Regex /${containerRegex.source}/ must have exactly one capture group`) 38 | continue 39 | } 40 | 41 | const matchStart = containerMatch.indices[1][0] 42 | const matchEnd = matchStart + containerMatch[1].length 43 | 44 | // Cursor is outside of the match 45 | if (cursor !== null && (cursor < matchStart || cursor > matchEnd)) { 46 | continue 47 | } 48 | 49 | if (!classRegex) { 50 | yield { 51 | classList: 52 | cursor !== null ? containerMatch[1].slice(0, cursor - matchStart) : containerMatch[1], 53 | range: [matchStart, matchEnd], 54 | } 55 | continue 56 | } 57 | 58 | // Handle class matches inside the "container" 59 | for (let classMatch of containerMatch[1].matchAll(classRegex)) { 60 | // Don't crash when there's no capture group 61 | if (classMatch[1] === undefined) { 62 | console.warn(`Regex /${classRegex.source}/ must have exactly one capture group`) 63 | continue 64 | } 65 | 66 | const classMatchStart = matchStart + classMatch.indices[1][0] 67 | const classMatchEnd = classMatchStart + classMatch[1].length 68 | 69 | // Cursor is outside of the match 70 | if (cursor !== null && (cursor < classMatchStart || cursor > classMatchEnd)) { 71 | continue 72 | } 73 | 74 | yield { 75 | classList: 76 | cursor !== null ? classMatch[1].slice(0, cursor - classMatchStart) : classMatch[1], 77 | range: [classMatchStart, classMatchEnd], 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/closest.ts: -------------------------------------------------------------------------------- 1 | import sift from 'sift-string' 2 | 3 | export function closest(input: string, options: string[]): string | undefined { 4 | return options.concat([]).sort((a, b) => sift(input, a) - sift(input, b))[0] 5 | } 6 | 7 | export function distance(a: string, b: string): number { 8 | return sift(a, b) 9 | } 10 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/colorEquivalents.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, PluginCreator } from 'postcss' 2 | import parseValue from 'postcss-value-parser' 3 | import { inGamut } from 'culori' 4 | import { formatColor, getColorFromValue } from './color' 5 | import type { Comment } from './comments' 6 | 7 | let allowedFunctions = ['rgb', 'rgba', 'hsl', 'hsla', 'lch', 'lab', 'oklch', 'oklab'] 8 | 9 | export function getEquivalentColor(value: string): string { 10 | const color = getColorFromValue(value) 11 | 12 | if (!color) return value 13 | if (typeof color === 'string') return value 14 | if (!inGamut('rgb')(color)) return value 15 | 16 | return formatColor(color) 17 | } 18 | 19 | export const equivalentColorValues: PluginCreator = Object.assign( 20 | ({ comments }: { comments: Comment[] }): Plugin => { 21 | return { 22 | postcssPlugin: 'plugin', 23 | Declaration(decl) { 24 | if (!allowedFunctions.some((fn) => decl.value.includes(fn))) { 25 | return 26 | } 27 | 28 | parseValue(decl.value).walk((node) => { 29 | if (node.type !== 'function') { 30 | return true 31 | } 32 | 33 | if (node.value === 'var') { 34 | return true 35 | } 36 | 37 | if (!allowedFunctions.includes(node.value)) { 38 | return false 39 | } 40 | 41 | const values = node.nodes.filter((n) => n.type === 'word').map((n) => n.value) 42 | if (values.length < 3) { 43 | return false 44 | } 45 | 46 | let color = `${node.value}(${values.join(' ')})` 47 | 48 | let equivalent = getEquivalentColor(color) 49 | 50 | if (equivalent === color) { 51 | return false 52 | } 53 | 54 | comments.push({ 55 | index: 56 | decl.source.start.offset + 57 | `${decl.prop}${decl.raws.between}`.length + 58 | node.sourceEndIndex, 59 | value: equivalent, 60 | }) 61 | 62 | return false 63 | }) 64 | }, 65 | } 66 | }, 67 | { 68 | postcss: true as const, 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/combinations.ts: -------------------------------------------------------------------------------- 1 | export function combinations(str: string): string[] { 2 | let fn = function (active: string, rest: string, a: string[]) { 3 | if (!active && !rest) return undefined 4 | if (!rest) { 5 | a.push(active) 6 | } else { 7 | fn(active + rest[0], rest.slice(1), a) 8 | fn(active, rest.slice(1), a) 9 | } 10 | return a 11 | } 12 | return fn('', str, []) 13 | } 14 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/comments.ts: -------------------------------------------------------------------------------- 1 | import { spliceChangesIntoString } from './splice-changes-into-string' 2 | 3 | export type Comment = { index: number; value: string } 4 | 5 | export function applyComments(str: string, comments: Comment[]): string { 6 | return spliceChangesIntoString( 7 | str, 8 | comments.map((c) => ({ 9 | start: c.index, 10 | end: c.index, 11 | replacement: ` /* ${c.value} */`, 12 | })), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The maximum bounds around the cursor when searching for class names 3 | */ 4 | export const SEARCH_RANGE = 15_000 5 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/css.ts: -------------------------------------------------------------------------------- 1 | import type { Position } from 'vscode-languageserver' 2 | import type { TextDocument } from 'vscode-languageserver-textdocument' 3 | import { isVueDoc, isSvelteDoc, isHtmlDoc } from './html' 4 | import { isJsDoc } from './js' 5 | import type { State } from './state' 6 | import { cssLanguages } from './languages' 7 | import { getLanguageBoundaries } from './getLanguageBoundaries' 8 | 9 | function getCssLanguages(state: State): string[] { 10 | const userCssLanguages = Object.keys(state.editor.userLanguages).filter((lang) => 11 | cssLanguages.includes(state.editor.userLanguages[lang]), 12 | ) 13 | 14 | return [...cssLanguages, ...userCssLanguages] 15 | } 16 | 17 | export function isCssLanguage(state: State, lang: string): boolean { 18 | return getCssLanguages(state).indexOf(lang) !== -1 19 | } 20 | 21 | export function isCssDoc(state: State, doc: TextDocument): boolean { 22 | return isCssLanguage(state, doc.languageId) 23 | } 24 | 25 | export function isCssContext(state: State, doc: TextDocument, position: Position): boolean { 26 | if (isCssDoc(state, doc)) { 27 | return true 28 | } 29 | 30 | if (isHtmlDoc(state, doc) || isVueDoc(doc) || isSvelteDoc(doc) || isJsDoc(state, doc)) { 31 | let str = doc.getText({ 32 | start: { line: 0, character: 0 }, 33 | end: position, 34 | }) 35 | 36 | let boundaries = getLanguageBoundaries(state, doc, str) 37 | 38 | return boundaries ? boundaries[boundaries.length - 1].type === 'css' : false 39 | } 40 | 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/docsUrl.ts: -------------------------------------------------------------------------------- 1 | import * as semver from './semver' 2 | 3 | export function docsUrl(version: string, paths: string | string[]): string { 4 | let major = 0 5 | let url = 'https://tailwindcss-v0.netlify.app/docs/' 6 | if (semver.gte(version, '0.99.0')) { 7 | major = 1 8 | url = 'https://v1.tailwindcss.com/docs/' 9 | } 10 | if (semver.gte(version, '1.99.0')) { 11 | major = 2 12 | url = 'https://v2.tailwindcss.com/docs/' 13 | } 14 | if (semver.gte(version, '2.99.0')) { 15 | major = 3 16 | url = 'https://v3.tailwindcss.com/docs/' 17 | } 18 | if (semver.gte(version, '3.99.0')) { 19 | major = 4 20 | url = 'https://tailwindcss.com/docs/' 21 | } 22 | const path = Array.isArray(paths) ? paths[major] || paths[paths.length - 1] : paths 23 | return `${url}${path}` 24 | } 25 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/equivalents.ts: -------------------------------------------------------------------------------- 1 | import type { TailwindCssSettings } from './state' 2 | import { equivalentPixelValues } from './pixelEquivalents' 3 | import { equivalentColorValues } from './colorEquivalents' 4 | import postcss, { type AcceptedPlugin } from 'postcss' 5 | import { applyComments, type Comment } from './comments' 6 | 7 | export function addEquivalents(css: string, settings: TailwindCssSettings): string { 8 | let comments: Comment[] = [] 9 | 10 | let plugins: AcceptedPlugin[] = [] 11 | 12 | if (settings.showPixelEquivalents) { 13 | plugins.push( 14 | equivalentPixelValues({ 15 | comments, 16 | rootFontSize: settings.rootFontSize, 17 | }), 18 | ) 19 | } 20 | 21 | plugins.push(equivalentColorValues({ comments })) 22 | 23 | try { 24 | postcss(plugins).process(css, { from: undefined }).css 25 | } catch { 26 | return css 27 | } 28 | 29 | return applyComments(css, comments) 30 | } 31 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/estimated-class-size.ts: -------------------------------------------------------------------------------- 1 | import { segment } from './segment' 2 | 3 | /** 4 | * Calculates the approximate size of a generated class 5 | * 6 | * This is meant to be a lower bound, as the actual size of a class can vary 7 | * depending on the actual CSS properties and values, configured theme, etc… 8 | */ 9 | export function estimatedClassSize(className: string): number { 10 | let size = 0 11 | 12 | // We estimate the size using the following structure which gives a reasonable 13 | // lower bound on the size of the generated CSS: 14 | // 15 | // .class-name { 16 | // &:variant-1 { 17 | // &:variant-2 { 18 | // … 19 | // } 20 | // } 21 | // } 22 | 23 | // Class name 24 | size += 1 + className.length + 3 25 | size += 2 26 | 27 | // Variants + nesting 28 | for (let [depth, variantName] of segment(className, ':').entries()) { 29 | size += (depth + 1) * 2 + 2 + variantName.length + 3 30 | size += (depth + 1) * 2 + 2 31 | } 32 | 33 | // ~1.95x is a rough growth factor due to the actual properties being present 34 | return size * 1.95 35 | } 36 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/flagEnabled.ts: -------------------------------------------------------------------------------- 1 | import type { State } from './state' 2 | import dlv from 'dlv' 3 | 4 | export function flagEnabled(state: State, flag: string): boolean { 5 | if (state.featureFlags.future.includes(flag)) { 6 | return state.config.future === 'all' || dlv(state.config, ['future', flag], false) 7 | } 8 | 9 | if (state.featureFlags.experimental.includes(flag)) { 10 | return state.config.experimental === 'all' || dlv(state.config, ['experimental', flag], false) 11 | } 12 | 13 | return false 14 | } 15 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/format-bytes.ts: -------------------------------------------------------------------------------- 1 | const UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte'] 2 | 3 | export function formatBytes(n: number): string { 4 | let i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1000)) 5 | return new Intl.NumberFormat('en', { 6 | notation: 'compact', 7 | style: 'unit', 8 | unit: UNITS[i], 9 | unitDisplay: 'narrow', 10 | }).format(n / 1000 ** i) 11 | } 12 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/getClassNameAtPosition.ts: -------------------------------------------------------------------------------- 1 | import type { State } from './state' 2 | import { combinations } from './combinations' 3 | import dlv from 'dlv' 4 | 5 | export function getClassNameParts(state: State, className: string): string[] { 6 | let separator = state.separator 7 | className = className.replace(/^\./, '') 8 | let parts: string[] = className.split(separator) 9 | 10 | if (parts.length === 1) { 11 | return dlv(state.classNames.classNames, [className, '__info', '__rule']) === true || 12 | Array.isArray(dlv(state.classNames.classNames, [className, '__info'])) 13 | ? [className] 14 | : null 15 | } 16 | 17 | let points = combinations('123456789'.substr(0, parts.length - 1)).map((x) => 18 | x.split('').map((x) => parseInt(x, 10)), 19 | ) 20 | 21 | let possibilities: string[][] = [ 22 | [className], 23 | ...points.map((p) => { 24 | let result = [] 25 | let i = 0 26 | p.forEach((x) => { 27 | result.push(parts.slice(i, x).join('-')) 28 | i = x 29 | }) 30 | result.push(parts.slice(i).join('-')) 31 | return result 32 | }), 33 | ] 34 | 35 | return possibilities.find((key) => { 36 | if ( 37 | dlv(state.classNames.classNames, [...key, '__info', '__rule']) === true || 38 | Array.isArray(dlv(state.classNames.classNames, [...key, '__info'])) 39 | ) { 40 | return true 41 | } 42 | return false 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/getClassNameDecls.ts: -------------------------------------------------------------------------------- 1 | import type { State } from './state' 2 | import { getClassNameParts } from './getClassNameAtPosition' 3 | import removeMeta from './removeMeta' 4 | import dlv from 'dlv' 5 | 6 | export function getClassNameDecls( 7 | state: State, 8 | className: string, 9 | ): Record | Record[] | null { 10 | const parts = getClassNameParts(state, className) 11 | if (!parts) return null 12 | 13 | const info = dlv(state.classNames.classNames, [...parts, '__info']) 14 | 15 | if (Array.isArray(info)) { 16 | return info.map(removeMeta) 17 | } 18 | 19 | return removeMeta(info) 20 | } 21 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/getClassNameMeta.ts: -------------------------------------------------------------------------------- 1 | import { State, ClassNameMeta } from './state' 2 | import { getClassNameParts } from './getClassNameAtPosition' 3 | import dlv from 'dlv' 4 | 5 | export function getClassNameMeta( 6 | state: State, 7 | classNameOrParts: string | string[], 8 | ): ClassNameMeta | ClassNameMeta[] { 9 | const parts = Array.isArray(classNameOrParts) 10 | ? classNameOrParts 11 | : getClassNameParts(state, classNameOrParts) 12 | if (!parts) return null 13 | const info = dlv(state.classNames.classNames, [...parts, '__info']) 14 | 15 | if (Array.isArray(info)) { 16 | return info.map((i) => ({ 17 | source: i.__source, 18 | pseudo: i.__pseudo, 19 | scope: i.__scope, 20 | context: i.__context, 21 | })) 22 | } 23 | 24 | return { 25 | source: info.__source, 26 | pseudo: info.__pseudo, 27 | scope: info.__scope, 28 | context: info.__context, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts: -------------------------------------------------------------------------------- 1 | import type { State } from './state' 2 | import * as jit from './jit' 3 | import { segment } from './segment' 4 | 5 | export function getVariantsFromClassName( 6 | state: State, 7 | className: string, 8 | ): { variants: string[]; offset: number } { 9 | let allVariants = state.variants.flatMap((variant) => { 10 | if (variant.values.length) { 11 | return variant.values.map((value) => 12 | value === 'DEFAULT' ? variant.name : `${variant.name}${variant.hasDash ? '-' : ''}${value}`, 13 | ) 14 | } 15 | return [variant.name] 16 | }) 17 | 18 | let parts = segment(className, state.separator) 19 | if (parts.length < 2) { 20 | return { variants: [], offset: 0 } 21 | } 22 | 23 | parts = parts.filter(Boolean) 24 | 25 | function isValidVariant(part: string) { 26 | if (allVariants.includes(part)) { 27 | return true 28 | } 29 | 30 | let className = `${part}${state.separator}[color:red]` 31 | 32 | if (state.v4) { 33 | // NOTE: This should never happen 34 | if (!state.designSystem) return false 35 | 36 | let prefix = state.designSystem.theme.prefix ?? '' 37 | 38 | if (prefix !== '') { 39 | className = `${prefix}:${className}` 40 | } 41 | 42 | // We don't use `compile()` so there's no overhead from PostCSS 43 | let compiled = state.designSystem.candidatesToCss([className]) 44 | 45 | // NOTE: This should never happen 46 | if (compiled.length !== 1) return false 47 | 48 | return compiled[0] !== null 49 | } 50 | 51 | if (state.jit) { 52 | if ((part.includes('[') && part.endsWith(']')) || part.includes('/')) { 53 | return jit.generateRules(state, [className]).rules.length > 0 54 | } 55 | } 56 | 57 | return false 58 | } 59 | 60 | let offset = 0 61 | let variants = new Set() 62 | 63 | for (let part of parts) { 64 | if (!isValidVariant(part)) break 65 | 66 | variants.add(part) 67 | offset += part.length + state.separator!.length 68 | } 69 | 70 | return { variants: Array.from(variants), offset } 71 | } 72 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/html.ts: -------------------------------------------------------------------------------- 1 | import type { Position } from 'vscode-languageserver' 2 | import type { TextDocument } from 'vscode-languageserver-textdocument' 3 | import type { State } from './state' 4 | import { htmlLanguages } from './languages' 5 | import { getLanguageBoundaries } from './getLanguageBoundaries' 6 | 7 | export function isHtmlDoc(state: State, doc: TextDocument): boolean { 8 | const userHtmlLanguages = Object.keys(state.editor.userLanguages).filter((lang) => 9 | htmlLanguages.includes(state.editor.userLanguages[lang]), 10 | ) 11 | 12 | return [...htmlLanguages, ...userHtmlLanguages].indexOf(doc.languageId) !== -1 13 | } 14 | 15 | export function isVueDoc(doc: TextDocument): boolean { 16 | return doc.languageId === 'vue' 17 | } 18 | 19 | export function isSvelteDoc(doc: TextDocument): boolean { 20 | return doc.languageId === 'svelte' 21 | } 22 | 23 | export function isHtmlContext(state: State, doc: TextDocument, position: Position): boolean { 24 | let str = doc.getText({ 25 | start: { line: 0, character: 0 }, 26 | end: position, 27 | }) 28 | 29 | let boundaries = getLanguageBoundaries(state, doc, str) 30 | 31 | return boundaries ? boundaries[boundaries.length - 1].type === 'html' : false 32 | } 33 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/isObject.ts: -------------------------------------------------------------------------------- 1 | export default function isObject(variable: any): boolean { 2 | return Object.prototype.toString.call(variable) === '[object Object]' 3 | } 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/isValidLocationForEmmetAbbreviation.ts: -------------------------------------------------------------------------------- 1 | import type { TextDocument } from 'vscode-languageserver-textdocument' 2 | import type { Range, Position } from 'vscode-languageserver' 3 | 4 | export function isValidLocationForEmmetAbbreviation( 5 | document: TextDocument, 6 | abbreviationRange: Range, 7 | ): boolean { 8 | const startAngle = '<' 9 | const endAngle = '>' 10 | const escape = '\\' 11 | const question = '?' 12 | let start: Position = { line: 0, character: 0 } 13 | 14 | let textToBackTrack = document.getText({ 15 | start: { 16 | line: start.line, 17 | character: start.character, 18 | }, 19 | end: { 20 | line: abbreviationRange.start.line, 21 | character: abbreviationRange.start.character, 22 | }, 23 | }) 24 | 25 | // Worse case scenario is when cursor is inside a big chunk of text which needs to backtracked 26 | // Backtrack only 500 offsets to ensure we dont waste time doing this 27 | if (textToBackTrack.length > 500) { 28 | textToBackTrack = textToBackTrack.substr(textToBackTrack.length - 500) 29 | } 30 | 31 | if (!textToBackTrack.trim()) { 32 | return true 33 | } 34 | 35 | let valid = true 36 | let foundSpace = false // If < is found before finding whitespace, then its valid abbreviation. E.g.: = 0) { 43 | const char = textToBackTrack[i] 44 | i-- 45 | if (!foundSpace && /\s/.test(char)) { 46 | foundSpace = true 47 | continue 48 | } 49 | if (char === question && textToBackTrack[i] === startAngle) { 50 | i-- 51 | continue 52 | } 53 | // Fix for https://github.com/Microsoft/vscode/issues/55411 54 | // A space is not a valid character right after < in a tag name. 55 | if (/\s/.test(char) && textToBackTrack[i] === startAngle) { 56 | i-- 57 | continue 58 | } 59 | if (char !== startAngle && char !== endAngle) { 60 | continue 61 | } 62 | if (i >= 0 && textToBackTrack[i] === escape) { 63 | i-- 64 | continue 65 | } 66 | if (char === endAngle) { 67 | if (i >= 0 && textToBackTrack[i] === '=') { 68 | continue // False alarm of cases like => 69 | } else { 70 | break 71 | } 72 | } 73 | if (char === startAngle) { 74 | valid = !foundSpace 75 | break 76 | } 77 | } 78 | 79 | return valid 80 | } 81 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/isWithinRange.ts: -------------------------------------------------------------------------------- 1 | import type { Position, Range } from 'vscode-languageserver' 2 | 3 | export function isWithinRange(position: Position, range: Range): boolean { 4 | if (position.line === range.start.line && position.character >= range.start.character) { 5 | if (position.line === range.end.line && position.character > range.end.character) { 6 | return false 7 | } else { 8 | return true 9 | } 10 | } 11 | if (position.line === range.end.line && position.character <= range.end.character) { 12 | if (position.line === range.start.line && position.character < range.end.character) { 13 | return false 14 | } else { 15 | return true 16 | } 17 | } 18 | if (position.line > range.start.line && position.line < range.end.line) { 19 | return true 20 | } 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/joinWithAnd.ts: -------------------------------------------------------------------------------- 1 | export function joinWithAnd(strings: string[]): string { 2 | return strings.reduce((acc, cur, i) => { 3 | if (i === 0) { 4 | return cur 5 | } 6 | if (strings.length > 1 && i === strings.length - 1) { 7 | return `${acc} and ${cur}` 8 | } 9 | return `${acc}, ${cur}` 10 | }, '') 11 | } 12 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/js.ts: -------------------------------------------------------------------------------- 1 | import type { Position } from 'vscode-languageserver' 2 | import type { TextDocument } from 'vscode-languageserver-textdocument' 3 | import type { State } from './state' 4 | import { jsLanguages } from './languages' 5 | import { getLanguageBoundaries } from './getLanguageBoundaries' 6 | 7 | export function isJsDoc(state: State, doc: TextDocument): boolean { 8 | const userJsLanguages = Object.keys(state.editor.userLanguages).filter((lang) => 9 | jsLanguages.includes(state.editor.userLanguages[lang]), 10 | ) 11 | 12 | return [...jsLanguages, ...userJsLanguages].indexOf(doc.languageId) !== -1 13 | } 14 | 15 | export function isJsContext(state: State, doc: TextDocument, position: Position): boolean { 16 | let str = doc.getText({ 17 | start: { line: 0, character: 0 }, 18 | end: position, 19 | }) 20 | 21 | let boundaries = getLanguageBoundaries(state, doc, str) 22 | 23 | return boundaries 24 | ? ['js', 'ts', 'jsx', 'tsx'].includes(boundaries[boundaries.length - 1].type) 25 | : false 26 | } 27 | 28 | export function isJsxContext(state: State, doc: TextDocument, position: Position): boolean { 29 | let str = doc.getText({ 30 | start: { line: 0, character: 0 }, 31 | end: position, 32 | }) 33 | 34 | let boundaries = getLanguageBoundaries(state, doc, str) 35 | 36 | return boundaries ? ['jsx', 'tsx'].includes(boundaries[boundaries.length - 1].type) : false 37 | } 38 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/language-blocks.ts: -------------------------------------------------------------------------------- 1 | import type { State } from '../util/state' 2 | import type { Range } from 'vscode-languageserver' 3 | import type { TextDocument } from 'vscode-languageserver-textdocument' 4 | import { getLanguageBoundaries } from '../util/getLanguageBoundaries' 5 | import { isCssDoc } from '../util/css' 6 | import { getTextWithoutComments } from './doc' 7 | 8 | export interface LanguageBlock { 9 | document: TextDocument 10 | range: Range | undefined 11 | lang: string 12 | readonly text: string 13 | } 14 | 15 | export function* getCssBlocks( 16 | state: State, 17 | document: TextDocument, 18 | ): Iterable { 19 | if (isCssDoc(state, document)) { 20 | yield { 21 | document, 22 | range: undefined, 23 | lang: document.languageId, 24 | get text() { 25 | return getTextWithoutComments(document, 'css') 26 | }, 27 | } 28 | } else { 29 | let boundaries = getLanguageBoundaries(state, document) 30 | if (!boundaries) return [] 31 | 32 | for (let boundary of boundaries) { 33 | if (boundary.type !== 'css') continue 34 | 35 | yield { 36 | document, 37 | range: boundary.range, 38 | lang: boundary.lang ?? document.languageId, 39 | get text() { 40 | return getTextWithoutComments(document, 'css', boundary.range) 41 | }, 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/languages.ts: -------------------------------------------------------------------------------- 1 | import type { EditorState } from './state' 2 | 3 | export const htmlLanguages: string[] = [ 4 | 'aspnetcorerazor', 5 | 'astro', 6 | 'astro-markdown', 7 | 'blade', 8 | 'django-html', 9 | 'edge', 10 | 'ejs', 11 | 'erb', 12 | 'gohtml', 13 | 'GoHTML', 14 | 'gohtmltmpl', 15 | 'haml', 16 | 'handlebars', 17 | 'hbs', 18 | 'html', 19 | 'HTML (Eex)', 20 | 'HTML (EEx)', 21 | 'html-eex', 22 | 'htmldjango', 23 | 'jade', 24 | 'latte', 25 | 'leaf', 26 | 'liquid', 27 | 'markdown', 28 | 'mdx', 29 | 'mustache', 30 | 'njk', 31 | 'nunjucks', 32 | 'phoenix-heex', 33 | 'php', 34 | 'razor', 35 | 'slim', 36 | 'surface', 37 | 'twig', 38 | ] 39 | 40 | export const cssLanguages: string[] = [ 41 | 'css', 42 | 'less', 43 | 'postcss', 44 | 'sass', 45 | 'scss', 46 | 'stylus', 47 | 'sugarss', 48 | 'tailwindcss', 49 | ] 50 | 51 | export const jsLanguages: string[] = [ 52 | 'javascript', 53 | 'javascriptreact', 54 | 'reason', 55 | 'rescript', 56 | 'typescript', 57 | 'typescriptreact', 58 | 'glimmer-js', 59 | 'glimmer-ts', 60 | ] 61 | 62 | export const specialLanguages: string[] = ['vue', 'svelte'] 63 | 64 | export const languages: string[] = [ 65 | ...cssLanguages, 66 | ...htmlLanguages, 67 | ...jsLanguages, 68 | ...specialLanguages, 69 | ] 70 | 71 | const semicolonlessLanguages = ['sass', 'sugarss', 'stylus'] 72 | 73 | export function isSemicolonlessCssLanguage( 74 | languageId: string, 75 | userLanguages: EditorState['userLanguages'] = {}, 76 | ): boolean { 77 | return ( 78 | semicolonlessLanguages.includes(languageId) || 79 | semicolonlessLanguages.includes(userLanguages[languageId]) 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/lazy.ts: -------------------------------------------------------------------------------- 1 | // https://www.codementor.io/@agustinchiappeberrini/lazy-evaluation-and-javascript-a5m7g8gs3 2 | 3 | export interface Lazy { 4 | (): T 5 | isLazy: boolean 6 | } 7 | 8 | export const lazy = (getter: () => T): Lazy => { 9 | let evaluated: boolean = false 10 | let _res: T = null 11 | const res = >function (): T { 12 | if (evaluated) return _res 13 | _res = getter.apply(this, arguments) 14 | evaluated = true 15 | return _res 16 | } 17 | res.isLazy = true 18 | return res 19 | } 20 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/naturalExpand.ts: -------------------------------------------------------------------------------- 1 | export function naturalExpand(value: number, total?: number): string { 2 | let length = typeof total === 'number' ? total.toString().length : 8 3 | return ('0'.repeat(length) + value).slice(-length) 4 | } 5 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/rangesEqual.ts: -------------------------------------------------------------------------------- 1 | import type { Range } from 'vscode-languageserver' 2 | 3 | export function rangesEqual(a: Range, b: Range): boolean { 4 | return ( 5 | a.start.line === b.start.line && 6 | a.start.character === b.start.character && 7 | a.end.line === b.end.line && 8 | a.end.character === b.end.character 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/removeMeta.ts: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | export default function removeMeta(obj: any): any { 4 | let result = {} 5 | for (let key in obj) { 6 | if (key.substr(0, 2) === '__') continue 7 | if (isObject(obj[key])) { 8 | result[key] = removeMeta(obj[key]) 9 | } else { 10 | result[key] = obj[key] 11 | } 12 | } 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/removeRangesFromString.ts: -------------------------------------------------------------------------------- 1 | import type { Range } from 'vscode-languageserver' 2 | import lineColumn from 'line-column' 3 | import { ensureArray } from './array' 4 | 5 | export function removeRangesFromString(str: string, rangeOrRanges: Range | Range[]): string { 6 | let ranges = ensureArray(rangeOrRanges) 7 | let finder = lineColumn(str + '\n', { origin: 0 }) 8 | let indexRanges: { start: number; end: number }[] = [] 9 | 10 | ranges.forEach((range) => { 11 | let start = finder.toIndex(range.start.line, range.start.character) 12 | let end = finder.toIndex(range.end.line, range.end.character) 13 | for (let i = start - 1; i >= 0; i--) { 14 | if (/\s/.test(str.charAt(i))) { 15 | start = i 16 | } else { 17 | break 18 | } 19 | } 20 | indexRanges.push({ start, end }) 21 | }) 22 | 23 | indexRanges.sort((a, b) => a.start - b.start) 24 | 25 | let result = '' 26 | let i = 0 27 | 28 | indexRanges.forEach((indexRange) => { 29 | result += str.substring(i, indexRange.start) 30 | i = indexRange.end 31 | }) 32 | 33 | result += str.substring(i) 34 | 35 | return result.trim() 36 | } 37 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/rewriting/calc.ts: -------------------------------------------------------------------------------- 1 | import { stringify, tokenize } from '@csstools/css-tokenizer' 2 | import { isFunctionNode, parseComponentValue } from '@csstools/css-parser-algorithms' 3 | import { calcFromComponentValues } from '@csstools/css-calc' 4 | 5 | export function evaluateExpression(str: string): string | null { 6 | let tokens = tokenize({ css: `calc(${str})` }) 7 | 8 | let components = parseComponentValue(tokens, {}) 9 | if (!components) return null 10 | 11 | let result = calcFromComponentValues([[components]], { 12 | // Ensure evaluation of random() is deterministic 13 | randomSeed: 1, 14 | 15 | // Limit precision to keep values environment independent 16 | precision: 4, 17 | }) 18 | 19 | // The result array is the same shape as the original so we're guaranteed to 20 | // have an element here 21 | let node = result[0][0] 22 | 23 | // If we have a top-level `calc(…)` node then the evaluation did not resolve 24 | // to a single value and we consider it to be incomplete 25 | if (isFunctionNode(node)) { 26 | if (node.name[1] === 'calc(') return null 27 | } 28 | 29 | return stringify(...node.tokens()) 30 | } 31 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/rewriting/index.ts: -------------------------------------------------------------------------------- 1 | export * from './replacements' 2 | export * from './var-fallbacks' 3 | export * from './calc' 4 | export * from './add-theme-values' 5 | export * from './inline-theme-values' 6 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/rewriting/inline-theme-values.ts: -------------------------------------------------------------------------------- 1 | import type { State, TailwindCssSettings } from '../state' 2 | 3 | import { evaluateExpression } from './calc' 4 | import { resolveVariableValue } from './lookup' 5 | import { replaceCssVars, replaceCssCalc } from './replacements' 6 | 7 | export function inlineThemeValues(css: string, state: State): string { 8 | if (!state.designSystem) return css 9 | 10 | css = replaceCssCalc(css, (expr) => { 11 | let inlined = replaceCssVars(expr.value, { 12 | replace({ name, fallback }) { 13 | if (!name.startsWith('--')) return null 14 | 15 | let value = resolveVariableValue(state.designSystem, name) 16 | if (value === null) return fallback 17 | 18 | return value 19 | }, 20 | }) 21 | 22 | return evaluateExpression(inlined) 23 | }) 24 | 25 | css = replaceCssVars(css, { 26 | replace({ name, fallback }) { 27 | if (!name.startsWith('--')) return null 28 | 29 | let value = resolveVariableValue(state.designSystem, name) 30 | if (value === null) return fallback 31 | 32 | return value 33 | }, 34 | }) 35 | 36 | return css 37 | } 38 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/rewriting/lookup.ts: -------------------------------------------------------------------------------- 1 | import { DesignSystem } from '../v4' 2 | 3 | // Resolve a variable value from the design system 4 | export function resolveVariableValue(design: DesignSystem, name: string): string | null { 5 | let prefix = design.theme.prefix ?? null 6 | 7 | if (prefix && name.startsWith(`--${prefix}`)) { 8 | name = `--${name.slice(prefix.length + 3)}` 9 | } 10 | 11 | return design.resolveThemeValue?.(name, true) ?? null 12 | } 13 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts: -------------------------------------------------------------------------------- 1 | import type { State } from '../state' 2 | import { resolveVariableValue } from './lookup' 3 | import { replaceCssVars } from './replacements' 4 | 5 | export function replaceCssVarsWithFallbacks(state: State, str: string): string { 6 | let seen = new Set() 7 | 8 | return replaceCssVars(str, { 9 | replace({ name, fallback }) { 10 | // Replace with the value from the design system first. The design system 11 | // take precedences over other sources as that emulates the behavior of a 12 | // browser where the fallback is only used if the variable is defined. 13 | if (state.designSystem && name.startsWith('--')) { 14 | // TODO: This isn't quite right as we might skip expanding a variable 15 | // that should be expanded 16 | if (seen.has(name)) return null 17 | let value = resolveVariableValue(state.designSystem, name) 18 | if (value !== null) { 19 | if (value.includes('var(')) { 20 | seen.add(name) 21 | } 22 | 23 | return value 24 | } 25 | } 26 | 27 | if (fallback) { 28 | return fallback 29 | } 30 | 31 | if ( 32 | name === '--tw-text-shadow-alpha' || 33 | name === '--tw-drop-shadow-alpha' || 34 | name === '--tw-shadow-alpha' 35 | ) { 36 | return '100%' 37 | } 38 | 39 | // Don't touch it since there's no suitable replacement 40 | return null 41 | }, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/screens.ts: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | export type MinMaxScreen = { 4 | min?: string 5 | max?: string 6 | } 7 | 8 | export type RawScreen = { 9 | raw: string 10 | } 11 | 12 | export type Screen = string | RawScreen | MinMaxScreen | MinMaxScreen[] 13 | 14 | function isRawScreen(screen: unknown): screen is RawScreen { 15 | return isObject(screen) && (screen as RawScreen).raw !== undefined 16 | } 17 | 18 | export function stringifyScreen(screen: Screen): string | undefined { 19 | if (!screen) return undefined 20 | if (typeof screen === 'string') return `@media (min-width: ${screen})` 21 | if (isRawScreen(screen)) { 22 | return `@media ${(screen as RawScreen).raw}` 23 | } 24 | let str = (Array.isArray(screen) ? screen : [screen]) 25 | .map((range) => { 26 | return [ 27 | typeof range.min === 'string' ? `(min-width: ${range.min})` : null, 28 | typeof range.max === 'string' ? `(max-width: ${range.max})` : null, 29 | ] 30 | .filter(Boolean) 31 | .join(' and ') 32 | }) 33 | .join(', ') 34 | return str ? `@media ${str}` : undefined 35 | } 36 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/semver.ts: -------------------------------------------------------------------------------- 1 | import semverGte from 'semver/functions/gte.js' 2 | import semverLte from 'semver/functions/lte.js' 3 | 4 | export function gte(v1: string, v2: string): boolean { 5 | if (v1.startsWith('0.0.0-insiders')) { 6 | return true 7 | } 8 | 9 | return semverGte(v1, v2) 10 | } 11 | 12 | export function lte(v1: string, v2: string): boolean { 13 | if (v1.startsWith('0.0.0-insiders')) { 14 | return false 15 | } 16 | 17 | return semverLte(v1, v2) 18 | } 19 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/splice-changes-into-string.ts: -------------------------------------------------------------------------------- 1 | export interface StringChange { 2 | start: number 3 | end: number 4 | replacement: string 5 | } 6 | 7 | /** 8 | * Apply the changes to the string such that a change in the length 9 | * of the string does not break the indexes of the subsequent changes. 10 | */ 11 | export function spliceChangesIntoString(str: string, changes: StringChange[]): string { 12 | // If there are no changes, return the original string 13 | if (!changes[0]) return str 14 | 15 | // Sort all changes in order to make it easier to apply them 16 | changes.sort((a, b) => { 17 | return a.end - b.end || a.start - b.start 18 | }) 19 | 20 | // Append original string between each chunk, and then the chunk itself 21 | // This is sort of a String Builder pattern, thus creating less memory pressure 22 | let result = '' 23 | 24 | let previous = changes[0] 25 | 26 | result += str.slice(0, previous.start) 27 | result += previous.replacement 28 | 29 | for (let i = 1; i < changes.length; ++i) { 30 | let change = changes[i] 31 | 32 | result += str.slice(previous.end, change.start) 33 | result += change.replacement 34 | 35 | previous = change 36 | } 37 | 38 | // Add leftover string from last chunk to end 39 | result += str.slice(previous.end) 40 | 41 | return result 42 | } 43 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/stringToPath.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L6735-L6744 2 | let rePropName = 3 | /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g 4 | let reEscapeChar = /\\(\\)?/g 5 | 6 | export function stringToPath(string: string): string[] { 7 | let result: string[] = [] 8 | if (string.charCodeAt(0) === 46 /* . */) { 9 | result.push('') 10 | } 11 | // @ts-ignore 12 | string.replace(rePropName, (match, number, quote, subString) => { 13 | result.push(quote ? subString.replace(reEscapeChar, '$1') : number || match) 14 | }) 15 | return result 16 | } 17 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/stringify.ts: -------------------------------------------------------------------------------- 1 | import removeMeta from './removeMeta' 2 | import dlv from 'dlv' 3 | import escapeClassName from 'css.escape' 4 | import { ensureArray } from './array' 5 | // @ts-ignore 6 | import stringifyObject from 'stringify-object' 7 | import isObject from './isObject' 8 | import type { Settings } from './state' 9 | import { addEquivalents } from './equivalents' 10 | 11 | export function stringifyConfigValue(x: any): string { 12 | if (isObject(x)) return `${Object.keys(x).length} values` 13 | if (typeof x === 'function') return 'ƒ' 14 | if (typeof x === 'string') return x 15 | return stringifyObject(x, { 16 | inlineCharacterLimit: Infinity, 17 | singleQuotes: false, 18 | transform: (obj, prop, originalResult) => { 19 | if (typeof obj[prop] === 'function') { 20 | return 'ƒ' 21 | } 22 | return originalResult 23 | }, 24 | }) 25 | } 26 | 27 | export function stringifyCss(className: string, obj: any, settings: Settings): string { 28 | if (obj.__rule !== true && !Array.isArray(obj)) return null 29 | 30 | if (Array.isArray(obj)) { 31 | const rules = obj.map((x) => stringifyCss(className, x, settings)).filter(Boolean) 32 | if (rules.length === 0) return null 33 | return rules.join('\n\n') 34 | } 35 | 36 | let css = `` 37 | const indent = ' '.repeat(settings.editor.tabSize) 38 | 39 | const context = dlv(obj, '__context', []) 40 | const props = Object.keys(removeMeta(obj)) 41 | if (props.length === 0) return null 42 | 43 | for (let i = 0; i < context.length; i++) { 44 | css += `${indent.repeat(i)}${context[i]} {\n` 45 | } 46 | 47 | const indentStr = indent.repeat(context.length) 48 | const decls = props.reduce((acc, curr, i) => { 49 | const propStr = ensureArray(obj[curr]) 50 | .map((val) => `${indentStr + indent}${curr}: ${val};`) 51 | .join('\n') 52 | return `${acc}${i === 0 ? '' : '\n'}${propStr}` 53 | }, '') 54 | css += `${indentStr}${augmentClassName(className, obj)} {\n${decls}\n${indentStr}}` 55 | 56 | for (let i = context.length - 1; i >= 0; i--) { 57 | css += `${indent.repeat(i)}\n}` 58 | } 59 | 60 | css = addEquivalents(css, settings.tailwindCSS) 61 | 62 | return css 63 | } 64 | 65 | function augmentClassName(className: string, obj: any): string { 66 | const pseudo = obj.__pseudo.join('') 67 | const scope = obj.__scope ? `${obj.__scope} ` : '' 68 | return `${scope}.${escapeClassName(className)}${pseudo}` 69 | } 70 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { createState, getDefaultTailwindSettings, Settings, State } from './state' 2 | import { TextDocument } from 'vscode-languageserver-textdocument' 3 | import type { DeepPartial } from '../types' 4 | import dedent, { type Dedent } from 'dedent' 5 | 6 | export const js: Dedent = dedent 7 | export const jsx: Dedent = dedent 8 | export const ts: Dedent = dedent 9 | export const tsx: Dedent = dedent 10 | export const css: Dedent = dedent 11 | export const html: Dedent = dedent 12 | export const pug: Dedent = dedent 13 | 14 | export function createDocument({ 15 | name, 16 | lang, 17 | content, 18 | settings, 19 | }: { 20 | name: string 21 | lang: string 22 | content: string | string[] 23 | settings?: DeepPartial 24 | }): { doc: TextDocument; state: State } { 25 | let doc = TextDocument.create( 26 | `file://${name}`, 27 | lang, 28 | 1, 29 | typeof content === 'string' ? content : content.join('\n'), 30 | ) 31 | let defaults = getDefaultTailwindSettings() 32 | settings ??= {} 33 | let state = createState({ 34 | editor: { 35 | getConfiguration: async () => ({ 36 | ...defaults, 37 | ...settings, 38 | tailwindCSS: { 39 | ...defaults.tailwindCSS, 40 | ...settings.tailwindCSS, 41 | lint: { 42 | ...defaults.tailwindCSS.lint, 43 | ...(settings.tailwindCSS?.lint ?? {}), 44 | }, 45 | experimental: { 46 | ...defaults.tailwindCSS.experimental, 47 | ...(settings.tailwindCSS?.experimental ?? {}), 48 | }, 49 | files: { 50 | ...defaults.tailwindCSS.files, 51 | ...(settings.tailwindCSS?.files ?? {}), 52 | }, 53 | }, 54 | editor: { 55 | ...defaults.editor, 56 | ...settings.editor, 57 | }, 58 | }), 59 | }, 60 | }) 61 | 62 | return { 63 | doc, 64 | state, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/v4/ast.ts: -------------------------------------------------------------------------------- 1 | export type Rule = { 2 | kind: 'rule' 3 | selector: string 4 | nodes: AstNode[] 5 | } 6 | 7 | export type Declaration = { 8 | kind: 'declaration' 9 | property: string 10 | value: string 11 | important: boolean 12 | } 13 | 14 | export type Comment = { 15 | kind: 'comment' 16 | value: string 17 | } 18 | 19 | export type AstNode = Rule | Declaration | Comment 20 | 21 | export function visit( 22 | nodes: AstNode[], 23 | cb: (node: AstNode, path: AstNode[]) => void, 24 | path: AstNode[] = [], 25 | ): void { 26 | for (let child of nodes) { 27 | path = [...path, child] 28 | cb(child, path) 29 | if (child.kind === 'rule') { 30 | visit(child.nodes, cb, path) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/v4/candidate.ts: -------------------------------------------------------------------------------- 1 | export type ArbitraryModifier = { 2 | kind: 'arbitrary' 3 | value: string 4 | dashedIdent: string | null 5 | } 6 | 7 | export type ArbitraryVariantValue = { 8 | kind: 'arbitrary' 9 | value: string 10 | } 11 | 12 | export type NamedVariantValue = { 13 | kind: 'named' 14 | value: string 15 | } 16 | 17 | export type NamedVariant = { 18 | kind: 'named' 19 | root: string 20 | modifier: ArbitraryModifier | NamedModifier | null 21 | value: ArbitraryVariantValue | NamedVariantValue | null 22 | } 23 | 24 | export type NamedModifier = { 25 | kind: 'named' 26 | value: string 27 | } 28 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/v4/design-system.ts: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import type { Rule } from './ast' 3 | import type { NamedVariant } from './candidate' 4 | 5 | export interface Theme { 6 | // Prefix didn't exist for earlier Tailwind versions 7 | prefix?: string 8 | entries(): [string, any][] 9 | } 10 | 11 | export interface ClassMetadata { 12 | modifiers: string[] 13 | } 14 | 15 | export type ClassEntry = [string, ClassMetadata] 16 | 17 | export interface VariantEntry { 18 | name: string 19 | isArbitrary: boolean 20 | values: string[] 21 | hasDash: boolean 22 | selectors: (options: { modifier?: string; value?: string }) => string[] 23 | } 24 | 25 | export type VariantFn = (rule: Rule, variant: NamedVariant) => null | void 26 | 27 | export interface ThemeEntry { 28 | kind: 'namespace' | 'variable' 29 | name: string 30 | } 31 | 32 | export interface DesignSystem { 33 | theme: Theme 34 | variants: Map 35 | utilities: Map 36 | candidatesToCss(classes: string[]): (string | null)[] 37 | getClassOrder(classes: string[]): [string, bigint | null][] 38 | getClassList(): ClassEntry[] 39 | getVariants(): VariantEntry[] 40 | 41 | // Optional because it did not exist in earlier v4 alpha versions 42 | resolveThemeValue?(path: string, forceInline?: boolean): string | undefined 43 | invalidCandidates?: Set 44 | } 45 | 46 | export interface DesignSystem { 47 | dependencies(): Set 48 | compile(classes: string[]): (postcss.Root | null)[] 49 | toCss(nodes: postcss.Root | postcss.Node[]): string 50 | } 51 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/v4/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ast' 2 | export * from './candidate' 3 | export * from './design-system' 4 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/v4/theme-keys.ts: -------------------------------------------------------------------------------- 1 | import { DesignSystem } from './design-system' 2 | 3 | export function resolveKnownThemeKeys(design: DesignSystem): string[] { 4 | let validThemeKeys = Array.from(design.theme.entries(), ([key]) => key) 5 | 6 | let prefixLength = design.theme.prefix?.length ?? 0 7 | 8 | return prefixLength > 0 9 | ? // Strip the configured prefix from the list of valid theme keys 10 | validThemeKeys.map((key) => `--${key.slice(prefixLength + 3)}`) 11 | : validThemeKeys 12 | } 13 | 14 | export function resolveKnownThemeNamespaces(design: DesignSystem): string[] { 15 | return [ 16 | '--breakpoint', 17 | '--color', 18 | '--animate', 19 | '--blur', 20 | '--radius', 21 | '--shadow', 22 | '--inset-shadow', 23 | '--drop-shadow', 24 | '--container', 25 | '--font', 26 | '--font-size', 27 | '--tracking', 28 | '--leading', 29 | '--ease', 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/src/util/validateApply.ts: -------------------------------------------------------------------------------- 1 | import type { State } from './state' 2 | import { getClassNameMeta } from './getClassNameMeta' 3 | import { flagEnabled } from './flagEnabled' 4 | import * as semver from './semver' 5 | 6 | export function validateApply( 7 | state: State, 8 | classNameOrParts: string | string[], 9 | ): { isApplyable: true } | { isApplyable: false; reason: string } | null { 10 | if (state.jit) { 11 | return { isApplyable: true } 12 | } 13 | 14 | const meta = getClassNameMeta(state, classNameOrParts) 15 | if (!meta) return null 16 | 17 | if (semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')) { 18 | return { isApplyable: true } 19 | } 20 | 21 | const className = Array.isArray(classNameOrParts) 22 | ? classNameOrParts.join(state.separator) 23 | : classNameOrParts 24 | 25 | let reason: string 26 | 27 | if (Array.isArray(meta)) { 28 | reason = `'@apply' cannot be used with '${className}' because it is included in multiple rulesets.` 29 | } else if (meta.source !== 'utilities') { 30 | reason = `'@apply' cannot be used with '${className}' because it is not a utility.` 31 | } else if (meta.context && meta.context.length > 0) { 32 | if (meta.context.length === 1) { 33 | reason = `'@apply' cannot be used with '${className}' because it is nested inside of an at-rule ('${meta.context[0]}').` 34 | } else { 35 | reason = `'@apply' cannot be used with '${className}' because it is nested inside of at-rules (${meta.context 36 | .map((c) => `'${c}'`) 37 | .join(', ')}).` 38 | } 39 | } else if (meta.pseudo && meta.pseudo.length > 0) { 40 | if (meta.pseudo.length === 1) { 41 | reason = `'@apply' cannot be used with '${className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')` 42 | } else { 43 | reason = `'@apply' cannot be used with '${className}' because its definition includes pseudo-selectors (${meta.pseudo 44 | .map((p) => `'${p}'`) 45 | .join(', ')}).` 46 | } 47 | } 48 | 49 | if (reason) { 50 | return { isApplyable: false, reason } 51 | } 52 | 53 | return { isApplyable: true } 54 | } 55 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "../../types"], 3 | "compilerOptions": { 4 | "module": "ES2022", 5 | "lib": ["ES2022"], 6 | "target": "ES2022", 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": false, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "Bundler", 17 | "skipLibCheck": true, 18 | "jsx": "react", 19 | "esModuleInterop": true, 20 | "isolatedDeclarations": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-service/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | testTimeout: 15000, 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-syntax/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tailwindcss/language-syntax", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "vitest", 7 | "build": " " 8 | }, 9 | "devDependencies": { 10 | "@types/node": "^18.19.33", 11 | "dedent": "^1.5.3", 12 | "vitest": "^3.1.4", 13 | "vscode-oniguruma": "^2.0.1", 14 | "vscode-textmate": "^9.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-syntax/tests/default-map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Map that can generate default values for keys that don't exist. 3 | * Generated default values are added to the map to avoid recomputation. 4 | */ 5 | export class DefaultMap extends Map { 6 | constructor(private factory: (key: T, self: DefaultMap) => V) { 7 | super() 8 | } 9 | 10 | get(key: T): V { 11 | let value = super.get(key) 12 | 13 | if (value === undefined) { 14 | value = this.factory(key, this) 15 | this.set(key, value) 16 | } 17 | 18 | return value 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-syntax/tests/scopes.ts: -------------------------------------------------------------------------------- 1 | export interface ScopeEntry { 2 | content: Promise<{ default: object }> 3 | inject: string[] 4 | } 5 | 6 | export const KNOWN_SCOPES: Record = { 7 | 'source.css': { 8 | content: import('../syntaxes/css.json'), 9 | inject: [ 10 | 'tailwindcss.at-rules.injection', 11 | 'tailwindcss.at-apply.injection', 12 | 'tailwindcss.theme-fn.injection', 13 | 'tailwindcss.screen-fn.injection', 14 | ], 15 | }, 16 | 17 | 'source.css.tailwind': { 18 | content: import('../../vscode-tailwindcss/syntaxes/source.css.tailwind.tmLanguage.json'), 19 | inject: [], 20 | }, 21 | 22 | 'tailwindcss.at-apply.injection': { 23 | content: import('../../vscode-tailwindcss/syntaxes/at-apply.tmLanguage.json'), 24 | inject: [], 25 | }, 26 | 27 | 'tailwindcss.at-rules.injection': { 28 | content: import('../../vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json'), 29 | inject: [], 30 | }, 31 | 32 | 'tailwindcss.theme-fn.injection': { 33 | content: import('../../vscode-tailwindcss/syntaxes/theme-fn.tmLanguage.json'), 34 | inject: [], 35 | }, 36 | 37 | 'tailwindcss.screen-fn.injection': { 38 | content: import('../../vscode-tailwindcss/syntaxes/screen-fn.tmLanguage.json'), 39 | inject: [], 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-syntax/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2022", 5 | "lib": ["ES2022"], 6 | "rootDir": "..", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "baseUrl": ".." 12 | }, 13 | "include": ["tests"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/tailwindcss-language-syntax/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | testTimeout: 15000, 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/.github/autocomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-intellisense/7f896134f5f7b60fe0d117261c9cc6ba003fc80d/packages/vscode-tailwindcss/.github/autocomplete.png -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-intellisense/7f896134f5f7b60fe0d117261c9cc6ba003fc80d/packages/vscode-tailwindcss/.github/banner.png -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/.github/hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-intellisense/7f896134f5f7b60fe0d117261c9cc6ba003fc80d/packages/vscode-tailwindcss/.github/hover.png -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/.github/linting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-intellisense/7f896134f5f7b60fe0d117261c9cc6ba003fc80d/packages/vscode-tailwindcss/.github/linting.png -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /*.vsix 3 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/.vscodeignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | .vscode/** 3 | .github/** 4 | src/** 5 | packages/** 6 | tests/** 7 | **/*.ts 8 | **/*.map 9 | .gitignore 10 | **/tsconfig.json 11 | **/*.node 12 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tailwind Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/tailwindcss-intellisense/7f896134f5f7b60fe0d117261c9cc6ba003fc80d/packages/vscode-tailwindcss/media/icon.png -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/src/api.ts: -------------------------------------------------------------------------------- 1 | import { workspace, CancellationTokenSource, OutputChannel, ExtensionContext, Uri } from 'vscode' 2 | import { anyWorkspaceFoldersNeedServer, fileMayBeTailwindRelated } from './analyze' 3 | 4 | interface ApiOptions { 5 | context: ExtensionContext 6 | outputChannel: OutputChannel 7 | } 8 | 9 | export async function createApi({ context, outputChannel }: ApiOptions) { 10 | let folderAnalysis: Promise | null = null 11 | 12 | async function workspaceNeedsLanguageServer() { 13 | if (folderAnalysis) return folderAnalysis 14 | 15 | let source: CancellationTokenSource | null = new CancellationTokenSource() 16 | source.token.onCancellationRequested(() => { 17 | source?.dispose() 18 | source = null 19 | 20 | outputChannel.appendLine( 21 | 'Server was not started. Search for Tailwind CSS-related files was taking too long.', 22 | ) 23 | }) 24 | 25 | // Cancel the search after roughly 15 seconds 26 | setTimeout(() => source?.cancel(), 15_000) 27 | context.subscriptions.push(source) 28 | 29 | folderAnalysis ??= anyWorkspaceFoldersNeedServer({ 30 | token: source.token, 31 | folders: workspace.workspaceFolders ?? [], 32 | }) 33 | 34 | let result = await folderAnalysis 35 | source?.dispose() 36 | return result 37 | } 38 | 39 | async function stylesheetNeedsLanguageServer(uri: Uri) { 40 | outputChannel.appendLine(`Checking if ${uri.fsPath} may be Tailwind-related…`) 41 | 42 | return fileMayBeTailwindRelated(uri) 43 | } 44 | 45 | return { 46 | workspaceNeedsLanguageServer, 47 | stylesheetNeedsLanguageServer, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/src/cssServer.ts: -------------------------------------------------------------------------------- 1 | import '@tailwindcss/language-server/src/language/css' 2 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/src/exclusions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | workspace, 3 | type WorkspaceConfiguration, 4 | type ConfigurationScope, 5 | type WorkspaceFolder, 6 | } from 'vscode' 7 | import picomatch from 'picomatch' 8 | import * as path from 'node:path' 9 | 10 | function getGlobalExcludePatterns(scope: ConfigurationScope | null): string[] { 11 | return Object.entries(workspace.getConfiguration('files', scope)?.get('exclude') ?? []) 12 | .filter(([, value]) => value === true) 13 | .map(([key]) => key) 14 | .filter(Boolean) 15 | } 16 | 17 | export function getExcludePatterns(scope: ConfigurationScope | null): string[] { 18 | return [ 19 | ...getGlobalExcludePatterns(scope), 20 | ...(workspace.getConfiguration('tailwindCSS', scope).get('files.exclude')).filter( 21 | Boolean, 22 | ), 23 | ] 24 | } 25 | 26 | export function isExcluded(file: string, folder: WorkspaceFolder): boolean { 27 | for (let pattern of getExcludePatterns(folder)) { 28 | let matcher = picomatch(path.join(folder.uri.fsPath, pattern)) 29 | 30 | if (matcher(file)) { 31 | return true 32 | } 33 | } 34 | 35 | return false 36 | } 37 | 38 | export function mergeExcludes( 39 | settings: WorkspaceConfiguration, 40 | scope: ConfigurationScope | null, 41 | ): any { 42 | return { 43 | ...settings, 44 | files: { 45 | ...settings.files, 46 | exclude: getExcludePatterns(scope), 47 | }, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/src/server.ts: -------------------------------------------------------------------------------- 1 | import '@tailwindcss/language-server/src/server' 2 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/src/servers/index.ts: -------------------------------------------------------------------------------- 1 | export * as css from './css' 2 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/syntaxes/at-apply.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "tailwindcss.at-apply.injection", 3 | "fileTypes": [], 4 | "injectionSelector": "L:meta.property-list.css -comment, meta.property-list.scss -comment", 5 | "name": "TailwindCSS", 6 | "patterns": [ 7 | { 8 | "name": "meta.at-rule.apply.tailwind", 9 | "begin": "(@)apply\\b", 10 | "beginCaptures": { 11 | "0": { 12 | "name": "keyword.control.at-rule.apply.tailwind" 13 | }, 14 | "1": { 15 | "name": "punctuation.definition.keyword.css" 16 | } 17 | }, 18 | "end": ";|(?=[}])", 19 | "endCaptures": { 20 | "0": { 21 | "name": "punctuation.terminator.apply.tailwind" 22 | } 23 | }, 24 | "patterns": [ 25 | { 26 | "include": "source.css#comment-block" 27 | }, 28 | { 29 | "match": "!\\s*important(?![\\w-])", 30 | "name": "keyword.other.important.css" 31 | }, 32 | { 33 | "match": "[^\\s;]+?", 34 | "name": "entity.other.attribute-name.class.css" 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /packages/vscode-tailwindcss/syntaxes/screen-fn.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "tailwindcss.screen-fn.injection", 3 | "fileTypes": [], 4 | "injectionSelector": "L:meta.property-value.css -comment, meta.at-rule.media.header.css -comment", 5 | "name": "TailwindCSS", 6 | "patterns": [ 7 | { 8 | "begin": "(?i)(? boolean 5 | } 6 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | let content: string 3 | export default content 4 | } 5 | --------------------------------------------------------------------------------