devicetree-language-server
v0.7.2
Published
Devicetree Language Server
Maintainers
Readme
DTS Language Server
This LSP is intended to be used with DTS Devicetree Specification Release v0.4 (https://devicetree.org)
Table of Contents
Features
Go to Definition
On node name/label reference; will list all the places where the node is altered. /delete-node/ cases are not listed. On property name; will list all the places where the property is assigned a value. Note: defining a property name with no assign (empty) is equal to assigning a truthful value and hence it will also be shown.

NOTE: If for example a node with name node1 has been created, then deleted, and then created again, depending on where the definition call is made in the file, in one case one will get the definition from before the delete keyword, and in the other case the definition from under the delete keyword.
Zephyr - DT_MACROS
You can also use Go to Definition on a selected number of DT_ APIs found in zephyr and get Hovers to help explain Node state or even DT_MACRO result for the current active Devicetree context

Go to Declarations
On node name/label reference; will list the first places where the node is created. On property name; will list the first places where the property is assigned a value for the first time. Note: defining a property name with no assign (empty) is equal to assigning a truthful value and hence it will also be shown.

NOTE: The declarations will stop at the definition, hence, if for example a node with name node1 has been created, then deleted, and then created again, depending on where the declarations call is made in the file, in one case one will get the declarations from before the delete keyword up to the delete keyword, and in the other case from the delete keyword (excluded) onwards.
Zephyr - DT_MACROS
You can also use Go to Declarations on a selected number of DT_ APIs found in zephyr and get Hovers to help explain Node state or even DT_MACRO result for the current active Devicetree context

Go to References - Find All References
- On node name/label reference; will list all the places where the node is used by name, label or in some path.
- On property name; will list all the places where the property referred to including /delete-property/.

NOTE: The references will stop at the definition, hence, if for example a node with name node1 has been created, then deleted, and then created again, depending on where the reference call is made in the file, in one case one will get the ones from before the delete keyword up to the delete keyword, and in the other case from the delete keyword (excluded) onwards.
Hover
On hover over the node name, a tooltip will show the final state of that node. If bindings are used it will also include the description from the binding files.

When hovering over a deleted state you can see the state of the item just before the delete action.

Zephyr - DT_MACROS
You can also hover on a selected number of DT_ APIs found in zephyr and get Hovers to help explain Node state or even DT_MACRO result for the current active Devicetree context

Formatting
This LSP follows the Zephyr Style Guide and is used in CI to validate all files upstream.

Semantic Tokens
Every element in the document will have semantic tokens to help highlight and color code the items.

Document Symbols
Every element in the document will have document symbols to help navigate the document in a tree format.

Workspace Symbols
You can also navigate the active context using workspace symbols.

Diagnostics
Reports generic syntax issues such as missing "," , "}" , ">" etc...

Reports when property has been replaced by a later definition and provides document link to where it has been redefined

Reports when node has been deleted and provides document link to where the delete was done

Reports when property has been deleted and provides document link to where it has been redefined

Reports label conflicts

Duplicate node name in the same node

Reports when deleting a node/property that does not exist

Reports CPreprocessor issues such as missing macro, invalid argument count etc.

Compares the node address and ensures that it matches the reg property, and that the reg values use the appropriate number of values as defined #address-cells

Reports property type mismatch errors

Reports prop-encoded-values errors when these need to follow some expected pattern e.g interrupts/nexus

Bus type validation when using Zephyr bindings

Completions
Completions are context aware of the document state on the line the action is requested.
Node path completion

Label reference completion reference node creation and property assignment

Delete Node:
Suggests appropriate type e.g. by reference or node name. Does not suggest keyword if no delete is possible.

Delete Property:
Suggests property names available in that context. Does not suggest keyword if no delete is possible.

Default values for standard types

Zephyr DT_MACRO Completion

Refactoring
Refactoring is possible on the following elements:
- Node names
- Node labels
- Property names

Given that in some cases the files included in a devicetree might come from an SDK which should not be edited, one can configure "lockRenameEdits" in the settings to lock refactoring from being permitted on any elements which would otherwise effect edits to the files listed in "lockRenameEdits".
Code Actions
- Adds missing syntax e.g. ';', '<', '>', ',' etc...
- Removes syntactically incorrect spaces:
- Between node name, '@' and address
- In node path reference
- Removes ';' when used without any statement
- Supports SourceFixAll/QuickFixes
Something else in mind?
Contributions are welcome or reach out on GitHub with requests.
Installation
The language server can be found on https://www.npmjs.com/package/devicetree-language-server.
To install run
npm i -g devicetree-language-server
The LSP has only been tested with Node 20.
Usage
This extension needs a client that supports Configuration Capability. The format for the configuration setting is of the type Settings as shown below:
At the moment, this LSP only supports bindings for the Zephyr project and has experimental support for Devicetree-Org Bindings.
interface Context {
ctxName?: string | number;
cwd?: string;
includePaths?: string[];
dtsFile: string;
overlays?: string[];
bindingType?: BindingType;
zephyrBindings?: string[];
deviceOrgTreeBindings?: string[];
deviceOrgBindingsMetaSchema?: string[];
lockRenameEdits?: string[];
formattingErrorAsDiagnostics?: boolean;
compileCommands?: string;
}
interface Settings {
cwd?: string;
defaultBindingType?: BindingType;
defaultZephyrBindings?: string[];
defaultDeviceOrgTreeBindings?: string[];
defaultDeviceOrgBindingsMetaSchema?: string[];
defaultIncludePaths?: string[];
contexts?: Context[];
preferredContext?: string | number;
defaultLockRenameEdits?: string[];
defaultShowFormattingErrorAsDiagnostics?: boolean;
autoChangeContext?: boolean;
allowAdhocContexts?: boolean;
}Zephyr configuration example
{
"devicetree.cwd": "/User/workspace/zephyr",
"devicetree.defaultIncludePaths": [
"./zephyr/dts",
"./zephyr/dts/arm",
"./zephyr/dts/arm64/",
"./zephyr/dts/riscv",
"./zephyr/dts/common",
"./zephyr/dts/vendor",
"./zephyr/include",
"./zephyr/dts/xtensa"
],
"devicetree.defaultBindingType": "Zephyr",
"devicetree.defaultZephyrBindings": ["./zephyr/dts/bindings"],
"devicetree.contexts": [
{
"devicetree.cwd": "/opt/nordic/ncs/v3.0.0",
"bindingType": "Zephyr",
"zephyrBindings": ["./zephyr/dts/bindings", "./nrf/dts/bindings"],
"includePaths": [
"./zephyr/dts",
"./zephyr/dts/arm",
"./zephyr/dts/arm64/",
"./zephyr/dts/riscv",
"./zephyr/dts/common",
"./zephyr/dts/vendor",
"./zephyr/include",
"./zephyr/dts/xtensa"
],
"dtsFile": "./zephyr/boards/nordic/nrf52840dk/nrf52840dk_nrf52840.dts",
"overlays": ["/User/project/myOverlay.overlay"]
}
]
}Linux
{
"devicetree.cwd": "/Users/user/Workspace/linux/",
"devicetree.defaultIncludePaths": ["include"],
"devicetree.defaultBindingType": "DevicetreeOrg",
"devicetree.defaultDeviceOrgBindingsMetaSchema": [],
"devicetree.defaultDeviceOrgTreeBindings": []
}With Devicetree-Org Bindings
{
"devicetree.cwd": "/Users/user/Workspace/linux/",
"devicetree.defaultIncludePaths": ["include"],
"devicetree.defaultBindingType": "DevicetreeOrg",
"devicetree.defaultDeviceOrgBindingsMetaSchema": [
"/Users/user/Workspace/linuxBindings/dt-schema/dtschema/meta-schemas" // https://github.com/devicetree-org/dt-schema/tree/main/dtschema/meta-schemas
],
"devicetree.defaultDeviceOrgTreeBindings": [
"/Users/user/Workspace/linuxBindings/dt-schema/dtschema/schemas", // https://github.com/devicetree-org/dt-schema/tree/main/dtschema/schemas
"/Users/user/Workspace/linux/Documentation/devicetree/bindings" // https://github.com/torvalds/linux/tree/master/Documentation/devicetree/bindings
]
}Note
Devicetree-Org bindings are experimental.
Sample Editor configurations
kakoune
Sample configuration in for kakoune with Zephyr 3.7.99 or later.
Contribution by topisani
hook -group lsp-project-zephyr global BufSetOption filetype=(devicetree) %{
eval %sh{
root=$(eval "$kak_opt_lsp_find_root" .build_info.yml $(: kak_buffile))
[ -e "$root/.build_info.yml" ] || exit 0
settings=$(cat ".build_info.yml" | yq -c '{ "defaultIncludePaths": .cmake.devicetree."include-dirs", "contexts": [{ "bindingType": "Zephyr", "zephyrBindings": .cmake.devicetree."bindings-dirs", "includePaths": .cmake.devicetree."include-dirs", "dtsFile": .cmake.devicetree.files[0], "overlays": .cmake.devicetree.files[1:] }], "preferredContext": 0 } | { devicetree: {settings: {"_": {devicetree: .}}}}' | yj -jt | sed '/^\[devicetree\]$/d')
cat <<EOF
set-option buffer lsp_servers %{
[devicetree]
root = '$root'
command = "npm"
args = ["x", "--", "devicetree-language-server", "--stdio"]
# command = "node"
# args = ["/home/topisani/git/dts-lsp/server/dist/server.js", "--stdio"]
settings_section = "_"
$settings
}
EOF
}
}Neovim - lazygit
Example setup using lazygit
return {
"neovim/nvim-lspconfig",
opts = function(_, opts)
-- Don't let Mason try to handle this custom server
opts.servers.devicetree_ls = nil
local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")
local capabilities = vim.lsp.protocol.make_client_capabilities()
-- Enable semantic tokens
capabilities.textDocument = capabilities.textDocument or {}
capabilities.textDocument.semanticTokens = {
dynamicRegistration = false,
requests = {
range = false,
full = true,
},
tokenTypes = {
"namespace", "class", "enum", "interface", "struct", "typeParameter", "type",
"parameter", "variable", "property", "enumMember", "decorator", "event", "function",
"method", "macro", "label", "comment", "string", "keyword", "number", "regexp", "operator",
},
tokenModifiers = {
"declaration", "definition", "readonly", "static", "deprecated", "abstract",
"async", "modification", "documentation", "defaultLibrary",
},
formats = {'relative'}
}
-- Enable formatting
capabilities.textDocument.formatting = {
dynamicRegistration = false
}
-- Enable folding range support
capabilities.textDocument.foldingRange = {
dynamicRegistration = false,
lineFoldingOnly = true,
}
if not configs.devicetree_ls then
configs.devicetree_ls = {
default_config = {
cmd = { "devicetree-language-server", "--stdio" },
filetypes = { "dts", "dtsi" },
root_dir = lspconfig.util.root_pattern("zephyr", ".git", "."),
settings = {
devicetree = {
defaultIncludePaths = {
"./zephyr/dts",
"./zephyr/dts/arm",
"./zephyr/dts/arm64/",
"./zephyr/dts/riscv",
"./zephyr/dts/common",
"./zephyr/dts/vendor",
"./zephyr/include"
},
cwd = "${workspaceFolder}",
defaultBindingType = "Zephyr",
defaultZephyrBindings = {
"./zephyr/dts/bindings"
},
autoChangeContext = true,
allowAdhocContexts = true,
contexts = {},
},
},
capabilities = capabilities,
},
}
end
vim.notify("Custom devicetree_ls LSP loaded with semantic tokens & folding")
-- Setup the LSP
lspconfig.devicetree_ls.setup({
capabilities = capabilities,
})
end,
}Helix
Contribution by bextract
[[language]]
name = "devicetree"
language-servers = ["devicetree_ls"]
[language-server.devicetree_ls]
command = "devicetree-language-server"
args = ["--stdio"]
config = { devicetree = { cwd = "/home/bex/zephyrproject/", defaultIncludePaths = ["./zephyr/dts","./zephyr/dts/arm","./zephyr/dts/arm64","./zephyr/dts/riscv","./zephyr/dts/common","./zephyr/dts/vendor","./zephyr/include","./zephyr/dts/xtensa"], defaultBindingType = "Zephyr", defaultZephyrBindings = ["./zephyr/dts/bindings"], contexts = [] } }coco nvim
{
"suggest.completionItemKindLabels": {
"keyword": "",
"variable": "",
"value": "",
"operator": "Ψ",
"constructor": "",
"function": "ƒ",
"reference": "渚",
"constant": "",
"method": "",
"struct": "פּ",
"class": "",
"interface": "",
"text": "",
"enum": "",
"enumMember": "",
"module": "",
"color": "",
"property": "",
"field": "料",
"unit": "",
"event": "鬒",
"file": "",
"folder": "",
"snippet": "",
"typeParameter": "",
"default": ""
},
"diagnostic.errorSign": "❗",
"diagnostic.warningSign": "💡",
"diagnostic.infoSign": "💡",
"diagnostic.hintSign": "💡",
"diagnostic.signPriority": 100,
"languageserver": {
"devicetree_ls": {
"command": "devicetree-language-server",
"args": ["--stdio"],
"filetypes": ["dts", "dtsi"],
"rootPatterns": [".git"],
"initializationOptions": {
"AutomaticWorkspaceInit": true
},
"settings": {
"devicetree": {
"cwd": "/home/user/Workspace/Kernel/linux",
"defaultIncludePaths": ["./include"],
"defaultBindingType": "DevicetreeOrg",
"contexts": []
}
}
}
}
}Vim 9 - yegappan lsp
Contribution by jclsn
set encoding=utf-8
set nocompatible
colorscheme habamax
call plug#begin('$MYVIMDIR/plugged')
Plug 'yegappan/lsp'
call plug#end()
let lspOpts = #{
\ aleSupport: v:false,
\ autoComplete: v:true,
\ autoHighlight: v:false,
\ autoHighlightDiags: v:true,
\ autoPopulateDiags: v:false,
\ completionMatcher: 'case',
\ completionMatcherValue: 1,
\ diagSignErrorText: '❗',
\ diagSignHintText: '💡',
\ diagSignInfoText: '💡',
\ diagSignWarningText: '💡',
\ diagSignPriority: {
\ 'Error': 100,
\ 'Warning': 99,
\ 'Information': 98,
\ 'Hint': 97
\ },
\ echoSignature: v:false,
\ hideDisabledCodeActions: v:false,
\ highlightDiagInline: v:true,
\ hoverInPreview: v:false,
\ ignoreMissingServer: v:false,
\ keepFocusInDiags: v:true,
\ keepFocusInReferences: v:true,
\ completionTextEdit: v:true,
\ diagVirtualTextAlign: 'above',
\ diagVirtualTextWrap: 'default',
\ noNewlineInCompletion: v:false,
\ omniComplete: v:null,
\ omniCompleteAllowBare: v:false,
\ outlineOnRight: v:false,
\ outlineWinSize: 20,
\ popupBorder: v:true,
\ popupBorderHighlight: 'Title',
\ popupBorderHighlightPeek: 'Special',
\ popupBorderSignatureHelp: v:false,
\ popupHighlightSignatureHelp: 'Pmenu',
\ popupHighlight: 'Normal',
\ semanticHighlight: v:true,
\ showDiagInBalloon: v:true,
\ showDiagInPopup: v:true,
\ showDiagOnStatusLine: v:false,
\ showDiagWithSign: v:true,
\ showDiagWithVirtualText: v:false,
\ showInlayHints: v:false,
\ showSignature: v:true,
\ snippetSupport: v:false,
\ ultisnipsSupport: v:false,
\ useBufferCompletion: v:false,
\ usePopupInCodeAction: v:false,
\ useQuickfixForLocations: v:false,
\ vsnipSupport: v:false,
\ bufferCompletionTimeout: 100,
\ customCompletionKinds: v:false,
\ completionKinds: {},
\ filterCompletionDuplicates: v:false,
\ condensedCompletionMenu: v:false,
\ }
autocmd User LspSetup call LspOptionsSet(lspOpts)
let lspServers = [#{
\ name: 'devicetree_ls',
\ filetype: ['dts', 'dtsi'],
\ path: 'devicetree-language-server',
\ args: ['--stdio'],
\
\ root_uri: {server_info -> lsp#utils#path_to_uri(
\ lsp#utils#find_nearest_parent_file_directory(expand('%:p'), '.git', '.')
\ )
\ },
\
\ initializationOptions: #{
\ settings: #{
\ devicetree: #{
\ cwd: getcwd(),
\ defaultIncludePaths: [
\ './include',
\ ],
\ defaultBindingType: 'DevicetreeOrg',
\ contexts: []
\ }
\ }
\ }
\ }]
autocmd User LspSetup call LspAddServer(lspServers)
" Remap leader key to space
nnoremap <SPACE> <Nop>
let mapleader = " "
let maplocalleader = "-"
nnoremap <leader>ac :LspCodeAction<CR>
nnoremap <silent> <leader>pe :LspDiagPrev<CR>
nnoremap <silent> <leader>ne :LspDiagNext<CR>
nnoremap <silent> <leader>pd :LspPeekDefinition<CR>
nnoremap <silent> <leader>pdc :LspPeekDeclaration<CR>
nnoremap <silent> <leader>pr :LspPeekReferences<CR>
nnoremap <silent> <leader>ol :LspOutline<CR>
nnoremap <silent> <leader>rn :LspRename<CR>
nnoremap <leader>cl :LspCodeLens<CR>
function! s:SmartHover() abort
let result = execute('LspHover')
if result =~ 'Error'
call feedkeys('K', 'n')
endif
endfunction
nnoremap <silent> K :call <SID>SmartHover()<CR>
nnoremap <silent> gd :LspGotoDefinition<CR>
nnoremap <silent> gy :LspGotoTypeDef<CR>
nnoremap <silent> gi :LspGotoImpl<CR>
nnoremap <silent> gdc :LspGotoDeclaration<CR>
command! -nargs=0 -bar -range=% Format <line1>,<line2>LspFormat
set formatexpr=lsp#lsp#FormatExpr() " Map LspFormat to the gq command