├── .editorconfig ├── .github ├── assignment.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── build ├── copy-jsbeautify.js ├── generateData.js ├── pipeline.yml ├── remove-sourcemap-refs.js └── update-jsbeautify.js ├── docs ├── customData.md └── customData.schema.json ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── beautify │ ├── beautify-css.d.ts │ ├── beautify-css.js │ ├── beautify-license │ └── esm │ │ └── beautify-css.js ├── cssLanguageService.ts ├── cssLanguageTypes.ts ├── data │ └── webCustomData.ts ├── languageFacts │ ├── builtinData.ts │ ├── colors.ts │ ├── dataManager.ts │ ├── dataProvider.ts │ ├── entry.ts │ └── facts.ts ├── parser │ ├── cssErrors.ts │ ├── cssNodes.ts │ ├── cssParser.ts │ ├── cssScanner.ts │ ├── cssSymbolScope.ts │ ├── lessParser.ts │ ├── lessScanner.ts │ ├── scssErrors.ts │ ├── scssParser.ts │ └── scssScanner.ts ├── services │ ├── cssCodeActions.ts │ ├── cssCompletion.ts │ ├── cssFolding.ts │ ├── cssFormatter.ts │ ├── cssHover.ts │ ├── cssNavigation.ts │ ├── cssSelectionRange.ts │ ├── cssValidation.ts │ ├── lessCompletion.ts │ ├── lint.ts │ ├── lintRules.ts │ ├── lintUtil.ts │ ├── pathCompletion.ts │ ├── scssCompletion.ts │ ├── scssNavigation.ts │ └── selectorPrinting.ts ├── test │ ├── css │ │ ├── codeActions.test.ts │ │ ├── completion.test.ts │ │ ├── customData.test.ts │ │ ├── folding.test.ts │ │ ├── formatter.test.ts │ │ ├── hover.test.ts │ │ ├── languageFacts.test.ts │ │ ├── lint.test.ts │ │ ├── navigation.test.ts │ │ ├── nodes.test.ts │ │ ├── parser.test.ts │ │ ├── scanner.test.ts │ │ ├── selectionRange.test.ts │ │ └── selectorPrinting.test.ts │ ├── less │ │ ├── formatter.test.ts │ │ ├── lessCompletion.test.ts │ │ ├── lessNavigation.test.ts │ │ ├── lint.test.ts │ │ ├── nodes.test.ts │ │ ├── parser.test.ts │ │ └── scanner.test.ts │ ├── scss │ │ ├── example.scss │ │ ├── formatter.test.ts │ │ ├── languageFacts.test.ts │ │ ├── linkFixture │ │ │ ├── both │ │ │ │ ├── _foo.scss │ │ │ │ └── foo.scss │ │ │ ├── index │ │ │ │ ├── bar │ │ │ │ │ └── _index.scss │ │ │ │ └── foo │ │ │ │ │ └── index.scss │ │ │ ├── module │ │ │ │ └── foo.scss │ │ │ ├── noUnderscore │ │ │ │ └── foo.scss │ │ │ └── underscore │ │ │ │ └── _foo.scss │ │ ├── lint.test.ts │ │ ├── parser.test.ts │ │ ├── scssCompletion.test.ts │ │ ├── scssNavigation.test.ts │ │ └── selectorPrinting.test.ts │ ├── testUtil │ │ ├── documentContext.ts │ │ └── fsProvider.ts │ └── util.test.ts ├── tsconfig.esm.json ├── tsconfig.json └── utils │ ├── arrays.ts │ ├── objects.ts │ ├── resources.ts │ └── strings.ts └── test ├── linksTestFixtures ├── .gitignore ├── a.css ├── green │ ├── c.css │ └── d.scss └── node_modules │ ├── @foo │ ├── bar │ │ ├── _baz.scss │ │ ├── _index.scss │ │ └── package.json │ └── baz │ │ └── package.json │ ├── bar-pattern │ └── package.json │ ├── bar │ └── package.json │ ├── conditional │ └── package.json │ ├── foo │ └── package.json │ ├── green │ ├── _e.scss │ ├── c.css │ ├── d.scss │ └── package.json │ ├── root-sass │ └── package.json │ └── root-style │ └── package.json └── pathCompletionFixtures ├── .foo.js ├── about ├── about.css └── about.html ├── index.html ├── scss ├── _foo.scss └── main.scss └── src ├── data └── foo.asar ├── feature.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | indent_style = tab 2 | indent_size = 2 3 | 4 | [*.json] 5 | indent_style = space -------------------------------------------------------------------------------- /.github/assignment.yml: -------------------------------------------------------------------------------- 1 | { 2 | perform: true, 3 | assignees: [ aeschli ] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x] 17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm ci 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | !src/test/scss/linkFixture/pkgImport/node_modules/ 4 | coverage/ 5 | .nyc_output/ 6 | npm-debug.log -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui": "tdd", 3 | "color": true, 4 | "spec": "./lib/umd/test/**/*.test.js", 5 | "recursive": true 6 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .github/ 3 | lib/*/test/ 4 | lib/**/*.js.map 5 | lib/*/*/*.d.ts 6 | src/ 7 | build/ 8 | coverage/ 9 | test/ 10 | .eslintrc.json 11 | .gitignore 12 | .travis.yml 13 | gulpfile.js 14 | tslint.json 15 | package-lock.json 16 | azure-pipelines.yml 17 | .editorconfig 18 | .mocharc.json 19 | .nyc_output/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Unit Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 9 | "stopOnEntry": false, 10 | "args": [ 11 | "--timeout", 12 | "999999", 13 | "--colors" 14 | ], 15 | "cwd": "${workspaceRoot}", 16 | "runtimeExecutable": null, 17 | "runtimeArgs": [], 18 | "env": {}, 19 | "sourceMaps": true, 20 | "outFiles": [ 21 | "${workspaceRoot}/lib/umd/**" 22 | ], 23 | "skipFiles": [ 24 | "/**" 25 | ], 26 | "smartStep": true, 27 | "preLaunchTask": "npm: watch" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "prettier.useTabs": true, 4 | "prettier.printWidth": 120, 5 | "prettier.semi": true, 6 | "git.branchProtection": [ 7 | "main" 8 | ], 9 | "git.branchProtectionPrompt": "alwaysCommitToNewBranch", 10 | "git.branchRandomName.enable": true, 11 | "githubPullRequests.assignCreated": "${user}", 12 | "githubPullRequests.defaultMergeMethod": "squash" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "watch", 20 | "problemMatcher": "$tsc-watch", 21 | "isBackground": true, 22 | "presentation": { 23 | "reveal": "never" 24 | }, 25 | "group": { 26 | "kind": "build", 27 | "isDefault": true 28 | } 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 6.3.0 / 2022-06-24 2 | ================ 3 | * new optional API `fileSystemProvider.getContent` 4 | 5 | 6.2.0 / 2022-11-09 6 | ================ 7 | * new API `LanguageService.prepareRename`, returning `Range` 8 | 9 | 6.1.0 / 2022-09-02 10 | ================ 11 | * new API `LanguageService.findDocumentSymbols2`, returning `DocumentSymbol[]` 12 | 13 | 6.0.0 / 2022-05-18 14 | ================ 15 | * Update to `vscode-languageserver-types@3.17` 16 | 17 | 5.4.0 / 2022-04-01 18 | ================== 19 | * new formatter settings: `braceStyle`, `preserveNewLines`, `maxPreserveNewLines`, `wrapLineLength`, `indentEmptyLines` 20 | 21 | 5.3.0 / 2022-03-23 22 | ================== 23 | * renamed `CSSFormatConfiguration.selectorSeparatorNewline` to `CSSFormatConfiguration.newlineBetweenSelectors` 24 | 25 | 5.2.0 / 2022-03-17 26 | ================== 27 | * new API `LanguageService.format`, based on the the css formatter from JS Beautifier (https://github.com/beautify-web/js-beautify) 28 | * new API `CSSFormatConfiguration` 29 | 30 | 5.1.0 / 2021-02-05 31 | ================== 32 | * new API `LanguageSettings.hover` 33 | * New parameter `CompletionSettings` for `LanguageService.doComplete` and `LanguageService.doComplete2` 34 | 35 | 5.0.0 / 2020-12-14 36 | ================== 37 | * Update to `vscode-languageserver-types@3.16` 38 | * Removed deprecated `findColorSymbols` 39 | 40 | 4.4.0 - 2020-11-30 41 | =================== 42 | * New parameter `HoverSettings` for `LanguageService.doHover`: Defines whether the hover contains element documentation and/or a reference to MDN. 43 | 44 | 4.3.0 - 2020-06-26 45 | =================== 46 | * module resolving in urls (`~foo/hello.html`) when using `LanguageService.findDocumentLinks2` and if `fileSystemProvider` is provided. 47 | * new API `LanguageService.doComplete2`. Support path completion if `fileSystemProvider.readDirectory` is provided. 48 | * `DocumentContext.resolveReference` can also return undefined (if the ref is invalid) 49 | 50 | 4.2.0 - 2020-05-14 51 | =================== 52 | * new API `LanguageServiceOptions.useDefaultDataProvider` to control whether the default data provider is used. Defaults to true 53 | * new API `LanguageService.setDataProviders` to update the data providers. 54 | 55 | 4.1.0 - 2020-02-23 56 | =================== 57 | * markdown descriptions in completions and hover 58 | * new API `LanguageServiceOptions.clientCapabilities` with `ClientCapabilities` for completion documentationFormat and hover content 59 | * extended format of CustomData (version 1.1) with MarkupContent contents and reference links 60 | * dynamically resolved links for scss include statements 61 | * new API `LanguageService.findDocumentLinks2`: Also returns dynamically resolved links if `fileSystemProvider` is provided 62 | * new API `LanguageServiceOptions.fileSystemProvider` with `FileSystemProvider` to query the file system (currently used to resolve the location of included files) 63 | * new API `CompletionSettings.completePropertyWithSemicolon` 64 | * new API `ICompletionParticipant.onCssMixinReference` 65 | * Switch to `TextDocument` from `vscode-languageserver-textdocument` (reexported from the main module) 66 | 67 | 4.0.0 / 2019-06-12 68 | =================== 69 | * `LanguageServiceOptions.customDataProviders` allows you to use custom datasets for properties, at-properties, pseudo-classes and pseudo-elements. 70 | * New API `LanguageService.getSelectionRanges` 71 | 72 | 3.0.12 / 2018-10-29 73 | =================== 74 | * Selector hover shows specificity 75 | * New linter setting `validProperties`: a comma separated list of all properties not to be included in validation checking. 76 | 77 | 3.0.10 / 2018-08-27 78 | =================== 79 | * New API `ICompletionParticipant.onCssImportPath` to participate on @import statement. 80 | * New API `LanguageService.doCodeActions2` returning code actions as `CodeAction[]`. 81 | 82 | 3.0.9 / 2018-07-25 83 | ================== 84 | * Use MDN data for to enhance CSS properties definition. See [#91](https://github.com/Microsoft/vscode-css-languageservice/pull/91). 85 | * New API `LanguageService.getFoldingRanges` returning folding ranges in the given document. 86 | 87 | 3.0.8 / 2018-03-08 88 | ================== 89 | * Provide ems modules in lib/esm 90 | 91 | 3.0.0 / 2017-01-11 92 | ================== 93 | * Changed API `LanguageService.getColorPresentations`: separate parameters `range` and `color` (to match LS API) 94 | 95 | 2.1.7 / 2017-09-21 96 | ================== 97 | * New API `LanguageService.getColorPresentations` returning presentations for a given color. 98 | * New API type `ColorPresentation` added. 99 | 100 | 2.1.4 / 2017-08-28 101 | ================== 102 | * New API `LanguageService.findDocumentColors` returning the location and value of all colors in a document. 103 | * New API types `ColorInformation` and `Color` added. 104 | * Deprecated `LanguageService.findColorSymbols`. Use `LanguageService.findDocumentColors` instead. 105 | 106 | 2.1.3 / 2017-08-15 107 | ================== 108 | * New argument `documentSettings` to `LanguageService.doValidation` to support resource specific settings. If present, document settings are used instead of the options passed in configure. 109 | 110 | 2.0.0 / 2017-02-17 111 | ================== 112 | * Updating to [language server type 3.0](https://github.com/Microsoft/vscode-languageserver-node/tree/master/types) API. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-css-languageservice 2 | Language services for CSS, LESS and SCSS 3 | 4 | [![npm Package](https://img.shields.io/npm/v/vscode-css-languageservice.svg?style=flat-square)](https://www.npmjs.org/package/vscode-css-languageservice) 5 | [![NPM Downloads](https://img.shields.io/npm/dm/vscode-css-languageservice.svg)](https://npmjs.org/package/vscode-css-languageservice) 6 | [![Build Status](https://github.com/microsoft/vscode-css-languageservice/actions/workflows/node.js.yml/badge.svg)](https://github.com/microsoft/vscode-css-languageservice/actions) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | Why? 10 | ---- 11 | The _vscode-css-languageservice_ contains the language smarts behind the CSS, LESS and SCSS editing experience of Visual Studio Code 12 | and the Monaco editor. 13 | - *doValidation* analyses an input string and returns syntax and lint errors. 14 | - *doComplete* provides completion proposals for a given location. 15 | - *doHover* provides a hover text for a given location. 16 | - *findDefinition* finds the definition of the symbol at the given location. 17 | - *findReferences* finds all references to the symbol at the given location. 18 | - *findDocumentHighlights* finds all symbols connected to the given location. 19 | - *findDocumentSymbols* provides all symbols in the given document 20 | - *doCodeActions* evaluates code actions for the given location, typically to fix a problem. 21 | - *findDocumentColors* evaluates all color symbols in the given document 22 | - *doRename* renames all symbols connected to the given location. 23 | - *prepareRename* the range of the node that can be renamed 24 | - *getFoldingRanges* returns folding ranges in the given document. 25 | 26 | Installation 27 | ------------ 28 | 29 | npm install --save vscode-css-languageservice 30 | 31 | 32 | API 33 | --- 34 | 35 | For the complete API see [cssLanguageService.ts](./src/cssLanguageService.ts) and [cssLanguageTypes.ts](./src/cssLanguageTypes.ts) 36 | 37 | 38 | Development 39 | ----------- 40 | 41 | 42 | - clone this repo, run `npm install` 43 | - `npm test` to compile and run tests 44 | 45 | How can I run and debug the service? 46 | 47 | - open the folder in VSCode. 48 | - set breakpoints, e.g. in `cssCompletion.ts` 49 | - run the Unit tests from the run viewlet and wait until a breakpoint is hit: 50 | ![image](https://user-images.githubusercontent.com/6461412/94239202-bdad4e80-ff11-11ea-99c3-cb9dbeb1c0b2.png) 51 | 52 | 53 | How can I run and debug the service inside an instance of VSCode? 54 | 55 | - run VSCode out of sources setup as described here: https://github.com/Microsoft/vscode/wiki/How-to-Contribute 56 | - run `npm link` in the folder of `vscode-css-languageservice` 57 | - use `npm link vscode-css-languageservice` in `vscode/extensions/css-language-features/server` to run VSCode with the latest changes from `vscode-css-languageservice` 58 | - run VSCode out of source (`vscode/scripts/code.sh|bat`) and open a `.css` file 59 | - in VSCode window that is open on the `vscode-css-languageservice` sources, run command `Debug: Attach to Node process` and pick the `code-oss` process with the `css-language-features` path 60 | ![image](https://user-images.githubusercontent.com/6461412/94242567-842b1200-ff16-11ea-8f85-3ebb72d06ba8.png) 61 | - set breakpoints, e.g. in `cssCompletion.ts` 62 | - in the instance run from sources, invoke code completion in the `.css` file 63 | 64 | 65 | 66 | **Note: All CSS entities (properties, at-rules, etc) are sourced from https://github.com/microsoft/vscode-custom-data/tree/master/web-data and transpiled here. For adding new property or fixing existing properties' completion/hover description, please open PR there).** 67 | 68 | 69 | License 70 | ------- 71 | 72 | (MIT License) 73 | 74 | Copyright 2016, 20 Microsoft 75 | 76 | With the exceptions of `build/mdn-documentation.js`, which is built upon content from [Mozilla Developer Network](https://developer.mozilla.org/docs/Web) 77 | and distributed under CC BY-SA 2.5. 78 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /build/copy-jsbeautify.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | function copy(from, to) { 5 | if (!fs.existsSync(to)) { 6 | fs.mkdirSync(to, { recursive: true }); 7 | } 8 | const files = fs.readdirSync(from); 9 | for (let file of files) { 10 | if (path.extname(file) === '.js') { 11 | const fromPath = path.join(from, file); 12 | const toPath = path.join(to, file); 13 | console.log(`copy ${fromPath} to ${toPath}`); 14 | fs.copyFileSync(fromPath, toPath); 15 | } 16 | } 17 | } 18 | 19 | const umdDir = path.join(__dirname, '..', 'lib', 'umd', 'beautify'); 20 | copy(path.join(__dirname, '..', 'src', 'beautify'), umdDir); 21 | 22 | const esmDir = path.join(__dirname, '..', 'lib', 'esm', 'beautify'); 23 | copy(path.join(__dirname, '..', 'src', 'beautify', 'esm'), esmDir); 24 | -------------------------------------------------------------------------------- /build/generateData.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | const fs = require('fs') 7 | const path = require('path') 8 | const os = require('os') 9 | 10 | const customData = require('@vscode/web-custom-data/data/browsers.css-data.json'); 11 | 12 | function toJavaScript(obj) { 13 | return JSON.stringify(obj, null, '\t'); 14 | } 15 | 16 | const DATA_TYPE = 'CSSDataV1'; 17 | const output = [ 18 | '/*---------------------------------------------------------------------------------------------', 19 | ' * Copyright (c) Microsoft Corporation. All rights reserved.', 20 | ' * Licensed under the MIT License. See License.txt in the project root for license information.', 21 | ' *--------------------------------------------------------------------------------------------*/', 22 | '// file generated from @vscode/web-custom-data NPM package', 23 | '', 24 | `import { ${DATA_TYPE} } from '../cssLanguageTypes';`, 25 | '', 26 | `export const cssData : ${DATA_TYPE} = ` + toJavaScript(customData) + ';' 27 | ]; 28 | 29 | var outputPath = path.resolve(__dirname, '../src/data/webCustomData.ts'); 30 | console.log('Writing to: ' + outputPath); 31 | var content = output.join(os.EOL); 32 | fs.writeFileSync(outputPath, content); 33 | console.log('Done'); 34 | -------------------------------------------------------------------------------- /build/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(Date:yyyyMMdd)$(Rev:.r) 2 | 3 | trigger: 4 | batch: true 5 | branches: 6 | include: 7 | - main 8 | pr: none 9 | 10 | resources: 11 | repositories: 12 | - repository: templates 13 | type: github 14 | name: microsoft/vscode-engineering 15 | ref: main 16 | endpoint: Monaco 17 | 18 | parameters: 19 | - name: quality 20 | displayName: Quality 21 | type: string 22 | default: latest 23 | values: 24 | - latest 25 | - next 26 | - name: publishPackage 27 | displayName: 🚀 Publish vscode-css-languageservice 28 | type: boolean 29 | default: false 30 | 31 | extends: 32 | template: azure-pipelines/npm-package/pipeline.yml@templates 33 | parameters: 34 | npmPackages: 35 | - name: vscode-css-languageservice 36 | 37 | buildSteps: 38 | - script: npm ci 39 | displayName: Install dependencies 40 | 41 | # the rest of the build steps are part of the 'prepack' script, automatically run when the pipeline invokes 'npm run pack' 42 | 43 | tag: ${{ parameters.quality }} 44 | preReleaseTag: next 45 | publishPackage: ${{ parameters.publishPackage }} 46 | publishRequiresApproval: false 47 | 48 | testPlatforms: 49 | - name: Linux 50 | nodeVersions: 51 | - 18.x 52 | - name: MacOS 53 | nodeVersions: 54 | - 18.x 55 | - name: Windows 56 | nodeVersions: 57 | - 18.x 58 | 59 | testSteps: 60 | - script: npm ci 61 | displayName: Install dependencies 62 | - script: npm test 63 | displayName: Test npm package 64 | -------------------------------------------------------------------------------- /build/remove-sourcemap-refs.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | function deleteRefs(dir) { 10 | const files = fs.readdirSync(dir); 11 | for (let file of files) { 12 | const filePath = path.join(dir, file); 13 | const stat = fs.statSync(filePath); 14 | if (stat.isDirectory()) { 15 | deleteRefs(filePath); 16 | } else if (path.extname(file) === '.js') { 17 | const content = fs.readFileSync(filePath, 'utf8'); 18 | const newContent = content.replace(/\/\/\# sourceMappingURL=[^]+.js.map/, '') 19 | if (content.length !== newContent.length) { 20 | console.log('remove sourceMappingURL in ' + filePath); 21 | fs.writeFileSync(filePath, newContent); 22 | } 23 | } else if (path.extname(file) === '.map') { 24 | fs.unlinkSync(filePath) 25 | console.log('remove ' + filePath); 26 | } 27 | } 28 | } 29 | 30 | let location = path.join(__dirname, '..', 'lib'); 31 | console.log('process ' + location); 32 | deleteRefs(location); -------------------------------------------------------------------------------- /build/update-jsbeautify.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 'use strict'; 7 | 8 | var path = require('path'); 9 | var fs = require('fs'); 10 | 11 | function getVersion(moduleName) { 12 | var packageJSONPath = path.join(__dirname, '..', 'node_modules', moduleName, 'package.json'); 13 | return readFile(packageJSONPath).then(function (content) { 14 | try { 15 | return JSON.parse(content).version; 16 | } catch (e) { 17 | return Promise.resolve(null); 18 | } 19 | }); 20 | } 21 | 22 | function readFile(path) { 23 | return new Promise((s, e) => { 24 | fs.readFile(path, (err, res) => { 25 | if (err) { 26 | e(err); 27 | } else { 28 | s(res.toString()); 29 | } 30 | }); 31 | }); 32 | 33 | } 34 | 35 | function update(moduleName, repoPath, dest, addHeader, patch) { 36 | var contentPath = path.join(__dirname, '..', 'node_modules', moduleName, repoPath); 37 | console.log('Reading from ' + contentPath); 38 | return readFile(contentPath).then(function (content) { 39 | return getVersion(moduleName).then(function (version) { 40 | let header = ''; 41 | if (addHeader) { 42 | header = '// copied from js-beautify/' + repoPath + '\n'; 43 | if (version) { 44 | header += '// version: ' + version + '\n'; 45 | } 46 | } 47 | try { 48 | if (patch) { 49 | content = patch(content); 50 | } 51 | fs.writeFileSync(dest, header + content); 52 | if (version) { 53 | console.log('Updated ' + path.basename(dest) + ' (' + version + ')'); 54 | } else { 55 | console.log('Updated ' + path.basename(dest)); 56 | } 57 | } catch (e) { 58 | console.error(e); 59 | } 60 | }); 61 | 62 | }, console.error); 63 | } 64 | 65 | update('js-beautify', 'js/lib/beautify-css.js', './src/beautify/beautify-css.js', true); 66 | update('js-beautify', 'LICENSE', './src/beautify/beautify-license'); 67 | 68 | // ESM version 69 | update('js-beautify', 'js/lib/beautify-css.js', './src/beautify/esm/beautify-css.js', true, function (contents) { 70 | let topLevelFunction = '(function() {'; 71 | let outputVar = 'var legacy_beautify_css'; 72 | let footer = 'var css_beautify = legacy_beautify_css;'; 73 | let index1 = contents.indexOf(topLevelFunction); 74 | let index2 = contents.indexOf(outputVar, index1); 75 | let index3 = contents.indexOf(footer, index2); 76 | if (index1 === -1) { 77 | throw new Error(`Problem patching beautify.css for ESM: '${topLevelFunction}' not found.`); 78 | } 79 | if (index2 === -1) { 80 | throw new Error(`Problem patching beautify.css for ESM: '${outputVar}' not found after '${topLevelFunction}'.`); 81 | } 82 | if (index3 === -1) { 83 | throw new Error(`Problem patching beautify.css for ESM: '${footer}' not found after '${outputVar}'.`); 84 | } 85 | return contents.substring(0, index1) + 86 | contents.substring(index2, index3) + 87 | `\nexport var css_beautify = legacy_beautify_css;`; 88 | }); 89 | -------------------------------------------------------------------------------- /docs/customData.md: -------------------------------------------------------------------------------- 1 | # Custom Data for CSS Language Service 2 | 3 | In VS Code, there are two ways of loading custom CSS datasets: 4 | 5 | 1. With setting `css.customData` 6 | ```json 7 | "css.customData": [ 8 | "./foo.css-data.json" 9 | ] 10 | ``` 11 | 2. With an extension that contributes `contributes.css.customData` 12 | 13 | Both setting point to a list of JSON files. This document describes the shape of the JSON files. 14 | 15 | You can read more about custom data at: https://github.com/microsoft/vscode-custom-data. 16 | 17 | ## Custom Data Format 18 | 19 | ### Overview 20 | 21 | The JSON have one required property, `version`, and 4 other top level properties: 22 | 23 | ```jsonc 24 | { 25 | "version": 1.1, 26 | "properties": [], 27 | "atDirectives": [], 28 | "pseudoClasses": [], 29 | "pseudoElements": [] 30 | } 31 | ``` 32 | 33 | Version denotes the schema version you are using. The latest schema version is `V1.1`. 34 | 35 | You can find other properties' shapes at [cssLanguageTypes.ts](../src/cssLanguageTypes.ts) or the [JSON Schema](./customData.schema.json). 36 | 37 | You should suffix your custom data file with `.css-data.json`, so VS Code will load the most recent schema for the JSON file to offer auto completion and error checking. 38 | 39 | ### Format 40 | 41 | All top-level properties share two basic properties, `name` and `description`. For example: 42 | 43 | ```jsonc 44 | { 45 | "version": 1.1, 46 | "properties": [ 47 | { "name": "foo", "description": "Foo property" } 48 | ], 49 | "atDirectives": [ 50 | { "name": "@foo", "description": "Foo at directive" } 51 | ], 52 | "pseudoClasses": [ 53 | { "name": ":foo", "description": "Foo pseudo class" } 54 | ], 55 | "pseudoElements": [ 56 | { "name": "::foo", "description": "Foo pseudo elements" } 57 | ] 58 | } 59 | ``` 60 | 61 | You can also specify 5 additional properties for them: 62 | 63 | ```jsonc 64 | { 65 | "properties": [ 66 | { 67 | "name": "foo", 68 | "description": "Foo property", 69 | "browsers": [ 70 | "E12", 71 | "S10", 72 | "C50", 73 | "IE10", 74 | "O37" 75 | ], 76 | "baseline": { 77 | "status": "high", 78 | "baseline_low_date": "2015-09-30", 79 | "baseline_high_date": "2018-03-30" 80 | }, 81 | "status": "standard", 82 | "references": [ 83 | { 84 | "name": "My foo property reference", 85 | "url": "https://www.foo.com/property/foo" 86 | } 87 | ], 88 | "relevance": 25 89 | } 90 | ] 91 | } 92 | ``` 93 | 94 | - `browsers`: A list of supported browsers. The format is `browserName + version`. For example: `['E10', 'C30', 'FF20']`. Here are all browser names: 95 | ``` 96 | export let browserNames = { 97 | E: 'Edge', 98 | FF: 'Firefox', 99 | FFA: 'Firefox on Android', 100 | S: 'Safari', 101 | SM: 'Safari on iOS', 102 | C: 'Chrome', 103 | CA: 'Chrome on Android', 104 | IE: 'IE', 105 | O: 'Opera' 106 | }; 107 | ``` 108 | The browser compatibility will be rendered at completion and hover. Items that is supported in only one browser are dropped from completion. 109 | 110 | - `baseline`: An object containing [Baseline](https://web-platform-dx.github.io/web-features/) information about the feature's browser compatibility, as defined by the [WebDX Community Group](https://web-platform-dx.github.io/web-features/webdx-cg/). 111 | 112 | - `status`: The Baseline status is either `"false"` (limited availability across major browsers), `"low"` (newly available across major browsers), or `"high"` (widely available across major browsers). 113 | 114 | - `baseline_low_date`: A date in the format `YYYY-MM-DD` representing when the feature became newly available, or undefined if it hasn't yet reached that status. 115 | 116 | - `baseline_high_date`: A date in the format `YYYY-MM-DD` representing when the feature became widely available, or undefined if it hasn't yet reached that status. The widely available date is always 30 months after the newly available date. 117 | 118 | - `status`: The status of the item. The format is: 119 | ``` 120 | export type EntryStatus = 'standard' | 'experimental' | 'nonstandard' | 'obsolete'; 121 | ``` 122 | The status will be rendered at the top of completion and hover. For example, `nonstandard` items are prefixed with the message `🚨️ Property is nonstandard. Avoid using it.`. 123 | 124 | - `references`: A list of references. They will be displayed in Markdown form in completion and hover as `[Ref1 Name](Ref1 URL) | [Ref2 Name](Ref2 URL) | ...`. 125 | 126 | - `relevance`: A number in the range [0, 100] used for sorting. Bigger number means more relevant and will be sorted first. Entries that do not specify a relevance will get 50 as default value. 127 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 6, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "typeLike", 20 | format: ["PascalCase"], 21 | }], 22 | 23 | curly: "warn", 24 | eqeqeq: "warn", 25 | "no-throw-literal": "warn", 26 | semi: "off", 27 | }, 28 | }]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-css-languageservice", 3 | "version": "6.3.6", 4 | "description": "Language service for CSS, LESS and SCSS", 5 | "main": "./lib/umd/cssLanguageService.js", 6 | "typings": "./lib/umd/cssLanguageService", 7 | "module": "./lib/esm/cssLanguageService.js", 8 | "author": "Microsoft Corporation", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Microsoft/vscode-css-languageservice" 12 | }, 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/Microsoft/vscode-css-languageservice" 16 | }, 17 | "devDependencies": { 18 | "@types/mocha": "^10.0.10", 19 | "@types/node": "18.x", 20 | "@typescript-eslint/eslint-plugin": "^8.33.0", 21 | "@typescript-eslint/parser": "^8.33.0", 22 | "@vscode/web-custom-data": "^0.6.0", 23 | "eslint": "^9.28.0", 24 | "js-beautify": "^1.15.4", 25 | "mocha": "^11.5.0", 26 | "rimraf": "^6.0.1", 27 | "source-map-support": "^0.5.21", 28 | "typescript": "^5.8.3" 29 | }, 30 | "dependencies": { 31 | "@vscode/l10n": "^0.0.18", 32 | "vscode-languageserver-textdocument": "^1.0.12", 33 | "vscode-languageserver-types": "3.17.5", 34 | "vscode-uri": "^3.1.0" 35 | }, 36 | "scripts": { 37 | "prepack": "npm run clean && npm run compile-esm && npm run test && npm run remove-sourcemap-refs", 38 | "compile": "tsc -p ./src && npm run copy-jsbeautify && npm run lint", 39 | "compile-esm": "tsc -p ./src/tsconfig.esm.json", 40 | "clean": "rimraf lib", 41 | "remove-sourcemap-refs": "node ./build/remove-sourcemap-refs.js", 42 | "watch": "npm run copy-jsbeautify && tsc -w -p ./src", 43 | "test": "npm run compile && npm run mocha", 44 | "mocha": "mocha --require source-map-support/register", 45 | "coverage": "npm run compile && npx nyc --reporter=html --reporter=text mocha", 46 | "lint": "eslint src/**/*.ts", 47 | "update-data": "npm install @vscode/web-custom-data -D && node ./build/generateData.js", 48 | "install-types-next": "npm install vscode-languageserver-types@next -f -S && npm install vscode-languageserver-textdocument@next -f -S", 49 | "copy-jsbeautify": "node ./build/copy-jsbeautify.js", 50 | "update-jsbeautify": "npm install js-beautify && node ./build/update-jsbeautify.js", 51 | "update-jsbeautify-next": "npm install js-beautify@next && node ./build/update-jsbeautify.js" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/beautify/beautify-css.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export interface IBeautifyCSSOptions { 7 | indent_size?: number; // (4) — indentation size, 8 | indent_char?: string; // (space) — character to indent with, 9 | selector_separator_newline?: boolean; // (true) - separate selectors with newline or not (e.g. "a,\nbr" or "a, br") 10 | end_with_newline?: boolean; // (false) - end with a newline 11 | newline_between_rules?: boolean; // (true) - add a new line after every css rule 12 | space_around_selector_separator?: boolean // (false) - ensure space around selector separators: '>', '+', '~' (e.g. "a>b" -> "a > b") 13 | brace_style?: 'collapse' | 'expand'; // (collapse) - place braces on the same line (collapse) or on a new line (expand) 14 | preserve_newlines?: boolean; // (true) - whether existing line breaks before elements should be preserved 15 | max_preserve_newlines?: number; // (32786) - maximum number of line breaks to be preserved in one chunk 16 | wrap_line_length?: number; // (undefined) - warp lines after a line offset 17 | indent_empty_lines?: number; // (false) - indent empty lines 18 | eol?: string; // end of line character to use 19 | } 20 | 21 | export interface IBeautifyCSS { 22 | (value:string, options:IBeautifyCSSOptions): string; 23 | } 24 | 25 | export declare var css_beautify:IBeautifyCSS; -------------------------------------------------------------------------------- /src/beautify/beautify-license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/cssLanguageService.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { Parser } from './parser/cssParser'; 8 | import { CSSCompletion } from './services/cssCompletion'; 9 | import { CSSHover } from './services/cssHover'; 10 | import { CSSNavigation } from './services/cssNavigation'; 11 | import { CSSCodeActions } from './services/cssCodeActions'; 12 | import { CSSValidation } from './services/cssValidation'; 13 | 14 | import { SCSSParser } from './parser/scssParser'; 15 | import { SCSSCompletion } from './services/scssCompletion'; 16 | import { LESSParser } from './parser/lessParser'; 17 | import { LESSCompletion } from './services/lessCompletion'; 18 | import { getFoldingRanges } from './services/cssFolding'; 19 | import { format } from './services/cssFormatter'; 20 | 21 | import { 22 | LanguageSettings, ICompletionParticipant, DocumentContext, LanguageServiceOptions, 23 | Diagnostic, Position, CompletionList, Hover, Location, DocumentHighlight, DocumentLink, 24 | SymbolInformation, Range, CodeActionContext, Command, CodeAction, ColorInformation, 25 | Color, ColorPresentation, WorkspaceEdit, FoldingRange, SelectionRange, TextDocument, 26 | ICSSDataProvider, CSSDataV1, HoverSettings, CompletionSettings, TextEdit, CSSFormatConfiguration, DocumentSymbol 27 | } from './cssLanguageTypes'; 28 | 29 | import { CSSDataManager } from './languageFacts/dataManager'; 30 | import { CSSDataProvider } from './languageFacts/dataProvider'; 31 | import { getSelectionRanges } from './services/cssSelectionRange'; 32 | import { SCSSNavigation } from './services/scssNavigation'; 33 | import { cssData } from './data/webCustomData'; 34 | 35 | export type Stylesheet = {}; 36 | export * from './cssLanguageTypes'; 37 | 38 | export interface LanguageService { 39 | configure(raw?: LanguageSettings): void; 40 | setDataProviders(useDefaultDataProvider: boolean, customDataProviders: ICSSDataProvider[]): void; 41 | doValidation(document: TextDocument, stylesheet: Stylesheet, documentSettings?: LanguageSettings): Diagnostic[]; 42 | parseStylesheet(document: TextDocument): Stylesheet; 43 | doComplete(document: TextDocument, position: Position, stylesheet: Stylesheet, settings?: CompletionSettings): CompletionList; 44 | doComplete2(document: TextDocument, position: Position, stylesheet: Stylesheet, documentContext: DocumentContext, settings?: CompletionSettings): Promise; 45 | setCompletionParticipants(registeredCompletionParticipants: ICompletionParticipant[]): void; 46 | doHover(document: TextDocument, position: Position, stylesheet: Stylesheet, settings?: HoverSettings): Hover | null; 47 | findDefinition(document: TextDocument, position: Position, stylesheet: Stylesheet): Location | null; 48 | findReferences(document: TextDocument, position: Position, stylesheet: Stylesheet): Location[]; 49 | findDocumentHighlights(document: TextDocument, position: Position, stylesheet: Stylesheet): DocumentHighlight[]; 50 | findDocumentLinks(document: TextDocument, stylesheet: Stylesheet, documentContext: DocumentContext): DocumentLink[]; 51 | /** 52 | * Return statically resolved links, and dynamically resolved links if `fsProvider` is proved. 53 | */ 54 | findDocumentLinks2(document: TextDocument, stylesheet: Stylesheet, documentContext: DocumentContext): Promise; 55 | findDocumentSymbols(document: TextDocument, stylesheet: Stylesheet): SymbolInformation[]; 56 | findDocumentSymbols2(document: TextDocument, stylesheet: Stylesheet): DocumentSymbol[]; 57 | doCodeActions(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: Stylesheet): Command[]; 58 | doCodeActions2(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: Stylesheet): CodeAction[]; 59 | findDocumentColors(document: TextDocument, stylesheet: Stylesheet): ColorInformation[]; 60 | getColorPresentations(document: TextDocument, stylesheet: Stylesheet, color: Color, range: Range): ColorPresentation[]; 61 | prepareRename(document: TextDocument, position: Position, stylesheet: Stylesheet): Range | undefined; 62 | doRename(document: TextDocument, position: Position, newName: string, stylesheet: Stylesheet): WorkspaceEdit; 63 | getFoldingRanges(document: TextDocument, context?: { rangeLimit?: number; }): FoldingRange[]; 64 | getSelectionRanges(document: TextDocument, positions: Position[], stylesheet: Stylesheet): SelectionRange[]; 65 | format(document: TextDocument, range: Range | undefined, options: CSSFormatConfiguration): TextEdit[]; 66 | 67 | } 68 | 69 | export function getDefaultCSSDataProvider(): ICSSDataProvider { 70 | return newCSSDataProvider(cssData); 71 | } 72 | 73 | export function newCSSDataProvider(data: CSSDataV1): ICSSDataProvider { 74 | return new CSSDataProvider(data); 75 | } 76 | 77 | function createFacade(parser: Parser, completion: CSSCompletion, hover: CSSHover, navigation: CSSNavigation, codeActions: CSSCodeActions, validation: CSSValidation, cssDataManager: CSSDataManager): LanguageService { 78 | return { 79 | configure: (settings) => { 80 | validation.configure(settings); 81 | completion.configure(settings?.completion); 82 | hover.configure(settings?.hover); 83 | navigation.configure(settings?.importAliases); 84 | }, 85 | setDataProviders: cssDataManager.setDataProviders.bind(cssDataManager), 86 | doValidation: validation.doValidation.bind(validation), 87 | parseStylesheet: parser.parseStylesheet.bind(parser), 88 | doComplete: completion.doComplete.bind(completion), 89 | doComplete2: completion.doComplete2.bind(completion), 90 | setCompletionParticipants: completion.setCompletionParticipants.bind(completion), 91 | doHover: hover.doHover.bind(hover), 92 | format, 93 | findDefinition: navigation.findDefinition.bind(navigation), 94 | findReferences: navigation.findReferences.bind(navigation), 95 | findDocumentHighlights: navigation.findDocumentHighlights.bind(navigation), 96 | findDocumentLinks: navigation.findDocumentLinks.bind(navigation), 97 | findDocumentLinks2: navigation.findDocumentLinks2.bind(navigation), 98 | findDocumentSymbols: navigation.findSymbolInformations.bind(navigation), 99 | findDocumentSymbols2: navigation.findDocumentSymbols.bind(navigation), 100 | doCodeActions: codeActions.doCodeActions.bind(codeActions), 101 | doCodeActions2: codeActions.doCodeActions2.bind(codeActions), 102 | findDocumentColors: navigation.findDocumentColors.bind(navigation), 103 | getColorPresentations: navigation.getColorPresentations.bind(navigation), 104 | prepareRename: navigation.prepareRename.bind(navigation), 105 | doRename: navigation.doRename.bind(navigation), 106 | getFoldingRanges, 107 | getSelectionRanges 108 | }; 109 | } 110 | 111 | const defaultLanguageServiceOptions = {}; 112 | 113 | export function getCSSLanguageService(options: LanguageServiceOptions = defaultLanguageServiceOptions): LanguageService { 114 | const cssDataManager = new CSSDataManager(options); 115 | return createFacade( 116 | new Parser(), 117 | new CSSCompletion(null, options, cssDataManager), 118 | new CSSHover(options && options.clientCapabilities, cssDataManager), 119 | new CSSNavigation(options && options.fileSystemProvider, false), 120 | new CSSCodeActions(cssDataManager), 121 | new CSSValidation(cssDataManager), 122 | cssDataManager 123 | ); 124 | } 125 | 126 | export function getSCSSLanguageService(options: LanguageServiceOptions = defaultLanguageServiceOptions): LanguageService { 127 | const cssDataManager = new CSSDataManager(options); 128 | return createFacade( 129 | new SCSSParser(), 130 | new SCSSCompletion(options, cssDataManager), 131 | new CSSHover(options && options.clientCapabilities, cssDataManager), 132 | new SCSSNavigation(options && options.fileSystemProvider), 133 | new CSSCodeActions(cssDataManager), 134 | new CSSValidation(cssDataManager), 135 | cssDataManager 136 | ); 137 | } 138 | 139 | export function getLESSLanguageService(options: LanguageServiceOptions = defaultLanguageServiceOptions): LanguageService { 140 | const cssDataManager = new CSSDataManager(options); 141 | return createFacade( 142 | new LESSParser(), 143 | new LESSCompletion(options, cssDataManager), 144 | new CSSHover(options && options.clientCapabilities, cssDataManager), 145 | new CSSNavigation(options && options.fileSystemProvider, true), 146 | new CSSCodeActions(cssDataManager), 147 | new CSSValidation(cssDataManager), 148 | cssDataManager 149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /src/cssLanguageTypes.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { 8 | Range, Position, DocumentUri, MarkupContent, MarkupKind, 9 | Color, ColorInformation, ColorPresentation, 10 | FoldingRange, FoldingRangeKind, SelectionRange, 11 | Diagnostic, DiagnosticSeverity, 12 | CompletionItem, CompletionItemKind, CompletionList, CompletionItemTag, 13 | InsertTextFormat, DefinitionLink, 14 | SymbolInformation, SymbolKind, DocumentSymbol, Location, Hover, MarkedString, 15 | CodeActionContext, Command, CodeAction, 16 | DocumentHighlight, DocumentLink, WorkspaceEdit, 17 | TextEdit, CodeActionKind, 18 | TextDocumentEdit, VersionedTextDocumentIdentifier, DocumentHighlightKind 19 | } from 'vscode-languageserver-types'; 20 | 21 | import { TextDocument } from 'vscode-languageserver-textdocument'; 22 | 23 | export { 24 | TextDocument, 25 | Range, Position, DocumentUri, MarkupContent, MarkupKind, 26 | Color, ColorInformation, ColorPresentation, 27 | FoldingRange, FoldingRangeKind, SelectionRange, 28 | Diagnostic, DiagnosticSeverity, 29 | CompletionItem, CompletionItemKind, CompletionList, CompletionItemTag, 30 | InsertTextFormat, DefinitionLink, 31 | SymbolInformation, SymbolKind, DocumentSymbol, Location, Hover, MarkedString, 32 | CodeActionContext, Command, CodeAction, 33 | DocumentHighlight, DocumentLink, WorkspaceEdit, 34 | TextEdit, CodeActionKind, 35 | TextDocumentEdit, VersionedTextDocumentIdentifier, DocumentHighlightKind 36 | }; 37 | 38 | export type LintSettings = { [key: string]: any }; 39 | 40 | export interface CompletionSettings { 41 | triggerPropertyValueCompletion: boolean; 42 | completePropertyWithSemicolon?: boolean; 43 | } 44 | 45 | export interface LanguageSettings { 46 | validate?: boolean; 47 | lint?: LintSettings; 48 | completion?: CompletionSettings; 49 | hover?: HoverSettings; 50 | importAliases?: AliasSettings; 51 | } 52 | 53 | export interface AliasSettings { 54 | [key: string]: string; 55 | } 56 | 57 | export interface HoverSettings { 58 | documentation?: boolean; 59 | references?: boolean 60 | } 61 | 62 | export interface PropertyCompletionContext { 63 | propertyName: string; 64 | range: Range; 65 | } 66 | 67 | export interface PropertyValueCompletionContext { 68 | propertyName: string; 69 | propertyValue?: string; 70 | range: Range; 71 | } 72 | 73 | export interface URILiteralCompletionContext { 74 | uriValue: string; 75 | position: Position; 76 | range: Range; 77 | } 78 | 79 | export interface ImportPathCompletionContext { 80 | pathValue: string; 81 | position: Position; 82 | range: Range; 83 | } 84 | 85 | export interface MixinReferenceCompletionContext { 86 | mixinName: string; 87 | range: Range; 88 | } 89 | 90 | export interface ICompletionParticipant { 91 | onCssProperty?: (context: PropertyCompletionContext) => void; 92 | onCssPropertyValue?: (context: PropertyValueCompletionContext) => void; 93 | onCssURILiteralValue?: (context: URILiteralCompletionContext) => void; 94 | onCssImportPath?: (context: ImportPathCompletionContext) => void; 95 | onCssMixinReference?: (context: MixinReferenceCompletionContext) => void; 96 | } 97 | 98 | export interface DocumentContext { 99 | resolveReference(ref: string, baseUrl: string): string | undefined; 100 | } 101 | 102 | /** 103 | * Describes what LSP capabilities the client supports 104 | */ 105 | export interface ClientCapabilities { 106 | /** 107 | * The text document client capabilities 108 | */ 109 | textDocument?: { 110 | /** 111 | * Capabilities specific to completions. 112 | */ 113 | completion?: { 114 | /** 115 | * The client supports the following `CompletionItem` specific 116 | * capabilities. 117 | */ 118 | completionItem?: { 119 | /** 120 | * Client supports the follow content formats for the documentation 121 | * property. The order describes the preferred format of the client. 122 | */ 123 | documentationFormat?: MarkupKind[]; 124 | }; 125 | 126 | }; 127 | /** 128 | * Capabilities specific to hovers. 129 | */ 130 | hover?: { 131 | /** 132 | * Client supports the follow content formats for the content 133 | * property. The order describes the preferred format of the client. 134 | */ 135 | contentFormat?: MarkupKind[]; 136 | }; 137 | }; 138 | } 139 | 140 | export namespace ClientCapabilities { 141 | export const LATEST: ClientCapabilities = { 142 | textDocument: { 143 | completion: { 144 | completionItem: { 145 | documentationFormat: [MarkupKind.Markdown, MarkupKind.PlainText] 146 | } 147 | }, 148 | hover: { 149 | contentFormat: [MarkupKind.Markdown, MarkupKind.PlainText] 150 | } 151 | } 152 | }; 153 | } 154 | 155 | export interface LanguageServiceOptions { 156 | /** 157 | * Unless set to false, the default CSS data provider will be used 158 | * along with the providers from customDataProviders. 159 | * Defaults to true. 160 | */ 161 | useDefaultDataProvider?: boolean; 162 | 163 | /** 164 | * Provide data that could enhance the service's understanding of 165 | * CSS property / at-rule / pseudo-class / pseudo-element 166 | */ 167 | customDataProviders?: ICSSDataProvider[]; 168 | 169 | /** 170 | * Abstract file system access away from the service. 171 | * Used for dynamic link resolving, path completion, etc. 172 | */ 173 | fileSystemProvider?: FileSystemProvider; 174 | 175 | /** 176 | * Describes the LSP capabilities the client supports. 177 | */ 178 | clientCapabilities?: ClientCapabilities; 179 | } 180 | 181 | export type EntryStatus = 'standard' | 'experimental' | 'nonstandard' | 'obsolete'; 182 | 183 | export interface IReference { 184 | name: string; 185 | url: string; 186 | } 187 | 188 | export interface IPropertyData { 189 | name: string; 190 | description?: string | MarkupContent; 191 | browsers?: string[]; 192 | baseline?: BaselineStatus; 193 | restrictions?: string[]; 194 | status?: EntryStatus; 195 | syntax?: string; 196 | values?: IValueData[]; 197 | references?: IReference[]; 198 | relevance?: number; 199 | atRule?: string; 200 | } 201 | export interface IDescriptorData { 202 | name: string, 203 | description?: string, 204 | references?: IReference[], 205 | syntax?: string, 206 | type?: string, 207 | values?: IValueData[] 208 | browsers?: string[]; 209 | baseline?: BaselineStatus; 210 | status?: EntryStatus; 211 | } 212 | export interface IAtDirectiveData { 213 | name: string; 214 | description?: string | MarkupContent; 215 | browsers?: string[]; 216 | baseline?: BaselineStatus; 217 | status?: EntryStatus; 218 | references?: IReference[]; 219 | descriptors?: IDescriptorData[]; 220 | } 221 | export interface IPseudoClassData { 222 | name: string; 223 | description?: string | MarkupContent; 224 | browsers?: string[]; 225 | baseline?: BaselineStatus; 226 | status?: EntryStatus; 227 | references?: IReference[]; 228 | } 229 | export interface IPseudoElementData { 230 | name: string; 231 | description?: string | MarkupContent; 232 | browsers?: string[]; 233 | baseline?: BaselineStatus; 234 | status?: EntryStatus; 235 | references?: IReference[]; 236 | } 237 | 238 | export interface IValueData { 239 | name: string; 240 | description?: string | MarkupContent; 241 | browsers?: string[]; 242 | baseline?: BaselineStatus; 243 | status?: EntryStatus; 244 | references?: IReference[]; 245 | } 246 | 247 | export interface CSSDataV1 { 248 | version: 1 | 1.1; 249 | properties?: IPropertyData[]; 250 | atDirectives?: IAtDirectiveData[]; 251 | pseudoClasses?: IPseudoClassData[]; 252 | pseudoElements?: IPseudoElementData[]; 253 | } 254 | 255 | export interface BaselineStatus { 256 | status: Baseline; 257 | baseline_low_date?: string; 258 | baseline_high_date?: string; 259 | } 260 | 261 | export type Baseline = 'false' | 'low' | 'high'; 262 | 263 | export interface ICSSDataProvider { 264 | provideProperties(): IPropertyData[]; 265 | provideAtDirectives(): IAtDirectiveData[]; 266 | providePseudoClasses(): IPseudoClassData[]; 267 | providePseudoElements(): IPseudoElementData[]; 268 | } 269 | 270 | export enum FileType { 271 | /** 272 | * The file type is unknown. 273 | */ 274 | Unknown = 0, 275 | /** 276 | * A regular file. 277 | */ 278 | File = 1, 279 | /** 280 | * A directory. 281 | */ 282 | Directory = 2, 283 | /** 284 | * A symbolic link to a file. 285 | */ 286 | SymbolicLink = 64 287 | } 288 | 289 | export interface FileStat { 290 | /** 291 | * The type of the file, e.g. is a regular file, a directory, or symbolic link 292 | * to a file. 293 | */ 294 | type: FileType; 295 | /** 296 | * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. 297 | */ 298 | ctime: number; 299 | /** 300 | * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. 301 | */ 302 | mtime: number; 303 | /** 304 | * The size in bytes. 305 | */ 306 | size: number; 307 | } 308 | 309 | export interface FileSystemProvider { 310 | stat(uri: DocumentUri): Promise; 311 | readDirectory?(uri: DocumentUri): Promise<[string, FileType][]>; 312 | getContent?(uri: DocumentUri, encoding?: string): Promise; 313 | } 314 | 315 | export interface CSSFormatConfiguration { 316 | /** indentation size. Default: 4 */ 317 | tabSize?: number; 318 | /** Whether to use spaces or tabs */ 319 | insertSpaces?: boolean; 320 | /** end with a newline: Default: false */ 321 | insertFinalNewline?: boolean; 322 | /** separate selectors with newline (e.g. "a,\nbr" or "a, br"): Default: true */ 323 | newlineBetweenSelectors?: boolean; 324 | /** add a new line after every css rule: Default: true */ 325 | newlineBetweenRules?: boolean; 326 | /** ensure space around selector separators: '>', '+', '~' (e.g. "a>b" -> "a > b"): Default: false */ 327 | spaceAroundSelectorSeparator?: boolean; 328 | /** put braces on the same line as rules (`collapse`), or put braces on own line, Allman / ANSI style (`expand`). Default `collapse` */ 329 | braceStyle?: 'collapse' | 'expand'; 330 | /** whether existing line breaks before elements should be preserved. Default: true */ 331 | preserveNewLines?: boolean; 332 | /** maximum number of line breaks to be preserved in one chunk. Default: unlimited */ 333 | maxPreserveNewLines?: number; 334 | /** maximum amount of characters per line (0/undefined = disabled). Default: disabled. */ 335 | wrapLineLength?: number; 336 | /** add indenting whitespace to empty lines. Default: false */ 337 | indentEmptyLines?: boolean; 338 | 339 | /** @deprecated Use newlineBetweenSelectors instead*/ 340 | selectorSeparatorNewline?: boolean; 341 | 342 | } 343 | -------------------------------------------------------------------------------- /src/languageFacts/dataManager.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { 8 | ICSSDataProvider, 9 | IPropertyData, 10 | IAtDirectiveData, 11 | IPseudoClassData, 12 | IPseudoElementData, 13 | } from '../cssLanguageTypes'; 14 | 15 | import * as objects from '../utils/objects'; 16 | import { cssData } from '../data/webCustomData'; 17 | import { CSSDataProvider } from './dataProvider'; 18 | 19 | export class CSSDataManager { 20 | private dataProviders: ICSSDataProvider[] = []; 21 | 22 | private _propertySet: { [k: string]: IPropertyData } = {}; 23 | private _atDirectiveSet: { [k: string]: IAtDirectiveData } = {}; 24 | private _pseudoClassSet: { [k: string]: IPseudoClassData } = {}; 25 | private _pseudoElementSet: { [k: string]: IPseudoElementData } = {}; 26 | 27 | private _properties: IPropertyData[] = []; 28 | private _atDirectives: IAtDirectiveData[] = []; 29 | private _pseudoClasses: IPseudoClassData[] = []; 30 | private _pseudoElements: IPseudoElementData[] = []; 31 | 32 | constructor(options?: { useDefaultDataProvider?: boolean, customDataProviders?: ICSSDataProvider[] }) { 33 | this.setDataProviders(options?.useDefaultDataProvider !== false, options?.customDataProviders || []); 34 | } 35 | 36 | setDataProviders(builtIn: boolean, providers: ICSSDataProvider[]) { 37 | this.dataProviders = []; 38 | if (builtIn) { 39 | this.dataProviders.push(new CSSDataProvider(cssData)); 40 | } 41 | this.dataProviders.push(...providers); 42 | this.collectData(); 43 | } 44 | 45 | /** 46 | * Collect all data & handle duplicates 47 | */ 48 | private collectData() { 49 | this._propertySet = {}; 50 | this._atDirectiveSet = {}; 51 | this._pseudoClassSet = {}; 52 | this._pseudoElementSet = {}; 53 | 54 | this.dataProviders.forEach(provider => { 55 | provider.provideProperties().forEach(p => { 56 | if (!this._propertySet[p.name]) { 57 | this._propertySet[p.name] = p; 58 | } 59 | }); 60 | provider.provideAtDirectives().forEach(p => { 61 | if (!this._atDirectiveSet[p.name]) { 62 | this._atDirectiveSet[p.name] = p; 63 | } 64 | }); 65 | provider.providePseudoClasses().forEach(p => { 66 | if (!this._pseudoClassSet[p.name]) { 67 | this._pseudoClassSet[p.name] = p; 68 | } 69 | }); 70 | provider.providePseudoElements().forEach(p => { 71 | if (!this._pseudoElementSet[p.name]) { 72 | this._pseudoElementSet[p.name] = p; 73 | } 74 | }); 75 | }); 76 | 77 | this._properties = objects.values(this._propertySet); 78 | this._atDirectives = objects.values(this._atDirectiveSet); 79 | this._pseudoClasses = objects.values(this._pseudoClassSet); 80 | this._pseudoElements = objects.values(this._pseudoElementSet); 81 | } 82 | 83 | getProperty(name: string) : IPropertyData | undefined { return this._propertySet[name]; } 84 | getAtDirective(name: string) : IAtDirectiveData | undefined { return this._atDirectiveSet[name]; } 85 | getPseudoClass(name: string) : IPseudoClassData | undefined { return this._pseudoClassSet[name]; } 86 | getPseudoElement(name: string) : IPseudoElementData | undefined { return this._pseudoElementSet[name]; } 87 | 88 | getProperties() : IPropertyData[] { 89 | return this._properties; 90 | } 91 | getAtDirectives() : IAtDirectiveData[] { 92 | return this._atDirectives; 93 | } 94 | getPseudoClasses() : IPseudoClassData[]{ 95 | return this._pseudoClasses; 96 | } 97 | getPseudoElements() : IPseudoElementData[] { 98 | return this._pseudoElements; 99 | } 100 | 101 | isKnownProperty(name: string): boolean { 102 | return name.toLowerCase() in this._propertySet; 103 | } 104 | 105 | isStandardProperty(name: string): boolean { 106 | return this.isKnownProperty(name) && 107 | (!this._propertySet[name.toLowerCase()].status || this._propertySet[name.toLowerCase()].status === 'standard'); 108 | } 109 | } -------------------------------------------------------------------------------- /src/languageFacts/dataProvider.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { CSSDataV1, ICSSDataProvider, IPropertyData, IAtDirectiveData, IPseudoClassData, IPseudoElementData } from '../cssLanguageTypes'; 8 | 9 | export class CSSDataProvider implements ICSSDataProvider { 10 | private _properties: IPropertyData[] = []; 11 | private _atDirectives: IAtDirectiveData[] = []; 12 | private _pseudoClasses: IPseudoClassData[] = []; 13 | private _pseudoElements: IPseudoElementData[] = []; 14 | 15 | /** 16 | * Currently, unversioned data uses the V1 implementation 17 | * In the future when the provider handles multiple versions of HTML custom data, 18 | * use the latest implementation for unversioned data 19 | */ 20 | constructor(data: CSSDataV1) { 21 | this.addData(data); 22 | } 23 | 24 | provideProperties() { 25 | return this._properties; 26 | } 27 | provideAtDirectives() { 28 | return this._atDirectives; 29 | } 30 | providePseudoClasses() { 31 | return this._pseudoClasses; 32 | } 33 | providePseudoElements() { 34 | return this._pseudoElements; 35 | } 36 | 37 | private addData(data: CSSDataV1) { 38 | if (Array.isArray(data.properties)) { 39 | for (const prop of data.properties) { 40 | if (isPropertyData(prop)) { 41 | this._properties.push(prop); 42 | } 43 | } 44 | } 45 | if (Array.isArray(data.atDirectives)) { 46 | for (const prop of data.atDirectives) { 47 | if (isAtDirective(prop)) { 48 | this._atDirectives.push(prop); 49 | } 50 | } 51 | } 52 | if (Array.isArray(data.pseudoClasses)) { 53 | for (const prop of data.pseudoClasses) { 54 | if (isPseudoClassData(prop)) { 55 | this._pseudoClasses.push(prop); 56 | } 57 | } 58 | } 59 | if (Array.isArray(data.pseudoElements)) { 60 | for (const prop of data.pseudoElements) { 61 | if (isPseudoElementData(prop)) { 62 | this._pseudoElements.push(prop); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | function isPropertyData(d: any) : d is IPropertyData { 70 | return typeof d.name === 'string'; 71 | } 72 | 73 | function isAtDirective(d: any) : d is IAtDirectiveData { 74 | return typeof d.name === 'string'; 75 | } 76 | 77 | function isPseudoClassData(d: any) : d is IPseudoClassData { 78 | return typeof d.name === 'string'; 79 | } 80 | 81 | function isPseudoElementData(d: any) : d is IPseudoElementData { 82 | return typeof d.name === 'string'; 83 | } -------------------------------------------------------------------------------- /src/languageFacts/entry.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { EntryStatus, BaselineStatus, IPropertyData, IAtDirectiveData, IPseudoClassData, IPseudoElementData, IValueData, MarkupContent, MarkupKind, MarkedString, HoverSettings } from '../cssLanguageTypes'; 8 | 9 | export const browserNames = { 10 | 'C': { 11 | name: 'Chrome', 12 | platform: 'desktop' 13 | }, 14 | 'CA': { 15 | name: 'Chrome', 16 | platform: 'Android' 17 | }, 18 | 'E': { 19 | name: 'Edge', 20 | platform: 'desktop' 21 | }, 22 | 'FF': { 23 | name: 'Firefox', 24 | platform: 'desktop' 25 | }, 26 | 'FFA': { 27 | name: 'Firefox', 28 | platform: 'Android' 29 | }, 30 | 'S': { 31 | name: 'Safari', 32 | platform: 'macOS' 33 | }, 34 | 'SM': { 35 | name: 'Safari', 36 | platform: 'iOS' 37 | } 38 | }; 39 | 40 | const shortCompatPattern = /(E|FFA|FF|SM|S|CA|C|IE|O)([\d|\.]+)?/; 41 | 42 | export const BaselineImages = { 43 | BASELINE_LIMITED: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmF5LXNoYXBlIHsKICAgICAgZmlsbDogI0M2QzZDNjsgLyogTGlnaHQgbW9kZSAqLwogICAgfQoKICAgIEBtZWRpYSAocHJlZmVycy1jb2xvci1zY2hlbWU6IGRhcmspIHsKICAgICAgLmdyYXktc2hhcGUgewogICAgICAgIGZpbGw6ICM1NjU2NTY7IC8qIERhcmsgbW9kZSAqLwogICAgICB9CiAgICB9CiAgPC9zdHlsZT4KICA8cGF0aCBkPSJNMTUwIDBMMjQwIDkwTDIxMCAxMjBMMTIwIDMwTDE1MCAwWiIgZmlsbD0iI0YwOTQwOSIvPgogIDxwYXRoIGQ9Ik00MjAgMzBMNTQwIDE1MEw0MjAgMjcwTDM5MCAyNDBMNDgwIDE1MEwzOTAgNjBMNDIwIDMwWiIgY2xhc3M9ImdyYXktc2hhcGUiLz4KICA8cGF0aCBkPSJNMzMwIDE4MEwzMDAgMjEwTDM5MCAzMDBMNDIwIDI3MEwzMzAgMTgwWiIgZmlsbD0iI0YwOTQwOSIvPgogIDxwYXRoIGQ9Ik0xMjAgMzBMMTUwIDYwTDYwIDE1MEwxNTAgMjQwTDEyMCAyNzBMMCAxNTBMMTIwIDMwWiIgY2xhc3M9ImdyYXktc2hhcGUiLz4KICA8cGF0aCBkPSJNMzkwIDBMNDIwIDMwTDE1MCAzMDBMMTIwIDI3MEwzOTAgMFoiIGZpbGw9IiNGMDk0MDkiLz4KPC9zdmc+', 44 | BASELINE_LOW: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ibHVlLXNoYXBlIHsKICAgICAgZmlsbDogI0E4QzdGQTsgLyogTGlnaHQgbW9kZSAqLwogICAgfQoKICAgIEBtZWRpYSAocHJlZmVycy1jb2xvci1zY2hlbWU6IGRhcmspIHsKICAgICAgLmJsdWUtc2hhcGUgewogICAgICAgIGZpbGw6ICMyRDUwOUU7IC8qIERhcmsgbW9kZSAqLwogICAgICB9CiAgICB9CgogICAgLmRhcmtlci1ibHVlLXNoYXBlIHsKICAgICAgICBmaWxsOiAjMUI2RUYzOwogICAgfQoKICAgIEBtZWRpYSAocHJlZmVycy1jb2xvci1zY2hlbWU6IGRhcmspIHsKICAgICAgICAuZGFya2VyLWJsdWUtc2hhcGUgewogICAgICAgICAgICBmaWxsOiAjNDE4NUZGOwogICAgICAgIH0KICAgIH0KCiAgPC9zdHlsZT4KICA8cGF0aCBkPSJNMTUwIDBMMTgwIDMwTDE1MCA2MEwxMjAgMzBMMTUwIDBaIiBjbGFzcz0iYmx1ZS1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0yMTAgNjBMMjQwIDkwTDIxMCAxMjBMMTgwIDkwTDIxMCA2MFoiIGNsYXNzPSJibHVlLXNoYXBlIi8+CiAgPHBhdGggZD0iTTQ1MCA2MEw0ODAgOTBMNDUwIDEyMEw0MjAgOTBMNDUwIDYwWiIgY2xhc3M9ImJsdWUtc2hhcGUiLz4KICA8cGF0aCBkPSJNNTEwIDEyMEw1NDAgMTUwTDUxMCAxODBMNDgwIDE1MEw1MTAgMTIwWiIgY2xhc3M9ImJsdWUtc2hhcGUiLz4KICA8cGF0aCBkPSJNNDUwIDE4MEw0ODAgMjEwTDQ1MCAyNDBMNDIwIDIxMEw0NTAgMTgwWiIgY2xhc3M9ImJsdWUtc2hhcGUiLz4KICA8cGF0aCBkPSJNMzkwIDI0MEw0MjAgMjcwTDM5MCAzMDBMMzYwIDI3MEwzOTAgMjQwWiIgY2xhc3M9ImJsdWUtc2hhcGUiLz4KICA8cGF0aCBkPSJNMzMwIDE4MEwzNjAgMjEwTDMzMCAyNDBMMzAwIDIxMEwzMzAgMTgwWiIgY2xhc3M9ImJsdWUtc2hhcGUiLz4KICA8cGF0aCBkPSJNOTAgNjBMMTIwIDkwTDkwIDEyMEw2MCA5MEw5MCA2MFoiIGNsYXNzPSJibHVlLXNoYXBlIi8+CiAgPHBhdGggZD0iTTM5MCAwTDQyMCAzMEwxNTAgMzAwTDAgMTUwTDMwIDEyMEwxNTAgMjQwTDM5MCAwWiIgY2xhc3M9ImRhcmtlci1ibHVlLXNoYXBlIi8+Cjwvc3ZnPg==', 45 | BASELINE_HIGH: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgIGZpbGw6ICNDNEVFRDA7IC8qIExpZ2h0IG1vZGUgKi8KICAgIH0KCiAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKSB7CiAgICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgICAgZmlsbDogIzEyNTIyNTsgLyogRGFyayBtb2RlICovCiAgICAgIH0KICAgIH0KICA8L3N0eWxlPgogIDxwYXRoIGQ9Ik00MjAgMzBMMzkwIDYwTDQ4MCAxNTBMMzkwIDI0MEwzMzAgMTgwTDMwMCAyMTBMMzkwIDMwMEw1NDAgMTUwTDQyMCAzMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0xNTAgMEwzMCAxMjBMNjAgMTUwTDE1MCA2MEwyMTAgMTIwTDI0MCA5MEwxNTAgMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0zOTAgMEw0MjAgMzBMMTUwIDMwMEwwIDE1MEwzMCAxMjBMMTUwIDI0MEwzOTAgMFoiIGZpbGw9IiMxRUE0NDYiLz4KPC9zdmc+' 46 | } 47 | 48 | function getEntryStatus(status: EntryStatus) { 49 | switch (status) { 50 | case 'nonstandard': 51 | return '🚨️ Property is nonstandard. Avoid using it.\n\n'; 52 | case 'obsolete': 53 | return '🚨️️️ Property is obsolete. Avoid using it.\n\n'; 54 | default: 55 | return ''; 56 | } 57 | } 58 | 59 | function getEntryBaselineStatus(baseline: BaselineStatus, browsers?: string[]): string { 60 | if (baseline.status === "false") { 61 | const missingBrowsers = getMissingBaselineBrowsers(browsers); 62 | let status = `Limited availability across major browsers`; 63 | if (missingBrowsers) { 64 | status += ` (Not fully implemented in ${missingBrowsers})`; 65 | } 66 | return status; 67 | } 68 | 69 | const baselineYear = baseline.baseline_low_date?.split('-')[0]; 70 | return `${baseline.status === 'low' ? 'Newly' : 'Widely'} available across major browsers (Baseline since ${baselineYear})`; 71 | } 72 | 73 | function getEntryBaselineImage(baseline?: BaselineStatus) { 74 | if (!baseline) { 75 | return ''; 76 | } 77 | 78 | let baselineImg: string; 79 | switch (baseline?.status) { 80 | case 'low': 81 | baselineImg = BaselineImages.BASELINE_LOW; 82 | break; 83 | case 'high': 84 | baselineImg = BaselineImages.BASELINE_HIGH; 85 | break; 86 | default: 87 | baselineImg = BaselineImages.BASELINE_LIMITED; 88 | } 89 | return `![Baseline icon](${baselineImg})`; 90 | } 91 | 92 | export function getEntryDescription(entry: IEntry2, doesSupportMarkdown: boolean, settings?: HoverSettings): MarkupContent | undefined { 93 | let result: MarkupContent; 94 | 95 | if (doesSupportMarkdown) { 96 | result = { 97 | kind: 'markdown', 98 | value: getEntryMarkdownDescription(entry, settings) 99 | }; 100 | } else { 101 | result = { 102 | kind: 'plaintext', 103 | value: getEntryStringDescription(entry, settings) 104 | }; 105 | } 106 | 107 | if (result.value === '') { 108 | return undefined; 109 | } 110 | 111 | return result; 112 | } 113 | 114 | export function textToMarkedString(text: string): MarkedString { 115 | text = text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash 116 | return text.replace(//g, '>'); 117 | } 118 | 119 | function getEntryStringDescription(entry: IEntry2, settings?: HoverSettings): string { 120 | if (!entry.description || entry.description === '') { 121 | return ''; 122 | } 123 | 124 | if (typeof entry.description !== 'string') { 125 | return entry.description.value; 126 | } 127 | 128 | let result: string = ''; 129 | 130 | if (settings?.documentation !== false) { 131 | let status = ''; 132 | if (entry.status) { 133 | status = getEntryStatus(entry.status); 134 | result += status; 135 | } 136 | 137 | result += entry.description; 138 | 139 | if (entry.baseline && !status) { 140 | result += `\n\n${getEntryBaselineStatus(entry.baseline, entry.browsers)}`; 141 | } 142 | 143 | if ('syntax' in entry) { 144 | result += `\n\nSyntax: ${entry.syntax}`; 145 | } 146 | } 147 | if (entry.references && entry.references.length > 0 && settings?.references !== false) { 148 | if (result.length > 0) { 149 | result += '\n\n'; 150 | } 151 | result += entry.references.map(r => { 152 | return `${r.name}: ${r.url}`; 153 | }).join(' | '); 154 | } 155 | 156 | return result; 157 | } 158 | 159 | function getEntryMarkdownDescription(entry: IEntry2, settings?: HoverSettings): string { 160 | if (!entry.description || entry.description === '') { 161 | return ''; 162 | } 163 | 164 | let result: string = ''; 165 | 166 | if (settings?.documentation !== false) { 167 | let status = ''; 168 | if (entry.status) { 169 | status = getEntryStatus(entry.status); 170 | result += status; 171 | } 172 | 173 | if (typeof entry.description === 'string') { 174 | result += textToMarkedString(entry.description); 175 | } else { 176 | result += entry.description.kind === MarkupKind.Markdown ? entry.description.value : textToMarkedString(entry.description.value); 177 | } 178 | 179 | if (entry.baseline && !status) { 180 | result += `\n\n${getEntryBaselineImage(entry.baseline)} _${getEntryBaselineStatus(entry.baseline, entry.browsers)}_`; 181 | } 182 | 183 | if ('syntax' in entry && entry.syntax) { 184 | result += `\n\nSyntax: ${textToMarkedString(entry.syntax)}`; 185 | } 186 | } 187 | if (entry.references && entry.references.length > 0 && settings?.references !== false) { 188 | if (result.length > 0) { 189 | result += '\n\n'; 190 | } 191 | result += entry.references.map(r => { 192 | return `[${r.name}](${r.url})`; 193 | }).join(' | '); 194 | } 195 | 196 | return result; 197 | } 198 | 199 | // TODO: Remove "as any" when tsconfig supports es2021+ 200 | const missingBaselineBrowserFormatter = new (Intl as any).ListFormat("en", { 201 | style: "long", 202 | type: "disjunction", 203 | }); 204 | 205 | /** 206 | * Input is like [E12, FF28, FM28, C29, CM29, IE11, O16] 207 | * Output is like `Safari` 208 | */ 209 | export function getMissingBaselineBrowsers(browsers?: string[]): string { 210 | if (!browsers) { 211 | return ''; 212 | } 213 | const missingBrowsers = new Map(Object.entries(browserNames)); 214 | for (const shortCompatString of browsers) { 215 | const match = shortCompatPattern.exec(shortCompatString); 216 | if (!match) { 217 | continue; 218 | } 219 | const browser = match[1]; 220 | missingBrowsers.delete(browser); 221 | } 222 | 223 | return missingBaselineBrowserFormatter.format(Object.values(Array.from(missingBrowsers.entries()).reduce((browsers: Record, [browserId, browser]) => { 224 | if (browser.name in browsers || browserId === 'E') { 225 | browsers[browser.name] = browser.name; 226 | return browsers; 227 | } 228 | // distinguish between platforms when applicable 229 | browsers[browser.name] = `${browser.name} on ${browser.platform}`; 230 | return browsers; 231 | }, {}))); 232 | } 233 | 234 | export type IEntry2 = IPropertyData | IAtDirectiveData | IPseudoClassData | IPseudoElementData | IValueData; 235 | 236 | /** 237 | * Todo@Pine: Drop these two types and use IEntry2 238 | */ 239 | export interface IEntry { 240 | name: string; 241 | description?: string | MarkupContent; 242 | browsers?: string[]; 243 | restrictions?: string[]; 244 | status?: EntryStatus; 245 | syntax?: string; 246 | values?: IValue[]; 247 | } 248 | 249 | export interface IValue { 250 | name: string; 251 | description?: string | MarkupContent; 252 | browsers?: string[]; 253 | } 254 | -------------------------------------------------------------------------------- /src/languageFacts/facts.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | export * from './entry'; 8 | export * from './colors'; 9 | export * from './builtinData'; -------------------------------------------------------------------------------- /src/parser/cssErrors.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as nodes from './cssNodes'; 8 | 9 | import * as l10n from '@vscode/l10n'; 10 | 11 | export class CSSIssueType implements nodes.IRule { 12 | id: string; 13 | message: string; 14 | 15 | public constructor(id: string, message: string) { 16 | this.id = id; 17 | this.message = message; 18 | } 19 | } 20 | 21 | export const ParseError = { 22 | NumberExpected: new CSSIssueType('css-numberexpected', l10n.t("number expected")), 23 | ConditionExpected: new CSSIssueType('css-conditionexpected', l10n.t("condition expected")), 24 | RuleOrSelectorExpected: new CSSIssueType('css-ruleorselectorexpected', l10n.t("at-rule or selector expected")), 25 | DotExpected: new CSSIssueType('css-dotexpected', l10n.t("dot expected")), 26 | ColonExpected: new CSSIssueType('css-colonexpected', l10n.t("colon expected")), 27 | SemiColonExpected: new CSSIssueType('css-semicolonexpected', l10n.t("semi-colon expected")), 28 | TermExpected: new CSSIssueType('css-termexpected', l10n.t("term expected")), 29 | ExpressionExpected: new CSSIssueType('css-expressionexpected', l10n.t("expression expected")), 30 | OperatorExpected: new CSSIssueType('css-operatorexpected', l10n.t("operator expected")), 31 | IdentifierExpected: new CSSIssueType('css-identifierexpected', l10n.t("identifier expected")), 32 | PercentageExpected: new CSSIssueType('css-percentageexpected', l10n.t("percentage expected")), 33 | URIOrStringExpected: new CSSIssueType('css-uriorstringexpected', l10n.t("uri or string expected")), 34 | URIExpected: new CSSIssueType('css-uriexpected', l10n.t("URI expected")), 35 | VariableNameExpected: new CSSIssueType('css-varnameexpected', l10n.t("variable name expected")), 36 | VariableValueExpected: new CSSIssueType('css-varvalueexpected', l10n.t("variable value expected")), 37 | PropertyValueExpected: new CSSIssueType('css-propertyvalueexpected', l10n.t("property value expected")), 38 | LeftCurlyExpected: new CSSIssueType('css-lcurlyexpected', l10n.t("{ expected")), 39 | RightCurlyExpected: new CSSIssueType('css-rcurlyexpected', l10n.t("} expected")), 40 | LeftSquareBracketExpected: new CSSIssueType('css-rbracketexpected', l10n.t("[ expected")), 41 | RightSquareBracketExpected: new CSSIssueType('css-lbracketexpected', l10n.t("] expected")), 42 | LeftParenthesisExpected: new CSSIssueType('css-lparentexpected', l10n.t("( expected")), 43 | RightParenthesisExpected: new CSSIssueType('css-rparentexpected', l10n.t(") expected")), 44 | CommaExpected: new CSSIssueType('css-commaexpected', l10n.t("comma expected")), 45 | PageDirectiveOrDeclarationExpected: new CSSIssueType('css-pagedirordeclexpected', l10n.t("page directive or declaraton expected")), 46 | UnknownAtRule: new CSSIssueType('css-unknownatrule', l10n.t("at-rule unknown")), 47 | UnknownKeyword: new CSSIssueType('css-unknownkeyword', l10n.t("unknown keyword")), 48 | SelectorExpected: new CSSIssueType('css-selectorexpected', l10n.t("selector expected")), 49 | StringLiteralExpected: new CSSIssueType('css-stringliteralexpected', l10n.t("string literal expected")), 50 | WhitespaceExpected: new CSSIssueType('css-whitespaceexpected', l10n.t("whitespace expected")), 51 | MediaQueryExpected: new CSSIssueType('css-mediaqueryexpected', l10n.t("media query expected")), 52 | IdentifierOrWildcardExpected: new CSSIssueType('css-idorwildcardexpected', l10n.t("identifier or wildcard expected")), 53 | WildcardExpected: new CSSIssueType('css-wildcardexpected', l10n.t("wildcard expected")), 54 | IdentifierOrVariableExpected: new CSSIssueType('css-idorvarexpected', l10n.t("identifier or variable expected")), 55 | }; 56 | -------------------------------------------------------------------------------- /src/parser/lessScanner.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as scanner from './cssScanner'; 8 | 9 | const _FSL = '/'.charCodeAt(0); 10 | const _NWL = '\n'.charCodeAt(0); 11 | const _CAR = '\r'.charCodeAt(0); 12 | const _LFD = '\f'.charCodeAt(0); 13 | const _TIC = '`'.charCodeAt(0); 14 | const _DOT = '.'.charCodeAt(0); 15 | 16 | let customTokenValue = scanner.TokenType.CustomToken; 17 | export const Ellipsis: scanner.TokenType = customTokenValue++; 18 | 19 | export class LESSScanner extends scanner.Scanner { 20 | 21 | protected scanNext(offset: number): scanner.IToken { 22 | 23 | // LESS: escaped JavaScript code `const a = "dddd"` 24 | const tokenType = this.escapedJavaScript(); 25 | if (tokenType !== null) { 26 | return this.finishToken(offset, tokenType); 27 | } 28 | 29 | if (this.stream.advanceIfChars([_DOT, _DOT, _DOT])) { 30 | return this.finishToken(offset, Ellipsis); 31 | } 32 | 33 | return super.scanNext(offset); 34 | } 35 | 36 | protected comment(): boolean { 37 | if (super.comment()) { 38 | return true; 39 | } 40 | if (!this.inURL && this.stream.advanceIfChars([_FSL, _FSL])) { 41 | this.stream.advanceWhileChar((ch: number) => { 42 | switch (ch) { 43 | case _NWL: 44 | case _CAR: 45 | case _LFD: 46 | return false; 47 | default: 48 | return true; 49 | } 50 | }); 51 | return true; 52 | } else { 53 | return false; 54 | } 55 | } 56 | 57 | private escapedJavaScript(): scanner.TokenType | null { 58 | const ch = this.stream.peekChar(); 59 | if (ch === _TIC) { 60 | this.stream.advance(1); 61 | this.stream.advanceWhileChar((ch) => { return ch !== _TIC; }); 62 | return this.stream.advanceIfChar(_TIC) ? scanner.TokenType.EscapedJavaScript : scanner.TokenType.BadEscapedJavaScript; 63 | } 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/parser/scssErrors.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as nodes from './cssNodes'; 8 | 9 | import * as l10n from '@vscode/l10n'; 10 | 11 | export class SCSSIssueType implements nodes.IRule { 12 | id: string; 13 | message: string; 14 | 15 | public constructor(id: string, message: string) { 16 | this.id = id; 17 | this.message = message; 18 | } 19 | } 20 | 21 | export const SCSSParseError = { 22 | FromExpected: new SCSSIssueType('scss-fromexpected', l10n.t("'from' expected")), 23 | ThroughOrToExpected: new SCSSIssueType('scss-throughexpected', l10n.t("'through' or 'to' expected")), 24 | InExpected: new SCSSIssueType('scss-fromexpected', l10n.t("'in' expected")), 25 | }; 26 | -------------------------------------------------------------------------------- /src/parser/scssScanner.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { TokenType, Scanner, IToken } from './cssScanner'; 8 | 9 | const _FSL = '/'.charCodeAt(0); 10 | const _NWL = '\n'.charCodeAt(0); 11 | const _CAR = '\r'.charCodeAt(0); 12 | const _LFD = '\f'.charCodeAt(0); 13 | 14 | const _DLR = '$'.charCodeAt(0); 15 | const _HSH = '#'.charCodeAt(0); 16 | const _CUL = '{'.charCodeAt(0); 17 | const _EQS = '='.charCodeAt(0); 18 | const _BNG = '!'.charCodeAt(0); 19 | const _LAN = '<'.charCodeAt(0); 20 | const _RAN = '>'.charCodeAt(0); 21 | const _DOT = '.'.charCodeAt(0); 22 | const _ATS = '@'.charCodeAt(0); 23 | 24 | let customTokenValue = TokenType.CustomToken; 25 | 26 | export const VariableName = customTokenValue++; 27 | export const InterpolationFunction: TokenType = customTokenValue++; 28 | export const Default: TokenType = customTokenValue++; 29 | export const EqualsOperator: TokenType = customTokenValue++; 30 | export const NotEqualsOperator: TokenType = customTokenValue++; 31 | export const GreaterEqualsOperator: TokenType = customTokenValue++; 32 | export const SmallerEqualsOperator: TokenType = customTokenValue++; 33 | export const Ellipsis: TokenType = customTokenValue++; 34 | export const Module: TokenType = customTokenValue++; 35 | 36 | export class SCSSScanner extends Scanner { 37 | 38 | protected scanNext(offset: number): IToken { 39 | 40 | // scss variable 41 | if (this.stream.advanceIfChar(_DLR)) { 42 | const content = ['$']; 43 | if (this.ident(content)) { 44 | return this.finishToken(offset, VariableName, content.join('')); 45 | } else { 46 | this.stream.goBackTo(offset); 47 | } 48 | } 49 | 50 | // scss: interpolation function #{..}) 51 | if (this.stream.advanceIfChars([_HSH, _CUL])) { 52 | return this.finishToken(offset, InterpolationFunction); 53 | } 54 | 55 | // operator == 56 | if (this.stream.advanceIfChars([_EQS, _EQS])) { 57 | return this.finishToken(offset, EqualsOperator); 58 | } 59 | 60 | // operator != 61 | if (this.stream.advanceIfChars([_BNG, _EQS])) { 62 | return this.finishToken(offset, NotEqualsOperator); 63 | } 64 | 65 | // operators <, <= 66 | if (this.stream.advanceIfChar(_LAN)) { 67 | if (this.stream.advanceIfChar(_EQS)) { 68 | return this.finishToken(offset, SmallerEqualsOperator); 69 | } 70 | return this.finishToken(offset, TokenType.Delim); 71 | } 72 | 73 | // ooperators >, >= 74 | if (this.stream.advanceIfChar(_RAN)) { 75 | if (this.stream.advanceIfChar(_EQS)) { 76 | return this.finishToken(offset, GreaterEqualsOperator); 77 | } 78 | return this.finishToken(offset, TokenType.Delim); 79 | } 80 | 81 | // ellipis 82 | if (this.stream.advanceIfChars([_DOT, _DOT, _DOT])) { 83 | return this.finishToken(offset, Ellipsis); 84 | } 85 | 86 | return super.scanNext(offset); 87 | } 88 | 89 | protected comment(): boolean { 90 | if (super.comment()) { 91 | return true; 92 | } 93 | if (!this.inURL && this.stream.advanceIfChars([_FSL, _FSL])) { 94 | this.stream.advanceWhileChar((ch: number) => { 95 | switch (ch) { 96 | case _NWL: 97 | case _CAR: 98 | case _LFD: 99 | return false; 100 | default: 101 | return true; 102 | } 103 | }); 104 | return true; 105 | } else { 106 | return false; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/services/cssCodeActions.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as nodes from '../parser/cssNodes'; 8 | import { difference } from '../utils/strings'; 9 | import { Rules } from '../services/lintRules'; 10 | import { 11 | Range, CodeActionContext, Diagnostic, Command, TextEdit, CodeAction, WorkspaceEdit, CodeActionKind, 12 | TextDocumentEdit, VersionedTextDocumentIdentifier, TextDocument, ICSSDataProvider 13 | } from '../cssLanguageTypes'; 14 | import * as l10n from '@vscode/l10n'; 15 | import { CSSDataManager } from '../languageFacts/dataManager'; 16 | 17 | export class CSSCodeActions { 18 | 19 | constructor(private readonly cssDataManager: CSSDataManager) { 20 | } 21 | 22 | public doCodeActions(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: nodes.Stylesheet): Command[] { 23 | return this.doCodeActions2(document, range, context, stylesheet).map(ca => { 24 | const textDocumentEdit: TextDocumentEdit | undefined = ca.edit && ca.edit.documentChanges && ca.edit.documentChanges[0] as TextDocumentEdit; 25 | return Command.create(ca.title, '_css.applyCodeAction', document.uri, document.version, textDocumentEdit && textDocumentEdit.edits); 26 | }); 27 | } 28 | 29 | public doCodeActions2(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: nodes.Stylesheet): CodeAction[] { 30 | const result: CodeAction[] = []; 31 | if (context.diagnostics) { 32 | for (const diagnostic of context.diagnostics) { 33 | this.appendFixesForMarker(document, stylesheet, diagnostic, result); 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | private getFixesForUnknownProperty(document: TextDocument, property: nodes.Property, marker: Diagnostic, result: CodeAction[]): void { 40 | 41 | interface RankedProperty { 42 | property: string; 43 | score: number; 44 | } 45 | 46 | const propertyName = property.getName(); 47 | const candidates: RankedProperty[] = []; 48 | 49 | this.cssDataManager.getProperties().forEach(p => { 50 | const score = difference(propertyName, p.name); 51 | if (score >= propertyName.length / 2 /*score_lim*/) { 52 | candidates.push({ property: p.name, score }); 53 | } 54 | }); 55 | 56 | // Sort in descending order. 57 | candidates.sort((a: RankedProperty, b: RankedProperty) => { 58 | return b.score - a.score || a.property.localeCompare(b.property); 59 | }); 60 | 61 | let maxActions = 3; 62 | for (const candidate of candidates) { 63 | const propertyName = candidate.property; 64 | const title = l10n.t("Rename to '{0}'", propertyName); 65 | const edit = TextEdit.replace(marker.range, propertyName); 66 | const documentIdentifier = VersionedTextDocumentIdentifier.create(document.uri, document.version); 67 | const workspaceEdit: WorkspaceEdit = { documentChanges: [TextDocumentEdit.create(documentIdentifier, [edit])] }; 68 | const codeAction = CodeAction.create(title, workspaceEdit, CodeActionKind.QuickFix); 69 | codeAction.diagnostics = [marker]; 70 | result.push(codeAction); 71 | if (--maxActions <= 0) { 72 | return; 73 | } 74 | } 75 | } 76 | 77 | private appendFixesForMarker(document: TextDocument, stylesheet: nodes.Stylesheet, marker: Diagnostic, result: CodeAction[]): void { 78 | 79 | if (marker.code !== Rules.UnknownProperty.id) { 80 | return; 81 | } 82 | const offset = document.offsetAt(marker.range.start); 83 | const end = document.offsetAt(marker.range.end); 84 | const nodepath = nodes.getNodePath(stylesheet, offset); 85 | 86 | for (let i = nodepath.length - 1; i >= 0; i--) { 87 | const node = nodepath[i]; 88 | if (node instanceof nodes.Declaration) { 89 | const property = (node).getProperty(); 90 | if (property && property.offset === offset && property.end === end) { 91 | this.getFixesForUnknownProperty(document, property, marker, result); 92 | return; 93 | } 94 | } 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/services/cssFolding.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { TextDocument, FoldingRange, FoldingRangeKind } from '../cssLanguageTypes'; 8 | import { TokenType, Scanner, IToken } from '../parser/cssScanner'; 9 | import { SCSSScanner, InterpolationFunction } from '../parser/scssScanner'; 10 | import { LESSScanner } from '../parser/lessScanner'; 11 | 12 | type DelimiterType = 'brace' | 'comment'; 13 | type Delimiter = { line: number, type: DelimiterType, isStart: boolean }; 14 | 15 | export function getFoldingRanges(document: TextDocument, context: { rangeLimit?: number; }): FoldingRange[] { 16 | const ranges = computeFoldingRanges(document); 17 | return limitFoldingRanges(ranges, context); 18 | } 19 | 20 | function computeFoldingRanges(document: TextDocument): FoldingRange[] { 21 | function getStartLine(t: IToken) { 22 | return document.positionAt(t.offset).line; 23 | } 24 | function getEndLine(t: IToken) { 25 | return document.positionAt(t.offset + t.len).line; 26 | } 27 | function getScanner() { 28 | switch (document.languageId) { 29 | case 'scss': 30 | return new SCSSScanner(); 31 | case 'less': 32 | return new LESSScanner(); 33 | default: 34 | return new Scanner(); 35 | } 36 | } 37 | function tokenToRange(t: IToken, kind?: FoldingRangeKind | string): FoldingRange | null { 38 | const startLine = getStartLine(t); 39 | const endLine = getEndLine(t); 40 | 41 | if (startLine !== endLine) { 42 | return { 43 | startLine, 44 | endLine, 45 | kind 46 | }; 47 | } else { 48 | return null; 49 | } 50 | } 51 | 52 | const ranges: FoldingRange[] = []; 53 | 54 | const delimiterStack: Delimiter[] = []; 55 | 56 | const scanner = getScanner(); 57 | scanner.ignoreComment = false; 58 | scanner.setSource(document.getText()); 59 | 60 | let token = scanner.scan(); 61 | let prevToken: IToken | null = null; 62 | while (token.type !== TokenType.EOF) { 63 | switch (token.type) { 64 | case TokenType.CurlyL: 65 | case InterpolationFunction: 66 | { 67 | delimiterStack.push({ line: getStartLine(token), type: 'brace', isStart: true }); 68 | break; 69 | } 70 | case TokenType.CurlyR: { 71 | if (delimiterStack.length !== 0) { 72 | const prevDelimiter = popPrevStartDelimiterOfType(delimiterStack, 'brace'); 73 | if (!prevDelimiter) { 74 | break; 75 | } 76 | 77 | let endLine = getEndLine(token); 78 | 79 | if (prevDelimiter.type === 'brace') { 80 | /** 81 | * Other than the case when curly brace is not on a new line by itself, for example 82 | * .foo { 83 | * color: red; } 84 | * Use endLine minus one to show ending curly brace 85 | */ 86 | if (prevToken && getEndLine(prevToken) !== endLine) { 87 | endLine--; 88 | } 89 | 90 | if (prevDelimiter.line !== endLine) { 91 | ranges.push({ 92 | startLine: prevDelimiter.line, 93 | endLine, 94 | kind: undefined 95 | }); 96 | } 97 | } 98 | } 99 | break; 100 | } 101 | /** 102 | * In CSS, there is no single line comment prefixed with // 103 | * All comments are marked as `Comment` 104 | */ 105 | case TokenType.Comment: { 106 | const commentRegionMarkerToDelimiter = (marker: string): Delimiter => { 107 | if (marker === '#region') { 108 | return { line: getStartLine(token), type: 'comment', isStart: true }; 109 | } else { 110 | return { line: getEndLine(token), type: 'comment', isStart: false }; 111 | } 112 | }; 113 | 114 | const getCurrDelimiter = (token: IToken): Delimiter | null => { 115 | const matches = token.text.match(/^\s*\/\*\s*(#region|#endregion)\b\s*(.*?)\s*\*\//); 116 | if (matches) { 117 | return commentRegionMarkerToDelimiter(matches[1]); 118 | } else if (document.languageId === 'scss' || document.languageId === 'less') { 119 | const matches = token.text.match(/^\s*\/\/\s*(#region|#endregion)\b\s*(.*?)\s*/); 120 | if (matches) { 121 | return commentRegionMarkerToDelimiter(matches[1]); 122 | } 123 | } 124 | 125 | return null; 126 | }; 127 | 128 | const currDelimiter = getCurrDelimiter(token); 129 | 130 | // /* */ comment region folding 131 | // All #region and #endregion cases 132 | if (currDelimiter) { 133 | if (currDelimiter.isStart) { 134 | delimiterStack.push(currDelimiter); 135 | } else { 136 | const prevDelimiter = popPrevStartDelimiterOfType(delimiterStack, 'comment'); 137 | if (!prevDelimiter) { 138 | break; 139 | } 140 | 141 | if (prevDelimiter.type === 'comment') { 142 | if (prevDelimiter.line !== currDelimiter.line) { 143 | ranges.push({ 144 | startLine: prevDelimiter.line, 145 | endLine: currDelimiter.line, 146 | kind: 'region' 147 | }); 148 | } 149 | } 150 | } 151 | } 152 | // Multiline comment case 153 | else { 154 | const range = tokenToRange(token, 'comment'); 155 | if (range) { 156 | ranges.push(range); 157 | } 158 | } 159 | 160 | break; 161 | } 162 | 163 | } 164 | prevToken = token; 165 | token = scanner.scan(); 166 | } 167 | 168 | return ranges; 169 | } 170 | 171 | function popPrevStartDelimiterOfType(stack: Delimiter[], type: DelimiterType): Delimiter | null { 172 | if (stack.length === 0) { 173 | return null; 174 | } 175 | 176 | for (let i = stack.length - 1; i >= 0; i--) { 177 | if (stack[i].type === type && stack[i].isStart) { 178 | return stack.splice(i, 1)[0]; 179 | } 180 | } 181 | 182 | return null; 183 | } 184 | 185 | /** 186 | * - Sort regions 187 | * - Remove invalid regions (intersections) 188 | * - If limit exceeds, only return `rangeLimit` amount of ranges 189 | */ 190 | function limitFoldingRanges(ranges: FoldingRange[], context: { rangeLimit?: number; }): FoldingRange[] { 191 | const maxRanges = context && context.rangeLimit || Number.MAX_VALUE; 192 | 193 | const sortedRanges = ranges.sort((r1, r2) => { 194 | let diff = r1.startLine - r2.startLine; 195 | if (diff === 0) { 196 | diff = r1.endLine - r2.endLine; 197 | } 198 | return diff; 199 | }); 200 | 201 | const validRanges: FoldingRange[] = []; 202 | let prevEndLine = -1; 203 | sortedRanges.forEach(r => { 204 | if (!(r.startLine < prevEndLine && prevEndLine < r.endLine)) { 205 | validRanges.push(r); 206 | prevEndLine = r.endLine; 207 | } 208 | }); 209 | 210 | if (validRanges.length < maxRanges) { 211 | return validRanges; 212 | } else { 213 | return validRanges.slice(0, maxRanges); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/services/cssFormatter.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CSSFormatConfiguration, Range, TextEdit, Position, TextDocument } from '../cssLanguageTypes'; 7 | import { css_beautify, IBeautifyCSSOptions } from '../beautify/beautify-css'; 8 | import { repeat } from '../utils/strings'; 9 | 10 | export function format(document: TextDocument, range: Range | undefined, options: CSSFormatConfiguration): TextEdit[] { 11 | let value = document.getText(); 12 | let includesEnd = true; 13 | let initialIndentLevel = 0; 14 | let inRule = false; 15 | const tabSize = options.tabSize || 4; 16 | if (range) { 17 | let startOffset = document.offsetAt(range.start); 18 | 19 | // include all leading whitespace iff at the beginning of the line 20 | let extendedStart = startOffset; 21 | while (extendedStart > 0 && isWhitespace(value, extendedStart - 1)) { 22 | extendedStart--; 23 | } 24 | if (extendedStart === 0 || isEOL(value, extendedStart - 1)) { 25 | startOffset = extendedStart; 26 | } else { 27 | // else keep at least one whitespace 28 | if (extendedStart < startOffset) { 29 | startOffset = extendedStart + 1; 30 | } 31 | } 32 | 33 | // include all following whitespace until the end of the line 34 | let endOffset = document.offsetAt(range.end); 35 | let extendedEnd = endOffset; 36 | while (extendedEnd < value.length && isWhitespace(value, extendedEnd)) { 37 | extendedEnd++; 38 | } 39 | if (extendedEnd === value.length || isEOL(value, extendedEnd)) { 40 | endOffset = extendedEnd; 41 | } 42 | range = Range.create(document.positionAt(startOffset), document.positionAt(endOffset)); 43 | 44 | // Test if inside a rule 45 | inRule = isInRule(value, startOffset); 46 | 47 | includesEnd = endOffset === value.length; 48 | value = value.substring(startOffset, endOffset); 49 | if (startOffset !== 0) { 50 | const startOfLineOffset = document.offsetAt(Position.create(range.start.line, 0)); 51 | initialIndentLevel = computeIndentLevel(document.getText(), startOfLineOffset, options); 52 | } 53 | if (inRule) { 54 | value = `{\n${trimLeft(value)}`; 55 | } 56 | } else { 57 | range = Range.create(Position.create(0, 0), document.positionAt(value.length)); 58 | } 59 | const cssOptions: IBeautifyCSSOptions = { 60 | indent_size: tabSize, 61 | indent_char: options.insertSpaces ? ' ' : '\t', 62 | end_with_newline: includesEnd && getFormatOption(options, 'insertFinalNewline', false), 63 | selector_separator_newline: getFormatOption(options, 'newlineBetweenSelectors', true), 64 | newline_between_rules: getFormatOption(options, 'newlineBetweenRules', true), 65 | space_around_selector_separator: getFormatOption(options, 'spaceAroundSelectorSeparator', false), 66 | brace_style: getFormatOption(options, 'braceStyle', 'collapse'), 67 | indent_empty_lines: getFormatOption(options, 'indentEmptyLines', false), 68 | max_preserve_newlines: getFormatOption(options, 'maxPreserveNewLines', undefined), 69 | preserve_newlines: getFormatOption(options, 'preserveNewLines', true), 70 | wrap_line_length: getFormatOption(options, 'wrapLineLength', undefined), 71 | eol: '\n' 72 | }; 73 | 74 | let result = css_beautify(value, cssOptions); 75 | if (inRule) { 76 | result = trimLeft(result.substring(2)); 77 | } 78 | if (initialIndentLevel > 0) { 79 | const indent = options.insertSpaces ? repeat(' ', tabSize * initialIndentLevel) : repeat('\t', initialIndentLevel); 80 | result = result.split('\n').join('\n' + indent); 81 | if (range.start.character === 0) { 82 | result = indent + result; // keep the indent 83 | } 84 | } 85 | return [{ 86 | range: range, 87 | newText: result 88 | }]; 89 | } 90 | 91 | function trimLeft(str: string) { 92 | return str.replace(/^\s+/, ''); 93 | } 94 | 95 | const _CUL = '{'.charCodeAt(0); 96 | const _CUR = '}'.charCodeAt(0); 97 | 98 | function isInRule(str: string, offset: number) { 99 | while (offset >= 0) { 100 | const ch = str.charCodeAt(offset); 101 | if (ch === _CUL) { 102 | return true; 103 | } else if (ch === _CUR) { 104 | return false; 105 | } 106 | offset--; 107 | } 108 | return false; 109 | } 110 | 111 | function getFormatOption(options: CSSFormatConfiguration, key: keyof CSSFormatConfiguration, dflt: any): any { 112 | if (options && options.hasOwnProperty(key)) { 113 | const value = options[key]; 114 | if (value !== null) { 115 | return value; 116 | } 117 | } 118 | return dflt; 119 | } 120 | 121 | function computeIndentLevel(content: string, offset: number, options: CSSFormatConfiguration): number { 122 | let i = offset; 123 | let nChars = 0; 124 | const tabSize = options.tabSize || 4; 125 | while (i < content.length) { 126 | const ch = content.charAt(i); 127 | if (ch === ' ') { 128 | nChars++; 129 | } else if (ch === '\t') { 130 | nChars += tabSize; 131 | } else { 132 | break; 133 | } 134 | i++; 135 | } 136 | return Math.floor(nChars / tabSize); 137 | } 138 | 139 | function isEOL(text: string, offset: number) { 140 | return '\r\n'.indexOf(text.charAt(offset)) !== -1; 141 | } 142 | 143 | function isWhitespace(text: string, offset: number) { 144 | return ' \t'.indexOf(text.charAt(offset)) !== -1; 145 | } 146 | -------------------------------------------------------------------------------- /src/services/cssHover.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as nodes from '../parser/cssNodes'; 8 | import * as languageFacts from '../languageFacts/facts'; 9 | import { SelectorPrinting } from './selectorPrinting'; 10 | import { startsWith } from '../utils/strings'; 11 | import { TextDocument, Range, Position, Hover, MarkedString, MarkupContent, MarkupKind, ClientCapabilities, HoverSettings } from '../cssLanguageTypes'; 12 | import { isDefined } from '../utils/objects'; 13 | import { CSSDataManager } from '../languageFacts/dataManager'; 14 | 15 | export class CSSHover { 16 | private supportsMarkdown: boolean | undefined; 17 | private readonly selectorPrinting: SelectorPrinting; 18 | private defaultSettings?: HoverSettings; 19 | 20 | constructor( 21 | private readonly clientCapabilities: ClientCapabilities | undefined, 22 | private readonly cssDataManager: CSSDataManager, 23 | ) { 24 | this.selectorPrinting = new SelectorPrinting(cssDataManager); 25 | } 26 | 27 | public configure(settings: HoverSettings | undefined) { 28 | this.defaultSettings = settings; 29 | } 30 | 31 | public doHover(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet, settings = this.defaultSettings): Hover | null { 32 | function getRange(node: nodes.Node) { 33 | return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); 34 | } 35 | const offset = document.offsetAt(position); 36 | const nodepath = nodes.getNodePath(stylesheet, offset); 37 | 38 | /** 39 | * nodepath is top-down 40 | * Build up the hover by appending inner node's information 41 | */ 42 | let hover: Hover | null = null; 43 | let selectorContexts: string[] = []; 44 | 45 | for (let i = 0; i < nodepath.length; i++) { 46 | const node = nodepath[i]; 47 | 48 | if (node instanceof nodes.Scope) { 49 | const scopeLimits = node.getChild(0) 50 | 51 | if (scopeLimits instanceof nodes.ScopeLimits) { 52 | const scopeName = `${scopeLimits.getName()}` 53 | selectorContexts.push(`@scope${scopeName ? ` ${scopeName}` : ''}`); 54 | } 55 | } 56 | 57 | if (node instanceof nodes.Media) { 58 | const mediaList = node.getChild(0); 59 | 60 | if (mediaList instanceof nodes.Medialist) { 61 | const name = '@media ' + mediaList.getText(); 62 | selectorContexts.push(name) 63 | } 64 | } 65 | 66 | if (node instanceof nodes.Selector) { 67 | hover = { 68 | contents: this.selectorPrinting.selectorToMarkedString(node, selectorContexts), 69 | range: getRange(node), 70 | }; 71 | break; 72 | } 73 | 74 | if (node instanceof nodes.SimpleSelector) { 75 | /** 76 | * Some sass specific at rules such as `@at-root` are parsed as `SimpleSelector` 77 | */ 78 | if (!startsWith(node.getText(), '@')) { 79 | hover = { 80 | contents: this.selectorPrinting.simpleSelectorToMarkedString(node), 81 | range: getRange(node), 82 | }; 83 | } 84 | break; 85 | } 86 | 87 | if (node instanceof nodes.Declaration) { 88 | const propertyName = node.getFullPropertyName(); 89 | const entry = this.cssDataManager.getProperty(propertyName); 90 | if (entry) { 91 | const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); 92 | if (contents) { 93 | hover = { 94 | contents, 95 | range: getRange(node), 96 | }; 97 | } else { 98 | hover = null; 99 | } 100 | } 101 | continue; 102 | } 103 | 104 | if (node instanceof nodes.UnknownAtRule) { 105 | const atRuleName = node.getText(); 106 | const entry = this.cssDataManager.getAtDirective(atRuleName); 107 | if (entry) { 108 | const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); 109 | if (contents) { 110 | hover = { 111 | contents, 112 | range: getRange(node), 113 | }; 114 | } else { 115 | hover = null; 116 | } 117 | } 118 | continue; 119 | } 120 | 121 | if (node instanceof nodes.Node && node.type === nodes.NodeType.PseudoSelector) { 122 | const selectorName = node.getText(); 123 | const entry = selectorName.slice(0, 2) === '::' ? this.cssDataManager.getPseudoElement(selectorName) : this.cssDataManager.getPseudoClass(selectorName); 124 | if (entry) { 125 | const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings); 126 | if (contents) { 127 | hover = { 128 | contents, 129 | range: getRange(node), 130 | }; 131 | } else { 132 | hover = null; 133 | } 134 | } 135 | continue; 136 | } 137 | } 138 | 139 | if (hover) { 140 | hover.contents = this.convertContents(hover.contents); 141 | } 142 | 143 | return hover; 144 | } 145 | 146 | private convertContents(contents: MarkupContent | MarkedString | MarkedString[]): MarkupContent | MarkedString | MarkedString[] { 147 | if (!this.doesSupportMarkdown()) { 148 | if (typeof contents === 'string') { 149 | return contents; 150 | } 151 | // MarkupContent 152 | else if ('kind' in contents) { 153 | return { 154 | kind: 'plaintext', 155 | value: contents.value, 156 | }; 157 | } 158 | // MarkedString[] 159 | else if (Array.isArray(contents)) { 160 | return contents.map((c) => { 161 | return typeof c === 'string' ? c : c.value; 162 | }); 163 | } 164 | // MarkedString 165 | else { 166 | return contents.value; 167 | } 168 | } 169 | 170 | return contents; 171 | } 172 | 173 | private doesSupportMarkdown() { 174 | if (!isDefined(this.supportsMarkdown)) { 175 | if (!isDefined(this.clientCapabilities)) { 176 | this.supportsMarkdown = true; 177 | return this.supportsMarkdown; 178 | } 179 | 180 | const hover = this.clientCapabilities.textDocument && this.clientCapabilities.textDocument.hover; 181 | this.supportsMarkdown = hover && hover.contentFormat && Array.isArray(hover.contentFormat) && hover.contentFormat.indexOf(MarkupKind.Markdown) !== -1; 182 | } 183 | return this.supportsMarkdown; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/services/cssSelectionRange.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { Range, Position, SelectionRange, TextDocument } from '../cssLanguageTypes'; 8 | import { Stylesheet, NodeType } from '../parser/cssNodes'; 9 | 10 | export function getSelectionRanges(document: TextDocument, positions: Position[], stylesheet: Stylesheet): SelectionRange[] { 11 | function getSelectionRange(position: Position): SelectionRange { 12 | const applicableRanges = getApplicableRanges(position); 13 | let current: SelectionRange | undefined = undefined; 14 | for (let index = applicableRanges.length - 1; index >= 0; index--) { 15 | current = SelectionRange.create(Range.create( 16 | document.positionAt(applicableRanges[index][0]), 17 | document.positionAt(applicableRanges[index][1]) 18 | ), current); 19 | } 20 | if (!current) { 21 | current = SelectionRange.create(Range.create(position, position)); 22 | } 23 | return current; 24 | } 25 | return positions.map(getSelectionRange); 26 | 27 | function getApplicableRanges(position: Position): number[][] { 28 | const offset = document.offsetAt(position); 29 | let currNode = stylesheet.findChildAtOffset(offset, true); 30 | 31 | if (!currNode) { 32 | return []; 33 | } 34 | 35 | const result = []; 36 | 37 | while (currNode) { 38 | if ( 39 | currNode.parent && 40 | currNode.offset === currNode.parent.offset && 41 | currNode.end === currNode.parent.end 42 | ) { 43 | currNode = currNode.parent; 44 | continue; 45 | } 46 | 47 | // The `{ }` part of `.a { }` 48 | if (currNode.type === NodeType.Declarations) { 49 | if (offset > currNode.offset && offset < currNode.end) { 50 | // Return `{ }` and the range inside `{` and `}` 51 | result.push([currNode.offset + 1, currNode.end - 1]); 52 | } 53 | } 54 | 55 | result.push([currNode.offset, currNode.end]); 56 | 57 | currNode = currNode.parent; 58 | } 59 | 60 | return result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/services/cssValidation.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as nodes from '../parser/cssNodes'; 8 | import { LintConfigurationSettings, Rules } from './lintRules'; 9 | import { LintVisitor } from './lint'; 10 | import { TextDocument, Range, Diagnostic, DiagnosticSeverity, LanguageSettings } from '../cssLanguageTypes'; 11 | import { CSSDataManager } from '../languageFacts/dataManager'; 12 | 13 | export class CSSValidation { 14 | 15 | private settings?: LanguageSettings; 16 | 17 | constructor(private cssDataManager: CSSDataManager) { 18 | } 19 | 20 | public configure(settings?: LanguageSettings) { 21 | this.settings = settings; 22 | } 23 | 24 | public doValidation(document: TextDocument, stylesheet: nodes.Stylesheet, settings: LanguageSettings | undefined = this.settings): Diagnostic[] { 25 | if (settings && settings.validate === false) { 26 | return []; 27 | } 28 | 29 | const entries: nodes.IMarker[] = []; 30 | entries.push.apply(entries, nodes.ParseErrorCollector.entries(stylesheet)); 31 | entries.push.apply(entries, LintVisitor.entries(stylesheet, document, new LintConfigurationSettings(settings && settings.lint), this.cssDataManager)); 32 | 33 | const ruleIds: string[] = []; 34 | for (const r in Rules) { 35 | ruleIds.push(Rules[r as keyof typeof Rules].id); 36 | } 37 | 38 | function toDiagnostic(marker: nodes.IMarker): Diagnostic { 39 | const range = Range.create(document.positionAt(marker.getOffset()), document.positionAt(marker.getOffset() + marker.getLength())); 40 | const source = document.languageId; 41 | 42 | return { 43 | code: marker.getRule().id, 44 | source: source, 45 | message: marker.getMessage(), 46 | severity: marker.getLevel() === nodes.Level.Warning ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error, 47 | range: range 48 | }; 49 | } 50 | 51 | return entries.filter(entry => entry.getLevel() !== nodes.Level.Ignore).map(toDiagnostic); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/services/lintRules.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as nodes from '../parser/cssNodes'; 8 | import { LintSettings } from '../cssLanguageTypes'; 9 | 10 | import * as l10n from '@vscode/l10n'; 11 | 12 | const Warning = nodes.Level.Warning; 13 | const Error = nodes.Level.Error; 14 | const Ignore = nodes.Level.Ignore; 15 | 16 | export class Rule implements nodes.IRule { 17 | 18 | public constructor(public id: string, public message: string, public defaultValue: nodes.Level) { 19 | // nothing to do 20 | } 21 | } 22 | 23 | export class Setting { 24 | 25 | public constructor(public id: string, public message: string, public defaultValue: any) { 26 | // nothing to do 27 | } 28 | } 29 | 30 | export const Rules = { 31 | AllVendorPrefixes: new Rule('compatibleVendorPrefixes', l10n.t("When using a vendor-specific prefix make sure to also include all other vendor-specific properties"), Ignore), 32 | IncludeStandardPropertyWhenUsingVendorPrefix: new Rule('vendorPrefix', l10n.t("When using a vendor-specific prefix also include the standard property"), Warning), 33 | DuplicateDeclarations: new Rule('duplicateProperties', l10n.t("Do not use duplicate style definitions"), Ignore), 34 | EmptyRuleSet: new Rule('emptyRules', l10n.t("Do not use empty rulesets"), Warning), 35 | ImportStatemement: new Rule('importStatement', l10n.t("Import statements do not load in parallel"), Ignore), 36 | BewareOfBoxModelSize: new Rule('boxModel', l10n.t("Do not use width or height when using padding or border"), Ignore), 37 | UniversalSelector: new Rule('universalSelector', l10n.t("The universal selector (*) is known to be slow"), Ignore), 38 | ZeroWithUnit: new Rule('zeroUnits', l10n.t("No unit for zero needed"), Ignore), 39 | RequiredPropertiesForFontFace: new Rule('fontFaceProperties', l10n.t("@font-face rule must define 'src' and 'font-family' properties"), Warning), 40 | HexColorLength: new Rule('hexColorLength', l10n.t("Hex colors must consist of three, four, six or eight hex numbers"), Error), 41 | ArgsInColorFunction: new Rule('argumentsInColorFunction', l10n.t("Invalid number of parameters"), Error), 42 | UnknownProperty: new Rule('unknownProperties', l10n.t("Unknown property."), Warning), 43 | UnknownAtRules: new Rule('unknownAtRules', l10n.t("Unknown at-rule."), Warning), 44 | IEStarHack: new Rule('ieHack', l10n.t("IE hacks are only necessary when supporting IE7 and older"), Ignore), 45 | UnknownVendorSpecificProperty: new Rule('unknownVendorSpecificProperties', l10n.t("Unknown vendor specific property."), Ignore), 46 | PropertyIgnoredDueToDisplay: new Rule('propertyIgnoredDueToDisplay', l10n.t("Property is ignored due to the display."), Warning), 47 | AvoidImportant: new Rule('important', l10n.t("Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored."), Ignore), 48 | AvoidFloat: new Rule('float', l10n.t("Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes."), Ignore), 49 | AvoidIdSelector: new Rule('idSelector', l10n.t("Selectors should not contain IDs because these rules are too tightly coupled with the HTML."), Ignore), 50 | }; 51 | 52 | export const Settings = { 53 | ValidProperties: new Setting('validProperties', l10n.t("A list of properties that are not validated against the `unknownProperties` rule."), []) 54 | }; 55 | 56 | export class LintConfigurationSettings { 57 | constructor(private conf: LintSettings = {}) { 58 | } 59 | 60 | getRule(rule: Rule): nodes.Level { 61 | if (this.conf.hasOwnProperty(rule.id)) { 62 | const level = toLevel(this.conf[rule.id]); 63 | if (level) { 64 | return level; 65 | } 66 | } 67 | return rule.defaultValue; 68 | } 69 | 70 | getSetting(setting: Setting): any { 71 | return this.conf[setting.id]; 72 | } 73 | } 74 | 75 | function toLevel(level: string): nodes.Level | null { 76 | switch (level) { 77 | case 'ignore': return nodes.Level.Ignore; 78 | case 'warning': return nodes.Level.Warning; 79 | case 'error': return nodes.Level.Error; 80 | } 81 | return null; 82 | } 83 | -------------------------------------------------------------------------------- /src/services/lintUtil.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as nodes from '../parser/cssNodes'; 8 | import { includes } from '../utils/arrays'; 9 | 10 | export class Element { 11 | 12 | public readonly fullPropertyName: string; 13 | public readonly node: nodes.Declaration; 14 | 15 | constructor(decl: nodes.Declaration) { 16 | this.fullPropertyName = decl.getFullPropertyName().toLowerCase(); 17 | this.node = decl; 18 | } 19 | } 20 | 21 | interface SideState { 22 | value: boolean; 23 | 24 | properties: Element[]; 25 | } 26 | 27 | interface BoxModel { 28 | width?: Element; 29 | 30 | height?: Element; 31 | 32 | top: SideState; 33 | 34 | right: SideState; 35 | 36 | bottom: SideState; 37 | 38 | left: SideState; 39 | } 40 | 41 | function setSide( 42 | model: BoxModel, 43 | side: K, 44 | value: boolean, 45 | property: Element 46 | ): void { 47 | const state = model[side]; 48 | 49 | state.value = value; 50 | 51 | if (value) { 52 | if (!includes(state.properties, property)) { 53 | state.properties.push(property); 54 | } 55 | } 56 | } 57 | 58 | function setAllSides(model: BoxModel, value: boolean, property: Element): void { 59 | setSide(model, 'top', value, property); 60 | setSide(model, 'right', value, property); 61 | setSide(model, 'bottom', value, property); 62 | setSide(model, 'left', value, property); 63 | } 64 | 65 | function updateModelWithValue( 66 | model: BoxModel, 67 | side: string | undefined, 68 | value: boolean, 69 | property: Element 70 | ): void { 71 | if (side === 'top' || side === 'right' || 72 | side === 'bottom' || side === 'left') { 73 | setSide(model, side, value, property); 74 | } else { 75 | setAllSides(model, value, property); 76 | } 77 | } 78 | 79 | function updateModelWithList(model: BoxModel, values: boolean[], property: Element): void { 80 | switch (values.length) { 81 | case 1: 82 | updateModelWithValue(model, undefined, values[0], property); 83 | break; 84 | case 2: 85 | updateModelWithValue(model, 'top', values[0], property); 86 | updateModelWithValue(model, 'bottom', values[0], property); 87 | updateModelWithValue(model, 'right', values[1], property); 88 | updateModelWithValue(model, 'left', values[1], property); 89 | break; 90 | case 3: 91 | updateModelWithValue(model, 'top', values[0], property); 92 | updateModelWithValue(model, 'right', values[1], property); 93 | updateModelWithValue(model, 'left', values[1], property); 94 | updateModelWithValue(model, 'bottom', values[2], property); 95 | break; 96 | case 4: 97 | updateModelWithValue(model, 'top', values[0], property); 98 | updateModelWithValue(model, 'right', values[1], property); 99 | updateModelWithValue(model, 'bottom', values[2], property); 100 | updateModelWithValue(model, 'left', values[3], property); 101 | break; 102 | } 103 | } 104 | 105 | function matches(value: nodes.Node, candidates: string[]) { 106 | for (let candidate of candidates) { 107 | if (value.matches(candidate)) { 108 | return true; 109 | } 110 | } 111 | return false; 112 | } 113 | 114 | /** 115 | * @param allowsKeywords whether the initial value of property is zero, so keywords `initial` and `unset` count as zero 116 | * @return `true` if this node represents a non-zero border; otherwise, `false` 117 | */ 118 | function checkLineWidth(value: nodes.Node, allowsKeywords: boolean = true): boolean { 119 | if (allowsKeywords && matches(value, ['initial', 'unset'])) { 120 | return false; 121 | } 122 | 123 | // a is a value and a unit 124 | // so use `parseFloat` to strip the unit 125 | return parseFloat(value.getText()) !== 0; 126 | } 127 | 128 | function checkLineWidthList(nodes: nodes.Node[], allowsKeywords: boolean = true): boolean[] { 129 | return nodes.map(node => checkLineWidth(node, allowsKeywords)); 130 | } 131 | 132 | /** 133 | * @param allowsKeywords whether keywords `initial` and `unset` count as zero 134 | * @return `true` if this node represents a non-zero border; otherwise, `false` 135 | */ 136 | function checkLineStyle(valueNode: nodes.Node, allowsKeywords: boolean = true): boolean { 137 | if (matches(valueNode, ['none', 'hidden'])) { 138 | return false; 139 | } 140 | 141 | if (allowsKeywords && matches(valueNode, ['initial', 'unset'])) { 142 | return false; 143 | } 144 | 145 | return true; 146 | } 147 | 148 | function checkLineStyleList(nodes: nodes.Node[], allowsKeywords: boolean = true): boolean[] { 149 | return nodes.map(node => checkLineStyle(node, allowsKeywords)); 150 | } 151 | 152 | function checkBorderShorthand(node: nodes.Node): boolean { 153 | const children = node.getChildren(); 154 | 155 | // the only child can be a keyword, a , or a 156 | // if either check returns false, the result is no border 157 | if (children.length === 1) { 158 | const value = children[0]; 159 | return checkLineWidth(value) && checkLineStyle(value); 160 | } 161 | 162 | // multiple children can't contain keywords 163 | // if any child means no border, the result is no border 164 | for (const child of children) { 165 | const value = child; 166 | if (!checkLineWidth(value, /* allowsKeywords: */ false) || 167 | !checkLineStyle(value, /* allowsKeywords: */ false)) { 168 | return false; 169 | } 170 | } 171 | return true; 172 | } 173 | 174 | export default function calculateBoxModel(propertyTable: Element[]): BoxModel { 175 | const model: BoxModel = { 176 | top: { value: false, properties: [] }, 177 | right: { value: false, properties: [] }, 178 | bottom: { value: false, properties: [] }, 179 | left: { value: false, properties: [] }, 180 | }; 181 | 182 | for (const property of propertyTable) { 183 | const value = property.node.value; 184 | if (typeof value === 'undefined') { 185 | continue; 186 | } 187 | 188 | switch (property.fullPropertyName) { 189 | case 'box-sizing': 190 | // has `box-sizing`, bail out 191 | return { 192 | top: { value: false, properties: [] }, 193 | right: { value: false, properties: [] }, 194 | bottom: { value: false, properties: [] }, 195 | left: { value: false, properties: [] }, 196 | }; 197 | case 'width': 198 | model.width = property; 199 | break; 200 | case 'height': 201 | model.height = property; 202 | break; 203 | default: 204 | const segments = property.fullPropertyName.split('-'); 205 | switch (segments[0]) { 206 | case 'border': 207 | switch (segments[1]) { 208 | case undefined: 209 | case 'top': 210 | case 'right': 211 | case 'bottom': 212 | case 'left': 213 | switch (segments[2]) { 214 | case undefined: 215 | updateModelWithValue(model, segments[1], checkBorderShorthand(value), property); 216 | break; 217 | case 'width': 218 | // the initial value of `border-width` is `medium`, not zero 219 | updateModelWithValue(model, segments[1], checkLineWidth(value, false), property); 220 | break; 221 | case 'style': 222 | // the initial value of `border-style` is `none` 223 | updateModelWithValue(model, segments[1], checkLineStyle(value, true), property); 224 | break; 225 | } 226 | break; 227 | case 'width': 228 | // the initial value of `border-width` is `medium`, not zero 229 | updateModelWithList(model, checkLineWidthList(value.getChildren(), false), property); 230 | break; 231 | case 'style': 232 | // the initial value of `border-style` is `none` 233 | updateModelWithList(model, checkLineStyleList(value.getChildren(), true), property); 234 | break; 235 | } 236 | break; 237 | case 'padding': 238 | if (segments.length === 1) { 239 | // the initial value of `padding` is zero 240 | updateModelWithList(model, checkLineWidthList(value.getChildren(), true), property); 241 | } else { 242 | // the initial value of `padding` is zero 243 | updateModelWithValue(model, segments[1], checkLineWidth(value, true), property); 244 | } 245 | break; 246 | } 247 | break; 248 | } 249 | } 250 | 251 | return model; 252 | } 253 | -------------------------------------------------------------------------------- /src/services/pathCompletion.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { DocumentUri } from 'vscode-languageserver-types'; 7 | import { ICompletionParticipant, URILiteralCompletionContext, ImportPathCompletionContext, FileType, DocumentContext, TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from '../cssLanguageTypes'; 8 | 9 | import { startsWith, endsWith } from '../utils/strings'; 10 | import { joinPath } from '../utils/resources'; 11 | 12 | export class PathCompletionParticipant implements ICompletionParticipant { 13 | private literalCompletions: URILiteralCompletionContext[] = []; 14 | private importCompletions: ImportPathCompletionContext[] = []; 15 | 16 | constructor(private readonly readDirectory: (uri: DocumentUri) => Promise<[string, FileType][]>) { 17 | } 18 | 19 | public onCssURILiteralValue(context: URILiteralCompletionContext) { 20 | this.literalCompletions.push(context); 21 | } 22 | 23 | public onCssImportPath(context: ImportPathCompletionContext) { 24 | this.importCompletions.push(context); 25 | } 26 | 27 | public async computeCompletions(document: TextDocument, documentContext: DocumentContext): Promise { 28 | const result: CompletionList = { items: [], isIncomplete: false }; 29 | for (const literalCompletion of this.literalCompletions) { 30 | const uriValue = literalCompletion.uriValue; 31 | const fullValue = stripQuotes(uriValue); 32 | if (fullValue === '.' || fullValue === '..') { 33 | result.isIncomplete = true; 34 | } else { 35 | const items = await this.providePathSuggestions(uriValue, literalCompletion.position, literalCompletion.range, document, documentContext); 36 | for (let item of items) { 37 | result.items.push(item); 38 | } 39 | } 40 | } 41 | for (const importCompletion of this.importCompletions) { 42 | const pathValue = importCompletion.pathValue; 43 | const fullValue = stripQuotes(pathValue); 44 | if (fullValue === '.' || fullValue === '..') { 45 | result.isIncomplete = true; 46 | } else { 47 | let suggestions = await this.providePathSuggestions(pathValue, importCompletion.position, importCompletion.range, document, documentContext); 48 | 49 | if (document.languageId === 'scss') { 50 | suggestions.forEach(s => { 51 | if (startsWith(s.label, '_') && endsWith(s.label, '.scss')) { 52 | if (s.textEdit) { 53 | s.textEdit.newText = s.label.slice(1, -5); 54 | } else { 55 | s.label = s.label.slice(1, -5); 56 | } 57 | } 58 | }); 59 | } 60 | for (let item of suggestions) { 61 | result.items.push(item); 62 | } 63 | } 64 | } 65 | return result; 66 | } 67 | 68 | private async providePathSuggestions(pathValue: string, position: Position, range: Range, document: TextDocument, documentContext: DocumentContext): Promise { 69 | const fullValue = stripQuotes(pathValue); 70 | const isValueQuoted = startsWith(pathValue, `'`) || startsWith(pathValue, `"`); 71 | const valueBeforeCursor = isValueQuoted 72 | ? fullValue.slice(0, position.character - (range.start.character + 1)) 73 | : fullValue.slice(0, position.character - range.start.character); 74 | 75 | const currentDocUri = document.uri; 76 | 77 | const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range; 78 | const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange); 79 | 80 | const valueBeforeLastSlash = valueBeforeCursor.substring(0, valueBeforeCursor.lastIndexOf('/') + 1); // keep the last slash 81 | 82 | let parentDir = documentContext.resolveReference(valueBeforeLastSlash || '.', currentDocUri); 83 | if (parentDir) { 84 | try { 85 | const result: CompletionItem[] = []; 86 | const infos = await this.readDirectory(parentDir); 87 | for (const [name, type] of infos) { 88 | // Exclude paths that start with `.` 89 | if (name.charCodeAt(0) !== CharCode_dot && (type === FileType.Directory || joinPath(parentDir, name) !== currentDocUri)) { 90 | result.push(createCompletionItem(name, type === FileType.Directory, replaceRange)); 91 | } 92 | } 93 | return result; 94 | } catch (e) { 95 | // ignore 96 | } 97 | } 98 | return []; 99 | } 100 | } 101 | 102 | const CharCode_dot = '.'.charCodeAt(0); 103 | 104 | function stripQuotes(fullValue: string) { 105 | if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { 106 | return fullValue.slice(1, -1); 107 | } else { 108 | return fullValue; 109 | } 110 | } 111 | 112 | function pathToReplaceRange(valueBeforeCursor: string, fullValue: string, fullValueRange: Range) { 113 | let replaceRange: Range; 114 | const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); 115 | if (lastIndexOfSlash === -1) { 116 | replaceRange = fullValueRange; 117 | } else { 118 | // For cases where cursor is in the middle of attribute value, like