├── test ├── fixtures │ └── testWorkspace │ │ └── theme.json ├── suite │ ├── extension.test.js │ ├── findThemeFile.test.js │ ├── index.js │ └── ThemeJSONParser.test.js └── runTest.js ├── icon.png ├── .gitignore ├── .vscodeignore ├── .vscode ├── extensions.json └── launch.json ├── jsconfig.json ├── src ├── util │ ├── index.js │ ├── registerAutocompleteProviders.js │ ├── userPrompts.js │ └── findThemeFile.js └── classes │ └── ThemeJSONParser.js ├── CHANGELOG.md ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE.txt ├── package.json ├── README.md ├── extension.js └── vsc-extension-quickstart.md /test/fixtures/testWorkspace/theme.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roseg43/vscode-wp-theme-json-autocomplete/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | 5 | # Ignore test workspace vscode config 6 | test/fixtures/testWorkspace/.vscode -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/jsconfig.json 8 | **/*.map 9 | **/.eslintrc.json 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "ES2020" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | const findThemeFile = require('./findThemeFile'); 2 | const {registerAutocompleteProviders} = require('./registerAutocompleteProviders'); 3 | const userPrompts = require('./userPrompts'); 4 | 5 | module.exports = { 6 | findThemeFile, 7 | registerAutocompleteProviders, 8 | userPrompts, 9 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "wordpress-theme-json-css-autosuggest" extension will be documented in this file. 4 | 5 | ## [0.0.1] 6 | 7 | - Initial release 8 | 9 | ## [1.0.0] - 2023-09-14 10 | 11 | ### Added 12 | * Added additional test coverage for file utilities. 13 | 14 | ### Fixed 15 | * Updated to v1 to align with semver 16 | * Extension no longer asks you to select a `theme.json` file if you've already selected one in the past or have set one via extension settings (#30) -------------------------------------------------------------------------------- /test/suite/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | const vscode = require('vscode'); 6 | // const myExtension = require('../extension'); 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "warn", 20 | "no-unreachable": "warn", 21 | "no-unused-vars": "warn", 22 | "constructor-super": "warn", 23 | "valid-typeof": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/suite/findThemeFile.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const findThemeFile = require('../../src/util/findThemeFile'); 3 | const vscode = require('vscode'); 4 | const path = require('path'); 5 | 6 | const workspaceFolder = path.resolve(__dirname, '../fixtures/testWorkspace/theme.json'); 7 | 8 | suite('theme.json file locator', () => { 9 | suite('Path checking', () => { 10 | setup((done) => { 11 | vscode.workspace.getConfiguration('wordpressThemeJsonCssAutosuggest').update('themeJsonPath', workspaceFolder).then(() => { 12 | done(); 13 | }); 14 | }); 15 | 16 | test('Uses the user-defined path if one is set in extension settings', async () => { 17 | const result = await findThemeFile(); 18 | assert.strictEqual(result, workspaceFolder); 19 | }); 20 | }) 21 | }); -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { runTests } = require('@vscode/test-electron'); 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../'); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | const workspaceFolder = path.resolve(__dirname, 'fixtures/testWorkspace'); 16 | // Download VS Code, unzip it and run the integration test 17 | await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs: [workspaceFolder] }); 18 | } catch (err) { 19 | console.error('Failed to run tests', err); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "1.0.0", 7 | "configurations": [ 8 | 9 | 10 | { 11 | "name": "Run Extension", 12 | "type": "extensionHost", 13 | "request": "launch", 14 | "args": [ 15 | "--extensionDevelopmentPath=${workspaceFolder}" 16 | ] 17 | }, 18 | { 19 | "name": "Extension Tests", 20 | "type": "extensionHost", 21 | "request": "launch", 22 | "runtimeExecutable": "${execPath}", 23 | "args": [ 24 | "${workspaceFolder}/test/fixtures/testWorkspace", 25 | "--disable-extensions", 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/test/suite/index", 28 | "${workspaceFolder}" 29 | ], 30 | "outFiles": ["${workspaceFolder}/test/**/*.js"] 31 | 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/suite/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Mocha = require('mocha'); 3 | const glob = require('glob'); 4 | 5 | function run() { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | const testFiles = new glob.Glob('**/**.test.js', { cwd: testsRoot }); 16 | const testFileStream = testFiles.stream(); 17 | 18 | testFileStream.on('data', (file) => { 19 | // Add files to the test suite 20 | mocha.addFile(path.resolve(testsRoot, file)); 21 | }); 22 | testFileStream.on('error', (err) => { 23 | e(err); 24 | }); 25 | testFileStream.on('end', () => { 26 | try { 27 | // Run the mocha test 28 | mocha.run(failures => { 29 | if (failures > 0) { 30 | e(new Error(`${failures} tests failed.`)); 31 | } else { 32 | c(); 33 | } 34 | }); 35 | } catch (err) { 36 | console.error(err); 37 | e(err); 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | module.exports = { 44 | run 45 | }; 46 | -------------------------------------------------------------------------------- /src/util/registerAutocompleteProviders.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | 3 | /** 4 | * Creates the autocomplete provider for CSS files. 5 | * @param {Array} values Array of values to be added to autocomplete suggestions. 6 | * @returns {vscode.Disposable} A disposable object that will dispose the provider when it is no longer needed. 7 | */ 8 | function registerAutocompleteProviders(values = []) { 9 | // Register a completion items provider for CSS files. 10 | const provider = vscode.languages.registerCompletionItemProvider( 11 | { 12 | // Pattern match css, sass, scss, and less files 13 | pattern: '**/*.{css,sass,scss,less}', 14 | }, 15 | { 16 | provideCompletionItems() { 17 | if (values.length) { 18 | return values.map( 19 | (value) => new vscode.CompletionItem({ 20 | label: value.name, 21 | description: `${value.value}`, 22 | }, vscode.CompletionItemKind.Variable) 23 | ); 24 | } 25 | 26 | // If no values are passed, return an empty array. 27 | return []; 28 | } 29 | }, 30 | '--' 31 | ); 32 | 33 | 34 | return provider; 35 | } 36 | 37 | module.exports = { 38 | registerAutocompleteProviders 39 | }; -------------------------------------------------------------------------------- /src/util/userPrompts.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | 3 | /** 4 | * Prompts the user to select a theme.json file from the workspace to use for the extension 5 | * when multiples are found. 6 | * @param {vscode.Uri[]} uris An array of paths to theme.json files 7 | * @returns {Promise} A promise that resolves to the selected path. 8 | */ 9 | async function multipleThemeFilePrompt(uris) { 10 | const options = uris.map((uri) => { 11 | return { 12 | label: uri.path, 13 | description: 'theme.json' 14 | } 15 | }); 16 | 17 | const selectionToken = await vscode.window.showQuickPick(options, { 18 | placeHolder: 'Multiple theme.json files found. Please select one to use for the extension.' 19 | }); 20 | 21 | // Set the selected path in the extension settings. 22 | vscode.workspace.getConfiguration('wordpressThemeJsonCssAutosuggest').update('themeJsonPath', selectionToken.label); 23 | 24 | const themeJson = require(selectionToken.label); 25 | const ThemeJSONParser = require('../classes/ThemeJSONParser'); 26 | 27 | ThemeJSONParser.update(themeJson); 28 | 29 | return selectionToken.label; 30 | } 31 | 32 | module.exports = { 33 | multipleThemeFilePrompt 34 | }; 35 | -------------------------------------------------------------------------------- /test/suite/ThemeJSONParser.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | suite('theme.json Parser', () => { 4 | 5 | suite('🌶️ Missing data scenarios', () => { 6 | test('Does not trigger an error if attempting to parse a root-level property that doesn\'t exist in theme.json', () => { 7 | const ThemeJSONParser = require('../../src/classes/ThemeJSONParser'); 8 | 9 | // This method will only ever be called if ThemeJSONParser.theme.settings exists. 10 | ThemeJSONParser.theme = { 11 | settings: {} 12 | }; 13 | 14 | assert.doesNotThrow(() => { 15 | ThemeJSONParser.parseThemeProperty('doesNotExist'); 16 | }); 17 | }); 18 | 19 | test('Does not trigger an error if attempting to parse a nested property that doesn\'t exist in theme.json', () => { 20 | const ThemeJSONParser = require('../../src/classes/ThemeJSONParser'); 21 | 22 | // This method will only ever be called if ThemeJSONParser.theme.settings exists. 23 | ThemeJSONParser.theme = { 24 | settings: {} 25 | }; 26 | 27 | assert.doesNotThrow(() => { 28 | ThemeJSONParser.parseThemeProperty('doesNotExist.nested'); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2023, Gabriel Rose 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress-theme-json-css-autosuggest", 3 | "displayName": "WordPress theme.json CSS Autosuggest", 4 | "description": "For WordPress Theme Developers - Get CSS Custom Properties generated via theme.json as part of your CSS autosuggestions", 5 | "version": "1.0.0", 6 | "publisher": "GabrielRose", 7 | "icon": "icon.png", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:roseg43/vscode-wp-theme-json-autocomplete.git" 11 | }, 12 | "engines": { 13 | "vscode": "^1.82.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "workspaceContains:**/theme.json" 20 | ], 21 | "main": "./extension.js", 22 | "contributes": { 23 | "configuration": { 24 | "type": "object", 25 | "title": "WordPress theme.json CSS Autosuggest", 26 | "properties": { 27 | "wordpressThemeJsonCssAutosuggest.enable": { 28 | "type": "boolean", 29 | "default": true, 30 | "description": "Enable this extension." 31 | }, 32 | "wordpressThemeJsonCssAutosuggest.themeJsonPath": { 33 | "type": "string", 34 | "default": "", 35 | "description": "The path to the theme directory. Leave blank to use the workspace root." 36 | } 37 | } 38 | } 39 | }, 40 | "scripts": { 41 | "lint": "eslint .", 42 | "pretest": "npm run lint", 43 | "test": "node ./test/runTest.js" 44 | }, 45 | "devDependencies": { 46 | "@types/mocha": "^10.0.1", 47 | "@types/node": "16.x", 48 | "@types/vscode": "^1.82.0", 49 | "@vscode/test-electron": "^2.3.4", 50 | "eslint": "^8.47.0", 51 | "glob": "^10.3.3", 52 | "mocha": "^10.2.0", 53 | "sinon": "^16.1.0", 54 | "typescript": "^5.1.6" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (VS Code) WordPress theme.json CSS Autosuggest 2 | ![wp-theme-json-vsc-example-1](https://github.com/roseg43/vscode-wp-theme-json-autocomplete/assets/7225212/ae739c34-fc5b-4d91-9000-bf35e007e0a3) 3 | 4 | This extension for Visual Studio Code simplifies working with design tokens defined in the `theme.json` file used by WordPress themes. It solves the problem of having to constantly refer back to your `theme.json` file for design token names, as well as having to remember all of the prefixes WordPress uses for different settings when they get converted to CSS Custom Properties. 5 | 6 | ## Features 7 | - Automatically finds any `theme.json` files in your workspace. If multiple files are detected, you'll be asked to select a `theme.json` file to use for autosuggestions. In a future version this extension will automatically find the `theme.json` file nearest to the file you're working in. 8 | - Updates suggestions whenever `theme.json` is saved. 9 | - Provides property value details in suggestions so that you can see what values are set in `theme.json` at a glance 10 | 11 | ## Installation 12 | The extension can be installed in two ways: manually via the `.vsix` file included in all releases, and through the [Extension Marketplace](https://marketplace.visualstudio.com/items?itemName=GabrielRose.wordpress-theme-json-css-autosuggest) 13 | 14 | ## Requirements 15 | This extension will only work inside of a folder or file workspace. 16 | 17 | ## Extension Settings 18 | This extension contributes the following settings: 19 | 20 | * `wordpressThemeJsonCssAutosuggest.enable`: Enable/disable this extension. 21 | * `wordpressThemeJsonCssAutosuggest.themeJsonPath`: The path to the theme directory. Leave blank to use the workspace root. 22 | 23 | ## Known Issues 24 | 25 | - Currently, the only way to change which `theme.json` file to use in a multi-theme workspace is to clear out `wordpressThemeJsonCssAutosuggest.themeJsonPath` manually. 26 | 27 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | // External dependencies 2 | const vscode = require('vscode'); 3 | 4 | // Internal dependencies 5 | const ThemeJSONParser = require('./src/classes/ThemeJSONParser'); 6 | const { 7 | registerAutocompleteProviders, 8 | findThemeFile, 9 | } = require('./src/util'); 10 | 11 | let providerInstance = null; 12 | 13 | /** 14 | * Called when the extension is activated (if the current workspace contains a theme.json file) 15 | * @param {vscode.ExtensionContext} context 16 | */ 17 | function activate(context) { 18 | // Add an on update callback to the ThemeJSONParser singleton that refreshes the autocomplete providers when updates are made. 19 | // TODO: Move this into ThemeJSONParser. It's here currently because we need the extension context. 20 | ThemeJSONParser.setOnUpdate(() => { 21 | if (providerInstance) { 22 | providerInstance.dispose(); 23 | } 24 | 25 | providerInstance = registerAutocompleteProviders( 26 | ThemeJSONParser.toArray() 27 | ); 28 | 29 | context.subscriptions.push(providerInstance); 30 | }); 31 | 32 | findThemeFile().then((path) => { 33 | if (!path) { 34 | return; 35 | } 36 | ThemeJSONParser.setThemePath(path); 37 | try { 38 | const themeJson = require(path); 39 | ThemeJSONParser.update(themeJson); 40 | } catch (e) { 41 | vscode.window.showErrorMessage('Error parsing theme.json file. Please check that it is valid JSON.'); 42 | } 43 | }); 44 | 45 | /** 46 | * Update our autocomplete provider when a theme.json file is saved. 47 | */ 48 | vscode.workspace.onDidSaveTextDocument((document) => { 49 | if (document.fileName === ThemeJSONParser.themePath) { 50 | try { 51 | const themeJson = JSON.parse(document.getText()); 52 | ThemeJSONParser.update(themeJson); 53 | } catch (e) { 54 | vscode.window.showErrorMessage('Error parsing theme.json file. Please check that it is valid JSON.'); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | function deactivate() {} 61 | 62 | module.exports = { 63 | activate, 64 | deactivate, 65 | } 66 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `extension.js` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `extension.js` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `extension.js`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 31 | * Press `F5` to run the tests in a new window with your extension loaded. 32 | * See the output of the test result in the debug console. 33 | * Make changes to `src/test/suite/extension.test.js` or create new test files inside the `test/suite` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | 37 | ## Go further 38 | 39 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 40 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 41 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 42 | -------------------------------------------------------------------------------- /src/util/findThemeFile.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const { multipleThemeFilePrompt } = require('./userPrompts'); 3 | 4 | /** 5 | * 1. Checks to see if a user-defined path is set in the extension settings. 6 | * - If so, searches that path for a theme.json file and return it if found. 7 | * 2. If no user defined settings, searches the workspace for a theme.json file. 8 | * 9 | * @returns {Promise} A Promise that resolves to path to the theme.json file, or an empty string if no theme.json file is found. 10 | */ 11 | async function getUserDefinedOrWorkspaceThemeFilePath() { 12 | const themeJsonPath = vscode.workspace.getConfiguration('wordpressThemeJsonCssAutosuggest').get('themeJsonPath') || ''; 13 | // Check to see if the path points to a file, or a directory. If it points to a file, confirm it exists. 14 | const isThemeFile = themeJsonPath.match(/theme\.json$/)?.length; 15 | 16 | if (isThemeFile) { 17 | try { 18 | const file = await vscode.workspace.fs.stat(vscode.Uri.file(themeJsonPath)) 19 | if (file.type === vscode.FileType.File || file.type === vscode.FileType.SymbolicLink ) { 20 | return themeJsonPath; 21 | } 22 | } catch (error) { 23 | console.error(error); 24 | // TODO: Display error to user: File path set in extension settings does not exist. 25 | } 26 | } 27 | 28 | // If the path points to a directory, search for a theme.json file inside of it. 29 | const searchPattern = themeJsonPath ? `${themeJsonPath}/**/theme.json`: '**/theme.json'; 30 | const themeJsonFiles = await vscode.workspace.findFiles(searchPattern); 31 | 32 | if (!themeJsonFiles.length) { 33 | return ''; 34 | } 35 | 36 | if (themeJsonFiles.length === 1) { 37 | return themeJsonFiles[0].path; 38 | } 39 | 40 | // If multiple theme.json files are found, display an error with an action to select one via multipleThemeFilePrompt. 41 | return await handleMultipleFilesDetected(themeJsonFiles); 42 | } 43 | 44 | /** 45 | * Handles cases where multiple theme files are detected. Displays an error, and prompts the user to either 46 | * - Select a theme.json file from the workspace to use for the extension 47 | * - Disable the extension for the current workspace 48 | * 49 | * @param {vscode.Uri[]} themeJsonFiles An array of paths to theme.json files 50 | * @returns {Promise} A Promise that resolves to the path to the selected theme.json file, or an empty string if no theme.json file is found. 51 | */ 52 | async function handleMultipleFilesDetected (themeJsonFiles) { 53 | const action = await vscode.window.showErrorMessage( 54 | 'Multiple theme.json files found.', 55 | 'Let me choose which to use', 56 | 'Disable extension for this workspace' 57 | ); 58 | switch (action) { 59 | case 'Let me choose which to use': 60 | return await multipleThemeFilePrompt(themeJsonFiles); 61 | 62 | case 'Disable extension for this workspace': 63 | vscode.workspace.getConfiguration('wordpressThemeJsonCssAutosuggest').update('enable', false); 64 | return ''; 65 | } 66 | } 67 | 68 | /** 69 | * Searches the workspace for a theme.json file, or the directory defined in the extension settings. 70 | * Prescriptive alias for `getUserDefinedOrWorkspaceThemeFilePath`. 71 | * 72 | * @returns {Promise} A Promise that resolves to the path to the theme.json file, or an empty string if no theme.json file is found. 73 | */ 74 | async function findThemeFile() { 75 | let themeJsonPath = await getUserDefinedOrWorkspaceThemeFilePath(); 76 | if (themeJsonPath) { 77 | return themeJsonPath; 78 | } 79 | } 80 | 81 | module.exports = findThemeFile; -------------------------------------------------------------------------------- /src/classes/ThemeJSONParser.js: -------------------------------------------------------------------------------- 1 | const { register } = require('module'); 2 | const { registerAutocompleteProviders } = require('../util'); 3 | 4 | let instance = null; 5 | 6 | /** 7 | * Class: Singleton that parses the theme.json file and converts values to CSS Custom Properties. 8 | * 9 | * @param {Object} options 10 | * @param {Object} options.json The theme.json file contents. 11 | * @param {function} options.onUpdate A callback function to be called when the theme.json file is updated. 12 | * 13 | * @property {Object} theme The theme.json file contents. 14 | * @property {String} themePath The path to the theme.json file. 15 | * @property {function} onUpdate A callback function to be called when the theme.json file is updated. 16 | * @property {Object} properties Contains Arrays of CSS Custom Property tokens. 17 | * @property {Array} properties.color CSS Custom Property tokens for color values. 18 | * @property {Array} properties.custom CSS Custom Property tokens for custom values. 19 | * @property {Array} properties.fontFamily CSS Custom Property tokens for font family values. 20 | * @property {Array} properties.fontSizes CSS Custom Property tokens for font size values. 21 | * @property {Array} properties.gradient CSS Custom Property tokens for gradient values. 22 | * @property {Array} properties.layout CSS Custom Property tokens for layout values. 23 | * @property {Array} properties.spacing CSS Custom Property tokens for spacing values. 24 | */ 25 | class ThemeJSONParser { 26 | theme = {}; 27 | themePath = ''; 28 | properties = { 29 | color: [], 30 | custom: [], 31 | fontFamily: [], 32 | fontSizes: [], 33 | gradient: [], 34 | layout: [], 35 | spacing: [], 36 | }; 37 | onUpdate = Object.create(Function); 38 | 39 | constructor({json = null, onUpdate = () => {}} = {}) { 40 | if (instance) { 41 | return instance; 42 | } 43 | 44 | if (!json) { 45 | return; 46 | } 47 | this.onUpdate = onUpdate; 48 | this.update(json) 49 | 50 | instance = this; 51 | } 52 | 53 | getInstance() { 54 | return instance; 55 | } 56 | 57 | /** 58 | * Checks the object's properties and sees if the schema matches an property and not further nested objects. 59 | * TODO: Move labels into their own class. 60 | * @param {*} obj 61 | * 62 | * @returns {Object|Boolean} Returns an object with a `label` and `value` property if the object matches a property schema. Returns `false` if not. 63 | */ 64 | #maybeGetObjectLabelAndValues(obj) { 65 | // settings.spacing.spacingSizes, settings.typography.fontSizes 66 | if (obj.name && obj.size) { 67 | return { 68 | label: obj.slug, 69 | value: obj.size, 70 | }; 71 | } 72 | 73 | //settings.color.palette 74 | if (obj.slug && obj.color) { 75 | return { 76 | label: obj.slug, 77 | value: obj.color, 78 | }; 79 | } 80 | 81 | // settings.typography.fontFamilies 82 | if (obj.fontFamily) { 83 | return { 84 | label: obj.slug, 85 | value: obj.fontFamily, 86 | }; 87 | } 88 | 89 | // settings.color.gradients 90 | if (obj.gradient) { 91 | return { 92 | label: obj.slug, 93 | value: obj.gradient, 94 | } 95 | } 96 | 97 | return false; 98 | } 99 | 100 | /** 101 | * Method: Loop through an object recursively. For each level, add an additional `--${key}` to the final CSS Custom Property name. 102 | * When hitting a key with a string value, add `--${key}: ${value};` to the final CSS Custom Property name, and push it to the `properties.custom` array. 103 | * 104 | * @param {Object|Array} objOrArray The object or array to loop through 105 | * @param {String} prefix The prefix to add to the CSS Custom Property name. Used to recursively construct strings like `--font--size--small` 106 | * @param {String} propertyCategory The category string WordPress uses to generate this property. Example values: `preset`, `custom` 107 | * 108 | * @returns {Array} An array of CSS Custom Property tokens 109 | */ 110 | toCssCustomPropertyString = (objOrArray, prefix = '', propertyCategory = 'preset') => { 111 | let parsedProperties = []; 112 | for (const key in objOrArray) { 113 | /* 114 | Store a modified version of the key, adding a single `-` character after any numbers that come before or after a character. 115 | Also convert camelCase to kebab-case. 116 | This is used when converting the object keys to CSS Custom Properties, 117 | as WordPress adds a single `-` character after any numbers that come before or after a character. 118 | */ 119 | const modifiedKey = key.replace(/([0-9])([a-zA-Z])/g, '$1-$2').replace(/([a-zA-Z])([0-9])/g, '$1-$2').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 120 | 121 | if (typeof objOrArray[key] === 'object') { 122 | // Check if this object matches a property schema and get the label and value if so. 123 | const maybeProperty = this.#maybeGetObjectLabelAndValues(objOrArray[key]); 124 | 125 | if (!maybeProperty) { 126 | // Continue iterating. Merge the results of the next call to this function with the current array of CSS Custom Properties. 127 | parsedProperties = [...parsedProperties, ...this.toCssCustomPropertyString(objOrArray[key], `${prefix}--${modifiedKey}`, propertyCategory)]; 128 | } else { 129 | // Object matches a theme.json property schema, add it to the array of CSS Custom Properties. 130 | const cssCustomProperty = { 131 | name: `wp--${propertyCategory}${prefix}--${maybeProperty.label}`, 132 | value: maybeProperty.value, 133 | }; 134 | parsedProperties.push(cssCustomProperty); 135 | } 136 | } else { 137 | 138 | const cssCustomProperty = { 139 | name: `wp--${propertyCategory}${prefix}--${modifiedKey}`, 140 | value: objOrArray[key], 141 | }; 142 | 143 | parsedProperties.push(cssCustomProperty); 144 | } 145 | } 146 | return parsedProperties; 147 | } 148 | 149 | /** 150 | * Loops through a single iterable property of theme.json and converts values to CSS Custom Properties 151 | * Format: `--wp--${propertyCategory}--{key1}--{key2}--{key3}: {value};` 152 | * 153 | * @param {String} propertyName The name of the property to parse. Supports nested properties. Example: `color.palette` 154 | * @param {String} prefix The prefix to add to the CSS Custom Property name. Used to recursively construct strings like `--font--size--small` 155 | * @param {String} propertyCategory The category string WordPress uses to generate this property. Example values: `preset`, `custom` 156 | * 157 | * @returns {Array} An array of CSS Custom Property tokens 158 | */ 159 | parseThemeProperty(propertyName, prefix = '', propertyCategory = 'preset') { 160 | // Get the property from the theme.json file. If propertyName contains a `.` character, use that to access nested properties. 161 | const property = propertyName.includes('.') ? propertyName.split('.').reduce((a, b) => { 162 | if (a && a[b]) { 163 | return a[b]; 164 | } 165 | 166 | // If the nested property doesn't exist, return false. 167 | return false; 168 | }, this.theme.settings) : this.theme.settings[propertyName]; 169 | 170 | if (!property) { 171 | return; 172 | } 173 | 174 | return this.toCssCustomPropertyString(property, prefix, propertyCategory); 175 | } 176 | 177 | /** 178 | * Converts the `properties` object into an array of CSS Custom Properties 179 | */ 180 | toArray() { 181 | const properties = []; 182 | for (const key in this.properties) { 183 | if (this.properties[key] && this.properties[key]?.length) { 184 | properties.push(...this.properties[key]); 185 | } 186 | } 187 | return properties; 188 | } 189 | 190 | /** 191 | * Updates the theme property and parses the theme.json file. Registers autocomplete providers. 192 | * 193 | * @param {*} themeJson The theme.json file contents. 194 | */ 195 | update(themeJson = {}) { 196 | if (!themeJson) { 197 | return; 198 | } 199 | 200 | this.theme = themeJson; 201 | this.properties = { 202 | ...this.properties, 203 | custom: this.parseThemeProperty('custom', '', 'custom'), 204 | color: this.parseThemeProperty('color.palette', '--color'), 205 | fontFamily: this.parseThemeProperty('typography.fontFamilies', '--font-family'), 206 | fontSizes: this.parseThemeProperty('typography.fontSizes', '--font-size'), 207 | gradient: this.parseThemeProperty('color.gradients', '--gradient'), 208 | layout: this.parseThemeProperty('layout', '--global', 'style'), 209 | spacing: this.parseThemeProperty('spacing.spacingSizes', '--spacing'), 210 | }; 211 | 212 | if (this.onUpdate && typeof this.onUpdate === 'function') { 213 | this.onUpdate(); 214 | } 215 | } 216 | 217 | /** 218 | * Sets this.onUpdate to the callback function passed in. 219 | * 220 | * @param {function} callback The function to be called when the theme.json file is updated. 221 | * @returns {void} 222 | */ 223 | setOnUpdate(callback) { 224 | if (typeof callback !== 'function') { 225 | return; 226 | } 227 | this.onUpdate = callback; 228 | } 229 | 230 | setThemePath(path) { 231 | this.themePath = path; 232 | } 233 | } 234 | 235 | const singletonThemeParser = new ThemeJSONParser(); 236 | module.exports = singletonThemeParser; --------------------------------------------------------------------------------