├── .gitignore ├── doc ├── copy.png ├── copied.png ├── bindings.png ├── completion.png ├── contexts.png ├── icons │ ├── adc.png │ ├── bus.png │ ├── dac.png │ ├── gpio.png │ ├── clock.png │ ├── flash.png │ └── interrupts.png └── devicetree_icon.png ├── src ├── test │ ├── test.c │ ├── test.invalid.c │ ├── output.h │ ├── runTest.ts │ ├── suite │ │ └── index.ts │ ├── test.h │ └── parser.test.ts ├── util.ts ├── diags.ts ├── compiledOutput.ts ├── zephyr.ts ├── parser.ts ├── preprocessor.ts ├── treeView.ts └── types.ts ├── .vscodeignore ├── icons ├── dark │ ├── dac.svg │ ├── flash.svg │ ├── clock.svg │ ├── overlay.svg │ ├── interrupts.svg │ ├── gpio.svg │ ├── circuit-board.svg │ ├── bus.svg │ ├── shield.svg │ ├── adc.svg │ ├── remove-shield.svg │ ├── add-shield.svg │ └── devicetree-inner.svg └── light │ ├── dac.svg │ ├── flash.svg │ ├── clock.svg │ ├── overlay.svg │ ├── interrupts.svg │ ├── gpio.svg │ ├── circuit-board.svg │ ├── bus.svg │ ├── shield.svg │ ├── adc.svg │ ├── remove-shield.svg │ ├── add-shield.svg │ └── devicetree-inner.svg ├── tsconfig.json ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .eslintrc.js ├── syntax ├── devicetree-language.json ├── bindings-schema.yaml └── dts.tmLanguage.json ├── LICENSE ├── webpack.config.js ├── README.md ├── CHANGELOG.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | *.vsix 4 | dist -------------------------------------------------------------------------------- /doc/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/copy.png -------------------------------------------------------------------------------- /doc/copied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/copied.png -------------------------------------------------------------------------------- /doc/bindings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/bindings.png -------------------------------------------------------------------------------- /doc/completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/completion.png -------------------------------------------------------------------------------- /doc/contexts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/contexts.png -------------------------------------------------------------------------------- /doc/icons/adc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/icons/adc.png -------------------------------------------------------------------------------- /doc/icons/bus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/icons/bus.png -------------------------------------------------------------------------------- /doc/icons/dac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/icons/dac.png -------------------------------------------------------------------------------- /doc/icons/gpio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/icons/gpio.png -------------------------------------------------------------------------------- /doc/icons/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/icons/clock.png -------------------------------------------------------------------------------- /doc/icons/flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/icons/flash.png -------------------------------------------------------------------------------- /doc/devicetree_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/devicetree_icon.png -------------------------------------------------------------------------------- /doc/icons/interrupts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trond-snekvik/vscode-devicetree/HEAD/doc/icons/interrupts.png -------------------------------------------------------------------------------- /src/test/test.c: -------------------------------------------------------------------------------- 1 | // Hello 2 | #pragma once 3 | 4 | This is test.c 5 | right here 6 | 7 | #define INCLUDED_TEST_C YES -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .vscode-test 3 | out 4 | test 5 | src 6 | node_modules 7 | **/*.map 8 | .gitignore 9 | tsconfig.json 10 | webpack.config.js 11 | dist/*.map -------------------------------------------------------------------------------- /icons/dark/dac.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/light/dac.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/dark/flash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/light/flash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/dark/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /icons/light/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /icons/dark/overlay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /icons/light/overlay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /icons/dark/interrupts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/light/interrupts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2019" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "." 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | ".vscode-test" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/test/test.invalid.c: -------------------------------------------------------------------------------- 1 | 2 | #define NORMAL 0 3 | #define NORMAL 0 // fail (duplicate) 4 | #define NORMAL_WITH_ARGS(a) valid 5 | #define NORMAL_WITH_ARGS(a) valid // fail (duplicate) 6 | #undef NON_EXISTING // fail 7 | #nonsense // fail 8 | #define // fail 9 | #undef // fail 10 | #if // fail 11 | #endif 12 | #include // fail 13 | #ifdef // fail 14 | #endif 15 | 16 | #ifndef // fail 17 | #endif 18 | 19 | -------------------------------------------------------------------------------- /src/test/output.h: -------------------------------------------------------------------------------- 1 | This is test.c 2 | right here 3 | 9999 is higher than 123 + 456 4 | SHOULD BE INCLUDED 5 | 1 + 123 + 456 6 | this is some text that will just be included as is. 7 | We found this in test.c: 1 8 | SOME NUMBERS: 1 + 123 + 456 9 | SUM: 1 + 123 + 456 + 1 + 2 + 3 + 8 + 9 10 | firstsecond test_first second_test test_firstsecond_test 11 | "this should be a string" 12 | 1, 2, 3, 4, 5 13 | 1, 2, 3, 4, 5 14 | 1, 2, 15 | 1, 2 16 | current line: 69 17 | current file: "test.h" -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": true 11 | }, 12 | "eslint.format.enable": true, 13 | "eslint.lintTask.enable": true, 14 | "prettier.tabWidth": 4, 15 | } -------------------------------------------------------------------------------- /icons/dark/gpio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icons/light/gpio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | } 20 | }; -------------------------------------------------------------------------------- /icons/dark/circuit-board.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/light/circuit-board.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/dark/bus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /icons/light/bus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /icons/dark/shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/light/shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": "build", 15 | "label": "npm: watch", 16 | "detail": "tsc -watch -p ./" 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "lint", 21 | "problemMatcher": "$eslint-compact", 22 | "isBackground": false, 23 | "group": "build" 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "webpack", 28 | "problemMatcher": [], 29 | "label": "npm: webpack", 30 | "detail": "webpack --mode development", 31 | "group": { 32 | "kind": "build", 33 | "isDefault": true 34 | } 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as path from 'path'; 7 | import { runTests } from 'vscode-test'; 8 | 9 | async function main() { 10 | try { 11 | // The folder containing the Extension Manifest package.json 12 | // Passed to `--extensionDevelopmentPath` 13 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 14 | 15 | // The path to the extension test runner script 16 | // Passed to --extensionTestsPath 17 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 18 | 19 | // Download VS Code, unzip it and run the integration test 20 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 21 | } catch (err) { 22 | console.error(err); 23 | console.error('Failed to run tests'); 24 | process.exit(1); 25 | } 26 | } 27 | 28 | main(); -------------------------------------------------------------------------------- /syntax/devicetree-language.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": ["/*", "*/"] 5 | }, 6 | "brackets": [["{", "}"], ["[", "]"], ["(", ")"], ["<", ">"], ["\"", "\""]], 7 | "autoClosingPairs": [ 8 | { "open": "{", "close": "}" }, 9 | { "open": "[", "close": "]" }, 10 | { "open": "(", "close": ")" }, 11 | { "open": "<", "close": ">" }, 12 | { "open": "\"", "close": "\"", "notIn": ["string"] }, 13 | { "open": "/*", "close": " */", "notIn": ["string"] } 14 | ], 15 | "autoCloseBefore": ";:.,=}])>\n\t", 16 | "surroundingPairs": [ 17 | ["{", "}"], 18 | ["[", "]"], 19 | ["(", ")"], 20 | ["<", ">"], 21 | ["\"", "\""] 22 | ], 23 | "wordPattern": "[&#]?[\\w,@\\/-]+", 24 | "indentationRules": { 25 | "increaseIndentPattern": "{", 26 | "decreaseIndentPattern": "}" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], 14 | "preLaunchTask": "npm: webpack" 15 | }, 16 | { 17 | "name": "Run Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": [ 22 | "--extensionDevelopmentPath=${workspaceFolder}", 23 | "--extensionTestsPath=${workspaceFolder}/out/src/test/suite/index" 24 | ], 25 | "outFiles": ["${workspaceFolder}/out/src/test/**/*.js"], 26 | "preLaunchTask": "npm: watch" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as path from 'path'; 7 | import * as Mocha from 'mocha'; 8 | import * as glob from 'glob'; 9 | 10 | export function run(): Promise { 11 | // Create the mocha test 12 | const mocha = new Mocha({ 13 | ui: 'tdd' 14 | }); 15 | mocha.useColors(true); 16 | 17 | const testsRoot = path.resolve(__dirname, '..'); 18 | 19 | return new Promise((c, e) => { 20 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 21 | if (err) { 22 | return e(err); 23 | } 24 | 25 | // Add files to the test suite 26 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 27 | 28 | try { 29 | // Run the mocha test 30 | mocha.run(failures => { 31 | if (failures > 0) { 32 | e(new Error(`${failures} tests failed.`)); 33 | } else { 34 | c(); 35 | } 36 | }); 37 | } catch (err) { 38 | e(err); 39 | } 40 | }); 41 | }); 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Trond Einar Snekvik 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 | -------------------------------------------------------------------------------- /icons/dark/adc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/light/adc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/dark/remove-shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/light/remove-shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/dark/add-shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/light/add-shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/dark/devicetree-inner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icons/light/devicetree-inner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: 'extension.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]' 18 | }, 19 | devtool: 'source-map', 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | node: { 28 | __dirname: false, 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }; 44 | module.exports = config; -------------------------------------------------------------------------------- /src/test/test.h: -------------------------------------------------------------------------------- 1 | // Hello 2 | 3 | #include "test.c" 4 | // Should only process test.c once because of #pragma once 5 | #include "test.c" 6 | #define SHOULD_PROCESS_FIRST_LINE_AFTER_GETTING_OUT_OF_PRAGMA_ONCE 1 // known issue 7 | 8 | #define YES 1 9 | 10 | #if YES 11 | #define SOME_SUM 123 + 456 12 | #else 13 | #error "oh no!" 14 | #endif 15 | 16 | #ifdef YES 17 | #define SOME_LARGE_NUMBER 9999 18 | #endif 19 | 20 | #if SOME_LARGE_NUMBER > SOME_SUM 21 | //comments are not included 22 | SOME_LARGE_NUMBER is higher than SOME_SUM 23 | #endif 24 | 25 | #ifdef HELLO 26 | #endif 27 | 28 | #if defined(YES) && !defined(NO) 29 | SHOULD BE INCLUDED 30 | #else 31 | SHOULD NOT BE INCLUDED 32 | #endif 33 | 34 | /* block comments aren't evaluated 35 | #ifndef YES 36 | // this shouldn't be in the compiled file. 37 | #else 38 | // this should. 39 | #endif 40 | THIS IS EXCLUDED 41 | */ 42 | YES + SOME_SUM 43 | this is some text that will just be included as is. 44 | 45 | We found this in test.c: INCLUDED_TEST_C 46 | 47 | #define RECURSIVE YES + SOME_SUM 48 | 49 | SOME NUMBERS: RECURSIVE 50 | 51 | #define MACRO(aa, bb, cc) aa + bb + cc 52 | 53 | SUM: MACRO(RECURSIVE, MACRO(1, 2, 3), 8 + 9) 54 | 55 | #define CONCAT_TEST(a, b) a##b test_##a b##_test test_##a##b##_test 56 | #define STRINGIFY(a) #a 57 | 58 | CONCAT_TEST(first, second) 59 | STRINGIFY(this should be a string) 60 | 61 | #define VAR_ARGS(a, b, ...) a, b, __VA_ARGS__ 62 | #define VAR_ARGS_OPT_ARGS(a, b, ...) a, b, ##__VA_ARGS__ 63 | 64 | VAR_ARGS(1, 2, 3, 4, 5) // 1, 2, 3, 4, 5 65 | VAR_ARGS_OPT_ARGS(1, 2, 3, 4, 5) // 1, 2, 3, 4, 5 66 | VAR_ARGS(1, 2) // 1, 2, 67 | VAR_ARGS_OPT_ARGS(1, 2) // 1, 2 68 | 69 | current line: __LINE__ 70 | current file: __FILE__ 71 | 72 | #ifdef TEST_DIAGS 73 | 74 | #ifdef TEST_VALID_DIRECTIVES 75 | 76 | #define NORMAL 0 77 | #define NO_VALUE 78 | #define NORMAL_WITH_ARGS(a) valid 79 | #define case_sensitive(a) valid 80 | #define CASE_SENSITIVE(a) should not be a duplicate 81 | #define NO_ARGUMENTS() valid 82 | #define SPACE_BEFORE_ARGUMENTS (a) valid 83 | #define NO_VALUE_WITH_ARGS(a) 84 | #define MULTIPLE_ARGS(a, b, c) a + b + c 85 | #define VARIABLE_ARGS(a, b, c, ...) a + b + c + __VA_ARGS__ 86 | 87 | #undef NO_VALUE 88 | #undef NORMAL 89 | 90 | #endif 91 | 92 | #ifdef TEST_INVALID_DIRECTIVES 93 | #include "test.invalid.c" 94 | #endif 95 | 96 | #endif -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as vscode from 'vscode'; 7 | 8 | export function countText(count: number, text: string, plural?: string): string { 9 | if (!plural) { 10 | plural = text + 's'; 11 | } 12 | 13 | let out = count.toString() + ' '; 14 | if (count === 1) { 15 | out += text; 16 | } else { 17 | out += plural; 18 | } 19 | 20 | return out; 21 | } 22 | 23 | export function capitalize(str: string): string { 24 | return str.replace(/([a-z])(\w+)/g, (word, first: string, rest: string) => { 25 | const acronyms = [ 26 | 'ADC', 'DAC', 'GPIO', 'SPI', 'I2C', 'RX', 'TX', 'DMA', 27 | ]; 28 | if (acronyms.includes(word.toUpperCase())) { 29 | return word.toUpperCase(); 30 | } 31 | return first.toUpperCase() + rest; 32 | }); 33 | } 34 | 35 | export function evaluateExpr(expr: string, start: vscode.Position, diags: vscode.Diagnostic[]=[]) { 36 | expr = expr.trim().replace(/([\d.]+|0x[\da-f]+)[ULf]+/gi, '$1'); 37 | let m: RegExpMatchArray; 38 | let level = 0; 39 | let text = ''; 40 | while ((m = expr.match(/(?:(?:<<|>>|&&|\|\||[!=<>]=|[|&~^<>!=+/*-]|\s*|0x[\da-fA-F]+|[\d.]+|'.')\s*)*([()]?)/)) && m[0].length) { 41 | text += m[0].replace(/'(.)'/g, (_, char: string) => char.codePointAt(0).toString()); 42 | if (m[1] === '(') { 43 | level++; 44 | } else if (m[1] === ')') { 45 | if (!level) { 46 | return undefined; 47 | } 48 | 49 | level--; 50 | } 51 | 52 | expr = expr.slice(m.index + m[0].length); 53 | } 54 | 55 | if (!text || level || expr) { 56 | diags.push(new vscode.Diagnostic(new vscode.Range(start.line, start.character + m.index, start.line, start.character + m.index), `Unterminated expression`)); 57 | return undefined; 58 | } 59 | 60 | try { 61 | return eval(text); 62 | } catch (e) { 63 | diags.push(new vscode.Diagnostic(new vscode.Range(start.line, start.character, start.line, start.character + text.length), `Unable to evaluate expression`)); 64 | return undefined; 65 | } 66 | } 67 | 68 | export function sizeString(size): string { 69 | const spec = [ 70 | { size: 1024 * 1024 * 1024, name: 'GB' }, 71 | { size: 1024 * 1024, name: 'MB' }, 72 | { size: 1024, name: 'kB' }, 73 | { size: 1, name: 'bytes' }, 74 | ].find(spec => Math.abs(size) >= spec.size && !(size % spec.size)); 75 | 76 | if (size % spec.size) { 77 | return (size / spec.size).toFixed(3) + ' ' + spec.name; 78 | } 79 | 80 | return (size / spec.size).toString() + ' ' + spec.name; 81 | } 82 | -------------------------------------------------------------------------------- /src/diags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | 9 | export class DiagnosticsSet { 10 | private sets: {[path: string]: {uri: vscode.Uri, diags: vscode.Diagnostic[], actions: vscode.CodeAction[]}} = {}; 11 | private last?: { uri: vscode.Uri, diag: vscode.Diagnostic }; 12 | 13 | get length() { 14 | return Object.values(this.sets).reduce((sum, s) => sum + s.diags.length, 0); 15 | } 16 | 17 | pushLoc(loc: vscode.Location, message: string, severity: vscode.DiagnosticSeverity=vscode.DiagnosticSeverity.Warning) { 18 | return this.push(loc.uri, new vscode.Diagnostic(loc.range, message, severity)); 19 | } 20 | 21 | private set(uri: vscode.Uri) { 22 | if (!(uri.toString() in this.sets)) { 23 | this.sets[uri.toString()] = {uri: uri, diags: [], actions: []}; 24 | } 25 | 26 | return this.sets[uri.toString()]; 27 | } 28 | 29 | push(uri: vscode.Uri, ...diags: vscode.Diagnostic[]) { 30 | this.set(uri).diags.push(...diags); 31 | 32 | this.last = { uri, diag: diags[diags.length - 1]}; 33 | 34 | return diags[diags.length - 1]; 35 | } 36 | 37 | pushAction(action: vscode.CodeAction, uri?: vscode.Uri) { 38 | if (uri) { 39 | /* overrides this.last */ 40 | } else if (this.last) { 41 | uri = this.last.uri; 42 | action.diagnostics = [this.last.diag]; 43 | } else { 44 | throw new Error("Pushing action without uri or existing diag"); 45 | } 46 | 47 | this.set(uri).actions.push(action); 48 | 49 | return action; 50 | } 51 | 52 | merge(other: DiagnosticsSet) { 53 | Object.values(other.sets).forEach(set => { 54 | this.push(set.uri, ...set.diags); 55 | set.actions.forEach(action => this.pushAction(action, set.uri)); 56 | }); 57 | } 58 | 59 | getActions(uri: vscode.Uri, range: vscode.Range | vscode.Position) { 60 | const set = this.sets[uri.toString()]; 61 | if (!set) { 62 | return []; 63 | } 64 | 65 | if (range instanceof vscode.Position) { 66 | range = new vscode.Range(range, range); 67 | } 68 | 69 | return set.actions.filter(action => action.diagnostics?.find(diag => diag.range.intersection(range as vscode.Range))); 70 | } 71 | 72 | clear() { 73 | this.sets = {}; 74 | this.last = undefined; 75 | } 76 | 77 | diags(uri: vscode.Uri) { 78 | return this.sets[uri.toString()]?.diags; 79 | } 80 | 81 | get all() { 82 | return Object.values(this.sets); 83 | } 84 | 85 | toString() { 86 | return this.all.flatMap(file => file.diags.map(d => `${path.basename(file.uri.fsPath)}:${d.range.start.line + 1}: ${vscode.DiagnosticSeverity[d.severity]}: ${d.message}`)).join('\n'); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /src/compiledOutput.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as dts from './dts'; 3 | 4 | type CompiledEntity = { start: number, end: number, entity?: dts.Node | dts.Property }; 5 | 6 | export class DTSDocumentProvider implements vscode.TextDocumentContentProvider { 7 | private readonly INDENT = ' '.repeat(8); 8 | private parser: dts.Parser; 9 | private changeEmitter: vscode.EventEmitter; 10 | onDidChange: vscode.Event; 11 | currUri?: vscode.Uri; 12 | 13 | entities: CompiledEntity[]; 14 | 15 | constructor(parser: dts.Parser) { 16 | this.changeEmitter = new vscode.EventEmitter(); 17 | this.onDidChange = this.changeEmitter.event; 18 | this.parser = parser; 19 | this.parser.onChange(ctx => { 20 | if (this.currUri && ctx.has(vscode.Uri.file(this.currUri.query))) { 21 | this.changeEmitter.fire(this.currUri); 22 | } 23 | }); 24 | } 25 | 26 | private async getDoc() { 27 | if (!this.currUri) { 28 | return; 29 | } 30 | 31 | return vscode.workspace.openTextDocument(this.currUri); 32 | } 33 | 34 | async entityRange(entity: dts.Node | dts.Property) { 35 | const doc = await this.getDoc(); 36 | if (!doc) { 37 | return; 38 | } 39 | 40 | const e = this.entities.find(e => e.entity === entity); 41 | if (!e) { 42 | return; 43 | } 44 | 45 | return new vscode.Range(doc.positionAt(e.start), doc.positionAt(e.end)); 46 | } 47 | 48 | async getEntity(pos: vscode.Position) { 49 | const doc = await this.getDoc(); 50 | if (!doc) { 51 | return; 52 | } 53 | 54 | const offset = doc.offsetAt(pos); 55 | return this.entities.find(e => e.start <= offset && e.end >= offset)?.entity; 56 | } 57 | 58 | is(doc: vscode.Uri) { 59 | return doc.toString() === this.currUri?.toString(); 60 | } 61 | 62 | provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { 63 | this.currUri = uri; 64 | const ctx = this.parser.ctx(vscode.Uri.file(uri.query)); 65 | if (!ctx) { 66 | return `/* Unable to resolve path ${uri.toString()} */`; 67 | } 68 | 69 | const entities = new Array(); 70 | let text = '/dts-v1/;\n\n'; 71 | const addEntity = (entity: dts.Node | dts.Property | undefined, content: string) => { 72 | const e = { entity, start: text.length }; 73 | entities.push(e); 74 | text += content; 75 | e.end = text.length; 76 | }; 77 | 78 | const addNode = (n: dts.Node, indent = '') => { 79 | text += indent; 80 | const nodeEntity = { entity: n, start: text.length }; 81 | entities.push(nodeEntity); 82 | const labels = n.labels(); 83 | if (labels?.length) { 84 | text += `${labels.join(': ')}: `; 85 | } 86 | 87 | text += `${n.fullName} {\n`; 88 | n.uniqueProperties().forEach(p => { 89 | text += indent + this.INDENT; 90 | if (p.boolean !== undefined) { 91 | addEntity(p, p.name); 92 | } else { 93 | addEntity(p, p.name); 94 | text += ' = '; 95 | addEntity(undefined, p.valueString((indent + this.INDENT + p.name + ' = ').length)); 96 | } 97 | 98 | text += ';\n'; 99 | }); 100 | 101 | n.children().forEach(c => addNode(c, indent + this.INDENT)); 102 | 103 | text += `${indent}};`; 104 | nodeEntity.end = text.length; 105 | text += '\n\n'; 106 | }; 107 | 108 | addNode(ctx.root); 109 | 110 | this.entities = entities.reverse(); // reverse to optimize the entity lookup 111 | return text; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/zephyr.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | import { env } from 'process'; 9 | import { ExecOptions, exec } from 'child_process'; 10 | import { existsSync, readFileSync } from 'fs'; 11 | import * as glob from 'glob'; 12 | import * as yaml from 'js-yaml'; 13 | 14 | export type BoardInfo = { identifier: string, name: string, type: string, arch: string, toolchain: string[], ram: number, flash: number, supported: string[] }; 15 | export type Board = { name: string, path: string, arch?: string, info?: BoardInfo | {[name: string]: any} }; 16 | const conf = vscode.workspace.getConfiguration(); 17 | export let zephyrRoot: string; 18 | let westExe: string; 19 | let westVersion: string; 20 | let boards: Board[]; 21 | export let modules: string[]; 22 | 23 | function west(...args: string[]): Promise { 24 | 25 | const command = westExe + ' ' + args.join(' '); 26 | 27 | const options: ExecOptions = { 28 | cwd: zephyrRoot ?? vscode.workspace.workspaceFolders?.find(w => w.name.match(/zephyr/i))?.uri.fsPath ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath, 29 | }; 30 | 31 | return new Promise((resolve, reject) => { 32 | exec(command, options, (err, out) => { 33 | if (err) { 34 | reject(err); 35 | } else { 36 | resolve(out); 37 | } 38 | }); 39 | }); 40 | } 41 | 42 | export function openConfig(entry: string) { 43 | vscode.commands.executeCommand('workbench.action.openSettings', entry); 44 | } 45 | 46 | async function findWest() { 47 | if (!(westExe = conf.get('devicetree.west') as string) && 48 | !(westExe = conf.get('kconfig.zephyr.west') as string)) { 49 | westExe = 'west'; 50 | } 51 | 52 | return west('-V').then(version => { 53 | westVersion = version.match(/v\d+\.\d+\.\d+/)?.[0]; 54 | }, (err: Error) => { 55 | vscode.window.showErrorMessage(`Couldn't find west (${err.name})`, 'Configure west path...').then(() => { 56 | openConfig('devicetree.west'); 57 | }); 58 | }); 59 | } 60 | 61 | async function findZephyrRoot() { 62 | if (!(zephyrRoot = conf.get('devicetree.zephyr') as string) && 63 | !(zephyrRoot = conf.get('kconfig.zephyr.base') as string) && 64 | !(zephyrRoot = env['ZEPHYR_BASE'] as string)) { 65 | return Promise.all([west('topdir'), west('config', 'zephyr.base')]).then(([topdir, zephyr]) => { 66 | zephyrRoot = path.join(topdir.trim(), zephyr.trim()); 67 | }, err => { 68 | vscode.window.showErrorMessage(`Couldn't find Zephyr root`, 'Configure...').then(() => { 69 | openConfig('devicetree.zephyr'); 70 | }); 71 | }); 72 | } 73 | } 74 | 75 | export function findBoard(board: string): Board { 76 | return boards.find(b => b.name === board); 77 | } 78 | 79 | export async function isBoardFile(uri: vscode.Uri) { 80 | if (path.extname(uri.fsPath) !== '.dts') { 81 | return false; 82 | } 83 | 84 | for (const root of boardRoots()) { 85 | if (uri.fsPath.startsWith(path.normalize(root))) { 86 | return true; 87 | } 88 | } 89 | 90 | return false; 91 | } 92 | 93 | export async function defaultBoard(): Promise { 94 | const dtsBoard = conf.get('devicetree.defaultBoard') as string; 95 | if (dtsBoard) { 96 | const path = findBoard(dtsBoard); 97 | if (path) { 98 | console.log('Using default board'); 99 | return path; 100 | } 101 | } 102 | 103 | const kconfigBoard = conf.get('kconfig.zephyr.board') as { board: string, arch: string, dir: string }; 104 | if (kconfigBoard?.dir && kconfigBoard.board) { 105 | const board = { name: kconfigBoard.board, path: path.join(kconfigBoard.dir, kconfigBoard.board + '.dts'), arch: kconfigBoard.arch }; 106 | if (existsSync(board.path)) { 107 | console.log('Using Kconfig board'); 108 | return board; 109 | } 110 | } 111 | 112 | console.log('Using fallback board'); 113 | return findBoard('nrf52dk_nrf52832') ?? findBoard('nrf52_pca10040'); 114 | } 115 | 116 | function boardRoots(): string[] { 117 | return modules.map(m => m + '/boards').filter(dir => existsSync(dir)); 118 | } 119 | 120 | async function findBoards() { 121 | boards = new Array(); 122 | return Promise.all(boardRoots().map(root => new Promise(resolve => glob(`**/*.dts`, { cwd: root }, (err, matches) => { 123 | if (!err) { 124 | matches.forEach(m => boards.push({name: path.basename(m, '.dts'), path: `${root}/${m}`, arch: m.split(/[/\\]/)?.[0]})); 125 | } 126 | 127 | resolve(); 128 | })))); 129 | } 130 | 131 | async function loadModules() { 132 | modules = await west('list', '-f', '{posixpath}').then(out => out.split(/\r?\n/).map(line => line.trim()), _ => []); 133 | await findBoards(); 134 | } 135 | 136 | export async function selectBoard(prompt='Set board'): Promise { 137 | return vscode.window.showQuickPick(boards.map(board => { label: board.name, description: board.arch, board }), { placeHolder: prompt }).then(board => board['board']); 138 | } 139 | 140 | export async function activate(ctx: vscode.ExtensionContext) { 141 | await findWest(); 142 | await findZephyrRoot(); 143 | if (zephyrRoot) { 144 | await loadModules(); 145 | return; 146 | } 147 | 148 | return new Promise(resolve => { 149 | ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => { 150 | if (e.affectsConfiguration('kconfig.zephyr.base') || e.affectsConfiguration('kconfig.zephyr.west') || 151 | e.affectsConfiguration('devicetree.zephyr') || e.affectsConfiguration('devicetree.west')) { 152 | await findWest(); 153 | await findZephyrRoot(); 154 | if (zephyrRoot) { 155 | await loadModules(); 156 | resolve(); 157 | } 158 | } 159 | })); 160 | }); 161 | } 162 | 163 | export function resolveBoardInfo(board: Board) { 164 | const file = path.join(path.dirname(board.path), board.name + '.yaml'); 165 | if (!existsSync(file)) { 166 | return; 167 | } 168 | 169 | const out = readFileSync(file, 'utf-8'); 170 | if (!out) { 171 | return; 172 | } 173 | 174 | board.info = yaml.load(out, { json: true }) || {}; 175 | } 176 | -------------------------------------------------------------------------------- /src/test/parser.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | import { after } from 'mocha'; 9 | import * as path from 'path'; 10 | import * as fs from 'fs'; 11 | import { preprocess, Define, MacroInstance, Line, toDefines } from '../preprocessor'; 12 | import { evaluateExpr } from '../util'; 13 | import { DiagnosticsSet } from '../diags'; 14 | 15 | // bake: Output needs to be manually verified 16 | const BAKE_OUTPUT = false; 17 | 18 | suite('Parser test suite', () => { 19 | after(() => { 20 | vscode.window.showInformationMessage('All tests done!'); 21 | }); 22 | 23 | test('Preprocessor', async () => { 24 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 25 | const inputFile = extensionDevelopmentPath + '/src/test/test.h'; 26 | const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(inputFile)); 27 | const diags = new DiagnosticsSet(); 28 | 29 | let result = await preprocess(doc, {}, [], diags); 30 | if (BAKE_OUTPUT) { 31 | fs.writeFileSync(extensionDevelopmentPath + '/src/test/output.h', result.lines.map(l => l.text).join('\n')); 32 | } 33 | 34 | const expected = fs.readFileSync(extensionDevelopmentPath + '/src/test/output.h', 'utf-8').split(/\r?\n/g); 35 | assert.equal(result.lines.length, expected.length); 36 | 37 | result.lines.forEach((l, i) => { 38 | assert.equal(l.text.trim(), expected[i].trim()); 39 | }); 40 | 41 | result = await preprocess(doc, toDefines([new Define('TEST_DIAGS', ''), new Define('TEST_VALID_DIRECTIVES', '')]), [], diags); 42 | assert.equal(diags.length, 0, diags.toString()); 43 | result = await preprocess(doc, toDefines([new Define('TEST_DIAGS', ''), new Define('TEST_INVALID_DIRECTIVES', '')]), [], diags); 44 | }); 45 | 46 | test('Nested macros', async () => { 47 | const doc = await vscode.workspace.openTextDocument({language: 'dts', content: ` 48 | #define SUM(a, b) a + b 49 | #define STR(a) # a 50 | #define LITERAL(a) a 51 | #define STR2(a) STR(a) 52 | #define CONCAT(a, b) a##b 53 | #define CONCAT2(a, b) CONCAT(a, b) 54 | #define CONCAT3(a, b, c) a##b##c 55 | #define VAL xyz 56 | #define VAL2 ffff 57 | #define PARAM_CONCAT(a) VAL##a 58 | #define PARAM_CONCAT2(a) VAL##undefined 59 | VAL == xyz 60 | CONCAT(p, q) == pq 61 | CONCAT(VAL, 1) == VAL1 62 | CONCAT(VAL, VAL) == VALVAL 63 | LITERAL(VAL) == xyz 64 | STR(VAL) == "VAL" 65 | STR2(VAL) == "xyz" 66 | CONCAT3(V, A, L) == xyz 67 | CONCAT2(V, AL) == xyz 68 | PARAM_CONCAT(2) == ffff 69 | PARAM_CONCAT2(2) == VALundefined 70 | `}); 71 | const diags = new DiagnosticsSet(); 72 | 73 | const result = await preprocess(doc, {}, [], diags); 74 | assert.equal(diags.all.length, 0, diags.all.toString()); 75 | result.lines.filter(line => line.text.includes('==')).map(line => { 76 | const actual = line.text.split('==')[0].trim(); 77 | const expected = line.raw.split('==')[1].trim(); 78 | // console.log(`${actual} == ${expected}`); 79 | return {actual, expected, line}; 80 | }).forEach((v) => assert.equal(v.actual, v.expected, v.line.raw)); 81 | }); 82 | 83 | test('Line remap', () => { 84 | const line = new Line('foo MACRO_1 MACRO_2 abc', 0, vscode.Uri.file('test'), [ 85 | new MacroInstance(new Define('MACRO_1', 'bar'), 'MACRO_1', 'bar', 4), 86 | new MacroInstance(new Define('MACRO_2', '1234'), 'MACRO_2', '1234', 12), 87 | ]); 88 | 89 | assert.equal(line.text, 'foo bar 1234 abc'); 90 | assert.equal(line.rawPos(0, true), 0); 91 | assert.equal(line.rawPos(4, true), 4); // start of first macro 92 | assert.equal(line.rawPos(5, true), 4); // middle of first macro 93 | assert.equal(line.rawPos(6, true), 4); // middle of first macro 94 | assert.equal(line.rawPos(7, true), 11); // right after first macro 95 | assert.equal(line.rawPos(8, true), 12); // start of second macro 96 | assert.equal(line.rawPos(11, true), 12); // middle of second macro 97 | assert.equal(line.rawPos(12, true), 19); // after second macro 98 | assert.equal(line.rawPos(13, false), 20); // after second macro 99 | 100 | assert.equal(line.rawPos(0, false), 0); 101 | assert.equal(line.rawPos(4, false), 11); // start of first macro 102 | assert.equal(line.rawPos(5, false), 11); // middle of first macro 103 | assert.equal(line.rawPos(6, false), 11); // middle of first macro 104 | assert.equal(line.rawPos(7, false), 11); // right after first macro 105 | assert.equal(line.rawPos(8, false), 19); // start of second macro 106 | assert.equal(line.rawPos(11, false), 19); // middle of second macro 107 | assert.equal(line.rawPos(12, false), 19); // after second macro 108 | assert.equal(line.rawPos(13, false), 20); // after second macro 109 | }); 110 | 111 | test('Expressions', () => { 112 | const position = new vscode.Position(0, 0); 113 | assert.equal(0, evaluateExpr('0', position, [])); 114 | assert.equal(1, evaluateExpr('1', position, [])); 115 | assert.equal(3, evaluateExpr('1 + 2', position, [])); 116 | assert.equal(3, evaluateExpr('(1 + 2)', position, [])); 117 | assert.equal(256, evaluateExpr('1 << 8', position, [])); 118 | assert.equal(256, evaluateExpr('(1 << 8)', position, [])); 119 | assert.equal(256, evaluateExpr('(1.0f << 8ULL)', position, [])); 120 | assert.equal(1, evaluateExpr('(1)', position, [])); 121 | assert.equal(3, evaluateExpr('(1) + (2)', position, [])); 122 | assert.equal(0, evaluateExpr('(1) + (2 + (3 * 5ULL)) - 18', position, [])); 123 | assert.equal(undefined, evaluateExpr('(', position, [])); 124 | assert.equal(undefined, evaluateExpr(')', position, [])); 125 | assert.equal(undefined, evaluateExpr('())', position, [])); 126 | assert.equal(undefined, evaluateExpr('level + 1', position, [])); 127 | assert.equal(undefined, evaluateExpr('1 + level', position, [])); 128 | assert.equal(undefined, evaluateExpr('1 + 2 level', position, [])); 129 | assert.equal(false, evaluateExpr('1 == 2', position, [])); 130 | assert.equal(true, evaluateExpr('1 <= 2', position, [])); 131 | assert.equal(true, evaluateExpr('1 < 2', position, [])); 132 | assert.equal(98, evaluateExpr("'a' + 1", position, [])); 133 | }); 134 | }); -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as vscode from 'vscode'; 7 | import { DiagnosticsSet } from './diags'; 8 | import { Line } from './preprocessor'; 9 | 10 | type Offset = { line: number, col: number }; 11 | 12 | export class ParserState { 13 | readonly token = /^[#-\w]+|./; 14 | readonly lines: Line[]; 15 | private offset: Offset; 16 | private prevRange: { start: Offset, length: number }; 17 | diags: DiagnosticsSet; 18 | uri: vscode.Uri; 19 | 20 | location(start?: Offset, end?: Offset) { 21 | if (!start) { 22 | start = this.prevRange.start; 23 | } 24 | 25 | if (!end) { 26 | end = { line: this.prevRange.start.line, col: this.prevRange.start.col + this.prevRange.length }; 27 | } 28 | 29 | const startLine = this.lines[start.line]; 30 | const endLine = this.lines[end.line]; 31 | 32 | return new vscode.Location(startLine.uri, 33 | new vscode.Range(startLine.number, startLine.rawPos(start.col, true), 34 | endLine.number, endLine.rawPos(end.col, false))); 35 | } 36 | 37 | getLine(uri: vscode.Uri, pos: vscode.Position) { 38 | return this.lines.find(l => l.contains(uri, pos)); 39 | } 40 | 41 | raw(loc: vscode.Location) { 42 | if (loc.range.isSingleLine) { 43 | return this.getLine(loc.uri, loc.range.start)?.raw.slice(loc.range.start.character, loc.range.end.character) ?? ''; 44 | } 45 | 46 | let i = this.lines.findIndex(l => l.contains(loc.uri, loc.range.start)); 47 | if (i < 0) { 48 | return ''; 49 | } 50 | 51 | let content = this.lines[i].raw.slice(loc.range.start.character); 52 | while (!this.lines[++i].contains(loc.uri, loc.range.end)) { 53 | content += this.lines[i].raw; 54 | } 55 | 56 | content += this.lines[i].raw.slice(0, loc.range.end.character); 57 | return content; 58 | } 59 | 60 | pushDiag(message: string, severity: vscode.DiagnosticSeverity=vscode.DiagnosticSeverity.Error, loc?: vscode.Location): vscode.Diagnostic { 61 | if (!loc) { 62 | loc = this.location(); 63 | } 64 | 65 | return this.diags.push(loc.uri, new vscode.Diagnostic(loc.range, message, severity)); 66 | } 67 | 68 | pushAction(title: string, kind?: vscode.CodeActionKind): vscode.CodeAction { 69 | return this.diags.pushAction(new vscode.CodeAction(title, kind)); 70 | } 71 | 72 | pushInsertAction(title: string, insert: string, loc?: vscode.Location): vscode.CodeAction { 73 | if (!loc) { 74 | loc = this.location(); 75 | } 76 | 77 | const action = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); 78 | action.edit = new vscode.WorkspaceEdit(); 79 | action.edit.insert(loc.uri, loc.range.end, insert); 80 | 81 | return this.diags.pushAction(action); 82 | } 83 | 84 | pushDeleteAction(title: string, loc?: vscode.Location): vscode.CodeAction { 85 | if (!loc) { 86 | loc = this.location(); 87 | } 88 | 89 | const action = new vscode.CodeAction(title, vscode.CodeActionKind.Refactor); 90 | action.edit = new vscode.WorkspaceEdit(); 91 | action.edit.delete(loc.uri, loc.range); 92 | 93 | return this.diags.pushAction(action); 94 | } 95 | 96 | pushSemicolonAction(loc?: vscode.Location): vscode.CodeAction { 97 | const action = this.pushInsertAction('Add semicolon', ';', loc); 98 | action.isPreferred = true; 99 | return action; 100 | } 101 | 102 | match(pattern?: RegExp): RegExpMatchArray | undefined { 103 | const match = this.peek(pattern ?? this.token); 104 | if (match) { 105 | this.prevRange.start = { ...this.offset }; 106 | this.prevRange.length = match[0].length; 107 | 108 | this.offset.col += match[0].length; 109 | if (this.offset.col === this.lines[this.offset.line].length) { 110 | this.offset.col = 0; 111 | this.offset.line++; 112 | } 113 | } 114 | 115 | return match; 116 | } 117 | 118 | eof(): boolean { 119 | return this.offset.line === this.lines.length; 120 | } 121 | 122 | get next(): string { 123 | return this.lines[this.offset.line].text.slice(this.offset.col); 124 | } 125 | 126 | skipWhitespace() { 127 | const prevRange = { ...this.prevRange }; 128 | 129 | while (this.match(/^\s+/)); 130 | 131 | /* Ignore whitespace in diagnostics ranges */ 132 | this.prevRange = prevRange; 133 | return !this.eof(); 134 | } 135 | 136 | skipToken() { 137 | const match = this.match(this.token); 138 | if (!match) { 139 | this.offset.line = this.lines.length; 140 | return ''; 141 | } 142 | 143 | return match[0]; 144 | } 145 | 146 | reset(offset: Offset) { 147 | this.offset = offset; 148 | } 149 | 150 | peek(pattern?: RegExp) { 151 | if (this.offset.line >= this.lines.length) { 152 | return undefined; 153 | } 154 | 155 | return this.next.match(pattern ?? this.token); 156 | } 157 | 158 | peekLocation(pattern?: RegExp): vscode.Location { 159 | const match = this.peek(pattern ?? this.token); 160 | if (!match) { 161 | return undefined; 162 | } 163 | 164 | const prev = this.location(); 165 | return new vscode.Location(prev.uri, new vscode.Range(prev.range.end, new vscode.Position(prev.range.end.line, prev.range.end.character + match[0].length))); 166 | } 167 | 168 | freeze(): Offset { 169 | return { ...this.offset }; 170 | } 171 | 172 | since(start: Offset) { 173 | return this.lines.slice(start.line, this.offset.line + 1).map((l, i) => { 174 | if (i === this.offset.line - start.line) { 175 | if (i === 0) { 176 | return l.text.slice(start.col, this.offset.col); 177 | } 178 | 179 | return l.text.slice(0, this.offset.col); 180 | } 181 | 182 | if (i === 0) { 183 | return l.text.slice(start.col); 184 | } 185 | 186 | return l.text; 187 | }).join('\n'); 188 | } 189 | 190 | constructor(uri: vscode.Uri, diags: DiagnosticsSet, lines: Line[]) { 191 | this.uri = uri; 192 | this.diags = diags; 193 | this.offset = {line: 0, col: 0}; 194 | this.prevRange = { start: this.offset, length: 0 }; 195 | this.lines = lines; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /syntax/bindings-schema.yaml: -------------------------------------------------------------------------------- 1 | $schema: "http://json-schema.org/draft-07/schema" 2 | $id: https://github.com/trond-snekvik/vscode-devicetree/tree/master/syntax/bindings-schema.yaml 3 | # title: Zephyr Project DeviceTree bindings schema 4 | # additionalProperties: false 5 | type: object 6 | properties: 7 | description: 8 | $ref: "#/definitions/description" 9 | compatible: 10 | type: string 11 | description: | 12 | Unique identifier used for connecting nodes to bindings. Should generally follow a "vendor,type", where vendor is a unique vendor identifier, such as a ticker. The compatible identifier usually matches the filename of the binding. Examples: "nordic,nrf-gpio", "microchip,mcp2515" 13 | include: 14 | $ref: "#/definitions/include" 15 | on-bus: 16 | type: string 17 | description: | 18 | If the node appears on a bus, then the bus type should be given. 19 | 20 | When looking for a binding for a node, the code checks if the binding for the parent node contains 'bus: '. If it does, then only bindings with a matching 'on-bus: ' are considered. This allows the same type of device to have different bindings depending on what bus it appears on. 21 | bus: 22 | type: string 23 | description: | 24 | If the node describes a bus, then the bus type should be given. 25 | properties: 26 | $ref: "#/definitions/properties" 27 | child-binding: 28 | $ref: "#/definitions/child-binding" 29 | 30 | patternProperties: 31 | -cells$: 32 | type: array 33 | items: 34 | type: string 35 | description: | 36 | If the binding describes an interrupt controller, GPIO controller, pinmux device, or any other node referenced by other nodes via 'phandle-array' properties, then *-cells should be given. 37 | 38 | To understand the purpose of *-cells, assume that some node has 39 | 40 | pwms = <&pwm-ctrl 1 2>; 41 | 42 | where &pwm-ctrl refers to a node whose binding is this file. 43 | 44 | The <1 2> part of the property value is called a *specifier* (this terminology is from the devicetree specification), and contains additional data associated with the GPIO. Here, the specifier has two cells, and the node pointed at by &gpio-ctrl is expected to have '#pwm-cells = <2>'. 45 | 46 | *-cells gives a name to each cell in the specifier. These names are used when generating identifiers. 47 | additionalProperties: false 48 | 49 | definitions: 50 | include: 51 | type: 52 | - array 53 | - string 54 | - object 55 | description: | 56 | Defines one or more bindings this binding inherits properties and other attributes from. 57 | items: 58 | $ref: "#/definitions/include-item" 59 | $ref: "#/definitions/include-item" 60 | 61 | include-item: 62 | type: 63 | - string 64 | - object 65 | pattern: .*\.yaml$ 66 | properties: 67 | name: 68 | type: string 69 | pattern: .*\.yaml$ 70 | description: | 71 | Filename of the included binding. Should include the .yaml file extension. 72 | property-allowlist: 73 | description: | 74 | Array of properties that will be imported from the included binding. 75 | type: array 76 | items: 77 | type: string 78 | property-blocklist: 79 | description: | 80 | Array of properties that will be blocked from the included binding. 81 | type: array 82 | items: 83 | type: string 84 | child-binding: 85 | type: object 86 | description: | 87 | Filtering rules applied to the child-binding's properties 88 | properties: 89 | property-allowlist: 90 | description: | 91 | Array of properties that will be allowed from the include's child binding 92 | type: array 93 | items: 94 | type: string 95 | property-blocklist: 96 | description: | 97 | Array of properties that will be blocked from the include's child binding 98 | type: array 99 | items: 100 | type: string 101 | additionalProperties: false 102 | 103 | child-binding: 104 | type: object 105 | description: | 106 | 'child-binding' can be used when a node has children that all share the same properties. Each child gets the contents of 'child-binding' as its binding (though an explicit 'compatible = ...' on the child node takes precedence, ifa binding is found for it). 107 | 108 | Child bindings can also be used recursively. 109 | properties: 110 | description: 111 | $ref: "#/definitions/description" 112 | properties: 113 | $ref: "#/definitions/properties" 114 | child-binding: 115 | $ref: "#/definitions/child-binding" 116 | include: 117 | $ref: "#/definitions/include" 118 | additionalProperties: false 119 | description: 120 | type: string 121 | description: Human readable description. The description shows up in documentation and type hover. 122 | properties: 123 | type: object 124 | description: Map of properties that can be included in the DeviceTree node. 125 | additionalProperties: false 126 | patternProperties: 127 | '[\w-]*': 128 | type: object 129 | description: Node property. 130 | properties: 131 | type: 132 | description: | 133 | The property type determines how the property values are interpreted. 134 | type: string 135 | enum: 136 | - string 137 | - int 138 | - boolean 139 | - array 140 | - uint8-array 141 | - string-array 142 | - phandle 143 | - phandles 144 | - phandle-array 145 | - path 146 | - compound 147 | required: 148 | type: boolean 149 | description: | 150 | Whether this property is required. 151 | 152 | Required properties must be included in the DeviceTree node. 153 | description: 154 | type: string 155 | description: A human readable help text for the property. 156 | enum: 157 | type: array 158 | items: 159 | type: 160 | - string 161 | - integer 162 | description: A property enum value restricts the possible values for the property. 163 | const: 164 | type: 165 | - integer 166 | - string 167 | description: Specifies that the value for the property is expected to be a specific value. 168 | default: 169 | type: 170 | - string 171 | - integer 172 | - array 173 | description: | 174 | If this property is omitted from the DeviceTree node, its value is determined by the default value. 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeviceTree for the Zephyr Project 2 | 3 | DeviceTree language support for the [Zephyr project](https://zephyrproject.org/) in VS Code. 4 | 5 | This extension is an independent community contribution, and is not part of the Zephyr Project. 6 | 7 | ![Code completion](doc/completion.png) 8 | 9 | ## Features 10 | 11 | - Syntax highlighting 12 | - Syntax validation 13 | - Code completion 14 | - Valid properties 15 | - Valid nodes 16 | - Existing child nodes 17 | - Node types 18 | - Phandle cell names 19 | - Preprocessor defines 20 | - Type checking 21 | - Hover 22 | - Go to definition 23 | - Go to type definition 24 | - Show references 25 | - Breadcrumbs navigation 26 | - Workspace symbols 27 | - Preview compiled DeviceTree output 28 | - Copy C identifier to clipboard 29 | - Show GPIO pin assignments 30 | - Manage DeviceTree contexts 31 | - Format selection 32 | - Edit in overlay file menu entry 33 | - Code completion for bindings files (depends on [Red Hat's YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml)) 34 | - Code completion for certain `DT_...` macros in C 35 | - Linting language rules 36 | - Required properties 37 | - Reference validity 38 | - Phandle cell formats 39 | - Node specific rules 40 | - Bus matching 41 | - SPI chip select entries 42 | - Nexus node map validity 43 | - Address collisions 44 | - Name property matches 45 | - GPIO pin collisions 46 | - Duplicate labels 47 | - Nexus node map entry exists 48 | - Flash partitions fit inside their area 49 | - Suggest converting nested node to reference 50 | - DeviceTree overview 51 | - GPIO assignments 52 | - Flash partitions 53 | - Interrupts 54 | - Buses and their nodes 55 | - ADC channels 56 | - DAC channels 57 | - Clock sources 58 | 59 | ### Copy C identifiers 60 | 61 | While selecting a node, property or value in a DeviceTree file, right click and select "DeviceTree: Copy C identifier to clipboard" to copy the matching C identifier. 62 | 63 | ![Copy identifier](doc/copy.png) 64 | 65 | If the selected symbol has a corresponding C macro, like `DT_PROP(DT_NODELABEL(adc), label)`, it will be copied to the clipboard for usage in C files. A message shows up on the status bar if there was anything to copy. 66 | 67 | ![Copied identifier](doc/copied.png) 68 | 69 | ### Manage DeviceTree contexts 70 | 71 | If you work with more than one application or board, you'll have multiple sets of DeviceTree contexts - one for each of your builds. Every time you open a new DeviceTree file, the extension will add a DeviceTree context (unless this file is already part of an existing context). Each context corresponds to a single compiled DeviceTree file that goes into a build, and consists of a board file and a list of overlay files. 72 | 73 | The DeviceTree contexts show up in the explorer sidebar: 74 | 75 | ![DeviceTree Contexts](doc/contexts.png) 76 | 77 | The DeviceTree contexts can be saved in a context file by pressing the Save button on the DeviceTree context explorer. This allows you to restore the contexts the next time you open the folder. The location of the context file can be changed by setting the "devicetree.ctxFile" configuration entry in the VS Code settings. 78 | 79 | It's possible to add shield files to the same context by pressing "DeviceTree: Add Shield..." on the context in the DeviceTree context explorer. Shield files will be processed ahead of the overlay file. 80 | 81 | #### DeviceTree overview 82 | 83 | Each DeviceTree context presents an overview over common resources and their users. Each entry in the overview is linked with a specific node or property in the DeviceTree, and pressing them will go to the primary definition to the linked node or property. 84 | 85 | ![](doc/icons/gpio.png) **GPIO:** 86 | 87 | A list of all known gpio controllers, determined by the `gpio-controller` property. Each GPIO controller presents a list of the allocated pins and their owners, inferred from `gpios` properties, `-pins` properties and STM32 `pinctrl` properties. 88 | 89 | ![](doc/icons/flash.png) **Flash** 90 | 91 | A list of all flash controllers, i.e. nodes based on the `soc-nv-flash` type binding. If the the flash controllers contain a `fixed-partitions` node, each partition will be listed with their size and address. Any unallocated space in the flash area will also be listed. 92 | 93 | ![](doc/icons/interrupts.png) **Interrupts** 94 | 95 | A list of all interrupt controllers, determined by the `interrupt-controller` property. Lists the allocated interrupts on the controller and their users, as well as any other available information, such as their priority and index. 96 | 97 | ![](doc/icons/bus.png) **Buses** 98 | 99 | A list of all known buses on the device, determined by the `bus` entry in the node's type binding. Lists important properties of the bus, such as clock speed and flow control, as well each node on the bus, as well as their address if the bus has an address space. If the bus is an SPI bus, the chip select configuration of each node is also listed if it is known. 100 | 101 | ![](doc/icons/adc.png) **ADCs** 102 | 103 | A list of all ADC controllers on the device, i.e. nodes based on the `adc-controller` type binding. Each ADC controller contains a list of all allocated channels, based on references made to the ADC instances using the `io-channels` property. 104 | 105 | ![](doc/icons/dac.png) **DACs** 106 | 107 | A list of all DAC controllers on the device, i.e. nodes based on the `dac-controller` type binding. Each DAC controller contains a list of all allocated channels, based on references made to the DAC instances using the `io-channels` property. 108 | 109 | ![](doc/icons/clock.png) **Clocks** 110 | 111 | A list of all clock controllers on the device, i.e. nodes based on the `clock-controller` type binding. Each clock controller contains a list of all users of the clock, as well as any additional information, such as clock bus and flags. 112 | 113 | ### Bindings files code completion and schema validation 114 | 115 | The DeviceTree bindings are described in yaml files. This extension provides a schema file for [Red Hat's YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml), which provides code completion, documentation and validation for YAML files under dts/bindings. 116 | 117 | ![Bindings completion](doc/bindings.png) 118 | 119 | ### C file macro argument completion 120 | 121 | The extension will use data from the most recent DeviceTree context to provide completion items for the arguments of the following macros in C: 122 | 123 | - `DT_ALIAS` 124 | - `DT_NODELABEL` 125 | - `DT_PATH` 126 | - `DT_CHOSEN` 127 | 128 | ## Installation 129 | 130 | The extension can be installed from the Visual Studio Extension marketplace. 131 | 132 | It's also possible to download specific releases from the GitHub repository by picking a devicetree-X.X.X.vsix package from the GitHub releases tab. Open Visual Studio Code and run the "Install from VSIX..." command, either through the command palette (Ctrl+Shift+P) or by opening the extensions panel, and pressing the ... menu in the top corner. Locate the VSIX package, press "Install" and reload Visual Studio Code once prompted. 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.3.1 Accept standard properties 2 | 3 | Minor bugfix release to add standard properties in the list of accepted properties on a node type. 4 | Bumps Webpack to v5 to address security vulnerability in dev environment. 5 | 6 | # v2.3.0 New binding include syntax 7 | 8 | This release adds support for [the new binding include syntax](https://github.com/zephyrproject-rtos/zephyr/pull/29498), updates the icon and fixes several minor bugs. 9 | 10 | ### Binding include syntax 11 | 12 | The new binding include syntax broke the bindings loading system, which has been reworked, fixing several minor bugs in the process. Future changes to the binding format will now be caught during loading, and only leads to exclusion of the broken binding file, instead of aborting the entire binding loading. 13 | 14 | The bindings file YAML schema has been updated to support the new structure as well. 15 | 16 | ### New icon 17 | 18 | The extension icon has been updated to follow the [new Zephyr documentation style](https://github.com/zephyrproject-rtos/zephyr/pull/33299), which updates the look of the quick start icons on the [Zephyr documentation homepage](https://docs.zephyrproject.org) 19 | 20 | ### Other changes 21 | 22 | New code actions: 23 | - Convert unnecessarily nested node entries 24 | - Nodes that only contain a single child node can now be collapsed into a child entry with a code action. 25 | - "Go to Type definition" for properties 26 | - The Go to Type definition action now tries to find the type definition of properties, opening the bindings file that defines the property. 27 | 28 | Bindings: 29 | - Support for new include syntax 30 | - Warns about invalid entries on all levels 31 | 32 | Bug fixes: 33 | - Prevent West from crashing when running without a workspace folder 34 | - Fix bindings schema for enum values 35 | - Filter diagnostics correctly when VS Code requests specific types 36 | 37 | Other changes: 38 | - Set settings title to "DeviceTree" 39 | - Don't perform settings sync on path settings 40 | 41 | # v2.2.0 DeviceTree in other languages 42 | 43 | This release introduces DeviceTree aware language support in C and YAML bindings files, to improve the overall DeviceTree user experience. It also includes improvements to the Context explorer overview tree and some major improvements to the interpretation of some DeviceTree syntax corner cases. 44 | 45 | ### C and YAML language support 46 | 47 | The DeviceTree extension now includes a schema for YAML bindings files that works as input to the RedHat YAML extension. It provides auto completion for field names, validates the values of each field and shows basic hover information for the various fields in the bindings files. 48 | 49 | Rudimentary C file support introduces DeviceTree aware completion for the various node and property ID macros, such as `DT_NODELABEL()`. The completion items are always based on the most recently viewed DeviceTree context, so if you switch between different overlay files a lot, make sure you pay attention to the suggestion documentation, which will show which context provided the value. 50 | 51 | ### Preprocessor rewrite 52 | 53 | The preprocessor macro expansion mechanism has been rewritten to fix invalid behavior for macro argument concatenation. This also improves the overall performance, as tokens are only parsed once and looked up in a hashmap of defines, as opposed to the old regex generation. 54 | 55 | This rewrite fixes all known issues with complex pinmux macros and other concatenation based macro expressions. It also fixes printing of macros which span over multiple values or includes whole nodes. While these macros would occasionally generate invalid syntax in the preview file before, they should now be printed with their expanded values whenever they hide multiple phandle cells or property values. 56 | 57 | ### Overview tree improvements 58 | 59 | The Context explorer overview tree has been enhanced with new colorful icons. This release adds ADC, DAC, Board and Clock sections, and now lists important bus properties, such as clock speed and flow control. Additionally, the GPIO pins view now fetches pins from nexus node maps as well as STM and Atmel pinmux configuration entries. A range of minor improvements have also been made to the overview tree: 60 | - Account for nodes with multiple interrupts 61 | - Show SPI chip select 62 | - Support partition-less flash instances 63 | - List every ADC channel for ADC users 64 | - Show type name in tooltip 65 | 66 | ### Other noteworthy changes: 67 | 68 | New lint checks: 69 | - Check Flash node fixed partitions range 70 | - Check whether nexus entries have matches 71 | - Treat phandle as an acceptable phandles 72 | 73 | Improvements to the DeviceTree parser: 74 | - Accept C `UL` integer suffixes 75 | - Recognize raw * and / in property values as an unbraced expression and generate a warning 76 | - Single character arithmetic support in expressions 77 | 78 | Bug fixes: 79 | - GPIO pins: 80 | - If a property referencing a GPIO pin was overwritten, both the existing and the overwritten value would show up in the GPIO overview. This has been fixed. 81 | - Pin assignments from the board file would linger in the overview if the overlay file changed after the initial parse run. 82 | - Lint: Value names array length should match the number of top level entries in the matching property. 83 | - Preprocessor: Accept any filename character in includes, except spaces 84 | - Fix glitchy behavior for invalid interrupt and flash partition values in the overview tree 85 | - Correct all type cells lookup usage for signature, lint and hover 86 | 87 | General improvements: 88 | - The compiled output view supports all read-only language features 89 | - Completion: Fix ampersand expansion for references 90 | - Hover: Split long macros over multiple lines 91 | - Show macro replacement for single integer values, not just expressions 92 | - NewApp command: Default to folder containing open file 93 | - Context names: Omit folder name for root level contexts 94 | - Override status enum, so snippet doesn't expand to deprecated "ok" value 95 | - Show nodelabels in compiled output 96 | - Show entry names in signature completion 97 | - Show line about required properties in completion items 98 | - Edit in overlay command: Only show when the context has an overlay file 99 | - Copy C identifier command: Return phandle macro for node references, not the node being pointed to. 100 | - Permit commands from non-file schema resources, to accomodate remote development. 101 | 102 | # v2.1.0 Context explorer overview tree 103 | 104 | Adds overview tree to the context explorer, providing a summary of gpio ports, interrupts, flash partitions and buses. 105 | 106 | Additional changes: 107 | - Use "DeviceTree" name throughout 108 | - Add right click menu command for editing nodes and properties in overlay file 109 | - Warn about expressions in property values without parenthesis 110 | - Activate when commands fire to prevent "missing implementation" warning 111 | - Include raw PHandles when looking for references 112 | - Mark macros with detail text in completion list 113 | - Rename "Copy C identifier to clipboard" to just "Copy C identifier" 114 | - Go to definition now includes node names and properties 115 | - Bug fixes: 116 | - Check #address-cells and #size-cells in semantic parent, not literal parent 117 | - Fix crash in code completion on root-level entries 118 | - Ensure all types from included bindings are loaded, regardless of discovery order 119 | - Fix bug where overlay files would be dropped erronously 120 | - Wait for Zephyr board lookup to complete before activating, preventing unexpected "missing board" warning 121 | - Now prioritizing spec defined standard properties below inherited properties 122 | - Lint: 123 | - Check for missing reg property when node has address 124 | - Warn about duplicate labels 125 | - Syntax: 126 | - Allow preprocessor entries in node contents 127 | - Treat path separators as part of cursor word 128 | 129 | # v2.0.0 Multiple file support 130 | 131 | This is a major rewrite of the Devicetree extension. 132 | The primary change is the new support for the C preprocessor, which enables support for multiple files and contexts. 133 | All language features are now supported in board files, as well as overlay files, and the extension no longer depends on a compiled dts output file to work. 134 | 135 | Highlights: 136 | - Preprocessor now evaluated, including defines and includes 137 | - Board files and overlay files are combined into one context, no need for compiled dts file 138 | - About a dozen new lint checks 139 | - Context aware auto completion 140 | - Board context explorer in sidebar 141 | - Integration with West 142 | - Full bindings support 143 | 144 | # v1.1.1 Minor Enhancements 145 | 146 | - Allow CPU child nodes 147 | - Search for bindings in zephyr and nrf subdirectories 148 | - Let property requiredness be an or-operation in type inheritance 149 | - Parse delete-property and diagnose both delete commands 150 | - Format statements, not nodes 151 | - Add completion for delete commands 152 | - Silence extension in non-workspace contexts 153 | - Lint checks for addresses, ranges and status 154 | - Support binary operations in expressions 155 | 156 | # v1.1.0 Update for Zephyr 2.2 157 | 158 | - Allows phandles without enclosing <> brackets. 159 | - Default include is *.dts, not *.dts_compiled 160 | - phandle arrays fetch parameter names from handle type 161 | - Include all dependencies, resolving build errors on windows 162 | 163 | # Version 1.0.0 164 | 165 | The first release of the DeviceTree extension includes support for: 166 | - Syntax highlighting 167 | - Code completion 168 | - Type checking (Zephyr only) 169 | - Syntax validation 170 | - Go to definition 171 | 172 | The first version is made specifically for the [Zephyr project RTOS](https://www.zephyrproject.org/). 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devicetree", 3 | "displayName": "DeviceTree for the Zephyr Project", 4 | "description": "Full DeviceTree language support for the Zephyr project", 5 | "version": "2.3.1", 6 | "publisher": "trond-snekvik", 7 | "author": { 8 | "email": "trond.snekvik@gmail.com", 9 | "name": "Trond Einar Snekvik", 10 | "url": "https://github.com/trond-snekvik" 11 | }, 12 | "engines": { 13 | "vscode": "^1.43.0" 14 | }, 15 | "license": "MIT", 16 | "categories": [ 17 | "Programming Languages", 18 | "Linters" 19 | ], 20 | "activationEvents": [ 21 | "onLanguage:dts", 22 | "onCommand:devicetree.newApp", 23 | "onCommand:devicetree.save" 24 | ], 25 | "icon": "doc/devicetree_icon.png", 26 | "repository": { 27 | "url": "https://www.github.com/trond-snekvik/vscode-devicetree", 28 | "type": "git" 29 | }, 30 | "main": "./dist/extension.js", 31 | "contributes": { 32 | "commands": [ 33 | { 34 | "command": "devicetree.showOutput", 35 | "title": "DeviceTree: Show compiled output", 36 | "enablement": "editorLangId == dts && !editorReadonly || sideBarVisible", 37 | "icon": "$(open-preview)" 38 | }, 39 | { 40 | "command": "devicetree.newApp", 41 | "title": "DeviceTree: New Application", 42 | "icon": "$(plus)" 43 | }, 44 | { 45 | "command": "devicetree.ctx.addShield", 46 | "title": "DeviceTree: Add shield...", 47 | "enablement": "editorLangId == dts && !editorReadonly || sideBarVisible", 48 | "icon": { 49 | "dark": "icons/dark/add-shield.svg", 50 | "light": "icons/light/add-shield.svg" 51 | } 52 | }, 53 | { 54 | "command": "devicetree.ctx.removeShield", 55 | "title": "DeviceTree: Remove shield", 56 | "enablement": "editorLangId == dts && !editorReadonly || sideBarVisible", 57 | "icon": { 58 | "dark": "icons/dark/remove-shield.svg", 59 | "light": "icons/light/remove-shield.svg" 60 | } 61 | }, 62 | { 63 | "command": "devicetree.ctx.rename", 64 | "title": "DeviceTree: Rename context", 65 | "enablement": "editorLangId == dts && !editorReadonly || sideBarVisible", 66 | "icon": "$(edit)" 67 | }, 68 | { 69 | "command": "devicetree.ctx.delete", 70 | "title": "DeviceTree: Delete this context", 71 | "icon": "$(trash)" 72 | }, 73 | { 74 | "command": "devicetree.save", 75 | "title": "DeviceTree: Save configuration", 76 | "enablement": "devicetree:dirtyConfig", 77 | "icon": "$(save)" 78 | }, 79 | { 80 | "command": "devicetree.ctx.setBoard", 81 | "title": "DeviceTree: Set board...", 82 | "enablement": "editorLangId == dts && !editorReadonly || sideBarVisible", 83 | "icon": "$(edit)" 84 | }, 85 | { 86 | "command": "devicetree.getMacro", 87 | "title": "DeviceTree: Copy C identifier", 88 | "enablement": "editorLangId == dts", 89 | "icon": "$(clippy)" 90 | }, 91 | { 92 | "command": "devicetree.edit", 93 | "title": "DeviceTree: Edit in overlay", 94 | "enablement": "editorLangId == dts && devicetree:ctx.hasOverlay", 95 | "icon": "$(edit)" 96 | } 97 | ], 98 | "languages": [ 99 | { 100 | "id": "dts", 101 | "aliases": [ 102 | "DeviceTree" 103 | ], 104 | "configuration": "syntax/devicetree-language.json", 105 | "extensions": [ 106 | ".dts", 107 | ".dtsi", 108 | ".dts_compiled", 109 | ".overlay", 110 | ".dts.pre.tmp" 111 | ], 112 | "firstLine": "/dts-v1/;" 113 | } 114 | ], 115 | "grammars": [ 116 | { 117 | "language": "dts", 118 | "scopeName": "source.dts", 119 | "path": "./syntax/dts.tmLanguage.json" 120 | } 121 | ], 122 | "menus": { 123 | "editor/title": [ 124 | { 125 | "command": "devicetree.ctx.addShield", 126 | "when": "editorLangId == dts && !editorReadonly", 127 | "group": "1_run" 128 | }, 129 | { 130 | "command": "devicetree.showOutput", 131 | "when": "editorLangId == dts && !editorReadonly", 132 | "group": "1_run" 133 | } 134 | ], 135 | "editor/context": [ 136 | { 137 | "command": "devicetree.getMacro", 138 | "when": "editorLangId == dts", 139 | "group": "9_cutcopypaste" 140 | }, 141 | { 142 | "command": "devicetree.edit", 143 | "when": "editorLangId == dts", 144 | "group": "1_modification" 145 | } 146 | ], 147 | "view/title": [ 148 | { 149 | "command": "devicetree.save", 150 | "when": "view == trond-snekvik.devicetree.ctx && devicetree:dirtyConfig", 151 | "group": "navigation" 152 | }, 153 | { 154 | "command": "devicetree.newApp", 155 | "when": "view == trond-snekvik.devicetree.ctx", 156 | "group": "navigation" 157 | } 158 | ], 159 | "view/item/context": [ 160 | { 161 | "command": "devicetree.ctx.addShield", 162 | "when": "viewItem == devicetree.ctx", 163 | "group": "inline" 164 | }, 165 | { 166 | "command": "devicetree.ctx.rename", 167 | "when": "viewItem == devicetree.ctx" 168 | }, 169 | { 170 | "command": "devicetree.ctx.delete", 171 | "when": "viewItem == devicetree.ctx" 172 | }, 173 | { 174 | "command": "devicetree.showOutput", 175 | "when": "viewItem == devicetree.ctx", 176 | "group": "inline" 177 | }, 178 | { 179 | "command": "devicetree.ctx.setBoard", 180 | "when": "viewItem == devicetree.board", 181 | "group": "inline" 182 | }, 183 | { 184 | "command": "devicetree.ctx.removeShield", 185 | "when": "viewItem == devicetree.shield", 186 | "group": "inline" 187 | } 188 | ] 189 | }, 190 | "views": { 191 | "explorer": [ 192 | { 193 | "name": "DeviceTree", 194 | "visibility": "collapsed", 195 | "id": "trond-snekvik.devicetree.ctx", 196 | "icon": "icons/dark/devicetree-inner.svg" 197 | } 198 | ] 199 | }, 200 | "viewsWelcome": [ 201 | { 202 | "view": "trond-snekvik.devicetree.ctx", 203 | "contents": "DeviceTree context view:\n\nWhen you open a DeviceTree file, it will show up here, along with information about its board and configuration." 204 | } 205 | ], 206 | "configuration": [ 207 | { 208 | "title": "DeviceTree", 209 | "properties": { 210 | "devicetree.bindings": { 211 | "type": "array", 212 | "description": "List of directories containing binding descriptors. Relative paths are executed from each workspace. Defaults to dts/bindings", 213 | "default": [ 214 | "${zephyrBase}/dts/bindings", 215 | "${zephyrBase}/../nrf/dts/bindings" 216 | ], 217 | "scope": "machine" 218 | }, 219 | "devicetree.west": { 220 | "type": "string", 221 | "description": "Path to the West executable", 222 | "scope": "machine" 223 | }, 224 | "devicetree.zephyr": { 225 | "type": "string", 226 | "description": "Path to the Zephyr repo", 227 | "scope": "machine" 228 | }, 229 | "devicetree.ctxFile": { 230 | "type": "string", 231 | "description": "File to store contexts in", 232 | "scope": "machine" 233 | }, 234 | "devicetree.defaultBoard": { 235 | "type": "string", 236 | "description": "Default DeviceTree board when overlay file has a generic name" 237 | } 238 | } 239 | } 240 | ], 241 | "yamlValidation": [ 242 | { 243 | "fileMatch": "dts/bindings/**/*.yaml", 244 | "url": "./syntax/bindings-schema.yaml" 245 | } 246 | ] 247 | }, 248 | "scripts": { 249 | "vscode:prepublish": "webpack --mode production", 250 | "compile": "tsc -p ./", 251 | "watch": "tsc -watch -p ./", 252 | "test": "npm run compile && node ./node_modules/vscode/bin/test", 253 | "webpack": "webpack --mode development", 254 | "webpack-dev": "webpack --mode development --watch", 255 | "test-compile": "tsc -p ./" 256 | }, 257 | "devDependencies": { 258 | "@types/find": "^0.2.1", 259 | "@types/glob": "5.0.35", 260 | "@types/js-yaml": "3.11.1", 261 | "@types/mocha": "^5.2.6", 262 | "@types/node": "^6.0.40", 263 | "@types/vscode": "^1.43.0", 264 | "@typescript-eslint/eslint-plugin": "^3.9.1", 265 | "@typescript-eslint/parser": "^3.9.1", 266 | "eslint": "^7.7.0", 267 | "eslint-config-airbnb-base": "^14.2.0", 268 | "eslint-plugin-import": "^2.22.0", 269 | "mocha": "^6.1.4", 270 | "ts-loader": "^8.0.0", 271 | "typescript": "^3.7.2", 272 | "vscode-test": "1.4.0", 273 | "webpack": "^5.36.1", 274 | "webpack-cli": "^3.3.12" 275 | }, 276 | "dependencies": { 277 | "glob": "7.1.6", 278 | "js-yaml": "^3.13.1" 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /syntax/dts.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "source.dts", 3 | "patterns": [ 4 | {"include": "#comment"}, 5 | {"include": "#block-comment"}, 6 | {"include": "#preprocessor-include"}, 7 | {"include": "#preprocessor-define"}, 8 | {"include": "#compiler-directive"}, 9 | {"include": "#root-node"}, 10 | {"include": "#property"}, 11 | {"include": "#preprocessor"}, 12 | {"include": "#node"}, 13 | {"include": "#label"}, 14 | {"include": "#node-ref"} 15 | ], 16 | "repository": { 17 | "preprocessor-include": { 18 | "match": "^\\s*(#\\s*include)\\s+(<.*?>|\".*?\")", 19 | "captures": { 20 | "1": {"name": "keyword.control.preprocessor"}, 21 | "2": {"name": "meta.preprocessor.include.c"} 22 | } 23 | }, 24 | "preprocessor-define": { 25 | "begin": "^\\s*(#\\s*define)\\s+(\\w+)(\\(.*?\\))?", 26 | "beginCaptures": { 27 | "1": { "name": "keyword.control.preprocessor" }, 28 | "2": { "name": "entity.name.function.preprocessor" }, 29 | "3": { "name": "entity.name.function.preprocessor" } 30 | }, 31 | "name": "entity.name.function.preprocessor", 32 | "end": "(?", 291 | "patterns":[ 292 | {"include": "#ref"}, 293 | {"include": "#paren-expr"}, 294 | {"include": "#number"}, 295 | {"match": "\\w+", "name": "entity.name.tag"}, 296 | {"include": "#block-comment"}, 297 | {"match": ";", "name": "invalid.illegal"} 298 | ] 299 | }, 300 | "ref": { 301 | "match": "(&)(?:([\\w\\-]+)|\\{([\\w/@-]+)\\})", 302 | "captures": { 303 | "1": {"name": "keyword.operator"}, 304 | "2": {"name": "support.class.ref"}, 305 | "3": {"name": "support.class.ref"} 306 | } 307 | }, 308 | "string": { 309 | "match": "\".*?\"", 310 | "name": "string.quoted.double" 311 | }, 312 | "uint8-array": { 313 | "begin": "\\[", 314 | "end": "\\]", 315 | "patterns": [ 316 | { 317 | "match": "[\\da-fA-F]{2}", 318 | "name": "constant.numeric" 319 | }, 320 | {"include": "#block-comment"} 321 | ] 322 | }, 323 | "expression": { 324 | "patterns": [ 325 | {"include": "#expr-op"}, 326 | {"include": "#number"}, 327 | {"include": "#paren-expr"}, 328 | {"include": "#expr-constant"} 329 | ] 330 | }, 331 | "expr-operator": { 332 | "match": "(?:(0x[\\da-fA-F]+|\\d+)|(\\w+))\\s*([+\\-*/&|^~!<>]|<<|>>|[!=<>]=|\\|\\|)\\s*(?:(0x[\\da-fA-F]+|\\d+)|(\\w+))", 333 | "captures": { 334 | "1": {"name": "constant.numeric"}, 335 | "2": {"name": "variable.parameter"}, 336 | "3": {"name": "keyword.operator"}, 337 | "4": {"name": "constant.numeric"}, 338 | "5": {"name": "variable.parameter"} 339 | } 340 | }, 341 | "expr-op": { 342 | "match": "([+\\-*/&|^~!<>]|<<|>>|[!=<>]=|\\|\\|)", 343 | "name": "keyword.operator" 344 | }, 345 | "expr-constant": { 346 | "match": "\\w+", 347 | "name": "entity.name.tag" 348 | }, 349 | "paren-expr": { 350 | "begin": "\\(", 351 | "end": "\\)", 352 | "patterns": [ 353 | {"include": "#expression"} 354 | ] 355 | } 356 | } 357 | } -------------------------------------------------------------------------------- /src/preprocessor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | import * as fs from 'fs'; 9 | import { DiagnosticsSet } from './diags'; 10 | import { evaluateExpr } from './util'; 11 | 12 | export type IncludeStatement = { loc: vscode.Location, dst: vscode.Uri }; 13 | 14 | export type Defines = { [name: string]: Define }; 15 | export type ProcessedFile = { lines: Line[], defines: Defines, includes: IncludeStatement[] }; 16 | 17 | export function toDefines(list: Define[]): Defines { 18 | const defines: Defines = {}; 19 | list.forEach(m => defines[m.name] = m); 20 | return defines; 21 | } 22 | 23 | function replace(text: string, macros: MacroInstance[]) { 24 | // Replace values from back to front: 25 | [...macros].sort((a, b) => b.start - a.start).forEach(m => { 26 | text = text.slice(0, m.start) + m.insert + text.slice(m.start + m.raw.length); 27 | }); 28 | 29 | return text; 30 | } 31 | 32 | function parseArgs(text: string): {args: string[], raw: string} { 33 | const args = new Array(); 34 | const start = text.match(/^\s*\(/); 35 | if (!start) { 36 | return {args, raw: ''}; 37 | } 38 | text = text.slice(start[0].length); 39 | let depth = 1; 40 | let arg = ''; 41 | let raw = start[0]; 42 | 43 | while (text.length) { 44 | const paramMatch = text.match(/^([^(),]*)(.)/); 45 | if (!paramMatch) { 46 | return { args: [], raw }; 47 | } 48 | 49 | raw += paramMatch[0]; 50 | arg += paramMatch[0]; 51 | text = text.slice(paramMatch[0].length); 52 | if (paramMatch[2] === '(') { 53 | depth++; 54 | } else { 55 | if (depth === 1) { 56 | args.push(arg.slice(0, arg.length-1).trim()); 57 | arg = ''; 58 | } 59 | 60 | if (paramMatch[2] === ')') { 61 | if (!--depth) { 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | 68 | if (depth) { 69 | return {args: [], raw}; 70 | } 71 | 72 | return { args, raw }; 73 | } 74 | 75 | function resolve(text: string, defines: Defines, loc: vscode.Location): string { 76 | return replace(text, findReplacements(text, defines, loc)); 77 | } 78 | 79 | function findReplacements(text: string, defines: Defines, loc: vscode.Location): MacroInstance[] { 80 | const macros = new Array(); 81 | const regex = new RegExp(/\w+|(? { 116 | if (i == all.length - 1) { 117 | if (arg === '...') { 118 | replacements['__VA_ARGS__'] = args.slice(i).join(', '); 119 | return; 120 | } 121 | 122 | if (arg.endsWith('...')) { 123 | replacements[arg.replace(/\.\.\.$/, '')] = args.slice(i).join(', '); 124 | return; 125 | } 126 | } 127 | replacements[arg] = args[i]; 128 | }); 129 | let insert = macro.value(loc).replace(/(?:,\s*##\s*(__VA_ARGS__)|(?<=##)\s*(\w+)\b|\b(\w+)\s*(?=##)|(? { 131 | let v = replacements[vaArgs]; 132 | if (v !== undefined) { 133 | // If the value is empty, we'll consume the comma: 134 | if (v) { 135 | return resolve(', ' + v, defines, loc); 136 | } 137 | 138 | return resolve(v, defines, loc); 139 | } 140 | 141 | v = replacements[concat1] ?? replacements[concat2]; 142 | if (v !== undefined) { 143 | return v; 144 | } 145 | 146 | v = replacements[stringified]; 147 | if (v !== undefined) { 148 | return `"${v}"`; 149 | } 150 | 151 | v = replacements[raw]; 152 | if (v !== undefined) { 153 | return resolve(v, defines, loc); 154 | } 155 | 156 | return original; 157 | }); 158 | 159 | 160 | insert = insert.replace(/\s*##\s*/g, ''); 161 | 162 | macros.push(new MacroInstance(macro, match[0] + rawArgs, resolve(insert, defines, loc), match.index)); 163 | } 164 | 165 | return macros; 166 | } 167 | 168 | export class Define { 169 | private _value: string; 170 | name: string; 171 | args?: string[] 172 | definition?: Line; 173 | undef?: Line; 174 | 175 | get isDefined() { 176 | return !this.undef; 177 | } 178 | 179 | value(loc: vscode.Location) { 180 | return this._value; 181 | } 182 | 183 | constructor(name: string, value: string, definition?: Line, args?: string[]) { 184 | this.name = name; 185 | this.definition = definition; 186 | this._value = value; 187 | this.args = args; 188 | } 189 | } 190 | 191 | export class LineMacro extends Define { 192 | value(loc: vscode.Location) { 193 | return (loc.range.start.line + 1).toString(); 194 | } 195 | 196 | constructor() { 197 | super('__LINE__', '0'); 198 | } 199 | } 200 | 201 | export class FileMacro extends Define { 202 | private cwd: string; 203 | 204 | value(loc: vscode.Location) { 205 | return `"${path.relative(this.cwd, loc.uri.fsPath).replace(/\\/g, '\\\\')}"`; 206 | } 207 | 208 | constructor(cwd: string) { 209 | super('__FILE__', ''); 210 | this.cwd = cwd; 211 | } 212 | } 213 | 214 | export class CounterMacro extends Define { 215 | private number = 0; 216 | 217 | value(loc: vscode.Location) { 218 | return (this.number++).toString(); 219 | } 220 | 221 | constructor() { 222 | super('__COUNTER__', '0'); 223 | } 224 | } 225 | 226 | export class MacroInstance { 227 | raw: string; 228 | insert: string; 229 | start: number; 230 | macro: Define; 231 | 232 | constructor(macro: Define, raw: string, insert: string, start: number) { 233 | this.macro = macro; 234 | this.raw = raw; 235 | this.insert = insert; 236 | this.start = start; 237 | } 238 | 239 | contains(col: number) { 240 | return col >= this.start && col < this.start + this.raw.length; 241 | } 242 | } 243 | 244 | function readLines(doc: vscode.TextDocument): Line[] | null { 245 | try { 246 | const text = doc.getText(); 247 | return text.split(/\r?\n/g).map((line, i) => new Line(line, i, doc.uri)); 248 | } catch (e) { 249 | return null; 250 | } 251 | } 252 | 253 | function evaluate(text: string, loc: vscode.Location, defines: Defines, diagSet: DiagnosticsSet): any { 254 | text = resolve(text, defines, loc); 255 | try { 256 | const diags = new Array(); 257 | const result = evaluateExpr(text, loc.range.start, diags); 258 | diags.forEach(d => diagSet.pushLoc(new vscode.Location(loc.uri, d.range), d.message, d.severity)); 259 | return result; 260 | } catch (e) { 261 | diagSet.pushLoc(loc, 'Evaluation failed: ' + e.toString(), vscode.DiagnosticSeverity.Error); 262 | } 263 | 264 | return 0; 265 | } 266 | 267 | export async function preprocess(doc: vscode.TextDocument, defines: Defines, includes: string[], diags: DiagnosticsSet): Promise { 268 | const pushLineDiag = (line: Line, message: string, severity: vscode.DiagnosticSeverity=vscode.DiagnosticSeverity.Warning) => { 269 | const diag = new vscode.Diagnostic(line.location.range, message, severity); 270 | diags.push(line.uri, diag); 271 | return diag; 272 | }; 273 | 274 | const timeStart = process.hrtime(); 275 | const result: ProcessedFile = { 276 | lines: new Array(), 277 | defines: { 278 | '__FILE__': new FileMacro(path.dirname(doc.uri.fsPath)), 279 | '__LINE__': new LineMacro(), 280 | '__COUNTER__': new CounterMacro(), 281 | ...defines, 282 | }, 283 | includes: new Array(), 284 | }; 285 | 286 | let rawLines = readLines(doc); 287 | if (rawLines === null) { 288 | diags.push(doc.uri, new vscode.Diagnostic(new vscode.Range(0, 0, 0, 0), 'Unable to read file', vscode.DiagnosticSeverity.Error)); 289 | return result; 290 | } 291 | 292 | const scopes: {line: Line, condition: boolean}[] = []; 293 | const once = new Array(); 294 | 295 | while (rawLines.length) { 296 | const line = rawLines.splice(0, 1)[0]; 297 | let text = line.text; 298 | 299 | try { 300 | text = text.replace(/\/\/.*/, ''); 301 | text = text.replace(/\/\*.*?\*\//, ''); 302 | 303 | const blockComment = text.match(/\/\*.*/); 304 | if (blockComment) { 305 | text = text.replace(blockComment[0], ''); 306 | while (rawLines) { 307 | const blockEnd = rawLines[0].text.match(/^.*?\*\//); 308 | if (blockEnd) { 309 | rawLines[0].text = rawLines[0].text.slice(blockEnd[0].length); 310 | break; 311 | } 312 | 313 | rawLines.splice(0, 1); 314 | } 315 | } 316 | 317 | const directive = text.match(/^\s*#\s*(\w+)/); 318 | if (directive) { 319 | while (text.endsWith('\\') && rawLines.length) { 320 | text = text.slice(0, text.length - 1) + ' ' + rawLines.splice(0, 1)[0].text; 321 | } 322 | 323 | let value = text.match(/^\s*#\s*(\w+)\s*(.*)/)[2].trim(); 324 | 325 | if (directive[1] === 'if') { 326 | if (!value) { 327 | pushLineDiag(line, 'Missing condition'); 328 | scopes.push({line: line, condition: false}); 329 | continue; 330 | } 331 | 332 | value = value.replace(new RegExp(`defined\\((.*?)\\)`, 'g'), (t, define) => { 333 | return result.defines[define]?.isDefined ? '1' : '0'; 334 | }); 335 | 336 | scopes.push({line: line, condition: !!evaluate(value, line.location, result.defines, diags)}); 337 | continue; 338 | } 339 | 340 | if (directive[1] === 'ifdef') { 341 | if (!value) { 342 | pushLineDiag(line, 'Missing condition'); 343 | scopes.push({line: line, condition: false}); 344 | continue; 345 | } 346 | 347 | scopes.push({ line: line, condition: result.defines[value]?.isDefined }); 348 | continue; 349 | } 350 | 351 | if (directive[1] === 'ifndef') { 352 | if (!value) { 353 | pushLineDiag(line, 'Missing condition'); 354 | scopes.push({line: line, condition: false}); 355 | continue; 356 | } 357 | 358 | scopes.push({ line: line, condition: !result.defines[value]?.isDefined }); 359 | continue; 360 | } 361 | 362 | if (directive[1] === 'else') { 363 | if (!scopes.length) { 364 | pushLineDiag(line, `Unexpected #else`); 365 | continue; 366 | } 367 | 368 | scopes[scopes.length - 1].condition = !scopes[scopes.length - 1].condition; 369 | continue; 370 | } 371 | 372 | if (directive[1] === 'elif') { 373 | 374 | if (!scopes.length) { 375 | pushLineDiag(line, `Unexpected #elsif`); 376 | continue; 377 | } 378 | 379 | if (!value) { 380 | pushLineDiag(line, 'Missing condition'); 381 | scopes.push({line: line, condition: false}); 382 | continue; 383 | } 384 | 385 | if (scopes[scopes.length - 1].condition) { 386 | scopes[scopes.length - 1].condition = false; 387 | continue; 388 | } 389 | 390 | let condition = resolve(value, result.defines, line.location); 391 | condition = condition.replace(new RegExp(`defined\\((.*?)\\)`, 'g'), (t, define) => { 392 | return result.defines[define]?.isDefined ? '1' : '0'; 393 | }); 394 | 395 | scopes[scopes.length - 1].condition = evaluate(condition, line.location, result.defines, diags); 396 | continue; 397 | } 398 | 399 | if (directive[1] === 'endif') { 400 | if (!scopes.length) { 401 | pushLineDiag(line, `Unexpected #endif`); 402 | continue; 403 | } 404 | 405 | scopes.pop(); 406 | continue; 407 | } 408 | 409 | // Skip everything else inside a disabled scope: 410 | if (!scopes.every(c => c.condition)) { 411 | continue; 412 | } 413 | 414 | if (directive[1] === 'define') { 415 | const define = value.match(/^(\w+)(?:\((.*?)\))?\s*(.*)/); 416 | if (!define) { 417 | pushLineDiag(line, 'Invalid define syntax'); 418 | continue; 419 | } 420 | 421 | const existing = result.defines[define[1]]; 422 | if (existing && !existing.undef) { 423 | pushLineDiag(line, 'Duplicate definition'); 424 | continue; 425 | } 426 | 427 | const macro = existing ?? new Define(define[1], define[3], line, define[2]?.split(',').map(a => a.trim())); 428 | macro.undef = undefined; 429 | result.defines[macro.name] = macro; 430 | continue; 431 | } 432 | 433 | if (directive[1] === 'undef') { 434 | const undef = value.match(/^\w+/); 435 | if (!value) { 436 | pushLineDiag(line, 'Invalid undef syntax'); 437 | continue; 438 | } 439 | 440 | const define = result.defines[undef[0]]; 441 | if (!define || define.undef) { 442 | pushLineDiag(line, 'Unknown define'); 443 | continue; 444 | } 445 | 446 | define.undef = line; 447 | continue; 448 | } 449 | 450 | if (directive[1] === 'pragma') { 451 | if (value === 'once') { 452 | if (once.some(uri => uri.fsPath === line.uri.fsPath)) { 453 | const lines = rawLines.findIndex(l => l.uri.fsPath !== line.uri.fsPath); 454 | if (lines > 0) { 455 | rawLines.splice(0, lines); 456 | } 457 | continue; 458 | } 459 | 460 | once.push(line.uri); 461 | } else { 462 | pushLineDiag(line, `Unknown pragma directive "${value}"`); 463 | } 464 | continue; 465 | } 466 | 467 | if (directive[1] === 'include') { 468 | const include = value.replace(/(?:"([^\s">]+)"|<([^\s">]+)>)/g, '$1$2').trim(); 469 | if (!include) { 470 | pushLineDiag(line, 'Invalid include'); 471 | continue; 472 | } 473 | 474 | const file = [path.resolve(path.dirname(line.uri.fsPath)), ...includes].map(dir => path.resolve(dir, include)).find(path => fs.existsSync(path)); 475 | if (!file) { 476 | pushLineDiag(line, `No such file: ${include}`, vscode.DiagnosticSeverity.Warning); 477 | continue; 478 | } 479 | 480 | const uri = vscode.Uri.file(file); 481 | 482 | const start = text.indexOf(value); 483 | result.includes.push({ loc: new vscode.Location(line.uri, new vscode.Range(line.number, start, line.number, start + value.length)), dst: uri }); 484 | 485 | // inject the included file's lines. They will be the next to be processed: 486 | const doc = await vscode.workspace.openTextDocument(uri); 487 | const lines = readLines(doc); 488 | if (lines === null) { 489 | pushLineDiag(line, 'Unable to read file'); 490 | } else { 491 | rawLines = [...lines, ...rawLines]; 492 | } 493 | continue; 494 | } 495 | 496 | if (directive[1] === 'error') { 497 | pushLineDiag(line, value ?? 'Error'); 498 | continue; 499 | } 500 | } 501 | 502 | if (!text) { 503 | continue; 504 | } 505 | 506 | if (!scopes.every(c => c.condition)) { 507 | continue; 508 | } 509 | 510 | result.lines.push(new Line(text, line.number, line.uri, findReplacements(text, result.defines, line.location))); 511 | } catch (e) { 512 | pushLineDiag(line, 'Preprocessor crashed: ' + e); 513 | } 514 | } 515 | 516 | scopes.forEach(s => pushLineDiag(s.line, 'Unterminated scope')); 517 | 518 | const procTime = process.hrtime(timeStart); 519 | // console.log(`Preprocessed ${doc.uri.fsPath} in ${(procTime[0] * 1e9 + procTime[1]) / 1000000} ms`); 520 | 521 | return result; 522 | } 523 | 524 | export class Line { 525 | raw: string; 526 | text: string; 527 | number: number; 528 | macros: MacroInstance[]; 529 | location: vscode.Location; 530 | 531 | get length(): number { 532 | return this.text.length; 533 | } 534 | 535 | rawPos(range: vscode.Range): vscode.Range; 536 | rawPos(position: vscode.Position, earliest: boolean): number; 537 | rawPos(offset: number, earliest: boolean): number; 538 | 539 | /** 540 | * Remap a location in the processed text to a location in the raw input text (real human readable location) 541 | * 542 | * For instance, if a processed line is 543 | * 544 | * foo bar 1234 545 | * 546 | * and the unprocessed line is 547 | * 548 | * foo MACRO_1 MACRO_2 549 | * 550 | * the outputs should map like this: 551 | * 552 | * remap(0) -> 0 553 | * remap(4) -> 4 (from the 'b' in bar) 554 | * remap(5) -> 4 (from the 'a' in bar) 555 | * remap(5, true) -> 6 (from the 'a' in bar) 556 | * remap(9) -> 8 (from the '2' in 1234) 557 | * 558 | * @param loc Location in processed text 559 | * @param earliest Whether to get the earliest matching position 560 | */ 561 | rawPos(loc: vscode.Position | vscode.Range | number, earliest=true) { 562 | if (loc instanceof vscode.Position) { 563 | return new vscode.Position(loc.line, this.rawPos(loc.character, earliest)); 564 | } 565 | 566 | if (loc instanceof vscode.Range) { 567 | return new vscode.Range(loc.start.line, this.rawPos(loc.start, true), loc.end.line, this.rawPos(loc.end, false)); 568 | } 569 | 570 | this.macros.find(m => { 571 | loc = loc; // Just tricking typescript :) 572 | if (m.start > loc) { 573 | return true; // As macros are sorted by their start pos, there's no need to go through the rest 574 | } 575 | 576 | // Is inside macro 577 | if (loc < m.start + m.insert.length) { 578 | loc = m.start; 579 | if (!earliest) { 580 | loc += m.raw.length; // clamp to end of macro 581 | } 582 | return true; 583 | } 584 | 585 | loc += m.raw.length - m.insert.length; 586 | }); 587 | 588 | return loc; 589 | } 590 | 591 | contains(uri: vscode.Uri, pos: vscode.Position) { 592 | return uri.toString() === this.location.uri.toString() && this.location.range.contains(pos); 593 | } 594 | 595 | get uri() { 596 | return this.location.uri; 597 | } 598 | 599 | macro(pos: vscode.Position) { 600 | return this.macros.find(m => m.contains(pos.character)); 601 | } 602 | 603 | constructor(raw: string, number: number, uri: vscode.Uri, macros: MacroInstance[]=[]) { 604 | this.raw = raw; 605 | this.number = number; 606 | this.macros = macros; 607 | this.location = new vscode.Location(uri, new vscode.Range(this.number, 0, this.number, this.raw.length)); 608 | this.text = replace(raw, this.macros); 609 | } 610 | } -------------------------------------------------------------------------------- /src/treeView.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | import { DTSCtx, DTSFile, Node, Parser, PHandle, Property} from './dts'; 9 | import { countText, sizeString } from './util'; 10 | import { resolveBoardInfo } from './zephyr'; 11 | 12 | function iconPath(name: string) { 13 | return { 14 | dark: __dirname + `/../icons/dark/${name}.svg`, 15 | light: __dirname + `/../icons/light/${name}.svg`, 16 | }; 17 | } 18 | 19 | class TreeInfoItem { 20 | ctx: DTSCtx; 21 | name: string; 22 | icon?: string; 23 | parent?: TreeInfoItem; 24 | path?: string; 25 | description?: string; 26 | tooltip?: string; 27 | private _children: TreeInfoItem[]; 28 | 29 | constructor(ctx: DTSCtx, name: string, icon?: string, description?: string) { 30 | this.ctx = ctx; 31 | this.name = name; 32 | this.icon = icon; 33 | this.description = description; 34 | this._children = []; 35 | } 36 | 37 | get children(): ReadonlyArray { 38 | return this._children; 39 | } 40 | 41 | get id(): string { 42 | if (this.parent) { 43 | return `${this.parent.id}.${this.name}(${this.description ?? ''})`; 44 | } 45 | return this.name; 46 | } 47 | 48 | addChild(child: TreeInfoItem | undefined) { 49 | if (child) { 50 | child.parent = this; 51 | this._children.push(child); 52 | } 53 | } 54 | } 55 | 56 | type NestedInclude = { uri: vscode.Uri, file: DTSFile }; 57 | type DTSTreeItem = DTSCtx | DTSFile | NestedInclude | TreeInfoItem; 58 | 59 | export class DTSTreeView implements 60 | vscode.TreeDataProvider { 61 | parser: Parser; 62 | treeView: vscode.TreeView; 63 | private treeDataChange: vscode.EventEmitter; 64 | onDidChangeTreeData: vscode.Event; 65 | 66 | constructor(parser: Parser) { 67 | this.parser = parser; 68 | 69 | this.treeDataChange = new vscode.EventEmitter(); 70 | this.onDidChangeTreeData = this.treeDataChange.event; 71 | 72 | this.parser.onChange(ctx => this.treeDataChange.fire()); 73 | this.parser.onDelete(ctx => this.treeDataChange.fire()); 74 | 75 | this.treeView = vscode.window.createTreeView('trond-snekvik.devicetree.ctx', {showCollapseAll: true, canSelectMany: false, treeDataProvider: this}); 76 | 77 | vscode.window.onDidChangeActiveTextEditor(e => { 78 | if (!e || !this.treeView.visible || !e.document) { 79 | return; 80 | } 81 | 82 | const file = this.parser.file(e.document.uri); 83 | if (file) { 84 | this.treeView.reveal(file); 85 | } 86 | }); 87 | } 88 | 89 | update() { 90 | this.treeDataChange.fire(); 91 | } 92 | 93 | 94 | private treeFileChildren(file: DTSFile, uri: vscode.Uri) { 95 | return file.includes 96 | .filter(i => i.loc.uri.toString() === uri.toString()) 97 | .map(i => ({ uri: i.dst, file })); 98 | } 99 | 100 | async getTreeItem(element: DTSTreeItem): Promise { 101 | await this.parser.stable(); 102 | try { 103 | if (element instanceof DTSCtx) { 104 | let file: DTSFile; 105 | if (element.overlays.length) { 106 | file = element.overlays[element.overlays.length - 1]; 107 | } else { 108 | file = element.boardFile; 109 | } 110 | 111 | if (!file) { 112 | return; 113 | } 114 | 115 | const item = new vscode.TreeItem(element.name, 116 | this.parser.currCtx === element ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed); 117 | item.contextValue = 'devicetree.ctx'; 118 | item.tooltip = 'DeviceTree Context'; 119 | item.id = ['devicetree', 'ctx', element.name, 'file', file.uri.fsPath.replace(/[/\\]/g, '.')].join('.'); 120 | item.iconPath = iconPath('devicetree-inner'); 121 | return item; 122 | } 123 | 124 | if (element instanceof DTSFile) { 125 | const item = new vscode.TreeItem(path.basename(element.uri.fsPath)); 126 | if (element.includes.length) { 127 | item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 128 | } 129 | item.resourceUri = element.uri; 130 | item.command = { command: 'vscode.open', title: 'Open file', arguments: [element.uri] }; 131 | item.id === ['devicetree', 'file', element.ctx.name, element.uri.fsPath.replace(/[/\\]/g, '.')].join('.'); 132 | if (element.ctx.boardFile === element) { 133 | item.iconPath = iconPath('circuit-board'); 134 | item.tooltip = 'Board file'; 135 | item.contextValue = 'devicetree.board'; 136 | } else { 137 | if (element.ctx.overlays.indexOf(element) === element.ctx.overlays.length - 1) { 138 | item.iconPath = iconPath('overlay'); 139 | item.contextValue = 'devicetree.overlay'; 140 | } else { 141 | item.iconPath = iconPath('shield'); 142 | item.contextValue = 'devicetree.shield'; 143 | } 144 | item.tooltip = 'Overlay'; 145 | } 146 | return item; 147 | } 148 | 149 | if (element instanceof TreeInfoItem) { 150 | const item = new vscode.TreeItem(element.name, element.children.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None); 151 | item.description = element.description; 152 | item.id = ['devicetree', 'ctx', element.ctx.name, 'item', element.id].join('.'); 153 | if (element.icon) { 154 | item.iconPath = iconPath(element.icon); 155 | } 156 | 157 | if (element.tooltip) { 158 | item.tooltip = element.tooltip; 159 | } 160 | 161 | if (element.path) { 162 | item.command = { 163 | command: 'devicetree.goto', 164 | title: 'Show', 165 | arguments: [element.path, element.ctx.files.pop().uri] 166 | }; 167 | } 168 | 169 | return item; 170 | } 171 | 172 | // Nested include 173 | const item = new vscode.TreeItem(path.basename(element.uri.fsPath)); 174 | item.resourceUri = element.uri; 175 | if (this.treeFileChildren(element.file, element.uri).length) { 176 | item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 177 | } 178 | item.iconPath = vscode.ThemeIcon.File; 179 | item.description = '- include'; 180 | item.command = { command: 'vscode.open', title: 'Open file', arguments: [element.uri] }; 181 | return item; 182 | } catch (e) { 183 | console.log(e); 184 | } 185 | } 186 | 187 | getChildren(element?: DTSTreeItem): vscode.ProviderResult { 188 | try { 189 | if (!element) { 190 | return this.parser.contexts; 191 | } 192 | 193 | if (element instanceof DTSCtx) { 194 | return this.getOverviewTree(element); 195 | } 196 | 197 | if (element instanceof DTSFile) { 198 | return this.treeFileChildren(element, element.uri); 199 | } 200 | 201 | if (element instanceof TreeInfoItem) { 202 | return Array.from(element.children); 203 | } 204 | 205 | // Nested include: 206 | return this.treeFileChildren(element.file, element.uri); 207 | } catch (e) { 208 | console.log(e); 209 | return []; 210 | } 211 | } 212 | 213 | private boardOverview(ctx: DTSCtx) { 214 | const board = new TreeInfoItem(ctx, 'Board', 'circuit-board'); 215 | 216 | if (!ctx.board) { 217 | return; 218 | } 219 | 220 | if (!ctx.board.info) { 221 | resolveBoardInfo(ctx.board); 222 | if (!ctx.board.info) { 223 | return; 224 | } 225 | } 226 | 227 | Object.entries({ 228 | name: 'Name:', 229 | arch: 'Architecture:', 230 | supported: 'Supported features', 231 | toolchain: 'Supported toolchains', 232 | }).forEach(([field, name]) => { 233 | if (field === 'name') { 234 | const model = ctx.root?.property('model')?.string; 235 | if (model) { 236 | board.addChild(new TreeInfoItem(ctx, name, undefined, model)); 237 | return; 238 | } 239 | } 240 | 241 | if (ctx.board.info[field]) { 242 | const item = new TreeInfoItem(ctx, name, undefined); 243 | if (Array.isArray(ctx.board.info[field])) { 244 | (ctx.board.info[field]).forEach(i => item.addChild(new TreeInfoItem(ctx, i))); 245 | } else { 246 | item.description = ctx.board.info[field].toString(); 247 | } 248 | 249 | board.addChild(item); 250 | } 251 | }); 252 | 253 | if (board.children) { 254 | return board; 255 | } 256 | } 257 | 258 | private gpioOverview(ctx: DTSCtx) { 259 | const gpio = new TreeInfoItem(ctx, 'GPIO', 'gpio'); 260 | ctx.nodeArray().filter(n => n.pins).forEach((n, _, all) => { 261 | const controller = new TreeInfoItem(ctx, n.uniqueName); 262 | n.pins.forEach((p, i) => { 263 | if (p) { 264 | const pin = new TreeInfoItem(ctx, `Pin ${i.toString()}`); 265 | pin.path = p.prop.path; 266 | pin.tooltip = p.prop.node.type?.description; 267 | if (p.pinmux) { 268 | const name = p.pinmux.name 269 | .replace((p.prop.node.labels()[0] ?? p.prop.node.name) + '_', '') 270 | .replace(/_?p[a-zA-Z]\d+$/, ''); 271 | pin.description = `${p.prop.node.uniqueName} • ${name}`; 272 | } else { 273 | pin.description = `${p.prop.node.uniqueName} • ${p.prop.name}`; 274 | } 275 | controller.addChild(pin); 276 | } 277 | }); 278 | 279 | controller.path = n.path; 280 | controller.description = n.pins.length + ' pins'; 281 | controller.tooltip = n.type?.description; 282 | if (!controller.children.length) { 283 | controller.description += ' • Nothing connected'; 284 | } else if (controller.children.length < n.pins.length) { 285 | controller.description += ` • ${controller.children.length} in use`; 286 | } 287 | 288 | gpio.addChild(controller); 289 | }); 290 | 291 | if (gpio.children) { 292 | return gpio; 293 | } 294 | } 295 | 296 | private flashOverview(ctx: DTSCtx) { 297 | const flash = new TreeInfoItem(ctx, 'Flash', 'flash'); 298 | ctx.nodeArray() 299 | .filter(n => n.parent && n.type.is('fixed-partitions')) 300 | .forEach((n, _, all) => { 301 | let parent = flash; 302 | if (all.length > 1) { 303 | parent = new TreeInfoItem(ctx, n.parent.uniqueName); 304 | flash.addChild(parent); 305 | } 306 | 307 | const regs = n.parent.regs(); 308 | const capacity = regs?.[0]?.sizes[0]?.val; 309 | if (capacity !== undefined) { 310 | parent.description = sizeString(capacity); 311 | } 312 | 313 | parent.path = n.parent.path; 314 | parent.tooltip = n.type?.description; 315 | 316 | let offset = 0; 317 | n.children().filter(c => c.regs()?.[0]?.addrs.length === 1).sort((a, b) => (a.regs()[0].addrs[0]?.val ?? 0) - (b.regs()[0].addrs[0]?.val ?? 0)).forEach(c => { 318 | const reg = c.regs(); 319 | const start = reg[0].addrs[0].val; 320 | const size = reg[0].sizes?.[0]?.val ?? 0; 321 | if (start > offset) { 322 | parent.addChild(new TreeInfoItem(ctx, `Free space @ 0x${offset.toString(16)}`, undefined, sizeString(start - offset))); 323 | } 324 | 325 | const partition = new TreeInfoItem(ctx, c.property('label')?.value?.[0]?.val as string ?? c.uniqueName); 326 | partition.description = sizeString(size); 327 | if (start < offset) { 328 | partition.description += ` - ${sizeString(offset - start)} overlap!`; 329 | } 330 | partition.tooltip = `0x${start.toString(16)} - 0x${(start + size - 1).toString(16)}`; 331 | partition.path = c.path; 332 | 333 | partition.addChild(new TreeInfoItem(ctx, 'Start', undefined, reg[0].addrs[0].toString(true))); 334 | 335 | if (size) { 336 | partition.addChild(new TreeInfoItem(ctx, 'Size', undefined, sizeString(reg[0].sizes[0].val))); 337 | } 338 | 339 | parent.addChild(partition); 340 | offset = start + size; 341 | }); 342 | 343 | if (capacity !== undefined && offset < capacity) { 344 | parent.addChild(new TreeInfoItem(ctx, `Free space @ 0x${offset.toString(16)}`, undefined, sizeString(capacity - offset))); 345 | } 346 | }); 347 | 348 | // Some devices don't have partitions defined. For these, show simple flash entries: 349 | if (!flash.children.length) { 350 | ctx.nodeArray().filter(n => n.type?.is('soc-nv-flash')).forEach((n, _, all) => { 351 | let parent = flash; 352 | if (all.length > 1) { 353 | parent = new TreeInfoItem(ctx, n.uniqueName); 354 | flash.addChild(parent); 355 | } 356 | 357 | parent.path = n.path; 358 | 359 | n.regs()?.filter(reg => reg.addrs.length === 1 && reg.sizes.length === 1).forEach((reg, i, areas) => { 360 | let area = parent; 361 | if (areas.length > 1) { 362 | area = new TreeInfoItem(ctx, `Area ${i+1}`); 363 | parent.addChild(area); 364 | } 365 | 366 | area.description = sizeString(reg.sizes[0].val); 367 | 368 | area.addChild(new TreeInfoItem(ctx, 'Start', undefined, reg.addrs[0].toString(true))); 369 | area.addChild(new TreeInfoItem(ctx, 'Size', undefined, sizeString(reg.sizes[0].val))); 370 | }); 371 | }); 372 | } 373 | 374 | if (flash.children.length) { 375 | return flash; 376 | } 377 | } 378 | 379 | private interruptOverview(ctx: DTSCtx) { 380 | const nodes = ctx.nodeArray(); 381 | const interrupts = new TreeInfoItem(ctx, 'Interrupts', 'interrupts'); 382 | const controllers = nodes.filter(n => n.property('interrupt-controller')); 383 | const controllerItems = controllers.map(n => ({ item: new TreeInfoItem(ctx, n.uniqueName), children: new Array<{ node: Node, interrupts: Property }>() })); 384 | nodes.filter(n => n.property('interrupts')).forEach(n => { 385 | const interrupts = n.property('interrupts'); 386 | let node = n; 387 | let interruptParent: Property; 388 | while (node && !(interruptParent = node.property('interrupt-parent'))) { 389 | node = node.parent; 390 | } 391 | 392 | if (!interruptParent?.pHandle) { 393 | return; 394 | } 395 | 396 | const ctrlIdx = controllers.findIndex(c => interruptParent.pHandle?.is(c)); 397 | if (ctrlIdx < 0) { 398 | return; 399 | } 400 | 401 | controllerItems[ctrlIdx].children.push({ node: n, interrupts }); 402 | }); 403 | 404 | controllerItems.filter(c => c.children.length).forEach((controller, i) => { 405 | const cells = controllers[i]?.type.cells('interrupt') as string[]; 406 | controller.children.sort((a, b) => a.interrupts.array?.[0] - b.interrupts.array?.[0]).forEach(child => { 407 | const childIrqs = child.interrupts.arrays; 408 | const irqNames = child.node.property('interrupt-names')?.stringArray; 409 | childIrqs?.forEach((cellValues, i, all) => { 410 | const irq = new TreeInfoItem(ctx, child.node.uniqueName); 411 | irq.path = child.node.path; 412 | irq.tooltip = child.node.type?.description; 413 | 414 | // Some nodes have more than one interrupt: 415 | if (all.length > 1) { 416 | irq.name += ` (${irqNames?.[i] ?? i})`; 417 | } 418 | 419 | const prioIdx = cells.indexOf('priority'); 420 | if (cellValues?.length > prioIdx) { 421 | irq.description = 'Priority: ' + cellValues[prioIdx]?.toString(); 422 | } 423 | 424 | cells?.forEach((cell, i) => irq.addChild(new TreeInfoItem(ctx, cell.replace(/^\w/, letter => letter.toUpperCase()) + ':', undefined, cellValues?.[i]?.toString() ?? 'N/A'))); 425 | controller.item.addChild(irq); 426 | }); 427 | }); 428 | 429 | controller.item.path = controllers[i].path; 430 | controller.item.tooltip = controllers[i].type?.description; 431 | interrupts.addChild(controller.item); 432 | }); 433 | 434 | // Skip second depth if there's just one interrupt controller 435 | if (interrupts.children.length === 1) { 436 | interrupts.children[0].icon = interrupts.icon; 437 | interrupts.children[0].description = interrupts.children[0].name; 438 | interrupts.children[0].name = interrupts.name; 439 | return interrupts.children[0]; 440 | } 441 | 442 | if (interrupts.children.length) { 443 | return interrupts; 444 | } 445 | } 446 | 447 | private busOverview(ctx: DTSCtx) { 448 | const buses = new TreeInfoItem(ctx, 'Buses', 'bus'); 449 | ctx.nodeArray().filter(node => node.type?.bus).forEach(node => { 450 | const bus = new TreeInfoItem(ctx, node.uniqueName, undefined, ''); 451 | if (!bus.name.toLowerCase().includes(node.type.bus.toLowerCase())) { 452 | bus.description = node.type.bus + ' '; 453 | } 454 | 455 | bus.path = node.path; 456 | bus.tooltip = node.type?.description; 457 | 458 | const busProps = [/.*-speed$/, /.*-pin$/, /^clock-frequency$/, /^hw-flow-control$/, /^dma-channels$/]; 459 | node.uniqueProperties().filter(prop => prop.value.length > 0 && busProps.some(regex => prop.name.match(regex))).forEach(prop => { 460 | const infoItem = new TreeInfoItem(ctx, prop.name.replace(/-/g, ' ') + ':', undefined, prop.value.map(v => v.toString(true)).join(', ')); 461 | infoItem.path = prop.path; 462 | bus.addChild(infoItem); 463 | }); 464 | 465 | const nodesItem = new TreeInfoItem(ctx, 'Nodes'); 466 | 467 | node.children().forEach(child => { 468 | const busEntry = new TreeInfoItem(ctx, child.localUniqueName); 469 | busEntry.path = child.path; 470 | busEntry.tooltip = child.type?.description; 471 | 472 | if (child.address !== undefined) { 473 | busEntry.description = `@ 0x${child.address.toString(16)}`; 474 | 475 | // SPI nodes have chip selects 476 | if (node.type.bus === 'spi') { 477 | const csGpios = node.property('cs-gpios'); 478 | const cs = csGpios?.entries?.[child.address]; 479 | if (cs) { 480 | const csEntry = new TreeInfoItem(ctx, `Chip select`); 481 | csEntry.description = `${cs.target.toString(true)} ${cs.cells.map(c => c.toString(true)).join(' ')}`; 482 | csEntry.path = csGpios.path; 483 | busEntry.addChild(csEntry); 484 | } 485 | } 486 | } 487 | 488 | nodesItem.addChild(busEntry); 489 | }); 490 | 491 | if (nodesItem.children.length) { 492 | bus.description += `• ${countText(nodesItem.children.length, 'node')}`; 493 | } else { 494 | nodesItem.description = '• Nothing connected'; 495 | } 496 | 497 | bus.addChild(nodesItem); 498 | buses.addChild(bus); 499 | }); 500 | 501 | if (buses.children.length) { 502 | return buses; 503 | } 504 | } 505 | 506 | private ioChannelOverview(type: 'ADC' | 'DAC', ctx: DTSCtx) { 507 | const nodes = ctx.nodeArray(); 508 | const adcs = new TreeInfoItem(ctx, type + 's', type.toLowerCase()); 509 | nodes.filter(node => node.type?.is(type.toLowerCase() + '-controller')).forEach(node => { 510 | const controller = new TreeInfoItem(ctx, node.uniqueName); 511 | controller.path = node.path; 512 | controller.tooltip = node.type?.description; 513 | nodes 514 | .filter(n => n.property('io-channels')?.entries?.some(entry => (entry.target instanceof PHandle) && entry.target.is(node))) 515 | .flatMap(usr => { 516 | const names = usr.property('io-channel-names')?.stringArray ?? []; 517 | return usr.property('io-channels').entries.filter(c => c.target.is(node)).map((channel, i, all) => ({node: usr, idx: channel.cells[0]?.val ?? -1, name: names[i] ?? ((all.length > 1) && i.toString())})); 518 | }) 519 | .sort((a, b) => a.idx - b.idx) 520 | .forEach(channel => { 521 | const entry = new TreeInfoItem(ctx, `Channel ${channel.idx}`, undefined, channel.node.uniqueName + (channel.name ? ` • ${channel.name}` : '')); 522 | entry.path = channel.node.path; 523 | controller.addChild(entry); 524 | }); 525 | 526 | if (!controller.children.length) { 527 | controller.addChild(new TreeInfoItem(ctx, '', undefined, 'No channels in use.')); 528 | } 529 | 530 | adcs.addChild(controller); 531 | }); 532 | 533 | if (adcs.children.length === 1) { 534 | adcs.children[0].icon = adcs.icon; 535 | adcs.children[0].description = adcs.children[0].name; 536 | adcs.children[0].name = adcs.name; 537 | return adcs.children[0]; 538 | } 539 | 540 | if (adcs.children.length) { 541 | return adcs; 542 | } 543 | } 544 | 545 | private clockOverview(ctx: DTSCtx) { 546 | const nodes = ctx.nodeArray(); 547 | const clocks = new TreeInfoItem(ctx, 'Clocks', 'clock'); 548 | nodes.filter(node => node.type?.is('clock-controller')).forEach(node => { 549 | const clock = new TreeInfoItem(ctx, node.uniqueName); 550 | clock.path = node.path; 551 | clock.tooltip = node.type?.description; 552 | const cells = node.type?.cells('clock'); 553 | nodes.forEach(user => { 554 | const clockProp = user.property('clocks'); 555 | const entries = clockProp?.entries?.filter(e => e.target.is(node)); 556 | entries?.forEach(e => { 557 | const userEntry = new TreeInfoItem(ctx, user.uniqueName); 558 | userEntry.path = user.path; 559 | userEntry.tooltip = user.type?.description; 560 | cells?.forEach((c, i) => { 561 | if (i < e.cells.length) { 562 | userEntry.addChild(new TreeInfoItem(ctx, c, undefined, e.cells[i].toString(true))); 563 | } 564 | }); 565 | clock.addChild(userEntry); 566 | }); 567 | }); 568 | 569 | if (!clock.children.length) { 570 | clock.addChild(new TreeInfoItem(ctx, '', undefined, 'No users')); 571 | } 572 | 573 | clocks.addChild(clock); 574 | }); 575 | 576 | if (clocks.children.length === 1) { 577 | clocks.children[0].icon = clocks.icon; 578 | clocks.children[0].description = clocks.children[0].name; 579 | clocks.children[0].name = clocks.name; 580 | return clocks.children[0]; 581 | } 582 | 583 | if (clocks.children.length) { 584 | return clocks; 585 | } 586 | } 587 | 588 | private getOverviewTree(ctx: DTSCtx): vscode.ProviderResult { 589 | const details = new TreeInfoItem(ctx, 'Overview'); 590 | details.addChild(this.boardOverview(ctx)); 591 | details.addChild(this.gpioOverview(ctx)); 592 | details.addChild(this.flashOverview(ctx)); 593 | details.addChild(this.interruptOverview(ctx)); 594 | details.addChild(this.busOverview(ctx)); 595 | details.addChild(this.ioChannelOverview('ADC', ctx)); 596 | details.addChild(this.ioChannelOverview('DAC', ctx)); 597 | details.addChild(this.clockOverview(ctx)); 598 | 599 | if (details.children.length) { 600 | return [details, ...ctx.files]; 601 | } 602 | 603 | return ctx.files; 604 | } 605 | 606 | getParent(element: DTSTreeItem): vscode.ProviderResult { 607 | if (element instanceof DTSCtx) { 608 | return; 609 | } 610 | } 611 | } 612 | 613 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Trond Snekvik 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import * as yaml from 'js-yaml'; 7 | import * as glob from 'glob'; 8 | import * as vscode from 'vscode'; 9 | import * as path from 'path'; 10 | import { readFile } from 'fs'; 11 | import { Node } from './dts'; 12 | import { DiagnosticsSet } from './diags'; 13 | 14 | export interface PropertyType { 15 | name: string; 16 | required: boolean; 17 | enum?: (string | number)[]; 18 | const?: string | number; 19 | default?: any; 20 | type: string | string[]; 21 | description?: string; 22 | constraint?: string; 23 | node?: NodeType; 24 | } 25 | 26 | type PropertyTypeMap = { [name: string]: PropertyType }; 27 | 28 | interface PropertyFilter { 29 | allow?: string[]; 30 | block?: string[]; 31 | } 32 | 33 | interface TypeInclude extends PropertyFilter { 34 | name: string; 35 | allow?: string[]; 36 | block?: string[]; 37 | childBinding?: boolean; 38 | } 39 | 40 | function filterProperties(props: PropertyTypeMap, filter: PropertyFilter): string[] { 41 | if (!filter.allow && !filter.block) { 42 | return Object.keys(props); 43 | } 44 | 45 | return Object.keys(props).filter( 46 | (name) => 47 | ( 48 | (!filter.allow || filter.allow.includes(name)) && 49 | (!filter.block || !filter.block.includes(name)) 50 | ) 51 | ); 52 | } 53 | 54 | export class NodeType { 55 | private _properties: PropertyTypeMap; 56 | private _include: TypeInclude[]; 57 | private _cells: {[cell: string]: string[]}; 58 | private _bus: string; 59 | private _onBus: string; 60 | private _isChild = false; 61 | readonly filename?: string; 62 | readonly compatible: string; 63 | readonly valid: boolean = true; 64 | readonly description?: string; 65 | readonly child?: NodeType; 66 | private loader?: TypeLoader; 67 | 68 | constructor(private tree: any, filename?: string) { 69 | this._onBus = tree['on-bus']; 70 | this._bus = tree['bus']; 71 | this._cells = {}; 72 | Object.keys(tree).filter(k => k.endsWith('-cells')).forEach(k => { 73 | this._cells[k.slice(0, k.length - '-cells'.length)] = tree[k]; 74 | }); 75 | this.compatible = tree.compatible ?? tree.name; 76 | this.description = tree.description; 77 | this.filename = filename; 78 | 79 | const childIncludes = new Array<{[key: string]: any}>(); 80 | 81 | // includes may either be an array of strings or an array of objects with "include" 82 | const processInclude = (i: string | {[key: string]: any}): TypeInclude => { 83 | if (typeof(i) === 'string') { 84 | return { 85 | // remove .yaml file extension: 86 | name: i.split('.')[0], 87 | }; 88 | } 89 | 90 | const incl = { 91 | // remove .yaml file extension: 92 | name: i.name.split('.')[0], 93 | block: i['property-blocklist'], 94 | allow: i['property-allowlist'], 95 | } as TypeInclude; 96 | 97 | // Child binding includes are transferred to the child's tree: 98 | childIncludes.push({ 99 | name: incl.name, 100 | ...i['child-binding'], 101 | }); 102 | 103 | return incl; 104 | }; 105 | 106 | if (Array.isArray(tree.include)) { 107 | this._include = tree.include.map(processInclude); 108 | } else if (tree.include) { 109 | this._include = [processInclude(tree.include)]; 110 | } else { 111 | this._include = []; 112 | } 113 | 114 | this._properties = tree.properties ?? {}; 115 | 116 | for (const name in this._properties) { 117 | this._properties[name].name = name; 118 | this._properties[name].node = this; 119 | } 120 | 121 | if ('child-binding' in tree) { 122 | // Transfer the child binding property list to the child type, so it can 123 | // handle it the same way parent types do: 124 | tree['child-binding'].include = childIncludes; 125 | this.child = new NodeType(tree['child-binding']); 126 | this.child._isChild = true; 127 | } 128 | 129 | return this; 130 | } 131 | 132 | setLoader(loader: TypeLoader) { 133 | this.loader = loader; 134 | this.child?.setLoader(loader); 135 | } 136 | 137 | private get inclusions(): NodeType[] { 138 | return this._include.flatMap(i => this.loader?.get(i.name) ?? []); 139 | } 140 | 141 | includes(name: string) { 142 | return this.inclusions.find(i => i.name === name); 143 | } 144 | 145 | cells(type: string): string[] { 146 | if (type.endsWith('-cells')) { 147 | type = type.slice(0, type.length - '-cells'.length); 148 | } 149 | 150 | return this._cells[type] ?? this.inclusions.find(i => i.cells(type))?.cells(type); 151 | } 152 | 153 | /// Whether this type matches the given type string, either directly or through inclusions 154 | is(type: string): boolean { 155 | return this.name === type || !!this.includes(type); 156 | } 157 | 158 | get bus(): string { 159 | return this._bus ?? this.inclusions.find(i => i.bus)?.bus; 160 | } 161 | 162 | get onBus(): string { 163 | return this._onBus ?? this.inclusions.find(i => i.onBus)?.onBus; 164 | } 165 | 166 | private get propMap(): PropertyTypeMap { 167 | const props = { ...this._properties }; 168 | 169 | // import properties from included bindings: 170 | this._include.forEach(spec => { 171 | this.loader?.get(spec.name).forEach(type => { 172 | if (this._isChild) { 173 | // If this is a child binding, we should be including bindings from the 174 | // child binding of the included type. Our parent transferred our include 175 | // spec to our tree before creating us. 176 | type = type.child; 177 | if (!type) { 178 | return; 179 | } 180 | } 181 | 182 | const filtered = filterProperties(type.propMap, spec); 183 | for (const name of filtered) { 184 | props[name] = { ...type.propMap[name], ...(props[name] ?? {}) }; 185 | } 186 | }); 187 | }); 188 | 189 | return props; 190 | } 191 | 192 | get properties(): PropertyType[] { 193 | return Object.values(this.propMap); 194 | } 195 | 196 | property(name: string) { 197 | return this.propMap[name]; 198 | } 199 | 200 | get name() { 201 | return this.compatible; 202 | } 203 | } 204 | 205 | class AbstractNodeType extends NodeType { 206 | readonly valid: boolean = false; 207 | } 208 | 209 | const standardProperties: PropertyTypeMap = { 210 | '#address-cells': { 211 | name: '#address-cells', 212 | required: false, 213 | type: 'int', 214 | description: `The #address-cells property defines the number of u32 cells used to encode the address field in a child node’s reg property.\n\nThe #address-cells and #size-cells properties are not inherited from ancestors in the devicetree. They shall be explicitly defined.\n\nA DTSpec-compliant boot program shall supply #address-cells and #size-cells on all nodes that have children. If missing, a client program should assume a default value of 2 for #address-cells, and a value of 1 for #size-cells`, 215 | }, 216 | '#size-cells': { 217 | name: '#size-cells', 218 | required: false, 219 | type: 'int', 220 | description: `The #size-cells property defines the number of u32 cells used to encode the size field in a child node’s reg property.\n\nThe #address-cells and #size-cells properties are not inherited from ancestors in the devicetree. They shall be explicitly defined.\n\nA DTSpec-compliant boot program shall supply #address-cells and #size-cells on all nodes that have children. If missing, a client program should assume a default value of 2 for #address-cells, and a value of 1 for #size-cells`, 221 | }, 222 | 'model': { 223 | name: 'model', 224 | required: false, 225 | type: 'string', 226 | description: `The model property value is a string that specifies the manufacturer’s model number of the device. The recommended format is: "manufacturer,model", where manufacturer is a string describing the name of the manufacturer (such as a stock ticker symbol), and model specifies the model number.`, 227 | }, 228 | 'compatible': { 229 | name: 'compatible', 230 | required: false, 231 | type: 'string-array', 232 | description: `The compatible property value consists of one or more strings that define the specific programming model for the device. This list of strings should be used by a client program for device driver selection. The property value consists of a concatenated list of null terminated strings, from most specific to most general. They allow a device to express its compatibility with a family of similar devices, potentially allowing a single device driver to match against several devices.\n\nThe recommended format is "manufacturer,model", where manufacturer is a string describing the name of the manufacturer (such as a stock ticker symbol), and model the model number.`, 233 | }, 234 | 'phandle': { 235 | name: 'phandle', 236 | type: 'int', 237 | required: false, 238 | description: `The phandle property specifies a numerical identifier for a node that is unique within the devicetree. The phandle property value is used by other nodes that need to refer to the node associated with the property.` 239 | }, 240 | 'status': { 241 | name: 'status', 242 | type: 'string', 243 | required: false, 244 | enum: ['okay', 'disabled'], 245 | description: 'The status property indicates the operational status of a device.', 246 | }, 247 | 'clock-frequency': { 248 | name: 'clock-frequency', 249 | type: 'int', 250 | required: false, 251 | description: 'Specifies the frequency of a clock in Hz.' 252 | }, 253 | 'clocks': { 254 | name: 'clocks', 255 | type: 'phandle-array', 256 | required: false, 257 | description: 'Clock input to the device.' 258 | }, 259 | 'ranges': { 260 | name: 'ranges', 261 | type: ['boolean', 'array'], 262 | description: 'The ranges property provides a means of defining a mapping or translation between the address space of the\n' + 263 | 'bus (the child address space) and the address space of the bus node’s parent (the parent address space).\n' + 264 | 'The format of the value of the ranges property is an arbitrary number of triplets of (child-bus-address,\n' + 265 | 'parentbus-address, length)\n' + 266 | '\n' + 267 | '- The child-bus-address is a physical address within the child bus’ address space. The number of cells to\n' + 268 | 'represent the address is bus dependent and can be determined from the #address-cells of this node (the\n' + 269 | 'node in which the ranges property appears).\n' + 270 | '- The parent-bus-address is a physical address within the parent bus’ address space. The number of cells\n' + 271 | 'to represent the parent address is bus dependent and can be determined from the #address-cells property\n' + 272 | 'of the node that defines the parent’s address space.\n' + 273 | '- The length specifies the size of the range in the child’s address space. The number of cells to represent\n' + 274 | 'the size can be determined from the #size-cells of this node (the node in which the ranges property\n' + 275 | 'appears).\n' + 276 | '\n' + 277 | 'If the property is defined with an value, it specifies that the parent and child address space is\n' + 278 | 'identical, and no address translation is required.\n' + 279 | 'If the property is not present in a bus node, it is assumed that no mapping exists between children of the node\n' + 280 | 'and the parent address space.\n', 281 | required: false 282 | }, 283 | 'reg-shift': { 284 | name: 'reg-shift', 285 | type: 'int', 286 | required: false, 287 | description: 'The reg-shift property provides a mechanism to represent devices that are identical in most\n' + 288 | 'respects except for the number of bytes between registers. The reg-shift property specifies in bytes\n' + 289 | 'how far the discrete device registers are separated from each other. The individual register location\n' + 290 | 'is calculated by using following formula: “registers address” << reg-shift. If unspecified, the default\n' + 291 | 'value is 0.\n' + 292 | 'For example, in a system where 16540 UART registers are located at addresses 0x0, 0x4, 0x8, 0xC,\n' + 293 | '0x10, 0x14, 0x18, and 0x1C, a reg-shift = 2 property would be used to specify register\n' + 294 | 'locations.`\n', 295 | }, 296 | 'label': { 297 | name: 'label', 298 | type: 'string', 299 | required: false, 300 | description: 'The label property defines a human readable string describing a device. The binding for a given device specifies the exact meaning of the property for that device.' 301 | }, 302 | 'reg': { 303 | name: 'reg', 304 | type: 'array', 305 | required: false, 306 | description: 'The reg property describes the address of the device’s resources within the address space defined by its parent\n' + 307 | 'bus. Most commonly this means the offsets and lengths of memory-mapped IO register blocks, but may have\n' + 308 | 'a different meaning on some bus types. Addresses in the address space defined by the root node are CPU real\n' + 309 | 'addresses.\n' + 310 | '\n' + 311 | 'The value is a prop-encoded-array, composed of an arbitrary number of pairs of address and length,\n' + 312 | 'address length. The number of u32 cells required to specify the address and length are bus-specific\n' + 313 | 'and are specified by the #address-cells and #size-cells properties in the parent of the device node. If the parent\n' + 314 | 'node specifies a value of 0 for #size-cells, the length field in the value of reg shall be omitted.\n', 315 | } 316 | }; 317 | 318 | const standardTypes = [ 319 | new NodeType({ 320 | name: '/', 321 | description: 'The devicetree has a single root node of which all other device nodes are descendants. The full path to the root node is /.', 322 | properties: { 323 | '#address-cells': { 324 | required: true, 325 | description: 'Specifies the number of cells to represent the address in the reg property in children of root', 326 | }, 327 | '#size-cells': { 328 | required: true, 329 | description: 'Specifies the number of cells to represent the size in the reg property in children of root.', 330 | }, 331 | 'model': { 332 | required: true, 333 | description: 'Specifies a string that uniquely identifies the model of the system board. The recommended format is `"manufacturer,model-number".`', 334 | }, 335 | 'compatible': { 336 | required: true, 337 | description: 'Specifies a list of platform architectures with which this platform is compatible. This property can be used by operating systems in selecting platform specific code. The recommended form of the property value is:\n"manufacturer,model"\nFor example:\ncompatible = "fsl,mpc8572ds"', 338 | }, 339 | }, 340 | title: 'Root node' 341 | }), 342 | new AbstractNodeType({ 343 | name: 'simple-bus', 344 | title: 'Internal I/O bus', 345 | description: 'System-on-a-chip processors may have an internal I/O bus that cannot be probed for devices. The devices on the bus can be accessed directly without additional configuration required. This type of bus is represented as a node with a compatible value of “simple-bus”.', 346 | properties: { 347 | 'compatible': { 348 | required: true, 349 | }, 350 | 'ranges': { 351 | required: true, 352 | }, 353 | } 354 | }), 355 | new NodeType({ 356 | name: '/cpus/', 357 | title: '/cpus', 358 | description: `A /cpus node is required for all devicetrees. It does not represent a real device in the system, but acts as a container for child cpu nodes which represent the systems CPUs.`, 359 | properties: { 360 | '#address-cells': { 361 | required: true, 362 | }, 363 | '#size-cells': { 364 | required: true, 365 | } 366 | } 367 | }), 368 | new NodeType({ 369 | name: '/cpus/cpu', 370 | title: 'CPU instance', 371 | description: 'A cpu node represents a hardware execution block that is sufficiently independent that it is capable of running an operating\n' + 372 | 'system without interfering with other CPUs possibly running other operating systems.\n' + 373 | 'Hardware threads that share an MMU would generally be represented under one cpu node. If other more complex CPU\n' + 374 | 'topographies are designed, the binding for the CPU must describe the topography (e.g. threads that don’t share an MMU).\n' + 375 | 'CPUs and threads are numbered through a unified number-space that should match as closely as possible the interrupt\n' + 376 | 'controller’s numbering of CPUs/threads.\n' + 377 | '\n' + 378 | 'Properties that have identical values across cpu nodes may be placed in the /cpus node instead. A client program must\n' + 379 | 'first examine a specific cpu node, but if an expected property is not found then it should look at the parent /cpus node.\n' + 380 | 'This results in a less verbose representation of properties which are identical across all CPUs.\n' + 381 | 'The node name for every CPU node should be cpu.`\n', 382 | properties: { 383 | 'device_type': { 384 | name: 'device_type', 385 | type: 'string', 386 | const: 'cpu', 387 | description: `Value shall be "cpu"`, 388 | required: true, 389 | }, 390 | 'reg': { 391 | type: ['int', 'array'], 392 | description: `The value of reg is a that defines a unique CPU/thread id for the CPU/threads represented by the CPU node. If a CPU supports more than one thread (i.e. multiple streams of execution) the reg property is an array with 1 element per thread. The #address-cells on the /cpus node specifies how many cells each element of the array takes. Software can determine the number of threads by dividing the size of reg by the parent node’s #address-cells. If a CPU/thread can be the target of an external interrupt the reg property value must be a unique CPU/thread id that is addressable by the interrupt controller. If a CPU/thread cannot be the target of an external interrupt, then reg must be unique and out of bounds of the range addressed by the interrupt controller. If a CPU/thread’s PIR (pending interrupt register) is modifiable, a client program should modify PIR to match the reg property value. If PIR cannot be modified and the PIR value is distinct from the interrupt controller number space, the CPUs binding may define a binding-specific representation of PIR values if desired.`, 393 | required: true 394 | } 395 | } 396 | }), 397 | new NodeType({ 398 | name: '/chosen/', 399 | title: '/Chosen node', 400 | description: `The /chosen node does not represent a real device in the system but describes parameters chosen or specified by the system firmware at run time. It shall be a child of the root node`, 401 | properties: { 402 | 'zephyr,flash': { 403 | name: 'zephyr,flash', 404 | type: 'phandle', 405 | required: false, 406 | description: 'Generates symbol CONFIG_FLASH' 407 | }, 408 | 'zephyr,sram': { 409 | name: 'zephyr,sram', 410 | type: 'phandle', 411 | required: false, 412 | description: 'Generates symbol CONFIG_SRAM_SIZE/CONFIG_SRAM_BASE_ADDRESS (via DT_SRAM_SIZE/DT_SRAM_BASE_ADDRESS)' 413 | }, 414 | 'zephyr,ccm': { 415 | name: 'zephyr,ccm', 416 | type: 'phandle', 417 | required: false, 418 | description: 'Generates symbol DT_CCM' 419 | }, 420 | 'zephyr,console': { 421 | name: 'zephyr,console', 422 | type: 'phandle', 423 | required: false, 424 | description: 'Generates symbol DT_UART_CONSOLE_ON_DEV_NAME' 425 | }, 426 | 'zephyr,shell-uart': { 427 | name: 'zephyr,shell-uart', 428 | type: 'phandle', 429 | required: false, 430 | description: 'Generates symbol DT_UART_SHELL_ON_DEV_NAME' 431 | }, 432 | 'zephyr,bt-uart': { 433 | name: 'zephyr,bt-uart', 434 | type: 'phandle', 435 | required: false, 436 | description: 'Generates symbol DT_BT_UART_ON_DEV_NAME' 437 | }, 438 | 'zephyr,uart-pipe': { 439 | name: 'zephyr,uart-pipe', 440 | type: 'phandle', 441 | required: false, 442 | description: 'Generates symbol DT_UART_PIPE_ON_DEV_NAME' 443 | }, 444 | 'zephyr,bt-mon-uart': { 445 | name: 'zephyr,bt-mon-uart', 446 | type: 'phandle', 447 | required: false, 448 | description: 'Generates symbol DT_BT_MONITOR_ON_DEV_NAME' 449 | }, 450 | 'zephyr,uart-mcumgr': { 451 | name: 'zephyr,uart-mcumgr', 452 | type: 'phandle', 453 | required: false, 454 | description: 'Generates symbol DT_UART_MCUMGR_ON_DEV_NAME' 455 | }, 456 | } 457 | }), 458 | new NodeType({ 459 | name: '/aliases/', 460 | title: 'Aliases', 461 | description: `A devicetree may have an aliases node (/aliases) that defines one or more alias properties. The alias node shall be at the root of the devicetree and have the node name /aliases. Each property of the /aliases node defines an alias. The property name specifies the alias name. The property value specifies the full path to a node in the devicetree. For example, the property serial0 = "/simple-bus@fe000000/ serial@llc500" defines the alias serial0. Alias names shall be a lowercase text strings of 1 to 31 characters from the following set of characters.\n\nAn alias value is a device path and is encoded as a string. The value represents the full path to a node, but the path does not need to refer to a leaf node. A client program may use an alias property name to refer to a full device path as all or part of its string value. A client program, when considering a string as a device path, shall detect and use the alias.`, 462 | }), 463 | new NodeType({ 464 | name: '/zephyr,user/', 465 | title: 'User defined properties', 466 | description: `Convenience node for application specific properties. Properties in /zephyr,user/ don't need a devicetree binding, and can be used for any purpose. The type of the properties in the /zephyr,user node will be inferred from their value.`, 467 | }), 468 | ]; 469 | 470 | export class TypeLoader { 471 | types: {[name: string]: NodeType[]}; 472 | folders: string[] = [] 473 | diags: vscode.DiagnosticCollection; 474 | baseType: NodeType; 475 | 476 | constructor() { 477 | this.diags = vscode.languages.createDiagnosticCollection('DeviceTree types'); 478 | this.baseType = new AbstractNodeType({ name: '', properties: { ...standardProperties } }); 479 | this.types = {}; 480 | standardTypes.forEach(type => this.addType(type)); 481 | } 482 | 483 | private addType(type: NodeType) { 484 | if (type.name in this.types) { 485 | this.types[type.name].push(type); 486 | } else { 487 | this.types[type.name] = [type]; 488 | } 489 | 490 | type.setLoader(this); 491 | } 492 | 493 | async addFolder(folder: string) { 494 | this.folders.push(folder); 495 | const g = glob.sync('**/*.yaml', { cwd: folder, ignore: 'test/*' }); 496 | return Promise.all(g.map(file => new Promise(resolve => { 497 | const filePath = path.resolve(folder, file); 498 | readFile(filePath, 'utf-8', (err, out) => { 499 | if (err) { 500 | console.log(`Couldn't open ${file}`); 501 | } else { 502 | try { 503 | const tree = yaml.load(out, { json: true }); 504 | this.addType(new NodeType({ name: path.basename(file, '.yaml'), ...tree }, filePath)); 505 | } catch (e) { 506 | const pos = 507 | "mark" in e 508 | ? new vscode.Position( 509 | e.mark.line - 1, 510 | e.mark.column 511 | ) 512 | : new vscode.Position(0, 0); 513 | this.diags.set(vscode.Uri.file(filePath), [ 514 | new vscode.Diagnostic( 515 | new vscode.Range(pos, pos), 516 | `Invalid type definition: ${e.message ?? e}`, 517 | vscode.DiagnosticSeverity.Error 518 | ), 519 | ]); 520 | } 521 | } 522 | 523 | resolve(); 524 | }); 525 | }))); 526 | } 527 | 528 | get(name: string): NodeType[] { 529 | if (!(name in this.types)) { 530 | return []; 531 | } 532 | 533 | return this.types[name]; 534 | } 535 | 536 | nodeType(node: Node): NodeType { 537 | const props = node.uniqueProperties(); 538 | 539 | const getBaseType = () => { 540 | const candidates = [node.path]; 541 | 542 | const compatibleProp = props.find(p => p.name === 'compatible'); 543 | if (compatibleProp) { 544 | const compatible = compatibleProp.stringArray; 545 | if (compatible) { 546 | candidates.push(...compatible); 547 | } 548 | } 549 | 550 | candidates.push(node.name); 551 | candidates.push(node.name.replace(/s$/, '')); 552 | 553 | if (node.path.match(/\/cpus\/cpu[^/]*\/$/)) { 554 | candidates.push('/cpus/cpu'); 555 | } 556 | 557 | let types: NodeType[]; 558 | if (candidates.some(c => (types = this.get(c)).length)) { 559 | return types; 560 | } 561 | 562 | if (node.parent?.type.child) { 563 | return [node.parent.type.child]; 564 | } 565 | 566 | return []; 567 | }; 568 | 569 | let types = getBaseType(); 570 | 571 | if (!types.length) { 572 | types = [this.baseType]; 573 | } 574 | 575 | if (node.parent?.type && types.length > 1) { 576 | return types.find(t => node.parent.type.bus === t.onBus) ?? types[0]; 577 | } 578 | 579 | return types[0]; 580 | } 581 | } 582 | --------------------------------------------------------------------------------