From e9ac79e714e5a43c86865cb86bfa19a29848660f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Feb 2025 11:11:36 -0500 Subject: [PATCH 001/108] Update third party software notices --- .../ThirdPartyNotices.txt | 649 ++++++++++-------- 1 file changed, 362 insertions(+), 287 deletions(-) diff --git a/packages/tailwindcss-language-server/ThirdPartyNotices.txt b/packages/tailwindcss-language-server/ThirdPartyNotices.txt index 26d8efa8..ee6b7439 100644 --- a/packages/tailwindcss-language-server/ThirdPartyNotices.txt +++ b/packages/tailwindcss-language-server/ThirdPartyNotices.txt @@ -1,3 +1,78 @@ +@csstools/css-parser-algorithms@2.1.1 + +The MIT License (MIT) + +Copyright 2022 Romain Menke, Antonio Laguna + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +================================================================================ + +@csstools/css-tokenizer@2.1.1 + +The MIT License (MIT) + +Copyright 2022 Romain Menke, Antonio Laguna + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +================================================================================ + +@csstools/media-query-list-parser@2.0.4 + +The MIT License (MIT) + +Copyright 2022 Romain Menke, Antonio Laguna + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +================================================================================ + @parcel/watcher@2.0.3 MIT License @@ -343,7 +418,33 @@ SOFTWARE. ================================================================================ -chokidar@3.5.1 +braces@3.0.3 + +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +================================================================================ + +chokidar@3.6.0 The MIT License (MIT) @@ -382,29 +483,28 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ================================================================================ -culori@0.20.1 - -MIT License +css.escape@1.5.1 -Copyright (c) 2018 Dan Burzo +Copyright Mathias Bynens -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: +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: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -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. +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. ================================================================================ @@ -507,6 +607,20 @@ THE SOFTWARE. ================================================================================ +detect-indent@6.0.0 + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +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: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +================================================================================ + dlv@1.1.3 # `dlv(obj, keypath)` [![NPM](https://img.shields.io/npm/v/dlv.svg)](https://npmjs.com/package/dlv) [![Build](https://travis-ci.org/developit/dlv.svg?branch=master)](https://travis-ci.org/developit/dlv) @@ -588,7 +702,7 @@ delve(obj, undefined, 'foo') === 'foo'; ================================================================================ -dset@3.1.2 +dset@3.1.4 The MIT License (MIT) @@ -614,31 +728,6 @@ THE SOFTWARE. ================================================================================ -enhanced-resolve@5.15.0 - -Copyright JS Foundation and other contributors - -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: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -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. - -================================================================================ - fast-glob@3.2.4 The MIT License (MIT) @@ -679,11 +768,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ================================================================================ -is-builtin-module@3.2.1 +klona@2.0.4 MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) Luke Edwards (lukeed.com) 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: @@ -693,37 +782,61 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ================================================================================ -klona@2.0.4 - -MIT License +line-column@1.0.2 -Copyright (c) Luke Edwards (lukeed.com) +Copyright (c) 2016 IRIDE Monad -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: +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: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -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. +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. ================================================================================ -minimatch@5.1.4 +moo@0.5.1 -The ISC License +BSD 3-Clause License -Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors +Copyright (c) 2017, Tim Radvan (tjvr) +All rights reserved. -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ @@ -819,7 +932,34 @@ OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -postcss@8.3.9 +postcss-value-parser@4.2.0 + +Copyright (c) Bogdan Chadkin + +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: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +================================================================================ + +postcss@8.4.31 The MIT License (MIT) @@ -870,15 +1010,89 @@ SOFTWARE. ================================================================================ -stack-trace@0.0.10 +semver@7.7.1 -Copyright (c) 2011 Felix Geisendörfer (felix@debuggable.com) +The ISC License - 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 +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +================================================================================ + +sift-string@0.0.2 + +# sift + + Fast String Distance (SIFT) Algorithm. + +[![NPM](https://nodei.co/npm/sift-string.png)](https://nodei.co/npm/sift-string/) + +[![Dependency Status](https://david-dm.org/timoxley/sift.png)](https://david-dm.org/timoxley/sift) + +## Installation + +#### Browserify/Node + + $ npm install sift-string + + +#### Component + + $ component install timoxley/sift + +## Demo + +[Demo](http://timoxley.github.com/sift/examples/spellcheck/) + +or if you want to check it out locally: + +```bash +# run only once to install npm dev dependencies +npm install . +# this will install && build the components and open the demo web page +npm run c-demo +``` + +## API + +### sift(string, string) + +Return 'distance' between two strings. + +## TODO + +* Dictionary Helper supply it emits suggestions. + +## Credit + +Code extracted from [MailCheck](https://github.com/kicksend/mailcheck) + +## License + + MIT + +================================================================================ + +stack-trace@0.0.10 + +Copyright (c) 2011 Felix Geisendörfer (felix@debuggable.com) + + 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: The above copyright notice and this permission notice shall be included in @@ -894,7 +1108,34 @@ Copyright (c) 2011 Felix Geisendörfer (felix@debuggable.com) ================================================================================ -tailwindcss@3.3.0 +stringify-object@3.3.0 + +Copyright (c) 2015, Yeoman team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +================================================================================ + +tailwindcss@3.4.17 MIT License @@ -920,7 +1161,33 @@ SOFTWARE. ================================================================================ -vscode-css-languageservice@5.4.1 +tmp-cache@1.1.0 + +The MIT License (MIT) + +Copyright (c) Luke Edwards (lukeed.com) + +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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +================================================================================ + +vscode-css-languageservice@6.2.9 The MIT License (MIT) @@ -946,7 +1213,7 @@ SOFTWARE. ================================================================================ -vscode-languageserver-textdocument@1.0.7 +vscode-jsonrpc@8.2.0 Copyright (c) Microsoft Corporation @@ -962,7 +1229,7 @@ THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ================================================================================ -vscode-languageserver@8.0.2 +vscode-languageclient@8.1.0 Copyright (c) Microsoft Corporation @@ -978,238 +1245,46 @@ THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ================================================================================ -vscode-uri@3.0.2 - -The MIT License (MIT) - -Copyright (c) Microsoft - -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: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -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. +vscode-languageserver-textdocument@1.0.11 -================================================================================ - -css.escape@1.5.1 - -Copyright Mathias Bynens - -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: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -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. - -================================================================================ +Copyright (c) Microsoft Corporation -detect-indent@6.0.0 +All rights reserved. MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) - 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -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. - -================================================================================ - -line-column@1.0.2 - -Copyright (c) 2016 IRIDE Monad - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. +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. ================================================================================ -moo@0.5.1 +vscode-languageserver@8.1.0 -BSD 3-Clause License +Copyright (c) Microsoft Corporation -Copyright (c) 2017, Tim Radvan (tjvr) All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -================================================================================ - -semver@7.3.7 - -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -================================================================================ - -sift-string@0.0.2 - -# sift - - Fast String Distance (SIFT) Algorithm. - -[![NPM](https://nodei.co/npm/sift-string.png)](https://nodei.co/npm/sift-string/) - -[![Dependency Status](https://david-dm.org/timoxley/sift.png)](https://david-dm.org/timoxley/sift) - -## Installation - -#### Browserify/Node - - $ npm install sift-string - - -#### Component - - $ component install timoxley/sift - -## Demo - -[Demo](http://timoxley.github.com/sift/examples/spellcheck/) - -or if you want to check it out locally: - -```bash -# run only once to install npm dev dependencies -npm install . -# this will install && build the components and open the demo web page -npm run c-demo -``` - -## API - -### sift(string, string) - -Return 'distance' between two strings. - -## TODO - -* Dictionary Helper supply it emits suggestions. - -## Credit - -Code extracted from [MailCheck](https://github.com/kicksend/mailcheck) - -## License - - MIT - -================================================================================ - -stringify-object@3.3.0 - -Copyright (c) 2015, Yeoman team -All rights reserved. +MIT License -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +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: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +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. ================================================================================ -tmp-cache@1.1.0 +vscode-uri@3.0.2 The MIT License (MIT) -Copyright (c) Luke Edwards (lukeed.com) +Copyright (c) Microsoft -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: +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: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -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. +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. \ No newline at end of file From 829b1bf680453b813c173953c8dbc5230e18be72 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Feb 2025 11:14:11 -0500 Subject: [PATCH 002/108] 0.14.7 --- packages/tailwindcss-language-server/package.json | 2 +- packages/tailwindcss-language-service/package.json | 2 +- packages/vscode-tailwindcss/CHANGELOG.md | 4 ++++ packages/vscode-tailwindcss/package.json | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index f8736d19..93b2ea93 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-server", - "version": "0.14.6", + "version": "0.14.7", "description": "Tailwind CSS Language Server", "license": "MIT", "repository": { diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index d9b2011d..39bea0b3 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-service", - "version": "0.14.6", + "version": "0.14.7", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index be197250..9889da1e 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,6 +2,10 @@ ## Prerelease +- Nothing yet! + +# 0.14.7 + - LSP: Declare capability for handling workspace folder change notifications ([#1223](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1223)) - Don't throw when resolving paths containing a `#` character ([#1225](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1225)) - Show `@theme` in symbol list in CSS language mode ([#1227](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1227)) diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 02cd21b7..cdeadb3c 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "vscode-tailwindcss", - "version": "0.14.6", + "version": "0.14.7", "displayName": "Tailwind CSS IntelliSense", "description": "Intelligent Tailwind CSS tooling for VS Code", "author": "Brad Cornes ", From 98b64b72a9d2cf0f321b47887162d1dcc32d66d3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Feb 2025 11:22:52 -0500 Subject: [PATCH 003/108] Fix tests on earlier Node versions --- packages/tailwindcss-language-server/tests/utils/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts index e02a8393..f7ee6e94 100644 --- a/packages/tailwindcss-language-server/tests/utils/client.ts +++ b/packages/tailwindcss-language-server/tests/utils/client.ts @@ -45,7 +45,6 @@ import { clearLanguageBoundariesCache } from '@tailwindcss/language-service/src/ import { DefaultMap } from '../../src/util/default-map' import { connect, ConnectOptions } from './connection' import type { DeepPartial } from './types' -import { styleText } from 'node:util' export interface DocumentDescriptor { /** @@ -237,7 +236,8 @@ export interface ClientWorkspace { function trace(msg: string, ...args: any[]) { console.log( - `${styleText(['bold', 'blue', 'inverse'], ' TEST ')} ${styleText('dim', msg)}`, + // `${styleText(['bold', 'blue', 'inverse'], ' TEST ')} ${styleText('dim', msg)}`, + ` TEST ${msg}`, ...args, ) } From 83010327316223eb596fae66e4909a354e7e54b4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 25 Feb 2025 11:28:47 -0500 Subject: [PATCH 004/108] Disable some concurrent tests The tests are now running _too_ fast and are revealing race conditions in the server --- .../tailwindcss-language-server/tests/hover/hover.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index 63c2e32c..5c8bcb7f 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -6,7 +6,7 @@ withFixture('basic', (c) => { name, { text, lang, position, exact = false, expected, expectedRange, settings }, ) { - test.concurrent(name, async ({ expect }) => { + test(name, async ({ expect }) => { let textDocument = await c.openDocument({ text, lang, settings }) let res = await c.sendRequest('textDocument/hover', { textDocument, @@ -180,7 +180,7 @@ withFixture('v4/basic', (c) => { name, { text, exact = false, lang, position, expected, expectedRange, settings }, ) { - test.concurrent(name, async ({ expect }) => { + test(name, async ({ expect }) => { let textDocument = await c.openDocument({ text, lang, settings }) let res = await c.sendRequest('textDocument/hover', { textDocument, @@ -314,7 +314,7 @@ withFixture('v4/basic', (c) => { withFixture('v4/css-loading-js', (c) => { async function testHover(name, { text, lang, position, expected, expectedRange, settings }) { - test.concurrent(name, async ({ expect }) => { + test(name, async ({ expect }) => { let textDocument = await c.openDocument({ text, lang, settings }) let res = await c.sendRequest('textDocument/hover', { textDocument, @@ -398,7 +398,7 @@ withFixture('v4/css-loading-js', (c) => { withFixture('v4/path-mappings', (c) => { async function testHover(name, { text, lang, position, expected, expectedRange, settings }) { - test.concurrent(name, async ({ expect }) => { + test(name, async ({ expect }) => { let textDocument = await c.openDocument({ text, lang, settings }) let res = await c.sendRequest('textDocument/hover', { textDocument, From eabd07e405ccdd1c9bf3e074ffb45ba917068f50 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 26 Feb 2025 12:52:07 -0500 Subject: [PATCH 005/108] Fix issue when require()-ing packages that resolve to paths containing `#` (#1235) Fixes #1221 (again, for real this time, I hope) --- .../src/testing/index.ts | 20 ++++- .../src/util/resolveFrom.ts | 12 +++ .../tests/env/v4.test.js | 88 ++++++++++++++++++- packages/vscode-tailwindcss/CHANGELOG.md | 2 +- 4 files changed, 119 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/src/testing/index.ts b/packages/tailwindcss-language-server/src/testing/index.ts index 2435ca0f..21296fc7 100644 --- a/packages/tailwindcss-language-server/src/testing/index.ts +++ b/packages/tailwindcss-language-server/src/testing/index.ts @@ -11,12 +11,13 @@ export interface TestUtils { export interface Storage { /** A list of files and their content */ - [filePath: string]: string | Uint8Array + [filePath: string]: string | Uint8Array | { [IS_A_SYMLINK]: true; filepath: string } } export interface TestConfig { name: string fs?: Storage + debug?: boolean prepare?(utils: TestUtils): Promise handle(utils: TestUtils & Extras): void | Promise @@ -56,6 +57,8 @@ async function setup(config: TestConfig): Promise { if (path.sep === '\\') return + if (config.debug) return + // Remove the directory on *nix systems. Recursive removal on Windows will // randomly fail b/c its slow and buggy. await fs.rm(doneDir, { recursive: true }) @@ -66,6 +69,14 @@ async function setup(config: TestConfig): Promise { } } +const IS_A_SYMLINK = Symbol('is-a-symlink') +export const symlinkTo = function (filepath: string) { + return { + [IS_A_SYMLINK]: true as const, + filepath, + } +} + async function prepareFileSystem(base: string, storage: Storage) { // Create a temporary directory to store the test files await fs.mkdir(base, { recursive: true }) @@ -74,6 +85,13 @@ async function prepareFileSystem(base: string, storage: Storage) { for (let [filepath, content] of Object.entries(storage)) { let fullPath = path.resolve(base, filepath) await fs.mkdir(path.dirname(fullPath), { recursive: true }) + + if (typeof content === 'object' && IS_A_SYMLINK in content) { + let target = path.resolve(base, content.filepath) + await fs.symlink(target, fullPath) + continue + } + await fs.writeFile(fullPath, content, { encoding: 'utf-8' }) } } diff --git a/packages/tailwindcss-language-server/src/util/resolveFrom.ts b/packages/tailwindcss-language-server/src/util/resolveFrom.ts index 2b62bbeb..c4ef1a15 100644 --- a/packages/tailwindcss-language-server/src/util/resolveFrom.ts +++ b/packages/tailwindcss-language-server/src/util/resolveFrom.ts @@ -56,5 +56,17 @@ export function resolveFrom(from?: string, id?: string): string { let result = resolver.resolveSync({}, from, id) if (result === false) throw Error() + + // The `enhanced-resolve` package supports resolving paths with fragment + // identifiers. For example, it can resolve `foo/bar#baz` to `foo/bar.js` + // However, it's also possible that a path contains a `#` character as part + // of the path itself. For example, `foo#bar` might point to a file named + // `foo#bar.js`. The resolver distinguishes between these two cases by + // escaping the `#` character with a NUL byte when it's part of the path. + // + // Since the real path doesn't actually contain NUL bytes, we need to remove + // them to get the correct path otherwise readFileSync will throw an error. + result = result.replace(/\0(.)/g, '$1') + return result } diff --git a/packages/tailwindcss-language-server/tests/env/v4.test.js b/packages/tailwindcss-language-server/tests/env/v4.test.js index 1ae5caf4..6e5aa34c 100644 --- a/packages/tailwindcss-language-server/tests/env/v4.test.js +++ b/packages/tailwindcss-language-server/tests/env/v4.test.js @@ -1,7 +1,7 @@ // @ts-check import { expect } from 'vitest' -import { css, defineTest, html, js, json } from '../../src/testing' +import { css, defineTest, html, js, json, symlinkTo } from '../../src/testing' import dedent from 'dedent' import { createClient } from '../utils/client' @@ -666,3 +666,89 @@ defineTest({ }) }, }) + +defineTest({ + // This test *always* passes inside Vitest because our custom version of + // `Module._resolveFilename` is not called. Our custom implementation is + // using enhanced-resolve under the hood which is affected by the `#` + // character issue being considered a fragment identifier. + // + // This most commonly happens when dealing with PNPM packages that point + // to a specific commit hash of a git repository. + // + // To simulate this, we need to: + // - Add a local package to package.json + // - Symlink that local package to a directory with `#` in the name + // - Then run the test in a separate process (`spawn` mode) + // + // We can't use `file:./a#b` because NPM considers `#` to be a fragment + // identifier and will not resolve the path the way we need it to. + name: 'v3: require() works when path is resolved to contain a `#`', + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "3.4.17", + "some-pkg": "file:./packages/some-pkg" + } + } + `, + 'tailwind.config.js': js` + module.exports = { + presets: [require('some-pkg/config/tailwind.config.js').default] + } + `, + 'packages/some-pkg': symlinkTo('packages/some-pkg#c3f1e'), + 'packages/some-pkg#c3f1e/package.json': json` + { + "name": "some-pkg", + "version": "1.0.0", + "main": "index.js" + } + `, + 'packages/some-pkg#c3f1e/config/tailwind.config.js': js` + export default { + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.example': { + color: 'red', + }, + }) + } + ] + } + `, + }, + prepare: async ({ root }) => ({ + client: await createClient({ + root, + mode: 'spawn', + }), + }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let hover = await document.hover({ line: 0, character: 13 }) + + expect(hover).toEqual({ + contents: { + language: 'css', + value: dedent` + .example { + color: red; + } + `, + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 19 }, + }, + }) + }, +}) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 9889da1e..9da256e0 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,7 @@ ## Prerelease -- Nothing yet! +- Don't throw when requiring() packages that resolve to a path containing a `#` character ([#1235](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1235)) # 0.14.7 From 4cb548e60519036a1328d989ab1ded266bb4abf9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 26 Feb 2025 12:52:55 -0500 Subject: [PATCH 006/108] Fix syntax error when resetting multi-word theme key namespaces (#1237) Fixes #1236 This PR fixes an issue where we'd show a syntax error for multi-word namespace resets like `--font-weight-*: initial;`: ```css @theme { --font-*: initial; /* this one is fine */ --font-weight-*: initial; /* this one shows a syntax error */ } ``` Screenshot 2025-02-26 at 12 16 38 Now both work as expected: Screenshot 2025-02-26 at 12 17 09 --- .../src/language/rewriting.test.ts | 16 ++++++++++++---- .../src/language/rewriting.ts | 2 +- .../tests/css/css-server.test.ts | 3 ++- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss-language-server/src/language/rewriting.test.ts b/packages/tailwindcss-language-server/src/language/rewriting.test.ts index 33596a2e..d3874418 100644 --- a/packages/tailwindcss-language-server/src/language/rewriting.test.ts +++ b/packages/tailwindcss-language-server/src/language/rewriting.test.ts @@ -69,20 +69,28 @@ test('@theme', () => { let input = [ // '@theme {', - ' color: red;', + ' --color: red;', + ' --font-*: initial;', + ' --font-weight-*: initial;', '}', '@theme inline reference static default {', - ' color: red;', + ' --color: red;', + ' --font-*: initial;', + ' --font-weight-*: initial;', '}', ] let output = [ // '.placeholder {', // wrong - ' color: red;', + ' --color: red;', + ' --font-_: initial;', + ' --font-weight-_: initial;', '}', '.placeholder {', // wrong - ' color: red;', + ' --color: red;', + ' --font-_: initial;', + ' --font-weight-_: initial;', '}', ] diff --git a/packages/tailwindcss-language-server/src/language/rewriting.ts b/packages/tailwindcss-language-server/src/language/rewriting.ts index 4dc7a429..4a983ad1 100644 --- a/packages/tailwindcss-language-server/src/language/rewriting.ts +++ b/packages/tailwindcss-language-server/src/language/rewriting.ts @@ -74,7 +74,7 @@ export function rewriteCss(css: string) { }) // Replace `--some-var-*` with `--some-var-_` - css = css.replace(/--([a-zA-Z0-9]+)-[*]/g, '--$1_') + css = css.replace(/--([a-zA-Z0-9-]+)-[*]/g, '--$1-_') return css } diff --git a/packages/tailwindcss-language-server/tests/css/css-server.test.ts b/packages/tailwindcss-language-server/tests/css/css-server.test.ts index 02146ccc..1ee3246b 100644 --- a/packages/tailwindcss-language-server/tests/css/css-server.test.ts +++ b/packages/tailwindcss-language-server/tests/css/css-server.test.ts @@ -219,6 +219,7 @@ defineTest({ @theme { --color-primary: #333; --leading-*: initial; + --font-weight-*: initial; } `, }) @@ -235,7 +236,7 @@ defineTest({ uri: '{workspace:default}/file-1.css', range: { start: { line: 1, character: 0 }, - end: { line: 4, character: 1 }, + end: { line: 5, character: 1 }, }, }, }, diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 9da256e0..2f2ce33c 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -3,6 +3,7 @@ ## Prerelease - Don't throw when requiring() packages that resolve to a path containing a `#` character ([#1235](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1235)) +- Fix syntax error when resetting multi-word theme key namespaces ([#1237](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1237)) # 0.14.7 From 4d9b7366301bb296467a75d32d00355744b477c7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 26 Feb 2025 13:08:00 -0500 Subject: [PATCH 007/108] 0.14.8 --- packages/tailwindcss-language-server/package.json | 2 +- packages/tailwindcss-language-service/package.json | 2 +- packages/vscode-tailwindcss/CHANGELOG.md | 4 ++++ packages/vscode-tailwindcss/package.json | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index 93b2ea93..16d4f73b 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-server", - "version": "0.14.7", + "version": "0.14.8", "description": "Tailwind CSS Language Server", "license": "MIT", "repository": { diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 39bea0b3..0cbdde25 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-service", - "version": "0.14.7", + "version": "0.14.8", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 2f2ce33c..18b39e02 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,6 +2,10 @@ ## Prerelease +- Nothing yet! + +# 0.14.8 + - Don't throw when requiring() packages that resolve to a path containing a `#` character ([#1235](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1235)) - Fix syntax error when resetting multi-word theme key namespaces ([#1237](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1237)) diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index cdeadb3c..08906fbd 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "vscode-tailwindcss", - "version": "0.14.7", + "version": "0.14.8", "displayName": "Tailwind CSS IntelliSense", "description": "Intelligent Tailwind CSS tooling for VS Code", "author": "Brad Cornes ", From 34660f8026baa79d2fe10e1ee7db826be66e1139 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 27 Feb 2025 14:59:54 +0100 Subject: [PATCH 008/108] Bring back issue template --- .github/ISSUE_TEMPLATE/bug-report.md | 47 ++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 3 -- 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..3809535c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,47 @@ +--- +name: Bug report +about: If you think something is broken with Tailwind CSS IntelliSense itself, create a bug report. +title: '' +labels: '' +assignees: '' +--- + +**What version of VS Code are you using?** + +For example: v1.78.2 + +**What version of Tailwind CSS IntelliSense are you using?** + +For example: v0.7.0 + +**What version of Tailwind CSS are you using?** + +For example: v2.0.4 + +**What package manager are you using?** + +For example: npm, yarn + +**What operating system are you using?** + +For example: macOS, Windows + +**Tailwind config** + +```js +// Paste the contents of your Tailwind config file here +``` + +**VS Code settings** + +```json +// Paste your VS Code settings in JSON format here +``` + +**Reproduction URL** + +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. + +**Describe your issue** + +Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1f144920..7c5bf993 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,3 @@ contact_links: - name: Feature Request url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas&title=%5BIntelliSense%5D%20 about: 'Suggest any ideas you have using our discussion forums.' - - name: Bug Report - url: https://github.com/tailwindlabs/tailwindcss-intellisense/issues/new?body=**What%20version%20of%20VS%20Code%20are%20you%20using%3F**%0A%0AFor%20example%3A%20v1.78.2%0A%0A**What%20version%20of%20Tailwind%20CSS%20IntelliSense%20are%20you%20using%3F**%0A%0AFor%20example%3A%20v0.7.0%0A%0A**What%20version%20of%20Tailwind%20CSS%20are%20you%20using%3F**%0A%0AFor%20example%3A%20v2.0.4%0A%0A**What%20package%20manager%20are%20you%20using%3F**%0A%0AFor%20example%3A%20npm%2C%20yarn%0A%0A**What%20operating%20system%20are%20you%20using%3F**%0A%0AFor%20example%3A%20macOS%2C%20Windows%0A%0A**Tailwind%20config**%0A%0A%60%60%60js%0A%2F%2F%20Paste%20the%20contents%20of%20your%20Tailwind%20config%20file%20here%0A%60%60%60%0A%0A**VS%20Code%20settings**%0A%0A%60%60%60json%0A%2F%2F%20Paste%20your%20VS%20Code%20settings%20in%20JSON%20format%20here%0A%60%60%60%0A%0A**Reproduction%20URL**%0A%0AA%20public%20GitHub%20repo%20that%20includes%20a%20minimal%20reproduction%20of%20the%20bug.%20**Please%20do%20not%20link%20to%20your%20actual%20project**%2C%20what%20we%20need%20instead%20is%20a%20_minimal_%20reproduction%20in%20a%20fresh%20project%20without%20any%20unnecessary%20code.%20This%20means%20it%20doesn%27t%20matter%20if%20your%20real%20project%20is%20private%2Fconfidential%2C%20since%20we%20want%20a%20link%20to%20a%20separate%2C%20isolated%20reproduction%20anyways.%0A%0A**Describe%20your%20issue**%0A%0ADescribe%20the%20problem%20you%27re%20seeing%2C%20any%20important%20steps%20to%20reproduce%20and%20what%20behavior%20you%20expect%20instead. - about: If you think something is broken with Tailwind CSS IntelliSense itself, create a bug report. From c8efecac2dd342ff8053b49aa0d0648cbc6bb370 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 27 Feb 2025 11:44:29 -0500 Subject: [PATCH 009/108] v4: Support loading bundled versions of some first-party plugins (#1240) see https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1224#issuecomment-2687637180 This adds support for loading these three plugins in v4 **only when using `@plugin`** if we can't find them: - `@tailwindcss/typography` - `@tailwindcss/forms` - `@tailwindcss/aspect-ratio` This coincides with behavior of the Standalone CLI where these are bundled instead of available as an NPM package. I've additionally logs when trying to import one of these plugins inside a JS file. This does not work right now and may take some effort to support because it will at least require the use of Node's experimental loader's API and potentially additional work as well. --- .../src/util/v4/design-system.ts | 23 +++++++++++ .../src/util/v4/plugins.ts | 5 +++ .../tests/env/v4.test.js | 39 +++++++++++++++++++ packages/vscode-tailwindcss/CHANGELOG.md | 2 +- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss-language-server/src/util/v4/plugins.ts diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index f32305ce..05d2ecd3 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -9,6 +9,7 @@ import { Resolver } from '../../resolver' import { pathToFileURL } from '../../utils' import type { Jiti } from 'jiti/lib/types' import { assets } from './assets' +import { plugins } from './plugins' const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/ const HAS_V4_THEME = /@theme\s*\{/ @@ -58,6 +59,28 @@ function createLoader({ return await jiti.import(url.href, { default: true }) } catch (err) { + // If the request was to load a first-party plugin and we can't resolve it + // locally, then fall back to the built-in plugins that we know about. + if (resourceType === 'plugin' && id in plugins) { + console.log('Loading bundled plugin for: ', id) + return await plugins[id]() + } + + // This checks for an error thrown by enhanced-resolve + if (err && typeof err.details === 'string') { + let details: string = err.details + let pattern = /^resolve '([^']+)'/ + let match = details.match(pattern) + if (match) { + let [_, importee] = match + if (importee in plugins) { + console.log( + `[error] Cannot load '${id}' plugins inside configs or plugins is not currently supported`, + ) + } + } + } + return onError(id, err, resourceType) } } diff --git a/packages/tailwindcss-language-server/src/util/v4/plugins.ts b/packages/tailwindcss-language-server/src/util/v4/plugins.ts new file mode 100644 index 00000000..16efc4a5 --- /dev/null +++ b/packages/tailwindcss-language-server/src/util/v4/plugins.ts @@ -0,0 +1,5 @@ +export const plugins = { + '@tailwindcss/forms': () => import('@tailwindcss/forms').then((m) => m.default), + '@tailwindcss/aspect-ratio': () => import('@tailwindcss/aspect-ratio').then((m) => m.default), + '@tailwindcss/typography': () => import('@tailwindcss/typography').then((m) => m.default), +} diff --git a/packages/tailwindcss-language-server/tests/env/v4.test.js b/packages/tailwindcss-language-server/tests/env/v4.test.js index 6e5aa34c..76de7f15 100644 --- a/packages/tailwindcss-language-server/tests/env/v4.test.js +++ b/packages/tailwindcss-language-server/tests/env/v4.test.js @@ -53,6 +53,45 @@ defineTest({ }, }) +defineTest({ + name: 'v4, no npm, bundled plugins', + fs: { + 'app.css': css` + @import 'tailwindcss'; + @plugin "@tailwindcss/aspect-ratio"; + @plugin "@tailwindcss/forms"; + @plugin "@tailwindcss/typography"; + `, + }, + + // Note this test MUST run in spawn mode because Vitest hooks into import, + // require, etc… already and we need to test that any hooks are working + // without outside interference. + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + + handle: async ({ client }) => { + let doc = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let hover = await doc.hover({ line: 0, character: 13 }) + expect(hover).not.toEqual(null) + + //
+ // ^ + hover = await doc.hover({ line: 0, character: 25 }) + expect(hover).not.toEqual(null) + + //
+ // ^ + hover = await doc.hover({ line: 0, character: 37 }) + expect(hover).not.toEqual(null) + }, +}) + defineTest({ /** * Plugins and configs that import stuff from the `tailwindcss` package do diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 18b39e02..d22e380c 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,7 @@ ## Prerelease -- Nothing yet! +- v4: Support loading bundled versions of some first-party plugins ([#1240](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1240)) # 0.14.8 From 23a03cbfda51a8711da24a92a5d8b4a30ad53a1d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 11 Mar 2025 15:19:16 -0400 Subject: [PATCH 010/108] Cancel initial file search if it takes too long (#1242) Fixes #986 (hopefully) There's a lot of detail about the problem in the linked issue and my response at the bottom. But briefly: - Searches can take way too long; even spinning "forever" for some people - This occurs more often if following symlinks is enabled in VSCode - We can cancel in-flight searches This PR starts the searches and cancels them after ~15s. If the extension hasn't found anything within 15s then it's likely it'll take ~15s any time you open that project and that would've been an absolutely horrible dev experience that we probably would've been notified about. It seems reasonable to set a time limit on the search do determine if we need to start a language server --- packages/vscode-tailwindcss/CHANGELOG.md | 1 + packages/vscode-tailwindcss/src/extension.ts | 35 +++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index d22e380c..01bf3b4a 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -3,6 +3,7 @@ ## Prerelease - v4: Support loading bundled versions of some first-party plugins ([#1240](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1240)) +- Cancel initial file search if it takes too long ([#1242](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1242)) # 0.14.8 diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index c0c7b9e7..a3748616 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -6,6 +6,7 @@ import type { ConfigurationScope, WorkspaceConfiguration, Selection, + CancellationToken, } from 'vscode' import { workspace as Workspace, @@ -16,6 +17,7 @@ import { Position, Range, RelativePattern, + CancellationTokenSource, } from 'vscode' import type { DocumentFilter, @@ -581,18 +583,34 @@ export async function activate(context: ExtensionContext) { return } - if (!(await anyFolderNeedsLanguageServer(Workspace.workspaceFolders ?? []))) { + let source: CancellationTokenSource | null = new CancellationTokenSource() + source.token.onCancellationRequested(() => { + source?.dispose() + source = null + outputChannel.appendLine( + 'Server was not started. Search for Tailwind CSS-related files was taking too long.', + ) + }) + + // Cancel the search after roughly 15 seconds + setTimeout(() => source?.cancel(), 15_000) + + if (!(await anyFolderNeedsLanguageServer(Workspace.workspaceFolders ?? [], source!.token))) { + source?.dispose() return } + source?.dispose() + await bootWorkspaceClient() } async function anyFolderNeedsLanguageServer( folders: readonly WorkspaceFolder[], + token: CancellationToken, ): Promise { for (let folder of folders) { - if (await folderNeedsLanguageServer(folder)) { + if (await folderNeedsLanguageServer(folder, token)) { return true } } @@ -600,7 +618,10 @@ export async function activate(context: ExtensionContext) { return false } - async function folderNeedsLanguageServer(folder: WorkspaceFolder): Promise { + async function folderNeedsLanguageServer( + folder: WorkspaceFolder, + token: CancellationToken, + ): Promise { let settings = Workspace.getConfiguration('tailwindCSS', folder) if (settings.get('experimental.configFile') !== null) { return true @@ -616,13 +637,19 @@ export async function activate(context: ExtensionContext) { new RelativePattern(folder, `**/${CONFIG_GLOB}`), exclude, 1, + token, ) for (let file of configFiles) { return true } - let cssFiles = await Workspace.findFiles(new RelativePattern(folder, `**/${CSS_GLOB}`), exclude) + let cssFiles = await Workspace.findFiles( + new RelativePattern(folder, `**/${CSS_GLOB}`), + exclude, + undefined, + token, + ) for (let file of cssFiles) { outputChannel.appendLine(`Checking if ${file.fsPath} may be Tailwind-related…`) From 07da721de845f5262a536c1451f47dcc705ccd71 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Mar 2025 09:30:57 -0400 Subject: [PATCH 011/108] =?UTF-8?q?Don=E2=80=99t=20throw=20when=20client?= =?UTF-8?q?=20is=20missing=20the=20`textDocument`=20capability=20(#1252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1251 --- packages/tailwindcss-language-server/src/tw.ts | 10 +++++----- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index efb12a34..35da9386 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -1024,10 +1024,10 @@ export class TW { function supportsDynamicRegistration(params: InitializeParams): boolean { return ( - params.capabilities.textDocument.hover?.dynamicRegistration && - params.capabilities.textDocument.colorProvider?.dynamicRegistration && - params.capabilities.textDocument.codeAction?.dynamicRegistration && - params.capabilities.textDocument.completion?.dynamicRegistration && - params.capabilities.textDocument.documentLink?.dynamicRegistration + params.capabilities.textDocument?.hover?.dynamicRegistration && + params.capabilities.textDocument?.colorProvider?.dynamicRegistration && + params.capabilities.textDocument?.codeAction?.dynamicRegistration && + params.capabilities.textDocument?.completion?.dynamicRegistration && + params.capabilities.textDocument?.documentLink?.dynamicRegistration ) } diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 01bf3b4a..cd3f2170 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -4,6 +4,7 @@ - v4: Support loading bundled versions of some first-party plugins ([#1240](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1240)) - Cancel initial file search if it takes too long ([#1242](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1242)) +- LSP: Don’t throw when the client does not provide `textDocument` in capabilities ([#1252](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1252)) # 0.14.8 From 0cddb23899846ab485506dbb77f86f478b757642 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Mar 2025 09:33:02 -0400 Subject: [PATCH 012/108] Rewrite all occurrences of `*` in a CSS variable name (#1256) Fixes #1248 --- .../src/language/css-server.ts | 2 +- .../src/language/rewriting.test.ts | 28 ++++++++++++++----- .../src/language/rewriting.ts | 26 ++++++++++------- .../tests/css/css-server.test.ts | 2 +- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/tailwindcss-language-server/src/language/css-server.ts b/packages/tailwindcss-language-server/src/language/css-server.ts index a146cea4..a43910b2 100644 --- a/packages/tailwindcss-language-server/src/language/css-server.ts +++ b/packages/tailwindcss-language-server/src/language/css-server.ts @@ -225,7 +225,7 @@ export class CssServer { if (match) { symbol.name = `${match[1]} ${match[2]?.trim() ?? match[3]?.trim()}` } - } else if (symbol.name === `.placeholder`) { + } else if (/^\._+$/.test(symbol.name)) { let doc = documents.get(symbol.location.uri) let text = doc.getText(symbol.location.range) let match = text.trim().match(/^(@[^\s]+)(?:([^{]+)[{]|([^;{]+);)/) diff --git a/packages/tailwindcss-language-server/src/language/rewriting.test.ts b/packages/tailwindcss-language-server/src/language/rewriting.test.ts index d3874418..23249932 100644 --- a/packages/tailwindcss-language-server/src/language/rewriting.test.ts +++ b/packages/tailwindcss-language-server/src/language/rewriting.test.ts @@ -50,16 +50,22 @@ test('@utility', () => { '@utility foo-* {', ' color: red;', '}', + '@utility bar-* {', + ' color: --value(--font-*-line-height);', + '}', ] let output = [ // - '.placeholder {', // wrong + '._______ {', ' color: red;', '}', - '.placeholder {', // wrong + '._______ {', ' color: red;', '}', + '._______ {', + ' color: --value(--font-_-line-height);', + '}', ] expect(rewriteCss(input.join('\n'))).toBe(output.join('\n')) @@ -70,11 +76,15 @@ test('@theme', () => { // '@theme {', ' --color: red;', + ' --*: initial;', + ' --text*: initial;', ' --font-*: initial;', ' --font-weight-*: initial;', '}', '@theme inline reference static default {', ' --color: red;', + ' --*: initial;', + ' --text*: initial;', ' --font-*: initial;', ' --font-weight-*: initial;', '}', @@ -82,13 +92,17 @@ test('@theme', () => { let output = [ // - '.placeholder {', // wrong + '._____ {', ' --color: red;', + ' --_: initial;', + ' --text_: initial;', ' --font-_: initial;', ' --font-weight-_: initial;', '}', - '.placeholder {', // wrong + '._____ {', ' --color: red;', + ' --_: initial;', + ' --text_: initial;', ' --font-_: initial;', ' --font-weight-_: initial;', '}', @@ -110,8 +124,8 @@ test('@custom-variant', () => { let output = [ // - '@media (℘) {}', // wrong - '.placeholder {', // wrong + '@media(℘) {}', + '.______________ {', ' &:hover {', ' @slot;', ' }', @@ -133,7 +147,7 @@ test('@variant', () => { let output = [ // - '.placeholder {', // wrong + '._______ {', ' &:hover {', ' @slot;', ' }', diff --git a/packages/tailwindcss-language-server/src/language/rewriting.ts b/packages/tailwindcss-language-server/src/language/rewriting.ts index 4a983ad1..1d660709 100644 --- a/packages/tailwindcss-language-server/src/language/rewriting.ts +++ b/packages/tailwindcss-language-server/src/language/rewriting.ts @@ -14,9 +14,10 @@ function replaceWithAtRule(delta = 0) { } function replaceWithStyleRule(delta = 0) { - return (_match: string, p1: string) => { + return (_match: string, name: string, p1: string) => { + let className = '_'.repeat(name.length) let spaces = ' '.repeat(p1.length + delta) - return `.placeholder${spaces}{` + return `.${className}${spaces}{` } } @@ -36,16 +37,16 @@ export function rewriteCss(css: string) { css = css.replace(/@screen(\s+[^{]+){/g, replaceWithAtRule(-2)) css = css.replace(/@variants(\s+[^{]+){/g, replaceWithAtRule()) css = css.replace(/@responsive(\s*){/g, replaceWithAtRule()) - css = css.replace(/@utility(\s+[^{]+){/g, replaceWithStyleRule()) - css = css.replace(/@theme(\s+[^{]*){/g, replaceWithStyleRule()) + css = css.replace(/@(utility)(\s+[^{]+){/g, replaceWithStyleRule()) + css = css.replace(/@(theme)(\s+[^{]*){/g, replaceWithStyleRule()) - css = css.replace(/@custom-variant(\s+[^;{]+);/g, (match: string) => { - let spaces = ' '.repeat(match.length - 11) - return `@media (${MEDIA_MARKER})${spaces}{}` + css = css.replace(/@(custom-variant)(\s+[^;{]+);/g, (match: string, name: string) => { + let spaces = ' '.repeat(match.length - name.length + 3) + return `@media(${MEDIA_MARKER})${spaces}{}` }) - css = css.replace(/@custom-variant(\s+[^{]+){/g, replaceWithStyleRule()) - css = css.replace(/@variant(\s+[^{]+){/g, replaceWithStyleRule()) + css = css.replace(/@(custom-variant)(\s+[^{]+){/g, replaceWithStyleRule()) + css = css.replace(/@(variant)(\s+[^{]+){/g, replaceWithStyleRule()) css = css.replace(/@layer(\s+[^{]{2,}){/g, replaceWithAtRule(-3)) css = css.replace(/@reference\s*([^;]{2,})/g, '@import $1') @@ -73,8 +74,13 @@ export function rewriteCss(css: string) { return match.replace(/[*]/g, '_') }) + // Replace `--*` with `--_` + // Replace `--some-var*` with `--some-var_` // Replace `--some-var-*` with `--some-var-_` - css = css.replace(/--([a-zA-Z0-9-]+)-[*]/g, '--$1-_') + // Replace `--text-*-line-height` with `--text-_-line-height` + css = css.replace(/--([a-zA-Z0-9-*]*)/g, (match) => { + return match.replace(/[*]/g, '_') + }) return css } diff --git a/packages/tailwindcss-language-server/tests/css/css-server.test.ts b/packages/tailwindcss-language-server/tests/css/css-server.test.ts index 1ee3246b..e8970e08 100644 --- a/packages/tailwindcss-language-server/tests/css/css-server.test.ts +++ b/packages/tailwindcss-language-server/tests/css/css-server.test.ts @@ -38,7 +38,7 @@ defineTest({ uri: '{workspace:default}/file-1.css', range: { start: { line: 1, character: 0 }, - end: { line: 1, character: 31 }, + end: { line: 1, character: 30 }, }, }, }, diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index cd3f2170..f5982607 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -5,6 +5,7 @@ - v4: Support loading bundled versions of some first-party plugins ([#1240](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1240)) - Cancel initial file search if it takes too long ([#1242](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1242)) - LSP: Don’t throw when the client does not provide `textDocument` in capabilities ([#1252](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1252)) +- v4: Allow `*` anywhere in a CSS variable name ([#1256](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1256)) # 0.14.8 From 7a4f8d5b64bd79f7be5a10de46817a2e48f36206 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Mar 2025 10:46:58 -0400 Subject: [PATCH 013/108] 0.14.9 --- packages/tailwindcss-language-server/package.json | 2 +- packages/tailwindcss-language-service/package.json | 2 +- packages/vscode-tailwindcss/CHANGELOG.md | 4 ++++ packages/vscode-tailwindcss/package.json | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index 16d4f73b..5ed431fb 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-server", - "version": "0.14.8", + "version": "0.14.9", "description": "Tailwind CSS Language Server", "license": "MIT", "repository": { diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 0cbdde25..f1047879 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-service", - "version": "0.14.8", + "version": "0.14.9", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index f5982607..ca4255d7 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,6 +2,10 @@ ## Prerelease +- Nothing yet! + +# 0.14.9 + - v4: Support loading bundled versions of some first-party plugins ([#1240](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1240)) - Cancel initial file search if it takes too long ([#1242](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1242)) - LSP: Don’t throw when the client does not provide `textDocument` in capabilities ([#1252](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1252)) diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 08906fbd..3b5fd76a 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "vscode-tailwindcss", - "version": "0.14.8", + "version": "0.14.9", "displayName": "Tailwind CSS IntelliSense", "description": "Intelligent Tailwind CSS tooling for VS Code", "author": "Brad Cornes ", From b4b3a2a0351a2135e1bd34df7a22aae53288e540 Mon Sep 17 00:00:00 2001 From: Laurynas Grigutis <34269850+LaurynasGr@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:54:20 +0200 Subject: [PATCH 014/108] Add `tailwindCSS.classFunctions` setting (#1258) This PR adds `tailwindCSS.classFunctions` option to the settings to add simple and performant class completions, hover previews, linting etc. for such cases: ```ts const classes = cn( 'pointer-events-auto relative flex bg-red-500', 'items-center justify-between overflow-hidden', 'md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm pl-md py-xs pr-xs gap-sm w-full', 'data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]', Date.now() > 15000 ? 'text-red-200' : 'text-red-700', 'data-[swipe=move]:transition-none', ) ``` ![image](https://github.com/user-attachments/assets/eb5728af-6412-4323-b14c-893472f2e897) ```ts const variants = cva( cn( 'pointer-events-auto relative flex bg-green-500', 'md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm pl-md py-xs pr-xs gap-sm w-full', 'md:h-[calc(100%-2rem)]', 'bg-red-700', ), { variants: { mobile: { default: 'bottom-0 left-0', fullScreen: ` inset-0 md:h-[calc(100%-2rem)] rounded-none bg-blue-700 `, }, }, defaultVariants: { mobile: 'default', }, }, ) ``` ![image](https://github.com/user-attachments/assets/47025c28-50bc-4aa5-874c-06434835141b) ```ts const tagged = cn` pointer-events-auto relative flex bg-red-500 items-center justify-between overflow-hidden md:min-w-[20rem] md:max-w-[37.5rem] md:py-sm pl-md py-xs pr-xs gap-sm w-full data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] md:h-[calc(100%-2rem)] text-green-700 data-[swipe=move]:transition-none ` ``` ![image](https://github.com/user-attachments/assets/0d112788-dd4f-48dc-aa01-0407b6c6b119) --------- Co-authored-by: Laurynas Grigutis Co-authored-by: Jordan Pittman --- .../tailwindcss-language-server/src/config.ts | 41 +- .../tests/utils/client.ts | 2 +- .../tests/utils/configuration.ts | 41 +- .../tests/utils/types.ts | 9 - .../tailwindcss-language-service/package.json | 2 + .../scripts/build.mjs | 2 +- .../scripts/tsconfig.build.json | 4 + .../src/completionProvider.ts | 23 +- .../tailwindcss-language-service/src/types.ts | 9 + .../src/util/find.test.ts | 800 ++++++++++++++++-- .../src/util/find.ts | 143 +++- .../src/util/lexers.ts | 9 + .../src/util/resolveRange.ts | 16 - .../src/util/state.ts | 73 +- .../tsconfig.json | 2 +- packages/vscode-tailwindcss/README.md | 20 + packages/vscode-tailwindcss/package.json | 8 + pnpm-lock.yaml | 11 + 18 files changed, 999 insertions(+), 216 deletions(-) delete mode 100644 packages/tailwindcss-language-server/tests/utils/types.ts create mode 100644 packages/tailwindcss-language-service/scripts/tsconfig.build.json create mode 100644 packages/tailwindcss-language-service/src/types.ts delete mode 100644 packages/tailwindcss-language-service/src/util/resolveRange.ts diff --git a/packages/tailwindcss-language-server/src/config.ts b/packages/tailwindcss-language-server/src/config.ts index d8364d06..a5e68f7d 100644 --- a/packages/tailwindcss-language-server/src/config.ts +++ b/packages/tailwindcss-language-server/src/config.ts @@ -1,6 +1,9 @@ import merge from 'deepmerge' import { isObject } from './utils' -import type { Settings } from '@tailwindcss/language-service/src/util/state' +import { + getDefaultTailwindSettings, + type Settings, +} from '@tailwindcss/language-service/src/util/state' import type { Connection } from 'vscode-languageserver' export interface SettingsCache { @@ -8,40 +11,6 @@ export interface SettingsCache { clear(): void } -function getDefaultSettings(): Settings { - return { - editor: { tabSize: 2 }, - tailwindCSS: { - inspectPort: null, - emmetCompletions: false, - classAttributes: ['class', 'className', 'ngClass', 'class:list'], - codeActions: true, - hovers: true, - suggestions: true, - validate: true, - colorDecorators: true, - rootFontSize: 16, - lint: { - cssConflict: 'warning', - invalidApply: 'error', - invalidScreen: 'error', - invalidVariant: 'error', - invalidConfigPath: 'error', - invalidTailwindDirective: 'error', - invalidSourceDirective: 'error', - recommendedVariantOrder: 'warning', - }, - showPixelEquivalents: true, - includeLanguages: {}, - files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] }, - experimental: { - classRegex: [], - configFile: null, - }, - }, - } -} - export function createSettingsCache(connection: Connection): SettingsCache { const cache: Map = new Map() @@ -73,7 +42,7 @@ export function createSettingsCache(connection: Connection): SettingsCache { tailwindCSS = isObject(tailwindCSS) ? tailwindCSS : {} return merge( - getDefaultSettings(), + getDefaultTailwindSettings(), { editor, tailwindCSS }, { arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray }, ) diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts index f7ee6e94..d6d317bb 100644 --- a/packages/tailwindcss-language-server/tests/utils/client.ts +++ b/packages/tailwindcss-language-server/tests/utils/client.ts @@ -44,7 +44,7 @@ import { createConfiguration, Configuration } from './configuration' import { clearLanguageBoundariesCache } from '@tailwindcss/language-service/src/util/getLanguageBoundaries' import { DefaultMap } from '../../src/util/default-map' import { connect, ConnectOptions } from './connection' -import type { DeepPartial } from './types' +import type { DeepPartial } from '@tailwindcss/language-service/src/types' export interface DocumentDescriptor { /** diff --git a/packages/tailwindcss-language-server/tests/utils/configuration.ts b/packages/tailwindcss-language-server/tests/utils/configuration.ts index 2b4d6fbc..8c08a518 100644 --- a/packages/tailwindcss-language-server/tests/utils/configuration.ts +++ b/packages/tailwindcss-language-server/tests/utils/configuration.ts @@ -1,4 +1,7 @@ -import type { Settings } from '@tailwindcss/language-service/src/util/state' +import { + getDefaultTailwindSettings, + type Settings, +} from '@tailwindcss/language-service/src/util/state' import { URI } from 'vscode-uri' import type { DeepPartial } from './types' import { CacheMap } from '../../src/cache-map' @@ -10,41 +13,7 @@ export interface Configuration { } export function createConfiguration(): Configuration { - let defaults: Settings = { - editor: { - tabSize: 2, - }, - tailwindCSS: { - inspectPort: null, - emmetCompletions: false, - includeLanguages: {}, - classAttributes: ['class', 'className', 'ngClass', 'class:list'], - suggestions: true, - hovers: true, - codeActions: true, - validate: true, - showPixelEquivalents: true, - rootFontSize: 16, - colorDecorators: true, - lint: { - cssConflict: 'warning', - invalidApply: 'error', - invalidScreen: 'error', - invalidVariant: 'error', - invalidConfigPath: 'error', - invalidTailwindDirective: 'error', - invalidSourceDirective: 'error', - recommendedVariantOrder: 'warning', - }, - experimental: { - classRegex: [], - configFile: {}, - }, - files: { - exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'], - }, - }, - } + let defaults = getDefaultTailwindSettings() /** * Settings per file or directory URI diff --git a/packages/tailwindcss-language-server/tests/utils/types.ts b/packages/tailwindcss-language-server/tests/utils/types.ts deleted file mode 100644 index 6be62396..00000000 --- a/packages/tailwindcss-language-server/tests/utils/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type DeepPartial = { - [P in keyof T]?: T[P] extends (infer U)[] - ? U[] - : T[P] extends (...args: any) => any - ? T[P] | undefined - : T[P] extends object - ? DeepPartial - : T[P] -} diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index f1047879..d5fddaaa 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -41,9 +41,11 @@ }, "devDependencies": { "@types/css.escape": "^1.5.2", + "@types/dedent": "^0.7.2", "@types/line-column": "^1.0.2", "@types/node": "^18.19.33", "@types/stringify-object": "^4.0.5", + "dedent": "^1.5.3", "esbuild": "^0.25.0", "esbuild-node-externals": "^1.9.0", "minimist": "^1.2.8", diff --git a/packages/tailwindcss-language-service/scripts/build.mjs b/packages/tailwindcss-language-service/scripts/build.mjs index 913debc3..128426be 100644 --- a/packages/tailwindcss-language-service/scripts/build.mjs +++ b/packages/tailwindcss-language-service/scripts/build.mjs @@ -30,7 +30,7 @@ let build = await esbuild.context({ // Call the tsc command to generate the types spawnSync( 'tsc', - ['--emitDeclarationOnly', '--outDir', path.resolve(__dirname, '../dist')], + ['-p', path.resolve(__dirname, './tsconfig.build.json'), '--emitDeclarationOnly', '--outDir', path.resolve(__dirname, '../dist')], { stdio: 'inherit', }, diff --git a/packages/tailwindcss-language-service/scripts/tsconfig.build.json b/packages/tailwindcss-language-service/scripts/tsconfig.build.json new file mode 100644 index 00000000..e80bb38f --- /dev/null +++ b/packages/tailwindcss-language-service/scripts/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "exclude": ["../src/**/*.test.ts"] +} \ No newline at end of file diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index c67eb0f5..fe43bb68 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -15,7 +15,7 @@ import removeMeta from './util/removeMeta' import { formatColor, getColor, getColorFromValue } from './util/color' import { isHtmlContext, isHtmlDoc, isVueDoc } from './util/html' import { isCssContext } from './util/css' -import { findLast, matchClassAttributes } from './util/find' +import { findLast, matchClassAttributes, matchClassFunctions } from './util/find' import { stringifyConfigValue, stringifyCss } from './util/stringify' import { stringifyScreen, Screen } from './util/screens' import isObject from './util/isObject' @@ -45,6 +45,8 @@ import type { ThemeEntry } from './util/v4' import { segment } from './util/segment' import { resolveKnownThemeKeys, resolveKnownThemeNamespaces } from './util/v4/theme-keys' import { SEARCH_RANGE } from './util/constants' +import { getLanguageBoundaries } from './util/getLanguageBoundaries' +import { isWithinRange } from './util/isWithinRange' let isUtil = (className) => Array.isArray(className.__info) @@ -747,6 +749,25 @@ async function provideClassAttributeCompletions( let matches = matchClassAttributes(str, settings.classAttributes) + let boundaries = getLanguageBoundaries(state, document) + + for (let boundary of boundaries ?? []) { + let isJsContext = boundary.type === 'js' || boundary.type === 'jsx' + if (!isJsContext) continue + if (!settings.classFunctions?.length) continue + if (!isWithinRange(position, boundary.range)) continue + + let str = document.getText(boundary.range) + let offset = document.offsetAt(boundary.range.start) + let fnMatches = matchClassFunctions(str, settings.classFunctions) + + fnMatches.forEach((match) => { + if (match.index) match.index += offset + }) + + matches.push(...fnMatches) + } + if (matches.length === 0) { return null } diff --git a/packages/tailwindcss-language-service/src/types.ts b/packages/tailwindcss-language-service/src/types.ts new file mode 100644 index 00000000..c84f0c7a --- /dev/null +++ b/packages/tailwindcss-language-service/src/types.ts @@ -0,0 +1,9 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends ((...args: any) => any) | ReadonlyArray | Date + ? T[P] + : T[P] extends (infer U)[] + ? U[] + : T[P] extends object + ? DeepPartial + : T[P] +} diff --git a/packages/tailwindcss-language-service/src/util/find.test.ts b/packages/tailwindcss-language-service/src/util/find.test.ts index 90dbcfb3..f9f17a96 100644 --- a/packages/tailwindcss-language-service/src/util/find.test.ts +++ b/packages/tailwindcss-language-service/src/util/find.test.ts @@ -1,81 +1,743 @@ -import type { State } from './state' +import { createState, getDefaultTailwindSettings, Settings, type DocumentClassList } from './state' import { test } from 'vitest' import { TextDocument } from 'vscode-languageserver-textdocument' import { findClassListsInHtmlRange } from './find' +import type { DeepPartial } from '../types' +import dedent from 'dedent' -test('test', async ({ expect }) => { - let content = [ - // - '', - ' ', - '', - ].join('\n') - - let doc = TextDocument.create('file://file.astro', 'astro', 1, content) - let state: State = { - blocklist: [], +const js = dedent +const html = dedent + +test('class regex works in astro', async ({ expect }) => { + let file = createDocument({ + name: 'file.astro', + lang: 'astro', + settings: { + tailwindCSS: { + classAttributes: ['class'], + experimental: { + classRegex: [ + ['cva\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'], + ['cn\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'], + ], + }, + }, + }, + content: [ + '', + ' ', + '', + ], + }) + + let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html') + + expect(classLists).toEqual([ + { + classList: 'p-4 sm:p-2 $', + range: { + start: { line: 0, character: 10 }, + end: { line: 0, character: 22 }, + }, + }, + { + classList: 'underline', + range: { + start: { line: 0, character: 33 }, + end: { line: 0, character: 42 }, + }, + }, + { + classList: 'line-through', + range: { + start: { line: 0, character: 46 }, + end: { line: 0, character: 58 }, + }, + }, + ]) +}) + +test('find class lists in functions', async ({ expect }) => { + let fileA = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['clsx', 'cva'], + }, + }, + content: js` + // These should match + let classes = clsx( + 'flex p-4', + 'block sm:p-0', + Date.now() > 100 ? 'text-white' : 'text-black', + ) + + // These should match + let classes = cva( + 'flex p-4', + 'block sm:p-0', + Date.now() > 100 ? 'text-white' : 'text-black', + ) + `, + }) + + let fileB = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['clsx', 'cva'], + }, + }, + content: js` + let classes = cn( + 'flex p-4', + 'block sm:p-0', + Date.now() > 100 ? 'text-white' : 'text-black', + ) + `, + }) + + let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js') + let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js') + + expect(classListsA).toEqual([ + // from clsx(…) + { + classList: 'flex p-4', + range: { + start: { line: 2, character: 3 }, + end: { line: 2, character: 11 }, + }, + }, + { + classList: 'block sm:p-0', + range: { + start: { line: 3, character: 3 }, + end: { line: 3, character: 15 }, + }, + }, + { + classList: 'text-white', + range: { + start: { line: 4, character: 22 }, + end: { line: 4, character: 32 }, + }, + }, + { + classList: 'text-black', + range: { + start: { line: 4, character: 37 }, + end: { line: 4, character: 47 }, + }, + }, + + // from cva(…) + { + classList: 'flex p-4', + range: { + start: { line: 9, character: 3 }, + end: { line: 9, character: 11 }, + }, + }, + { + classList: 'block sm:p-0', + range: { + start: { line: 10, character: 3 }, + end: { line: 10, character: 15 }, + }, + }, + { + classList: 'text-white', + range: { + start: { line: 11, character: 22 }, + end: { line: 11, character: 32 }, + }, + }, + { + classList: 'text-black', + range: { + start: { line: 11, character: 37 }, + end: { line: 11, character: 47 }, + }, + }, + ]) + + // none from cn(…) since it's not in the list of class functions + expect(classListsB).toEqual([]) +}) + +test('find class lists in nested fn calls', async ({ expect }) => { + let file = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['clsx', 'cva'], + }, + }, + + content: js` + // NOTE: All strings inside a matched class function will be treated as class lists + // TODO: Nested calls tha are *not* class functions should have their content ignored + let classes = clsx( + 'flex', + cn({ + 'bg-red-500': true, + 'text-white': Date.now() > 100, + }), + clsx( + 'fixed', + 'absolute inset-0' + ), + cva( + ['bottom-0', 'border'], + { + variants: { + mobile: { + default: 'bottom-0 left-0', + large: \` + inset-0 + rounded-none + \`, + }, + } + } + ) + ) + `, + }) + + let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html') + + expect(classLists).toMatchObject([ + { + classList: 'flex', + range: { + start: { line: 3, character: 3 }, + end: { line: 3, character: 7 }, + }, + }, + + // TODO: This should be ignored because they're inside cn(…) + { + classList: 'bg-red-500', + range: { + start: { line: 5, character: 5 }, + end: { line: 5, character: 15 }, + }, + }, + + // TODO: This should be ignored because they're inside cn(…) + { + classList: 'text-white', + range: { + start: { line: 6, character: 5 }, + end: { line: 6, character: 15 }, + }, + }, + + { + classList: 'fixed', + range: { + start: { line: 9, character: 5 }, + end: { line: 9, character: 10 }, + }, + }, + { + classList: 'absolute inset-0', + range: { + start: { line: 10, character: 5 }, + end: { line: 10, character: 21 }, + }, + }, + { + classList: 'bottom-0', + range: { + start: { line: 13, character: 6 }, + end: { line: 13, character: 14 }, + }, + }, + { + classList: 'border', + range: { + start: { line: 13, character: 18 }, + end: { line: 13, character: 24 }, + }, + }, + { + classList: 'bottom-0 left-0', + range: { + start: { line: 17, character: 20 }, + end: { line: 17, character: 35 }, + }, + }, + { + classList: `inset-0\n rounded-none\n `, + range: { + start: { line: 19, character: 12 }, + // TODO: Fix the range calculation. Its wrong on this one + end: { line: 20, character: 24 }, + }, + }, + ]) +}) + +test('find class lists in nested fn calls (only nested matches)', async ({ expect }) => { + let file = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['clsx', 'cva'], + }, + }, + + content: js` + let classes = cn( + 'flex', + cn({ + 'bg-red-500': true, + 'text-white': Date.now() > 100, + }), + // NOTE: The only class lists appear inside this function because cn is + // not in the list of class functions + clsx( + 'fixed', + 'absolute inset-0' + ), + ) + `, + }) + + let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'html') + + expect(classLists).toMatchObject([ + { + classList: 'fixed', + range: { + start: { line: 9, character: 5 }, + end: { line: 9, character: 10 }, + }, + }, + { + classList: 'absolute inset-0', + range: { + start: { line: 10, character: 5 }, + end: { line: 10, character: 21 }, + }, + }, + ]) +}) + +test('find class lists in tagged template literals', async ({ expect }) => { + let fileA = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['clsx', 'cva'], + }, + }, + content: js` + // These should match + let classes = clsx\` + flex p-4 + block sm:p-0 + \${Date.now() > 100 ? 'text-white' : 'text-black'} + \` + + // These should match + let classes = cva\` + flex p-4 + block sm:p-0 + \${Date.now() > 100 ? 'text-white' : 'text-black'} + \` + `, + }) + + let fileB = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['clsx', 'cva'], + }, + }, + content: js` + let classes = cn\` + flex p-4 + block sm:p-0 + \${Date.now() > 100 ? 'text-white' : 'text-black'} + \` + `, + }) + + let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js') + let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js') + + expect(classListsA).toEqual([ + // from clsx`…` + { + classList: 'flex p-4\n block sm:p-0\n $', + range: { + start: { line: 2, character: 2 }, + end: { line: 4, character: 3 }, + }, + }, + { + classList: 'text-white', + range: { + start: { line: 4, character: 24 }, + end: { line: 4, character: 34 }, + }, + }, + { + classList: 'text-black', + range: { + start: { line: 4, character: 39 }, + end: { line: 4, character: 49 }, + }, + }, + + // from cva`…` + { + classList: 'flex p-4\n block sm:p-0\n $', + range: { + start: { line: 9, character: 2 }, + end: { line: 11, character: 3 }, + }, + }, + { + classList: 'text-white', + range: { + start: { line: 11, character: 24 }, + end: { line: 11, character: 34 }, + }, + }, + { + classList: 'text-black', + range: { + start: { line: 11, character: 39 }, + end: { line: 11, character: 49 }, + }, + }, + ]) + + // none from cn`…` since it's not in the list of class functions + expect(classListsB).toEqual([]) +}) + +test('classFunctions can be a regex', async ({ expect }) => { + let fileA = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['tw\\.[a-z]+'], + }, + }, + content: js` + let classes = tw.div('flex p-4') + `, + }) + + let fileB = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classFunctions: ['tw\\.[a-z]+'], + }, + }, + content: js` + let classes = tw.div.foo('flex p-4') + `, + }) + + let classListsA = await findClassListsInHtmlRange(fileA.state, fileA.doc, 'js') + let classListsB = await findClassListsInHtmlRange(fileB.state, fileB.doc, 'js') + + expect(classListsA).toEqual([ + { + classList: 'flex p-4', + range: { + start: { line: 0, character: 22 }, + end: { line: 0, character: 30 }, + }, + }, + ]) + + // none from tw.div.foo(`…`) since it does not match a class function + expect(classListsB).toEqual([]) +}) + +test('classFunctions regexes only match on function names', async ({ expect }) => { + let file = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + // A function name itself cannot contain a `:` + classFunctions: [':\\s*tw\\.[a-z]+'], + }, + }, + content: js` + let classes = tw.div('flex p-4') + let classes = { foo: tw.div('flex p-4') } + `, + }) + + let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + + expect(classLists).toEqual([]) +}) + +test('Finds consecutive instances of a class function', async ({ expect }) => { + let file = createDocument({ + name: 'file.js', + lang: 'javascript', + settings: { + tailwindCSS: { + classFunctions: ['cn'], + }, + }, + content: js` + export const list = [ + cn('relative flex bg-red-500'), + cn('relative flex bg-red-500'), + cn('relative flex bg-red-500'), + ] + `, + }) + + let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + + expect(classLists).toEqual([ + { + classList: 'relative flex bg-red-500', + range: { + start: { line: 1, character: 6 }, + end: { line: 1, character: 30 }, + }, + }, + { + classList: 'relative flex bg-red-500', + range: { + start: { line: 2, character: 6 }, + end: { line: 2, character: 30 }, + }, + }, + { + classList: 'relative flex bg-red-500', + range: { + start: { line: 3, character: 6 }, + end: { line: 3, character: 30 }, + }, + }, + ]) +}) + +test('classFunctions & classAttributes should not duplicate matches', async ({ expect }) => { + let file = createDocument({ + name: 'file.jsx', + lang: 'javascriptreact', + settings: { + tailwindCSS: { + classAttributes: ['className'], + classFunctions: ['cva', 'clsx'], + }, + }, + content: js` + const Component = ({ className }) => ( +
+ CONTENT +
+ ) + const OtherComponent = ({ className }) => ( +
+ CONTENT +
+ ) + `, + }) + + let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + + expect(classLists).toEqual([ + { + classList: 'relative flex', + range: { + start: { line: 3, character: 7 }, + end: { line: 3, character: 20 }, + }, + }, + { + classList: 'inset-0 md:h-[calc(100%-2rem)]', + range: { + start: { line: 4, character: 7 }, + end: { line: 4, character: 37 }, + }, + }, + { + classList: 'rounded-none bg-blue-700', + range: { + start: { line: 5, character: 12 }, + end: { line: 5, character: 36 }, + }, + }, + { + classList: 'relative flex', + range: { + start: { line: 14, character: 7 }, + end: { line: 14, character: 20 }, + }, + }, + { + classList: 'inset-0 md:h-[calc(100%-2rem)]', + range: { + start: { line: 15, character: 7 }, + end: { line: 15, character: 37 }, + }, + }, + { + classList: 'rounded-none bg-blue-700', + range: { + start: { line: 16, character: 12 }, + end: { line: 16, character: 36 }, + }, + }, + ]) +}) + +test('classFunctions should only match in JS-like contexts', async ({ expect }) => { + let file = createDocument({ + name: 'file.html', + lang: 'html', + settings: { + tailwindCSS: { + classAttributes: ['className'], + classFunctions: ['clsx'], + }, + }, + content: html` + + clsx('relative flex') clsx('relative flex') + + + + + + clsx('relative flex') clsx('relative flex') + + + + `, + }) + + let classLists = await findClassListsInHtmlRange(file.state, file.doc, 'js') + + expect(classLists).toEqual([ + { + classList: 'relative flex', + range: { + start: { line: 5, character: 16 }, + end: { line: 5, character: 29 }, + }, + }, + { + classList: 'relative flex', + range: { + start: { line: 6, character: 16 }, + end: { line: 6, character: 29 }, + }, + }, + { + classList: 'relative flex', + range: { + start: { line: 14, character: 16 }, + end: { line: 14, character: 29 }, + }, + }, + { + classList: 'relative flex', + range: { + start: { line: 15, character: 16 }, + end: { line: 15, character: 29 }, + }, + }, + ]) +}) + +function createDocument({ + name, + lang, + content, + settings, +}: { + name: string + lang: string + content: string | string[] + settings: DeepPartial +}) { + let doc = TextDocument.create( + `file://${name}`, + lang, + 1, + typeof content === 'string' ? content : content.join('\n'), + ) + let defaults = getDefaultTailwindSettings() + let state = createState({ editor: { - userLanguages: {}, getConfiguration: async () => ({ - editor: { - tabSize: 1, - }, + ...defaults, + ...settings, tailwindCSS: { - classAttributes: ['class'], - experimental: { - classRegex: [ - ['cva\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'], - ['cn\\(([^)]*)\\)', '["\'`]([^"\'`]*).*?["\'`]'], - ], - }, - } as any, - }), - } as any, - } as any - - let classLists = await findClassListsInHtmlRange(state, doc, 'html') - - expect(classLists).toMatchInlineSnapshot(` - [ - { - "classList": "p-4 sm:p-2 $", - "range": { - "end": { - "character": 22, - "line": 0, + ...defaults.tailwindCSS, + ...settings.tailwindCSS, + lint: { + ...defaults.tailwindCSS.lint, + ...(settings.tailwindCSS?.lint ?? {}), }, - "start": { - "character": 10, - "line": 0, - }, - }, - }, - { - "classList": "underline", - "range": { - "end": { - "character": 42, - "line": 0, + experimental: { + ...defaults.tailwindCSS.experimental, + ...(settings.tailwindCSS?.experimental ?? {}), }, - "start": { - "character": 33, - "line": 0, + files: { + ...defaults.tailwindCSS.files, + ...(settings.tailwindCSS?.files ?? {}), }, }, - }, - { - "classList": "line-through", - "range": { - "end": { - "character": 58, - "line": 0, - }, - "start": { - "character": 46, - "line": 0, - }, + editor: { + ...defaults.editor, + ...settings.editor, }, - }, - ] - `) -}) + }), + }, + }) + + return { + doc, + state, + } +} diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index f38d7a16..03218798 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -9,7 +9,7 @@ import { isJsxContext } from './js' import { dedupeByRange, flatten } from './array' import { getClassAttributeLexer, getComputedClassAttributeLexer } from './lexers' import { getLanguageBoundaries } from './getLanguageBoundaries' -import { resolveRange } from './resolveRange' +import { absoluteRange } from './absoluteRange' import { getTextWithoutComments } from './doc' import { isSemicolonlessCssLanguage } from './languages' import { customClassesIn } from './classes' @@ -164,20 +164,66 @@ export function matchClassAttributes(text: string, attributes: string[]): RegExp return findAll(new RegExp(re.source.replace('ATTRS', attrs.join('|')), 'gi'), text) } +export function matchClassFunctions(text: string, fnNames: string[]): RegExpMatchArray[] { + // 1. Validate the list of function name patterns provided by the user + let names = fnNames.filter((x) => typeof x === 'string') + if (names.length === 0) return [] + + // 2. Extract function names in the document + // This is intentionally scoped to JS syntax for now but should be extended to + // other languages in the future + // + // This regex the JS pattern for an identifier + function call with some + // additional constraints: + // + // - It needs to be in an expression position — so it must be preceded by + // whitespace, parens, curlies, commas, whitespace, etc… + // - It must look like a fn call or a tagged template literal + let FN_NAMES = /(?<=^|[:=,;\s{()])([\p{ID_Start}$_][\p{ID_Continue}$_.]*)[(`]/dgiu + let foundFns = findAll(FN_NAMES, text) + + // 3. Match against the function names in the document + let re = /^(NAMES)$/ + let isClassFn = new RegExp(re.source.replace('NAMES', names.join('|')), 'i') + + let matches = foundFns.filter((fn) => isClassFn.test(fn[1])) + + return matches +} + export async function findClassListsInHtmlRange( state: State, doc: TextDocument, type: 'html' | 'js' | 'jsx', range?: Range, ): Promise { + if (!state.editor) return [] + const text = getTextWithoutComments(doc, type, range) - const matches = matchClassAttributes( - text, - (await state.editor.getConfiguration(doc.uri)).tailwindCSS.classAttributes, - ) + const settings = (await state.editor.getConfiguration(doc.uri)).tailwindCSS + const matches = matchClassAttributes(text, settings.classAttributes) - const result: DocumentClassList[] = [] + let boundaries = getLanguageBoundaries(state, doc) + + for (let boundary of boundaries ?? []) { + let isJsContext = boundary.type === 'js' || boundary.type === 'jsx' + if (!isJsContext) continue + if (!settings.classFunctions?.length) continue + + let str = doc.getText(boundary.range) + let offset = doc.offsetAt(boundary.range.start) + let fnMatches = matchClassFunctions(str, settings.classFunctions) + + fnMatches.forEach((match) => { + if (match.index) match.index += offset + }) + + matches.push(...fnMatches) + } + + const existingResultSet = new Set() + const results: DocumentClassList[] = [] matches.forEach((match) => { const subtext = text.substr(match.index + match[0].length - 1) @@ -222,46 +268,53 @@ export async function findClassListsInHtmlRange( }) } - result.push( - ...classLists - .map(({ value, offset }) => { - if (value.trim() === '') { - return null - } + classLists.forEach(({ value, offset }) => { + if (value.trim() === '') { + return null + } - const before = value.match(/^\s*/) - const beforeOffset = before === null ? 0 : before[0].length - const after = value.match(/\s*$/) - const afterOffset = after === null ? 0 : -after[0].length - - const start = indexToPosition( - text, - match.index + match[0].length - 1 + offset + beforeOffset, - ) - const end = indexToPosition( - text, - match.index + match[0].length - 1 + offset + value.length + afterOffset, - ) - - return { - classList: value.substr(beforeOffset, value.length + afterOffset), - range: { - start: { - line: (range?.start.line || 0) + start.line, - character: (end.line === 0 ? range?.start.character || 0 : 0) + start.character, - }, - end: { - line: (range?.start.line || 0) + end.line, - character: (end.line === 0 ? range?.start.character || 0 : 0) + end.character, - }, - }, - } - }) - .filter((x) => x !== null), - ) + const before = value.match(/^\s*/) + const beforeOffset = before === null ? 0 : before[0].length + const after = value.match(/\s*$/) + const afterOffset = after === null ? 0 : -after[0].length + + const start = indexToPosition(text, match.index + match[0].length - 1 + offset + beforeOffset) + const end = indexToPosition( + text, + match.index + match[0].length - 1 + offset + value.length + afterOffset, + ) + + const result: DocumentClassList = { + classList: value.substr(beforeOffset, value.length + afterOffset), + range: { + start: { + line: (range?.start.line || 0) + start.line, + character: (end.line === 0 ? range?.start.character || 0 : 0) + start.character, + }, + end: { + line: (range?.start.line || 0) + end.line, + character: (end.line === 0 ? range?.start.character || 0 : 0) + end.character, + }, + }, + } + + const resultKey = [ + result.classList, + result.range.start.line, + result.range.start.character, + result.range.end.line, + result.range.end.character, + ].join(':') + + // No need to add the result if it was already matched + if (!existingResultSet.has(resultKey)) { + existingResultSet.add(resultKey) + results.push(result) + } + }) }) - return result + return results } export async function findClassListsInRange( @@ -407,14 +460,14 @@ export function findHelperFunctionsInRange( helper, path, ranges: { - full: resolveRange( + full: absoluteRange( { start: indexToPosition(text, startIndex), end: indexToPosition(text, startIndex + match.groups.path.length), }, range, ), - path: resolveRange( + path: absoluteRange( { start: indexToPosition(text, startIndex + quotesBefore.length), end: indexToPosition(text, startIndex + quotesBefore.length + path.length), diff --git a/packages/tailwindcss-language-service/src/util/lexers.ts b/packages/tailwindcss-language-service/src/util/lexers.ts index 38ecc2d7..a8315f51 100644 --- a/packages/tailwindcss-language-service/src/util/lexers.ts +++ b/packages/tailwindcss-language-service/src/util/lexers.ts @@ -29,6 +29,14 @@ const classAttributeStates: () => { [x: string]: moo.Rules } = () => ({ rbrace: { match: new RegExp('(? { start2: { match: "'", push: 'singleClassList' }, start3: { match: '{', push: 'interpBrace' }, start4: { match: '`', push: 'tickClassList' }, + start5: { match: '(', push: 'interpParen' }, }, ...classAttributeStates(), }) diff --git a/packages/tailwindcss-language-service/src/util/resolveRange.ts b/packages/tailwindcss-language-service/src/util/resolveRange.ts deleted file mode 100644 index d90fa5b9..00000000 --- a/packages/tailwindcss-language-service/src/util/resolveRange.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Range } from 'vscode-languageserver' - -export function resolveRange(range: Range, relativeTo?: Range) { - return { - start: { - line: (relativeTo?.start.line || 0) + range.start.line, - character: - (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) + range.start.character, - }, - end: { - line: (relativeTo?.start.line || 0) + range.end.line, - character: - (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) + range.end.character, - }, - } -} diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 95afe8ec..3bdb1bc4 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -46,6 +46,7 @@ export type TailwindCssSettings = { emmetCompletions: boolean includeLanguages: Record classAttributes: string[] + classFunctions: string[] suggestions: boolean hovers: boolean codeActions: boolean @@ -64,7 +65,7 @@ export type TailwindCssSettings = { recommendedVariantOrder: DiagnosticSeveritySetting } experimental: { - classRegex: string[] + classRegex: string[] | [string, string][] configFile: string | Record | null } files: { @@ -171,3 +172,73 @@ export type ClassNameMeta = { scope: string[] context: string[] } + +/** + * @internal + */ +export function getDefaultTailwindSettings(): Settings { + return { + editor: { tabSize: 2 }, + tailwindCSS: { + inspectPort: null, + emmetCompletions: false, + classAttributes: ['class', 'className', 'ngClass', 'class:list'], + classFunctions: [], + codeActions: true, + hovers: true, + suggestions: true, + validate: true, + colorDecorators: true, + rootFontSize: 16, + lint: { + cssConflict: 'warning', + invalidApply: 'error', + invalidScreen: 'error', + invalidVariant: 'error', + invalidConfigPath: 'error', + invalidTailwindDirective: 'error', + invalidSourceDirective: 'error', + recommendedVariantOrder: 'warning', + }, + showPixelEquivalents: true, + includeLanguages: {}, + files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] }, + experimental: { + classRegex: [], + configFile: null, + }, + }, + } +} + +/** + * @internal + */ +export function createState( + partial: Omit, 'editor'> & { + editor?: Partial + }, +): State { + return { + enabled: true, + ...partial, + editor: { + get connection(): Connection { + throw new Error('Not implemented') + }, + folder: '/', + userLanguages: {}, + capabilities: { + configuration: true, + diagnosticRelatedInformation: true, + itemDefaults: [], + }, + getConfiguration: () => { + throw new Error('Not implemented') + }, + getDocumentSymbols: async () => [], + readDirectory: async () => [], + ...partial.editor, + }, + } +} diff --git a/packages/tailwindcss-language-service/tsconfig.json b/packages/tailwindcss-language-service/tsconfig.json index 605ece3c..883356e7 100644 --- a/packages/tailwindcss-language-service/tsconfig.json +++ b/packages/tailwindcss-language-service/tsconfig.json @@ -1,6 +1,5 @@ { "include": ["src", "../../types"], - "exclude": ["src/**/*.test.ts"], "compilerOptions": { "module": "NodeNext", "lib": ["ES2022"], @@ -15,6 +14,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "moduleResolution": "NodeNext", + "skipLibCheck": true, "jsx": "react", "esModuleInterop": true } diff --git a/packages/vscode-tailwindcss/README.md b/packages/vscode-tailwindcss/README.md index 62165308..efc937cc 100644 --- a/packages/vscode-tailwindcss/README.md +++ b/packages/vscode-tailwindcss/README.md @@ -90,6 +90,26 @@ Enable completions when using [Emmet](https://emmet.io/)-style syntax, for examp The HTML attributes for which to provide class completions, hover previews, linting etc. **Default: `class`, `className`, `ngClass`, `class:list`** +### `tailwindCSS.classFunctions` + +Functions in which to provide completions, hover previews, linting etc. Currently, this works for both function calls and tagged template literals in JavaScript / TypeScript. + +Example: + +```json +{ + "tailwindCSS.classFunctions": ["tw", "clsx"] +} +``` + +```javascript +let classes = tw`flex bg-red-500` +let classes2 = clsx([ + "flex bg-red-500", + { "text-red-500": true } +]) +``` + ### `tailwindCSS.colorDecorators` Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions. **Default: `true`** diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 3b5fd76a..036ca936 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -184,6 +184,14 @@ ], "markdownDescription": "The HTML attributes for which to provide class completions, hover previews, linting etc." }, + "tailwindCSS.classFunctions": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "markdownDescription": "The function or tagged template literal names for which to provide class completions, hover previews, linting etc." + }, "tailwindCSS.suggestions": { "type": "boolean", "default": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1048aaa..2378a376 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,6 +300,9 @@ importers: '@types/css.escape': specifier: ^1.5.2 version: 1.5.2 + '@types/dedent': + specifier: ^0.7.2 + version: 0.7.2 '@types/line-column': specifier: ^1.0.2 version: 1.0.2 @@ -309,6 +312,9 @@ importers: '@types/stringify-object': specifier: ^4.0.5 version: 4.0.5 + dedent: + specifier: ^1.5.3 + version: 1.5.3 esbuild: specifier: ^0.25.0 version: 0.25.0 @@ -969,6 +975,9 @@ packages: '@types/debounce@1.2.0': resolution: {integrity: sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==} + '@types/dedent@0.7.2': + resolution: {integrity: sha512-kRiitIeUg1mPV9yH4VUJ/1uk2XjyANfeL8/7rH1tsjvHeO9PJLBHJIYsFWmAvmGj5u8rj+1TZx7PZzW2qLw3Lw==} + '@types/dlv@1.1.4': resolution: {integrity: sha512-m8KmImw4Jt+4rIgupwfivrWEOnj1LzkmKkqbh075uG13eTQ1ZxHWT6T0vIdSQhLIjQCiR0n0lZdtyDOPO1x2Mw==} @@ -3227,6 +3236,8 @@ snapshots: '@types/debounce@1.2.0': {} + '@types/dedent@0.7.2': {} + '@types/dlv@1.1.4': {} '@types/estree@1.0.6': {} From b68a681ac748a4669ad3f4287a40b8c5f9a62c95 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 09:57:07 -0400 Subject: [PATCH 015/108] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index ca4255d7..dd3aa562 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,7 @@ ## Prerelease -- Nothing yet! +- Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258)) # 0.14.9 From 0d9bd2e8486a5cd3276fa47bd9a2749537e6a566 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:01:32 -0400 Subject: [PATCH 016/108] v4: Show completions after any valid variant (#1263) Fixes #943 Fixes #1257 This doesn't ensure variants like `not-supports-*` are suggested because `not-supports:` isn't valid. Only something with a value is e.g. `not-supports-display`. Making that work (it really should) is a larger task and requires designing an interactive suggestion API for v4. --- .../tests/completions/completions.test.js | 70 +++++++++++++++++- .../src/completionProvider.ts | 27 ------- .../src/util/getVariantsFromClassName.ts | 73 +++++++++---------- .../src/util/v4/design-system.ts | 2 +- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 5 files changed, 104 insertions(+), 69 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index 2727fcb0..f5393a40 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -1,5 +1,7 @@ -import { test } from 'vitest' +import { test, expect, describe } from 'vitest' import { withFixture } from '../common' +import { css, defineTest } from '../../src/testing' +import { createClient } from '../utils/client' function buildCompletion(c) { return async function completion({ @@ -670,3 +672,69 @@ withFixture('v4/workspaces', (c) => { }) }) }) + +defineTest({ + name: 'v4: Completions show after a variant arbitrary value', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 23 }) + + expect(completion?.items.length).toBe(12289) + }, +}) + +defineTest({ + name: 'v4: Completions show after an arbitrary variant', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 22 }) + + expect(completion?.items.length).toBe(12289) + }, +}) + +defineTest({ + name: 'v4: Completions show after a variant with a bare value', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 31 }) + + expect(completion?.items.length).toBe(12289) + }, +}) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index fe43bb68..1579fd93 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -189,16 +189,8 @@ export function completionsFromClassList( }), ) } else { - let shouldSortVariants = !semver.gte(state.version, '2.99.0') let resultingVariants = [...existingVariants, variant.name] - if (shouldSortVariants) { - let allVariants = state.variants.map(({ name }) => name) - resultingVariants = resultingVariants.sort( - (a, b) => allVariants.indexOf(b) - allVariants.indexOf(a), - ) - } - let selectors: string[] = [] try { @@ -223,25 +215,6 @@ export function completionsFromClassList( .map((selector) => addPixelEquivalentsToMediaQuery(selector)) .join(', '), textEditText: resultingVariants[resultingVariants.length - 1] + sep, - additionalTextEdits: - shouldSortVariants && resultingVariants.length > 1 - ? [ - { - newText: - resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep, - range: { - start: { - ...classListRange.start, - character: classListRange.end.character - partialClassName.length, - }, - end: { - ...replacementRange.start, - character: replacementRange.start.character, - }, - }, - }, - ] - : [], }), ) } diff --git a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts index b58bb29f..c30e729a 100644 --- a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts +++ b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts @@ -1,5 +1,6 @@ import type { State } from './state' import * as jit from './jit' +import { segment } from './segment' export function getVariantsFromClassName( state: State, @@ -13,60 +14,52 @@ export function getVariantsFromClassName( } return [variant.name] }) - let variants = new Set() - let offset = 0 - let parts = splitAtTopLevelOnly(className, state.separator) + + let parts = segment(className, state.separator) if (parts.length < 2) { - return { variants: Array.from(variants), offset } + return { variants: [], offset: 0 } } + parts = parts.filter(Boolean) - for (let part of parts) { - if ( - allVariants.includes(part) || - (state.jit && - ((part.includes('[') && part.endsWith(']')) || part.includes('/')) && - jit.generateRules(state, [`${part}${state.separator}[color:red]`]).rules.length > 0) - ) { - variants.add(part) - offset += part.length + state.separator.length - continue + function isValidVariant(part: string) { + if (allVariants.includes(part)) { + return true } - break - } + let className = `${part}${state.separator}[color:red]` - return { variants: Array.from(variants), offset } -} + if (state.v4) { + // NOTE: This should never happen + if (!state.designSystem) return false -// https://github.com/tailwindlabs/tailwindcss/blob/a8a2e2a7191fbd4bee044523aecbade5823a8664/src/util/splitAtTopLevelOnly.js -function splitAtTopLevelOnly(input: string, separator: string): string[] { - let stack: string[] = [] - let parts: string[] = [] - let lastPos = 0 + // We don't use `compile()` so there's no overhead from PostCSS + let compiled = state.designSystem.candidatesToCss([className]) - for (let idx = 0; idx < input.length; idx++) { - let char = input[idx] + // NOTE: This should never happen + if (compiled.length !== 1) return false - if (stack.length === 0 && char === separator[0]) { - if (separator.length === 1 || input.slice(idx, idx + separator.length) === separator) { - parts.push(input.slice(lastPos, idx)) - lastPos = idx + separator.length - } + return compiled[0] !== null } - if (char === '(' || char === '[' || char === '{') { - stack.push(char) - } else if ( - (char === ')' && stack[stack.length - 1] === '(') || - (char === ']' && stack[stack.length - 1] === '[') || - (char === '}' && stack[stack.length - 1] === '{') - ) { - stack.pop() + if (state.jit) { + if ((part.includes('[') && part.endsWith(']')) || part.includes('/')) { + return jit.generateRules(state, [className]).rules.length > 0 + } } + + return false } - parts.push(input.slice(lastPos)) + let offset = 0 + let variants = new Set() - return parts + for (let part of parts) { + if (!isValidVariant(part)) break + + variants.add(part) + offset += part.length + state.separator!.length + } + + return { variants: Array.from(variants), offset } } diff --git a/packages/tailwindcss-language-service/src/util/v4/design-system.ts b/packages/tailwindcss-language-service/src/util/v4/design-system.ts index cce64d4b..3fb3c401 100644 --- a/packages/tailwindcss-language-service/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-service/src/util/v4/design-system.ts @@ -44,6 +44,6 @@ export interface DesignSystem { export interface DesignSystem { dependencies(): Set - compile(classes: string[]): postcss.Root[] + compile(classes: string[]): (postcss.Root | null)[] toCss(nodes: postcss.Root | postcss.Node[]): string } diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index dd3aa562..7b9e68fa 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -3,6 +3,7 @@ ## Prerelease - Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258)) +- v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263)) # 0.14.9 From 9f9208af592347924461f8a2c8ac26a09ccb14e3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:25:34 -0400 Subject: [PATCH 017/108] =?UTF-8?q?Add=20support=20for=20`@source=20not`?= =?UTF-8?q?=20and=20`@source=20inline(=E2=80=A6)`=20(#1262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntelliSense counterpart of https://github.com/tailwindlabs/tailwindcss/pull/17147 --- .../tailwindcss-language-server/package.json | 1 + .../src/projects.ts | 35 +++++- .../tailwindcss-language-server/src/tw.ts | 13 +++ .../tests/code-lens/source-inline.test.ts | 101 ++++++++++++++++++ .../tests/completions/at-config.test.js | 97 +++++++++++++++++ .../document-links/document-links.test.js | 38 +++++++ .../tests/hover/hover.test.js | 95 ++++++++++++++++ .../tests/utils/client.ts | 26 +++++ .../tailwindcss-language-service/package.json | 1 + .../src/codeLensProvider.ts | 78 ++++++++++++++ .../src/completionProvider.ts | 13 +++ .../src/completions/file-paths.test.ts | 16 +++ .../src/completions/file-paths.ts | 10 +- .../getInvalidSourceDiagnostics.ts | 7 +- .../src/documentLinksProvider.ts | 2 +- .../src/features.ts | 20 ++-- .../src/hoverProvider.ts | 27 +++-- .../src/util/estimated-class-size.ts | 35 ++++++ .../src/util/format-bytes.ts | 11 ++ .../src/util/state.ts | 5 + packages/vscode-tailwindcss/CHANGELOG.md | 2 + packages/vscode-tailwindcss/package.json | 6 ++ pnpm-lock.yaml | 6 ++ 23 files changed, 624 insertions(+), 21 deletions(-) create mode 100644 packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts create mode 100644 packages/tailwindcss-language-service/src/codeLensProvider.ts create mode 100644 packages/tailwindcss-language-service/src/util/estimated-class-size.ts create mode 100644 packages/tailwindcss-language-service/src/util/format-bytes.ts diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index 5ed431fb..fa0a7e91 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -42,6 +42,7 @@ "@tailwindcss/line-clamp": "0.4.2", "@tailwindcss/oxide": "^4.0.0-alpha.19", "@tailwindcss/typography": "0.5.7", + "@types/braces": "3.0.1", "@types/color-name": "^1.1.3", "@types/culori": "^2.1.0", "@types/debounce": "1.2.0", diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index b55ee078..afc4515f 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -15,6 +15,8 @@ import type { Disposable, DocumentLinkParams, DocumentLink, + CodeLensParams, + CodeLens, } from 'vscode-languageserver/node' import { FileChangeType } from 'vscode-languageserver/node' import type { TextDocument } from 'vscode-languageserver-textdocument' @@ -35,6 +37,7 @@ import stackTrace from 'stack-trace' import extractClassNames from './lib/extractClassNames' import { klona } from 'klona/full' import { doHover } from '@tailwindcss/language-service/src/hoverProvider' +import { getCodeLens } from '@tailwindcss/language-service/src/codeLensProvider' import { Resolver } from './resolver' import { doComplete, @@ -110,6 +113,7 @@ export interface ProjectService { onColorPresentation(params: ColorPresentationParams): Promise onCodeAction(params: CodeActionParams): Promise onDocumentLinks(params: DocumentLinkParams): Promise + onCodeLens(params: CodeLensParams): Promise sortClassLists(classLists: string[]): string[] dependencies(): Iterable @@ -212,6 +216,7 @@ export async function createProjectService( let state: State = { enabled: false, + features: [], completionItemData: { _projectKey: projectKey, }, @@ -462,6 +467,14 @@ export async function createProjectService( // and this should be determined there and passed in instead let features = supportedFeatures(tailwindcssVersion, tailwindcss) log(`supported features: ${JSON.stringify(features)}`) + state.features = features + + if (params.initializationOptions?.testMode) { + state.features = [ + ...state.features, + ...(params.initializationOptions.additionalFeatures ?? []), + ] + } if (!features.includes('css-at-theme')) { tailwindcss = tailwindcss.default ?? tailwindcss @@ -688,6 +701,15 @@ export async function createProjectService( state.v4 = true state.v4Fallback = true state.jit = true + state.features = features + + if (params.initializationOptions?.testMode) { + state.features = [ + ...state.features, + ...(params.initializationOptions.additionalFeatures ?? []), + ] + } + state.modules = { tailwindcss: { version: tailwindcssVersion, module: tailwindcss }, postcss: { version: null, module: null }, @@ -1150,7 +1172,7 @@ export async function createProjectService( }, tryInit, async dispose() { - state = { enabled: false } + state = { enabled: false, features: [] } for (let disposable of disposables) { ;(await disposable).dispose() } @@ -1177,6 +1199,17 @@ export async function createProjectService( return doHover(state, document, params.position) }, null) }, + async onCodeLens(params: CodeLensParams): Promise { + return withFallback(async () => { + if (!state.enabled) return null + let document = documentService.getDocument(params.textDocument.uri) + if (!document) return null + let settings = await state.editor.getConfiguration(document.uri) + if (!settings.tailwindCSS.codeLens) return null + if (await isExcluded(state, document)) return null + return getCodeLens(state, document) + }, null) + }, async onCompletion(params: CompletionParams): Promise { return withFallback(async () => { if (!state.enabled) return null diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 35da9386..fc6a87a8 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -19,6 +19,8 @@ import type { DocumentLink, InitializeResult, WorkspaceFolder, + CodeLensParams, + CodeLens, } from 'vscode-languageserver/node' import { CompletionRequest, @@ -30,6 +32,7 @@ import { FileChangeType, DocumentLinkRequest, TextDocumentSyncKind, + CodeLensRequest, } from 'vscode-languageserver/node' import { URI } from 'vscode-uri' import normalizePath from 'normalize-path' @@ -757,6 +760,7 @@ export class TW { this.connection.onDocumentColor(this.onDocumentColor.bind(this)) this.connection.onColorPresentation(this.onColorPresentation.bind(this)) this.connection.onCodeAction(this.onCodeAction.bind(this)) + this.connection.onCodeLens(this.onCodeLens.bind(this)) this.connection.onDocumentLinks(this.onDocumentLinks.bind(this)) this.connection.onRequest(this.onRequest.bind(this)) } @@ -809,6 +813,7 @@ export class TW { capabilities.add(HoverRequest.type, { documentSelector: null }) capabilities.add(DocumentColorRequest.type, { documentSelector: null }) capabilities.add(CodeActionRequest.type, { documentSelector: null }) + capabilities.add(CodeLensRequest.type, { documentSelector: null }) capabilities.add(DocumentLinkRequest.type, { documentSelector: null }) capabilities.add(CompletionRequest.type, { @@ -931,6 +936,11 @@ export class TW { return this.getProject(params.textDocument)?.onCodeAction(params) ?? null } + async onCodeLens(params: CodeLensParams): Promise { + await this.init() + return this.getProject(params.textDocument)?.onCodeLens(params) ?? null + } + async onDocumentLinks(params: DocumentLinkParams): Promise { await this.init() return this.getProject(params.textDocument)?.onDocumentLinks(params) ?? null @@ -961,6 +971,9 @@ export class TW { hoverProvider: true, colorProvider: true, codeActionProvider: true, + codeLensProvider: { + resolveProvider: false, + }, documentLinkProvider: {}, completionProvider: { resolveProvider: true, diff --git a/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts b/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts new file mode 100644 index 00000000..55f6f22e --- /dev/null +++ b/packages/tailwindcss-language-server/tests/code-lens/source-inline.test.ts @@ -0,0 +1,101 @@ +import { expect } from 'vitest' +import { css, defineTest } from '../../src/testing' +import { createClient } from '../utils/client' + +defineTest({ + name: 'Code lenses are displayed for @source inline(…)', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ + client: await createClient({ + root, + features: ['source-inline'], + }), + }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'css', + text: css` + @import 'tailwindcss'; + @source inline("{,{hover,focus}:}{flex,underline,bg-red-{50,{100..900.100},950}}"); + `, + }) + + let lenses = await document.codeLenses() + + expect(lenses).toEqual([ + { + range: { + start: { line: 1, character: 15 }, + end: { line: 1, character: 81 }, + }, + command: { + title: 'Generates 15 classes', + command: '', + }, + }, + ]) + }, +}) + +defineTest({ + name: 'The user is warned when @source inline(…) generates a lerge amount of CSS', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ + client: await createClient({ + root, + features: ['source-inline'], + }), + }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'css', + text: css` + @import 'tailwindcss'; + @source inline("{,dark:}{,{sm,md,lg,xl,2xl}:}{,{hover,focus,active}:}{flex,underline,bg-red-{50,{100..900.100},950}{,/{0..100}}}"); + `, + }) + + let lenses = await document.codeLenses() + + expect(lenses).toEqual([ + { + range: { + start: { line: 1, character: 15 }, + end: { line: 1, character: 129 }, + }, + command: { + title: 'Generates 14,784 classes', + command: '', + }, + }, + { + range: { + start: { line: 1, character: 15 }, + end: { line: 1, character: 129 }, + }, + command: { + title: 'At least 3MB of CSS', + command: '', + }, + }, + { + range: { + start: { line: 1, character: 15 }, + end: { line: 1, character: 129 }, + }, + command: { + title: 'This may slow down your bundler/browser', + command: '', + }, + }, + ]) + }, +}) diff --git a/packages/tailwindcss-language-server/tests/completions/at-config.test.js b/packages/tailwindcss-language-server/tests/completions/at-config.test.js index 15d99ac6..60ee1495 100644 --- a/packages/tailwindcss-language-server/tests/completions/at-config.test.js +++ b/packages/tailwindcss-language-server/tests/completions/at-config.test.js @@ -271,6 +271,51 @@ withFixture('v4/dependencies', (c) => { }) }) + test.concurrent('@source not', async ({ expect }) => { + let result = await completion({ + text: '@source not "', + lang: 'css', + position: { + line: 0, + character: 13, + }, + }) + + expect(result).toEqual({ + isIncomplete: false, + items: [ + { + label: 'index.html', + kind: 17, + data: expect.anything(), + textEdit: { + newText: 'index.html', + range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } }, + }, + }, + { + label: 'sub-dir/', + kind: 19, + command: { command: 'editor.action.triggerSuggest', title: '' }, + data: expect.anything(), + textEdit: { + newText: 'sub-dir/', + range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } }, + }, + }, + { + label: 'tailwind.config.js', + kind: 17, + data: expect.anything(), + textEdit: { + newText: 'tailwind.config.js', + range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } }, + }, + }, + ], + }) + }) + test.concurrent('@source directory', async ({ expect }) => { let result = await completion({ text: '@source "./sub-dir/', @@ -297,6 +342,58 @@ withFixture('v4/dependencies', (c) => { }) }) + test.concurrent('@source not directory', async ({ expect }) => { + let result = await completion({ + text: '@source not "./sub-dir/', + lang: 'css', + position: { + line: 0, + character: 23, + }, + }) + + expect(result).toEqual({ + isIncomplete: false, + items: [ + { + label: 'colors.js', + kind: 17, + data: expect.anything(), + textEdit: { + newText: 'colors.js', + range: { start: { line: 0, character: 23 }, end: { line: 0, character: 23 } }, + }, + }, + ], + }) + }) + + test.concurrent('@source inline(…)', async ({ expect }) => { + let result = await completion({ + text: '@source inline("', + lang: 'css', + position: { + line: 0, + character: 16, + }, + }) + + expect(result).toEqual(null) + }) + + test.concurrent('@source not inline(…)', async ({ expect }) => { + let result = await completion({ + text: '@source not inline("', + lang: 'css', + position: { + line: 0, + character: 20, + }, + }) + + expect(result).toEqual(null) + }) + test.concurrent('@import "…" source(…)', async ({ expect }) => { let result = await completion({ text: '@import "tailwindcss" source("', diff --git a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js index 861f74c9..0fc76cb6 100644 --- a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js +++ b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js @@ -131,6 +131,44 @@ withFixture('v4/basic', (c) => { ], }) + testDocumentLinks('source not: file exists', { + text: '@source not "index.html";', + lang: 'css', + expected: [ + { + target: `file://${path + .resolve('./tests/fixtures/v4/basic/index.html') + .replace(/@/g, '%40')}`, + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 24 } }, + }, + ], + }) + + testDocumentLinks('source not: file does not exist', { + text: '@source not "does-not-exist.html";', + lang: 'css', + expected: [ + { + target: `file://${path + .resolve('./tests/fixtures/v4/basic/does-not-exist.html') + .replace(/@/g, '%40')}`, + range: { start: { line: 0, character: 12 }, end: { line: 0, character: 33 } }, + }, + ], + }) + + testDocumentLinks('@source inline(…)', { + text: '@source inline("m-{1,2,3}");', + lang: 'css', + expected: [], + }) + + testDocumentLinks('@source not inline(…)', { + text: '@source not inline("m-{1,2,3}");', + lang: 'css', + expected: [], + }) + testDocumentLinks('Directories in source(…) show links', { text: ` @import "tailwindcss" source("../../"); diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index 5c8bcb7f..ac97e414 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -293,6 +293,101 @@ withFixture('v4/basic', (c) => { }, }) + testHover('css @source not glob expansion', { + exact: true, + lang: 'css', + text: `@source not "../{app,components}/**/*.jsx"`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- ../app/**/*.jsx', + '- ../components/**/*.jsx', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 42 }, + }, + }, + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 18 }, + }, + }) + + testHover('css @source inline glob expansion', { + exact: true, + lang: 'css', + text: `@source inline("{hover:,active:,}m-{1,2,3}")`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- hover:m-1', + '- hover:m-2', + '- hover:m-3', + '- active:m-1', + '- active:m-2', + '- active:m-3', + '- m-1', + '- m-2', + '- m-3', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 15 }, + end: { line: 0, character: 43 }, + }, + }, + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 15 }, + }, + }) + + testHover('css @source not inline glob expansion', { + exact: true, + lang: 'css', + text: `@source not inline("{hover:,active:,}m-{1,2,3}")`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- hover:m-1', + '- hover:m-2', + '- hover:m-3', + '- active:m-1', + '- active:m-2', + '- active:m-3', + '- m-1', + '- m-2', + '- m-3', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 19 }, + end: { line: 0, character: 47 }, + }, + }, + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 18 }, + }, + }) + testHover('--theme() works inside @media queries', { lang: 'tailwindcss', text: `@media (width>=--theme(--breakpoint-xl)) { .foo { color: red; } }`, diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts index d6d317bb..3a860230 100644 --- a/packages/tailwindcss-language-server/tests/utils/client.ts +++ b/packages/tailwindcss-language-server/tests/utils/client.ts @@ -1,6 +1,8 @@ import type { Settings } from '@tailwindcss/language-service/src/util/state' import { ClientCapabilities, + CodeLens, + CodeLensRequest, CompletionList, CompletionParams, Diagnostic, @@ -45,6 +47,7 @@ import { clearLanguageBoundariesCache } from '@tailwindcss/language-service/src/ import { DefaultMap } from '../../src/util/default-map' import { connect, ConnectOptions } from './connection' import type { DeepPartial } from '@tailwindcss/language-service/src/types' +import type { Feature } from '@tailwindcss/language-service/src/features' export interface DocumentDescriptor { /** @@ -94,6 +97,11 @@ export interface ClientDocument { */ reopen(): Promise + /** + * Code lenses in the document + */ + codeLenses(): Promise + /** * The diagnostics for the current version of this document */ @@ -163,6 +171,14 @@ export interface ClientOptions extends ConnectOptions { * Settings to provide the server immediately when it starts */ settings?: DeepPartial + + /** + * Additional features to force-enable + * + * These should normally be enabled by the server based on the project + * and the Tailwind CSS version it detects + */ + features?: Feature[] } export interface Client extends ClientWorkspace { @@ -387,6 +403,7 @@ export async function createClient(opts: ClientOptions): Promise { workspaceFolders, initializationOptions: { testMode: true, + additionalFeatures: opts.features, ...opts.options, }, }) @@ -677,6 +694,14 @@ export async function createClientWorkspace({ return results } + async function codeLenses() { + return await conn.sendRequest(CodeLensRequest.type, { + textDocument: { + uri: uri.toString(), + }, + }) + } + return { uri, reopen, @@ -687,6 +712,7 @@ export async function createClientWorkspace({ symbols, completions, diagnostics, + codeLenses, } } diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index d5fddaaa..e73f6ca3 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -40,6 +40,7 @@ "vscode-languageserver-textdocument": "1.0.11" }, "devDependencies": { + "@types/braces": "3.0.1", "@types/css.escape": "^1.5.2", "@types/dedent": "^0.7.2", "@types/line-column": "^1.0.2", diff --git a/packages/tailwindcss-language-service/src/codeLensProvider.ts b/packages/tailwindcss-language-service/src/codeLensProvider.ts new file mode 100644 index 00000000..b00df983 --- /dev/null +++ b/packages/tailwindcss-language-service/src/codeLensProvider.ts @@ -0,0 +1,78 @@ +import type { Range, TextDocument } from 'vscode-languageserver-textdocument' +import type { State } from './util/state' +import type { CodeLens } from 'vscode-languageserver' +import braces from 'braces' +import { findAll, indexToPosition } from './util/find' +import { absoluteRange } from './util/absoluteRange' +import { formatBytes } from './util/format-bytes' +import { estimatedClassSize } from './util/estimated-class-size' + +export async function getCodeLens(state: State, doc: TextDocument): Promise { + if (!state.enabled) return [] + + let groups: CodeLens[][] = await Promise.all([ + // + sourceInlineCodeLens(state, doc), + ]) + + return groups.flat() +} + +const SOURCE_INLINE_PATTERN = /@source(?:\s+not)?\s*inline\((?'[^']+'|"[^"]+")/dg +async function sourceInlineCodeLens(state: State, doc: TextDocument): Promise { + if (!state.features.includes('source-inline')) return [] + + let text = doc.getText() + + let countFormatter = new Intl.NumberFormat('en', { + maximumFractionDigits: 2, + }) + + let lenses: CodeLens[] = [] + + for (let match of findAll(SOURCE_INLINE_PATTERN, text)) { + let glob = match.groups.glob.slice(1, -1) + + // Perform brace expansion + let expanded = new Set(braces.expand(glob)) + if (expanded.size < 2) continue + + let slice: Range = absoluteRange({ + start: indexToPosition(text, match.indices.groups.glob[0]), + end: indexToPosition(text, match.indices.groups.glob[1]), + }) + + let size = 0 + for (let className of expanded) { + size += estimatedClassSize(className) + } + + lenses.push({ + range: slice, + command: { + title: `Generates ${countFormatter.format(expanded.size)} classes`, + command: '', + }, + }) + + if (size >= 1_000_000) { + lenses.push({ + range: slice, + command: { + title: `At least ${formatBytes(size)} of CSS`, + command: '', + }, + }) + + lenses.push({ + range: slice, + command: { + title: `This may slow down your bundler/browser`, + command: '', + }, + }) + } + } + + return lenses +} diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 1579fd93..3cdd511a 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1866,6 +1866,19 @@ function provideCssDirectiveCompletions( }, }) + if (state.features.includes('source-not')) { + items.push({ + label: '@source not', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use the \`@source not\` directive to ignore files when scanning.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#source', + )})`, + }, + }) + } + items.push({ label: '@plugin', documentation: { diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts index c79fcbdd..ed521392 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts @@ -15,6 +15,7 @@ test('Detecting v3 directives that point to files', async () => { // The following are not supported in v3 await expect(find('@plugin "./')).resolves.toEqual(null) await expect(find('@source "./')).resolves.toEqual(null) + await expect(find('@source not "./')).resolves.toEqual(null) await expect(find('@import "tailwindcss" source("./')).resolves.toEqual(null) await expect(find('@tailwind utilities source("./')).resolves.toEqual(null) }) @@ -42,6 +43,12 @@ test('Detecting v4 directives that point to files', async () => { suggest: 'source', }) + await expect(find('@source not "./')).resolves.toEqual({ + directive: 'source', + partial: './', + suggest: 'source', + }) + await expect(find('@import "tailwindcss" source("./')).resolves.toEqual({ directive: 'import', partial: './', @@ -54,3 +61,12 @@ test('Detecting v4 directives that point to files', async () => { suggest: 'directory', }) }) + +test('@source inline is ignored', async () => { + function find(text: string) { + return findFileDirective({ enabled: true, v4: true }, text) + } + + await expect(find('@source inline("')).resolves.toEqual(null) + await expect(find('@source not inline("')).resolves.toEqual(null) +}) diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.ts b/packages/tailwindcss-language-service/src/completions/file-paths.ts index a99325be..acf4f9fa 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.ts @@ -1,7 +1,10 @@ import type { State } from '../util/state' // @config, @plugin, @source -const PATTERN_CUSTOM_V4 = /@(?config|plugin|source)\s*(?'[^']*|"[^"]*)$/ +// - @source inline("…") is *not* a file directive +// - @source not inline("…") is *not* a file directive +const PATTERN_CUSTOM_V4 = + /@(?config|plugin|source)(?\s+not)?\s*(?'[^']*|"[^"]*)$/ const PATTERN_CUSTOM_V3 = /@(?config)\s*(?'[^']*|"[^"]*)$/ // @import … source('…') @@ -26,6 +29,7 @@ export async function findFileDirective(state: State, text: string): Promise 0 let directive = match.groups.directive let partial = match.groups.partial?.slice(1) ?? '' // remove leading quote @@ -40,6 +44,7 @@ export async function findFileDirective(state: State, text: string): Promise 0 + if (isNot) return null + let directive = match.groups.directive let partial = match.groups.partial.slice(1) // remove leading quote diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index 2ac52a08..24fdcd9c 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -14,7 +14,7 @@ const PATTERN_UTIL_SOURCE = // @source … const PATTERN_AT_SOURCE = - /(?:\s|^)@(?source)\s*(?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg + /(?:\s|^)@(?source)\s*(?not)?\s*(?'[^']*'?|"[^"]*"?|[a-z]*(?:\([^)]+\))?|\)|;)/dg const HAS_DRIVE_LETTER = /^[A-Z]:/ @@ -135,6 +135,11 @@ export function getInvalidSourceDiagnostics( }) } + // `@source inline(…)` is fine + else if (directive === 'source' && source.startsWith('inline(')) { + // + } + // - `@import "tailwindcss" source(no)` // - `@tailwind utilities source('')` else if (directive === 'source' && source !== 'none' && !isQuoted) { diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index 76e39ed7..ea2e7bd9 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -18,7 +18,7 @@ export function getDocumentLinks( if (state.v4) { patterns.push( /@plugin\s*(?'[^']+'|"[^"]+")/g, - /@source\s*(?'[^']+'|"[^"]+")/g, + /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/g, /@import\s*('[^']*'|"[^"]*")\s*(layer\([^)]+\)\s*)?source\((?'[^']*'?|"[^"]*"?)/g, /@reference\s*('[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?)/g, /@tailwind\s*utilities\s*source\((?'[^']*'?|"[^"]*"?)/g, diff --git a/packages/tailwindcss-language-service/src/features.ts b/packages/tailwindcss-language-service/src/features.ts index 60181bf7..6aa9236e 100644 --- a/packages/tailwindcss-language-service/src/features.ts +++ b/packages/tailwindcss-language-service/src/features.ts @@ -15,6 +15,8 @@ export type Feature = | 'jit' | 'separator:root' | 'separator:options' + | 'source-not' + | 'source-inline' /** * Determine a list of features that are supported by the given Tailwind CSS version @@ -39,15 +41,21 @@ export function supportedFeatures(version: string, mod?: unknown): Feature[] { } if (isInsidersV4) { - return ['css-at-theme', 'layer:base', 'content-list'] + return ['css-at-theme', 'layer:base', 'content-list', 'source-inline', 'source-not'] } - if (!isInsidersV3 && semver.gte(version, '4.0.0-alpha.1')) { - return ['css-at-theme', 'layer:base', 'content-list'] - } + if (!isInsidersV3) { + if (semver.gte(version, '4.1.0')) { + return ['css-at-theme', 'layer:base', 'content-list', 'source-inline', 'source-not'] + } - if (!isInsidersV3 && version.startsWith('0.0.0-oxide')) { - return ['css-at-theme', 'layer:base', 'content-list'] + if (semver.gte(version, '4.0.0-alpha.1')) { + return ['css-at-theme', 'layer:base', 'content-list'] + } + + if (version.startsWith('0.0.0-oxide')) { + return ['css-at-theme', 'layer:base', 'content-list'] + } } if (semver.gte(version, '0.99.0')) { diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index eb5a7d1c..1db68bed 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -168,19 +168,24 @@ async function provideSourceGlobHover( let text = getTextWithoutComments(document, 'css', range) - let pattern = /@source\s*(?'[^']+'|"[^"]+")/dg + let patterns = [ + /@source(?:\s+not)?\s*(?'[^']+'|"[^"]+")/dg, + /@source(?:\s+not)?\s*inline\((?'[^']+'|"[^"]+")/dg, + ] - for (let match of findAll(pattern, text)) { - let path = match.groups.path.slice(1, -1) + let matches = patterns.flatMap((pattern) => findAll(pattern, text)) - // Ignore paths that don't need brace expansion - if (!path.includes('{') || !path.includes('}')) continue + for (let match of matches) { + let glob = match.groups.glob.slice(1, -1) + + // Ignore globs that don't need brace expansion + if (!glob.includes('{') || !glob.includes('}')) continue - // Ignore paths that don't contain the current position + // Ignore glob that don't contain the current position let slice: Range = absoluteRange( { - start: indexToPosition(text, match.indices.groups.path[0]), - end: indexToPosition(text, match.indices.groups.path[1]), + start: indexToPosition(text, match.indices.groups.glob[0]), + end: indexToPosition(text, match.indices.groups.glob[1]), }, range, ) @@ -188,8 +193,8 @@ async function provideSourceGlobHover( if (!isWithinRange(position, slice)) continue // Perform brace expansion - let paths = new Set(braces.expand(path)) - if (paths.size < 2) continue + let expanded = new Set(braces.expand(glob)) + if (expanded.size < 2) continue return { range: slice, @@ -197,7 +202,7 @@ async function provideSourceGlobHover( // '**Expansion**', '```plaintext', - ...Array.from(paths, (path) => `- ${path}`), + ...Array.from(expanded, (entry) => `- ${entry}`), '```', ]), } diff --git a/packages/tailwindcss-language-service/src/util/estimated-class-size.ts b/packages/tailwindcss-language-service/src/util/estimated-class-size.ts new file mode 100644 index 00000000..57dc4235 --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/estimated-class-size.ts @@ -0,0 +1,35 @@ +import { segment } from './segment' + +/** + * Calculates the approximate size of a generated class + * + * This is meant to be a lower bound, as the actual size of a class can vary + * depending on the actual CSS properties and values, configured theme, etc… + */ +export function estimatedClassSize(className: string) { + let size = 0 + + // We estimate the size using the following structure which gives a reasonable + // lower bound on the size of the generated CSS: + // + // .class-name { + // &:variant-1 { + // &:variant-2 { + // … + // } + // } + // } + + // Class name + size += 1 + className.length + 3 + size += 2 + + // Variants + nesting + for (let [depth, variantName] of segment(className, ':').entries()) { + size += (depth + 1) * 2 + 2 + variantName.length + 3 + size += (depth + 1) * 2 + 2 + } + + // ~1.95x is a rough growth factor due to the actual properties being present + return size * 1.95 +} diff --git a/packages/tailwindcss-language-service/src/util/format-bytes.ts b/packages/tailwindcss-language-service/src/util/format-bytes.ts new file mode 100644 index 00000000..a7b0b050 --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/format-bytes.ts @@ -0,0 +1,11 @@ +const UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte'] + +export function formatBytes(n: number) { + let i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1000)) + return new Intl.NumberFormat('en', { + notation: 'compact', + style: 'unit', + unit: UNITS[i], + unitDisplay: 'narrow', + }).format(n / 1000 ** i) +} diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 3bdb1bc4..119e5f59 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -4,6 +4,7 @@ import type { Postcss } from 'postcss' import type { KeywordColor } from './color' import type * as culori from 'culori' import type { DesignSystem } from './v4' +import type { Feature } from '../features' export type ClassNamesTree = { [key: string]: ClassNamesTree @@ -49,6 +50,7 @@ export type TailwindCssSettings = { classFunctions: string[] suggestions: boolean hovers: boolean + codeLens: boolean codeActions: boolean validate: boolean showPixelEquivalents: boolean @@ -141,6 +143,7 @@ export interface State { classListContainsMetadata?: boolean pluginVersions?: string completionItemData?: Record + features: Feature[] // postcssPlugins?: { before: any[]; after: any[] } } @@ -185,6 +188,7 @@ export function getDefaultTailwindSettings(): Settings { classAttributes: ['class', 'className', 'ngClass', 'class:list'], classFunctions: [], codeActions: true, + codeLens: true, hovers: true, suggestions: true, validate: true, @@ -221,6 +225,7 @@ export function createState( ): State { return { enabled: true, + features: [], ...partial, editor: { get connection(): Connection { diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 7b9e68fa..faf20065 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -4,6 +4,8 @@ - Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258)) - v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263)) +- v4: Add support for upcoming `@source not` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) +- v4: Add support for upcoming `@source inline(…)` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) # 0.14.9 diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 036ca936..876a661b 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -210,6 +210,12 @@ "markdownDescription": "Enable code actions.", "scope": "language-overridable" }, + "tailwindCSS.codeLens": { + "type": "boolean", + "default": true, + "markdownDescription": "Enable code lens.", + "scope": "language-overridable" + }, "tailwindCSS.colorDecorators": { "type": "boolean", "default": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2378a376..67d58759 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: '@tailwindcss/typography': specifier: 0.5.7 version: 0.5.7(tailwindcss@3.4.17) + '@types/braces': + specifier: 3.0.1 + version: 3.0.1 '@types/color-name': specifier: ^1.1.3 version: 1.1.4 @@ -297,6 +300,9 @@ importers: specifier: 1.0.11 version: 1.0.11 devDependencies: + '@types/braces': + specifier: 3.0.1 + version: 3.0.1 '@types/css.escape': specifier: ^1.5.2 version: 1.5.2 From 182600d55606e81ea600e57be15580cf32b7fe73 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 10:47:17 -0400 Subject: [PATCH 018/108] Ensure `classFunctions` completions work inside ` + `, + }) + + // let classes = clsx('') + // ^ + let completion = await document.completions({ line: 1, character: 22 }) + + expect(completion?.items.length).toBe(12289) + }, +}) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 3cdd511a..62c54594 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -22,7 +22,7 @@ import isObject from './util/isObject' import { braceLevel, parenLevel } from './util/braceLevel' import * as emmetHelper from 'vscode-emmet-helper-bundled' import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation' -import { isJsDoc, isJsxContext } from './util/js' +import { isJsContext, isJsDoc, isJsxContext } from './util/js' import { naturalExpand } from './util/naturalExpand' import * as semver from './util/semver' import { getTextWithoutComments } from './util/doc' @@ -986,7 +986,11 @@ async function provideClassNameCompletions( return provideAtApplyCompletions(state, document, position, context) } - if (isHtmlContext(state, document, position) || isJsxContext(state, document, position)) { + if ( + isHtmlContext(state, document, position) || + isJsContext(state, document, position) || + isJsxContext(state, document, position) + ) { return provideClassAttributeCompletions(state, document, position, context) } diff --git a/packages/tailwindcss-language-service/src/util/js.ts b/packages/tailwindcss-language-service/src/util/js.ts index 1197d1e6..0c405a8d 100644 --- a/packages/tailwindcss-language-service/src/util/js.ts +++ b/packages/tailwindcss-language-service/src/util/js.ts @@ -12,6 +12,19 @@ export function isJsDoc(state: State, doc: TextDocument): boolean { return [...jsLanguages, ...userJsLanguages].indexOf(doc.languageId) !== -1 } +export function isJsContext(state: State, doc: TextDocument, position: Position): boolean { + let str = doc.getText({ + start: { line: 0, character: 0 }, + end: position, + }) + + let boundaries = getLanguageBoundaries(state, doc, str) + + return boundaries + ? ['js', 'ts', 'jsx', 'tsx'].includes(boundaries[boundaries.length - 1].type) + : false +} + export function isJsxContext(state: State, doc: TextDocument, position: Position): boolean { let str = doc.getText({ start: { line: 0, character: 0 }, diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index faf20065..468cdd08 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,7 @@ ## Prerelease -- Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258)) +- Detect classes in JS/TS functions and tagged template literals with the `tailwindCSS.classFunctions` setting ([#1258](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1258), [#1272](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1272)) - v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263)) - v4: Add support for upcoming `@source not` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) - v4: Add support for upcoming `@source inline(…)` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) From 2f41b839a3856efd1ebe10b656e7b4704cbd57d8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 19 Mar 2025 11:48:21 -0400 Subject: [PATCH 019/108] LSP: Refresh internal caches when settings are updated (#1273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We use the pull model (`workspace/configuration`) to get settings for a document and we cache these internally so we don't repeat these calls multiple times for a given request. We're set up to listen for configuration refresh notifications and update the project settings when we get them. Unfortunately, we didn't actually _register_ for these notifications, so we never got them. This meant that if you changed the settings for an already opened file or workspace folder, the language server would not react to these changes. This PR fixes this by registering for configuration change notifications and now open files with color decorators, completions, etc… should react to changes in the settings as needed. If settings are updated and our langauge server doesn't react to or handle these changes, it is definitely a bug. Hopefully this will squash all of those particular ones but… we'll see. 😅 --- packages/tailwindcss-language-server/src/projects.ts | 8 +++----- packages/tailwindcss-language-server/src/tw.ts | 6 ++++++ packages/vscode-tailwindcss/CHANGELOG.md | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index afc4515f..a8012920 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -1181,11 +1181,9 @@ export async function createProjectService( if (state.enabled) { refreshDiagnostics() } - if (settings.editor?.colorDecorators) { - updateCapabilities() - } else { - connection.sendNotification('@/tailwindCSS/clearColors') - } + + updateCapabilities() + connection.sendNotification('@/tailwindCSS/clearColors') }, onFileEvents, async onHover(params: TextDocumentPositionParams): Promise { diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index fc6a87a8..48d7b5f7 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -33,6 +33,7 @@ import { DocumentLinkRequest, TextDocumentSyncKind, CodeLensRequest, + DidChangeConfigurationNotification, } from 'vscode-languageserver/node' import { URI } from 'vscode-uri' import normalizePath from 'normalize-path' @@ -799,6 +800,7 @@ export class TW { private updateCapabilities() { if (!supportsDynamicRegistration(this.initializeParams)) { + this.connection.client.register(DidChangeConfigurationNotification.type, undefined) return } @@ -810,12 +812,16 @@ export class TW { let capabilities = BulkRegistration.create() + // TODO: We should *not* be re-registering these capabilities + // IDEA: These should probably be static registrations up front capabilities.add(HoverRequest.type, { documentSelector: null }) capabilities.add(DocumentColorRequest.type, { documentSelector: null }) capabilities.add(CodeActionRequest.type, { documentSelector: null }) capabilities.add(CodeLensRequest.type, { documentSelector: null }) capabilities.add(DocumentLinkRequest.type, { documentSelector: null }) + capabilities.add(DidChangeConfigurationNotification.type, undefined) + // TODO: Only re-register this if trigger characters change capabilities.add(CompletionRequest.type, { documentSelector: null, resolveProvider: true, diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 468cdd08..fa2d07c3 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -6,6 +6,7 @@ - v4: Make sure completions show after variants using arbitrary and bare values ([#1263](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1263)) - v4: Add support for upcoming `@source not` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) - v4: Add support for upcoming `@source inline(…)` feature ([#1262](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1262)) +- LSP: Refresh internal caches when settings are updated ([#1273](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1273)) # 0.14.9 From 747884f34b31c797db0915d1719276eae35431f1 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 20 Mar 2025 12:37:21 -0400 Subject: [PATCH 020/108] Ignore regex literals when analyzing language boundaries (#1275) Fixes #929 --- .../tests/env/v4.test.js | 48 +++++ .../src/util/doc.ts | 150 ++++++++++++++ .../src/util/find.test.ts | 61 +----- .../src/util/language-boundaries.test.ts | 191 ++++++++++++++++++ .../src/util/test-utils.ts | 65 ++++++ 5 files changed, 455 insertions(+), 60 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/util/language-boundaries.test.ts create mode 100644 packages/tailwindcss-language-service/src/util/test-utils.ts diff --git a/packages/tailwindcss-language-server/tests/env/v4.test.js b/packages/tailwindcss-language-server/tests/env/v4.test.js index 76de7f15..3a7780af 100644 --- a/packages/tailwindcss-language-server/tests/env/v4.test.js +++ b/packages/tailwindcss-language-server/tests/env/v4.test.js @@ -791,3 +791,51 @@ defineTest({ }) }, }) + +defineTest({ + options: { only: true }, + name: 'regex literals do not break language boundaries', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let doc = await client.open({ + lang: 'javascriptreact', + text: js` + export default function Page() { + let styles = "str".match(/ +
+
+ `, + }) + + let boundaries = getLanguageBoundaries(file.state, file.doc) + + expect(boundaries).toEqual([ + { + type: 'html', + range: { + start: { line: 0, character: 0 }, + end: { line: 1, character: 2 }, + }, + }, + { + type: 'css', + range: { + start: { line: 1, character: 2 }, + end: { line: 5, character: 2 }, + }, + }, + { + type: 'html', + range: { + start: { line: 5, character: 2 }, + end: { line: 7, character: 6 }, + }, + }, + ]) +}) + +test('script tags in HTML are treated as a separate boundary', ({ expect }) => { + let file = createDocument({ + name: 'file.html', + lang: 'html', + content: html` +
+ +
+
+ `, + }) + + let boundaries = getLanguageBoundaries(file.state, file.doc) + + expect(boundaries).toEqual([ + { + type: 'html', + range: { + start: { line: 0, character: 0 }, + end: { line: 1, character: 2 }, + }, + }, + { + type: 'js', + range: { + start: { line: 1, character: 2 }, + end: { line: 5, character: 2 }, + }, + }, + { + type: 'html', + range: { + start: { line: 5, character: 2 }, + end: { line: 7, character: 6 }, + }, + }, + ]) +}) + +test('Vue files detect