├── .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 |
5 |
--------------------------------------------------------------------------------
/icons/light/dac.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/dark/flash.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/light/flash.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/dark/clock.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/icons/light/clock.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/icons/dark/overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/icons/light/overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/icons/dark/interrupts.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/light/interrupts.svg:
--------------------------------------------------------------------------------
1 |
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 |
12 |
--------------------------------------------------------------------------------
/icons/light/gpio.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/icons/light/circuit-board.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/icons/dark/bus.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/icons/light/bus.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/icons/dark/shield.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/light/shield.svg:
--------------------------------------------------------------------------------
1 |
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 |
5 |
--------------------------------------------------------------------------------
/icons/light/adc.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/dark/remove-shield.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/icons/light/remove-shield.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/icons/dark/add-shield.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/icons/light/add-shield.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/icons/dark/devicetree-inner.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/icons/light/devicetree-inner.svg:
--------------------------------------------------------------------------------
1 |
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 | 
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 | 
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 | 
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 | 
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 |  **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 |  **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 |  **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 |  **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 |  **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 |  **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 |  **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 | 
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 |
--------------------------------------------------------------------------------