diff --git a/client/src/language/languageClient.ts b/client/src/language/languageClient.ts index 52367f09..6a0fca2c 100644 --- a/client/src/language/languageClient.ts +++ b/client/src/language/languageClient.ts @@ -8,7 +8,8 @@ import * as path from 'path' import { workspace, type ExtensionContext, - window + window, + ConfigurationTarget } from 'vscode' import { @@ -55,7 +56,7 @@ export async function activateLanguageServer (context: ExtensionContext): Promis if (param.filePath?.endsWith('.conf') === true) { const doc = await workspace.openTextDocument(param.filePath) const { languageId } = doc - // The modifications from other extensions may happen later than this handler, hence the setTimtOut + // The modifications from other extensions may happen later than this handler, hence the setTimeOut setTimeout(() => { if (languageId !== 'bitbake') { void window.showErrorMessage(`Failed to associate this file (${param.filePath}) with BitBake Language mode. Current language mode: ${languageId}. Please make sure there is no other extension that is causing the conflict. (e.g. Txt Syntax)`) @@ -64,6 +65,16 @@ export async function activateLanguageServer (context: ExtensionContext): Promis } }) + // Enable suggestions when inside strings, but server side disables suggestions on pure string content, they are onlyavailable in the variable expansion + window.onDidChangeActiveTextEditor((editor) => { + if (editor !== null && editor?.document.languageId === 'bitbake') { + void workspace.getConfiguration('editor').update('quickSuggestions', { strings: true }, ConfigurationTarget.Workspace) + } else { + // Reset to default settings + void workspace.getConfiguration('editor').update('quickSuggestions', { strings: false }, ConfigurationTarget.Workspace) + } + }) + // Start the client and launch the server await client.start() diff --git a/server/src/__tests__/analyzer.test.ts b/server/src/__tests__/analyzer.test.ts new file mode 100644 index 00000000..5ed07124 --- /dev/null +++ b/server/src/__tests__/analyzer.test.ts @@ -0,0 +1,106 @@ +import { generateParser } from '../tree-sitter/parser' +import Analyzer from '../tree-sitter/analyzer' +import { FIXTURE_DOCUMENT } from './fixtures/fixtures' + +// Needed as the param +const DUMMY_URI = 'dummy_uri' + +async function getAnalyzer (): Promise { + const parser = await generateParser() + const analyzer = new Analyzer() + analyzer.initialize(parser) + return analyzer +} + +const initialize = jest.spyOn(Analyzer.prototype, 'initialize') +const wordAtPoint = jest.spyOn(Analyzer.prototype, 'wordAtPoint') + +describe('analyze', () => { + it('instantiates an analyzer', async () => { + // Alternative: Spy on something (logger) within the analyzer instead of spying on every function in the Analyzer + await getAnalyzer() + + expect(initialize).toHaveBeenCalled() + }) + + it('analyzes simple correct bb file', async () => { + const analyzer = await getAnalyzer() + const diagnostics = await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.CORRECT + }) + expect(diagnostics).toEqual([]) + }) + + it('analyzes the document and returns global declarations', async () => { + const analyzer = await getAnalyzer() + await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.DECLARATION + }) + + const globalDeclarations = analyzer.getGlobalDeclarationSymbols(DUMMY_URI) + + expect(globalDeclarations).toMatchInlineSnapshot(` + [ + { + "kind": 13, + "location": { + "range": { + "end": { + "character": 11, + "line": 0, + }, + "start": { + "character": 0, + "line": 0, + }, + }, + "uri": "${DUMMY_URI}", + }, + "name": "FOO", + }, + { + "kind": 13, + "location": { + "range": { + "end": { + "character": 11, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + "uri": "${DUMMY_URI}", + }, + "name": "BAR", + }, + ] + `) + }) + + it('analyzes the document and returns word at point', async () => { + const analyzer = await getAnalyzer() + await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.DECLARATION + }) + + const word1 = analyzer.wordAtPoint( + DUMMY_URI, + 0, + 0 + ) + const word2 = analyzer.wordAtPoint( + DUMMY_URI, + 1, + 0 + ) + + expect(wordAtPoint).toHaveBeenCalled() + expect(word1).toEqual('FOO') + expect(word2).toEqual('BAR') + }) +}) diff --git a/server/src/__tests__/completions.test.ts b/server/src/__tests__/completions.test.ts new file mode 100644 index 00000000..ee28cdcc --- /dev/null +++ b/server/src/__tests__/completions.test.ts @@ -0,0 +1,116 @@ +import { onCompletionHandler } from '../connectionHandlers/onCompletion' +import { analyzer } from '../tree-sitter/analyzer' +import { FIXTURE_DOCUMENT } from './fixtures/fixtures' +import { generateParser } from '../tree-sitter/parser' + +const DUMMY_URI = 'dummy_uri' + +/** + * The onCompletion handler doesn't allow other parameters, so we can't pass the analyzer and therefore the same + * instance used in the handler is used here. Documents are reset before each test for a clean state. + * A possible alternative is making the entire server a class and the analyzer a member + */ +describe('On Completion', () => { + beforeAll(async () => { + if (!analyzer.hasParser()) { + const parser = await generateParser() + analyzer.initialize(parser) + } + analyzer.resetAnalyzedDocuments() + }) + + beforeEach(() => { + analyzer.resetAnalyzedDocuments() + }) + + it('expects reserved variables, keywords and snippets in completion item lists', async () => { + // nothing is analyzed yet, only the static completion items are provided + const result = onCompletionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 0, + character: 1 + } + }) + + expect('length' in result).toBe(true) + + expect(result).toEqual( + expect.arrayContaining([ + { + kind: 14, + label: 'python' + } + ]) + ) + + expect(result).toEqual( + expect.arrayContaining([ + { + kind: 6, + label: 'DESCRIPTION' + } + ]) + ) + + expect(result).toEqual( + /* eslint-disable no-template-curly-in-string */ + expect.arrayContaining([ + { + documentation: { + value: '```man\ndo_bootimg (bitbake-language-server)\n\n\n```\n```bitbake\ndef do_bootimg():\n\t# Your code here\n\t${1:pass}\n```\n---\nCreates a bootable live image. See the IMAGE_FSTYPES variable for additionalinformation on live image types.\n\n[Reference](https://docs.yoctoproject.org/singleindex.html#do-bootimg)', + kind: 'markdown' + }, + insertText: [ + 'def do_bootimg():', + '\t# Your code here', + '\t${1:pass}' + ].join('\n'), + insertTextFormat: 2, + label: 'do_bootimg', + kind: 15 + } + ]) + ) + }) + + it("doesn't provide suggestions when it is pure string content", async () => { + await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.COMPLETION + }) + + const result = onCompletionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 1, + character: 10 + } + }) + + expect(result).toEqual([]) + }) + + it('provides suggestions when it is in variable expansion', async () => { + await analyzer.analyze({ + uri: DUMMY_URI, + document: FIXTURE_DOCUMENT.COMPLETION + }) + + const result = onCompletionHandler({ + textDocument: { + uri: DUMMY_URI + }, + position: { + line: 1, + character: 13 + } + }) + + expect(result).not.toEqual([]) + }) +}) diff --git a/server/src/__tests__/fixtures/completion.bb b/server/src/__tests__/fixtures/completion.bb new file mode 100644 index 00000000..28bd3043 --- /dev/null +++ b/server/src/__tests__/fixtures/completion.bb @@ -0,0 +1,2 @@ +FOO = '123' +MYVAR = 'F${F}' \ No newline at end of file diff --git a/server/src/__tests__/fixtures/correct.bb b/server/src/__tests__/fixtures/correct.bb new file mode 100644 index 00000000..4c838a9f --- /dev/null +++ b/server/src/__tests__/fixtures/correct.bb @@ -0,0 +1,5 @@ +SUMMARY = "i.MX M4 core Demo images" + +python do_foo(){ + print '123' +} \ No newline at end of file diff --git a/server/src/__tests__/fixtures/declarations.bb b/server/src/__tests__/fixtures/declarations.bb new file mode 100644 index 00000000..bedea9ce --- /dev/null +++ b/server/src/__tests__/fixtures/declarations.bb @@ -0,0 +1,2 @@ +FOO = '123' +BAR = '456' \ No newline at end of file diff --git a/server/src/__tests__/fixtures/fixtures.ts b/server/src/__tests__/fixtures/fixtures.ts new file mode 100644 index 00000000..2bd49a14 --- /dev/null +++ b/server/src/__tests__/fixtures/fixtures.ts @@ -0,0 +1,35 @@ +/** + * Inspired by bash-language-sever + * Repo: https://github.com/bash-lsp/bash-language-server + */ + +import path from 'path' +import fs from 'fs' +import { TextDocument } from 'vscode-languageserver-textdocument' + +const FIXTURE_FOLDER = path.join(__dirname, './') + +type FIXTURE_URI_KEY = keyof typeof FIXTURE_URI + +function getDocument (uri: string): TextDocument { + return TextDocument.create( + uri, + 'bitbake', + 0, + fs.readFileSync(uri.replace('file://', ''), 'utf8') + ) +} + +export const FIXTURE_URI = { + CORRECT: `file://${path.join(FIXTURE_FOLDER, 'correct.bb')}`, + DECLARATION: `file://${path.join(FIXTURE_FOLDER, 'declarations.bb')}`, + COMPLETION: `file://${path.join(FIXTURE_FOLDER, 'completion.bb')}` + +} + +export const FIXTURE_DOCUMENT: Record = ( + Object.keys(FIXTURE_URI) as FIXTURE_URI_KEY[] +).reduce((acc, cur: FIXTURE_URI_KEY) => { + acc[cur] = getDocument(FIXTURE_URI[cur]) + return acc +}, {}) diff --git a/server/src/completions/bitbake-variables.ts b/server/src/completions/bitbake-variables.ts new file mode 100644 index 00000000..366220ae --- /dev/null +++ b/server/src/completions/bitbake-variables.ts @@ -0,0 +1,136 @@ +export const BITBAKE_VARIABLES = [ + 'ASSUME_PROVIDED', + 'AZ_SAS', + 'B', + 'BB_ALLOWED_NETWORKS', + 'BB_BASEHASH_IGNORE_VARS', + 'BB_CACHEDIR', + 'BB_CHECK_SSL_CERTS', + 'BB_HASH_CODEPARSER_VALS', + 'BB_CONSOLELOG', + 'BB_CURRENTTASK', + 'BB_DANGLINGAPPENDS_WARNONLY', + 'BB_DEFAULT_TASK', + 'BB_DEFAULT_UMASK', + 'BB_DISKMON_DIRS', + 'BB_DISKMON_WARNINTERVAL', + 'BB_ENV_PASSTHROUGH', + 'BB_ENV_PASSTHROUGH_ADDITIONS', + 'BB_FETCH_PREMIRRORONLY', + 'BB_FILENAME', + 'BB_GENERATE_MIRROR_TARBALLS', + 'BB_GENERATE_SHALLOW_TARBALLS', + 'BB_GIT_SHALLOW', + 'BB_GIT_SHALLOW_DEPTH', + 'BB_GLOBAL_PYMODULES', + 'BB_HASHCHECK_FUNCTION', + 'BB_HASHCONFIG_IGNORE_VARS', + 'BB_HASHSERVE', + 'BB_HASHSERVE_UPSTREAM', + 'BB_INVALIDCONF', + 'BB_LOGCONFIG', + 'BB_LOGFMT', + 'BB_MULTI_PROVIDER_ALLOWED', + 'BB_NICE_LEVEL', + 'BB_NO_NETWORK', + 'BB_NUMBER_PARSE_THREADS', + 'BB_NUMBER_THREADS', + 'BB_ORIGENV', + 'BB_PRESERVE_ENV', + 'BB_PRESSURE_MAX_CPU', + 'BB_PRESSURE_MAX_IO', + 'BB_PRESSURE_MAX_MEMORY', + 'BB_RUNFMT', + 'BB_RUNTASK', + 'BB_SCHEDULER', + 'BB_SCHEDULERS', + 'BB_SETSCENE_DEPVALID', + 'BB_SIGNATURE_EXCLUDE_FLAGS', + 'BB_SIGNATURE_HANDLER', + 'BB_SRCREV_POLICY', + 'BB_STRICT_CHECKSUM', + 'BB_TASK_IONICE_LEVEL', + 'BB_TASK_NICE_LEVEL', + 'BB_TASKHASH', + 'BB_VERBOSE_LOGS', + 'BB_WORKERCONTEXT', + 'BBCLASSEXTEND', + 'BBDEBUG', + 'BBFILE_COLLECTIONS', + 'BBFILE_PATTERN', + 'BBFILE_PRIORITY', + 'BBFILES', + 'BBFILES_DYNAMIC', + 'BBINCLUDED', + 'BBINCLUDELOGS', + 'BBINCLUDELOGS_LINES', + 'BBLAYERS', + 'BBLAYERS_FETCH_DIR', + 'BBMASK', + 'BBMULTICONFIG', + 'BBPATH', + 'BBSERVER', + 'BBTARGETS', + 'BITBAKE_UI', + 'BUILDNAME', + 'BZRDIR', + 'CACHE', + 'CVSDIR', + 'DEFAULT_PREFERENCE', + 'DEPENDS', + 'DESCRIPTION', + 'DL_DIR', + 'EXCLUDE_FROM_WORLD', + 'FAKEROOT', + 'FAKEROOTBASEENV', + 'FAKEROOTCMD', + 'FAKEROOTDIRS', + 'FAKEROOTENV', + 'FAKEROOTNOENV', + 'FETCHCMD', + 'FILE', + 'FILESPATH', + 'FILE_LAYERNAME', + 'GITDIR', + 'HGDIR', + 'HOMEPAGE', + 'INHERIT', + 'LAYERDEPENDS', + 'LAYERDIR', + 'LAYERDIR_RE', + 'LAYERSERIES_COMPAT', + 'LAYERVERSION', + 'LICENSE', + 'MIRRORS', + 'OVERRIDES', + 'PACKAGES', + 'PACKAGES_DYNAMIC', + 'PE', + 'PERSISTENT_DIR', + 'PF', + 'PN', + 'PR', + 'PREFERRED_PROVIDER', + 'PREFERRED_PROVIDERS', + 'PREFERRED_VERSION', + 'PREMIRRORS', + 'PROVIDES', + 'PRSERV_HOST', + 'PV', + 'RDEPENDS', + 'REPODIR', + 'REQUIRED_VERSION', + 'RPROVIDES', + 'RRECOMMENDS', + 'SECTION', + 'SRC_URI', + 'SRCDATE', + 'SRCREV', + 'SRCREV_FORMAT', + 'STAMP', + 'STAMPCLEAN', + 'SUMMARY', + 'SVNDIR', + 'T', + 'TOPDIR' +] diff --git a/server/src/completions/reserved-keywords.ts b/server/src/completions/reserved-keywords.ts new file mode 100644 index 00000000..15ac8245 --- /dev/null +++ b/server/src/completions/reserved-keywords.ts @@ -0,0 +1,81 @@ +const BITBAKE_KEYWORDS = [ + 'python', + 'def', + 'include', + 'import', + 'require', + 'inherit', + 'addtask', + 'deltask', + 'after', + 'before', + 'export', + 'fakeroot', + 'EXPORT_FUNCTIONS', + 'INHERIT' +] + +const PYTHON_KEYWORDS = [ + 'def', + 'from', + 'import', + 'if', + 'else', + 'return', + 'or', + 'elif', + 'for', + 'while', + 'break', + 'continue', + 'yield', + 'try', + 'except', + 'finally', + 'raise', + 'assert', + 'as', + 'pass', + 'del', + 'with', + 'async', + 'await' +] + +const SHELL_KEYWORDS = [ + 'if', + 'then', + 'else', + 'elif', + 'fi', + 'case', + 'esac', + 'for', + 'while', + 'until', + 'do', + 'done', + 'in', + 'function', + 'select', + 'time', + 'coproc', + 'break', + 'continue', + 'return', + 'exit', + 'unset', + 'export', + 'readonly', + 'declare', + 'local', + 'eval', + 'exec', + 'trap' +] + +export const RESERVED_KEYWORDS = [...new Set([ + ...BITBAKE_KEYWORDS, + ...PYTHON_KEYWORDS, + ...SHELL_KEYWORDS +])] diff --git a/server/src/completions/snippets.ts b/server/src/completions/snippets.ts new file mode 100644 index 00000000..71110546 --- /dev/null +++ b/server/src/completions/snippets.ts @@ -0,0 +1,289 @@ +/** + * Inspired by bash-language-sever + * Repo: https://github.com/bash-lsp/bash-language-server + */ + +import { InsertTextFormat, type CompletionItem, CompletionItemKind, MarkupKind } from 'vscode-languageserver' + +/* eslint-disable no-template-curly-in-string */ +export const SNIPPETS: CompletionItem[] = [ + { + label: 'do_build', + insertText: 'def do_build():\n\t# Your code here\n\t${1:pass}', + documentation: 'The default task for all recipes. This task depends on all other normaltasks required to build a recipe.' + }, + { + label: 'do_compile', + insertText: 'def do_compile():\n\t# Your code here\n\t${1:pass}', + documentation: 'Compiles the source code. This task runs with the current workingdirectory set to ${B}.' + }, + { + label: 'do_compile_ptest_base', + insertText: 'def do_compile_ptest_base():\n\t# Your code here\n\t${1:pass}', + documentation: 'Compiles the runtime test suite included in the software being built.' + }, + { + label: 'do_configure', + insertText: 'def do_configure():\n\t# Your code here\n\t${1:pass}', + documentation: 'Configures the source by enabling and disabling any build-time andconfiguration options for the software being built. The task runs withthe current working directory set to ${B}.' + }, + { + label: 'do_configure_ptest_base', + insertText: 'def do_configure_ptest_base():\n\t# Your code here\n\t${1:pass}', + documentation: 'Configures the runtime test suite included in the software being built.' + }, + { + label: 'do_deploy', + insertText: 'def do_deploy():\n\t# Your code here\n\t${1:pass}', + documentation: 'Writes output files that are to be deployed to ${DEPLOY_DIR_IMAGE}. Thetask runs with the current working directory set to ${B}.' + }, + { + label: 'do_fetch', + insertText: 'def do_fetch():\n\t# Your code here\n\t${1:pass}', + documentation: 'Fetches the source code. This task uses the SRC_URI variable and theargument’s prefix to determine the correctfetchermodule.' + }, + { + label: 'do_image', + insertText: 'def do_image():\n\t# Your code here\n\t${1:pass}', + documentation: 'Starts the image generation process. The do_image task runs afterthe OpenEmbedded build system has run thedo_rootfs taskduring which packages areidentified for installation into the image and the root filesystem iscreated, complete with post-processing.' + }, + { + label: 'do_image_complete', + insertText: 'def do_image_complete():\n\t# Your code here\n\t${1:pass}', + documentation: 'Completes the image generation process. The do_image_complete taskruns after the OpenEmbedded build system has run thedo_image taskduring which imagepre-processing occurs and through dynamically generated do_image_*tasks the image is constructed.' + }, + { + label: 'do_install', + insertText: 'def do_install():\n\t# Your code here\n\t${1:pass}', + documentation: 'Copies files that are to be packaged into the holding area ${D}. This task runs with the currentworking directory set to ${B},which is thecompilation directory. The do_install task, as well as other tasksthat either directly or indirectly depend on the installed files (e.g.do_package, do_package_write_*, anddo_rootfs), rununderfakeroot.' + }, + { + label: 'do_install_ptest_base', + insertText: 'def do_install_ptest_base():\n\t# Your code here\n\t${1:pass}', + documentation: 'Copies the runtime test suite files from the compilation directory to aholding area.' + }, + { + label: 'do_package', + insertText: 'def do_package():\n\t# Your code here\n\t${1:pass}', + documentation: 'Analyzes the content of the holding area ${D} and splits the content intosubsetsbased on available packages and files. This task makes use of thePACKAGES and FILESvariables.' + }, + { + label: 'do_package_qa', + insertText: 'def do_package_qa():\n\t# Your code here\n\t${1:pass}', + documentation: 'Runs QA checks on packaged files. For more information on these checks,see the insane class.' + }, + { + label: 'do_package_write_deb', + insertText: 'def do_package_write_deb():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates Debian packages (i.e. *.deb files) and places them in the ${DEPLOY_DIR_DEB} directory inthe package feeds area. For more information, see the“PackageFeeds” section inthe Yocto Project Overview and Concepts Manual.' + }, + { + label: 'do_package_write_ipk', + insertText: 'def do_package_write_ipk():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates IPK packages (i.e. *.ipkfiles) and places them in the ${DEPLOY_DIR_IPK} directory inthe package feeds area. For more information, see the“PackageFeeds” section inthe Yocto Project Overview and Concepts Manual.' + }, + { + label: 'do_package_write_rpm', + insertText: 'def do_package_write_rpm():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates RPM packages (i.e. *.rpmfiles) and places them in the ${DEPLOY_DIR_RPM} directory inthe package feeds area. For more information, see the“PackageFeeds” section inthe Yocto Project Overview and Concepts Manual.' + }, + { + label: 'do_packagedata', + insertText: 'def do_packagedata():\n\t# Your code here\n\t${1:pass}', + documentation: 'Saves package metadata generated by thedo_package taskinPKGDATA_DIR to make it available globally.' + }, + { + label: 'do_patch', + insertText: 'def do_patch():\n\t# Your code here\n\t${1:pass}', + documentation: 'Locates patch files and applies them to the source code.' + }, + { + label: 'do_populate_lic', + insertText: 'def do_populate_lic():\n\t# Your code here\n\t${1:pass}', + documentation: 'Writes license information for the recipe that is collected later whenthe image is constructed.' + }, + { + label: 'do_populate_sdk', + insertText: 'def do_populate_sdk():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates the file and directory structure for an installable SDK. See the“SDKGeneration”section in the Yocto Project Overview and Concepts Manual for moreinformation.' + }, + { + label: 'do_populate_sdk_ext', + insertText: 'def do_populate_sdk_ext():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates the file and directory structure for an installable extensibleSDK (eSDK). See the “SDK Generation”section in the Yocto Project Overview and Concepts Manual for moreinformation.' + }, + { + label: 'do_populate_sysroot', + insertText: 'def do_populate_sysroot():\n\t# Your code here\n\t${1:pass}', + documentation: 'Stages (copies) a subset of the files installed by thedo_install taskinto the appropriatesysroot. For information on how to access these files from otherrecipes, see the STAGING_DIR* variables.Directories that would typically not be needed by other recipes at buildtime (e.g. /etc) are not copiedby default.' + }, + { + label: 'do_prepare_recipe_sysroot', + insertText: 'def do_prepare_recipe_sysroot():\n\t# Your code here\n\t${1:pass}', + documentation: 'Installs the files into the individual recipe specific sysroots (i.e.recipe-sysroot and recipe-sysroot-native under ${WORKDIR} based upon thedependencies specified by DEPENDS). See the“staging” class for more information.' + }, + { + label: 'do_rm_work', + insertText: 'def do_rm_work():\n\t# Your code here\n\t${1:pass}', + documentation: 'Removes work files after the OpenEmbedded build system has finished withthem. You can learn more by looking at the“rm_work” section.' + }, + { + label: 'do_unpack', + insertText: 'def do_unpack():\n\t# Your code here\n\t${1:pass}', + documentation: 'Unpacks the source code into a working directory pointed to by ${WORKDIR}. The Svariable also plays a role in where unpacked source files ultimatelyreside. For more information on how source files are unpacked, see the“SourceFetching”section in the Yocto Project Overview and Concepts Manual and also seethe WORKDIR and S variable descriptions.' + }, + { + label: 'do_checkuri', + insertText: 'def do_checkuri():\n\t# Your code here\n\t${1:pass}', + documentation: 'Validates the SRC_URI value.' + }, + { + label: 'do_clean', + insertText: 'def do_clean():\n\t# Your code here\n\t${1:pass}', + documentation: 'Removes all output files for a target from thedo_unpack taskforward (i.e. do_unpack,do_configure,do_compile,do_install, anddo_package).' + }, + { + label: 'do_cleanall', + insertText: 'def do_cleanall():\n\t# Your code here\n\t${1:pass}', + documentation: 'Removes all output files, shared state(sstate) cache, anddownloaded source files for a target (i.e. the contents ofDL_DIR). Essentially, the do_cleanall task isidentical to the do_cleansstate taskwith the added removal of downloaded source files.' + }, + { + label: 'do_cleansstate', + insertText: 'def do_cleansstate():\n\t# Your code here\n\t${1:pass}', + documentation: 'Removes all output files and shared state(sstate) cache for atarget. Essentially, the do_cleansstate task is identical to thedo_clean taskwith the added removal ofshared state (sstate)cache.' + }, + { + label: 'do_pydevshell', + insertText: 'def do_pydevshell():\n\t# Your code here\n\t${1:pass}', + documentation: 'Starts a shell in which an interactive Python interpreter allows you tointeract with the BitBake build environment. From within this shell, youcan directly examine and set bits from the data store and executefunctions as if within the BitBake environment. See the “Using a PythonDevelopment Shell” section inthe Yocto Project Development Tasks Manual for more information aboutusing pydevshell.' + }, + { + label: 'do_devshell', + insertText: 'def do_devshell():\n\t# Your code here\n\t${1:pass}', + documentation: 'Starts a shell whose environment is set up for development, debugging,or both. See the “Using a Development Shell” section in theYocto Project Development Tasks Manual for more information about usingdevshell.' + }, + { + label: 'do_listtasks', + insertText: 'def do_listtasks():\n\t# Your code here\n\t${1:pass}', + documentation: 'Lists all defined tasks for a target.' + }, + { + label: 'do_package_index', + insertText: 'def do_package_index():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates or updates the index in the Package Feeds area.' + }, + { + label: 'do_bootimg', + insertText: 'def do_bootimg():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates a bootable live image. See the IMAGE_FSTYPES variable for additionalinformation on live image types.' + }, + { + label: 'do_bundle_initramfs', + insertText: 'def do_bundle_initramfs():\n\t# Your code here\n\t${1:pass}', + documentation: 'Combines an Initramfs image and kernel together toform a single image.' + }, + { + label: 'do_rootfs', + insertText: 'def do_rootfs():\n\t# Your code here\n\t${1:pass}', + documentation: 'Creates the root filesystem (file and directory structure) for an image.See the “Image Generation”section in the Yocto Project Overview and Concepts Manual for moreinformation on how the root filesystem is created.' + }, + { + label: 'do_testimage', + insertText: 'def do_testimage():\n\t# Your code here\n\t${1:pass}', + documentation: 'Boots an image and performs runtime tests within the image. Forinformation on automatically testing images, see the“Performing Automated Runtime Testing”section in the Yocto Project Development Tasks Manual.' + }, + { + label: 'do_testimage_auto', + insertText: 'def do_testimage_auto():\n\t# Your code here\n\t${1:pass}', + documentation: 'Boots an image and performs runtime tests within the image immediatelyafter it has been built. This task is enabled when you setTESTIMAGE_AUTO equal to “1”.' + }, + { + label: 'do_compile_kernelmodules', + insertText: 'def do_compile_kernelmodules():\n\t# Your code here\n\t${1:pass}', + documentation: 'Runs the step that builds the kernel modules (if needed). Building akernel consists of two steps: 1) the kernel (vmlinux) is built, and2) the modules are built (i.e. make modules).' + }, + { + label: 'do_diffconfig', + insertText: 'def do_diffconfig():\n\t# Your code here\n\t${1:pass}', + documentation: 'When invoked by the user, this task creates a file containing thedifferences between the original config as produced bydo_kernel_configme task and thechanges made by the user with other methods (i.e. using(do_kernel_menuconfig). Once thefile of differences is created, it can be used to create a configfragment that only contains the differences. You can invoke this taskfrom the command line as follows:' + }, + { + label: 'do_kernel_checkout', + insertText: 'def do_kernel_checkout():\n\t# Your code here\n\t${1:pass}', + documentation: 'Converts the newly unpacked kernel source into a form with which theOpenEmbedded build system can work. Because the kernel source can befetched in several different ways, the do_kernel_checkout task makessure that subsequent tasks are given a clean working tree copy of thekernel with the correct branches checked out.' + }, + { + label: 'do_kernel_configcheck', + insertText: 'def do_kernel_configcheck():\n\t# Your code here\n\t${1:pass}', + documentation: 'Validates the configuration produced by thedo_kernel_menuconfig task. Thedo_kernel_configcheck task produces warnings when a requestedconfiguration does not appear in the final .config file or when youoverride a policy configuration in a hardware configuration fragment.You can run this task explicitly and view the output by using thefollowing command:' + }, + { + label: 'do_kernel_configme', + insertText: 'def do_kernel_configme():\n\t# Your code here\n\t${1:pass}', + documentation: 'After the kernel is patched by the do_patchtask, the do_kernel_configme task assembles and merges all thekernel config fragments into a merged configuration that can then bepassed to the kernel configuration phase proper. This is also the timeduring which user-specified defconfigs are applied if present, and whereconfiguration modes such as --allnoconfig are applied.' + }, + { + label: 'do_kernel_menuconfig', + insertText: 'def do_kernel_menuconfig():\n\t# Your code here\n\t${1:pass}', + documentation: 'Invoked by the user to manipulate the .config file used to build alinux-yocto recipe. This task starts the Linux kernel configurationtool, which you then use to modify the kernel configuration.' + }, + { + label: 'do_kernel_metadata', + insertText: 'def do_kernel_metadata():\n\t# Your code here\n\t${1:pass}', + documentation: 'Collects all the features required for a given kernel build, whether thefeatures come from SRC_URI or from Gitrepositories. After collection, the do_kernel_metadata taskprocesses the features into a series of config fragments and patches,which can then be applied by subsequent tasks such asdo_patch anddo_kernel_configme.' + }, + { + label: 'do_menuconfig', + insertText: 'def do_menuconfig():\n\t# Your code here\n\t${1:pass}', + documentation: 'Runs make menuconfigfor the kernel. For information onmenuconfig, see the“Usingmenuconfig”section in the Yocto Project Linux Kernel Development Manual.' + }, + { + label: 'do_savedefconfig', + insertText: 'def do_savedefconfig():\n\t# Your code here\n\t${1:pass}', + documentation: 'When invoked by the user, creates a defconfig file that can be usedinstead of the default defconfig. The saved defconfig contains thedifferences between the default defconfig and the changes made by theuser using other methods (i.e. thedo_kernel_menuconfig task. Youcan invoke the task using the following command:' + }, + { + label: 'do_shared_workdir', + insertText: 'def do_shared_workdir():\n\t# Your code here\n\t${1:pass}', + documentation: 'After the kernel has been compiled but before the kernel modules havebeen compiled, this task copies files required for module builds andwhich are generated from the kernel build into the shared workdirectory. With these copies successfully copied, thedo_compile_kernelmodules taskcan successfully build the kernel modules in the next step of the build.' + }, + { + label: 'do_sizecheck', + insertText: 'def do_sizecheck():\n\t# Your code here\n\t${1:pass}', + documentation: 'After the kernel has been built, this task checks the size of thestripped kernel image againstKERNEL_IMAGE_MAXSIZE. If thatvariable was set and the size of the stripped kernel exceeds that size,the kernel build produces a warning to that effect.' + }, + { + label: 'do_strip', + insertText: 'def do_strip():\n\t# Your code here\n\t${1:pass}', + documentation: 'If KERNEL_IMAGE_STRIP_EXTRA_SECTIONSis defined, this task stripsthe sections named in that variable from vmlinux. This stripping istypically used to remove nonessential sections such as .commentsections from a size-sensitive configuration.' + }, + { + label: 'do_validate_branches', + insertText: 'def do_validate_branches():\n\t# Your code here\n\t${1:pass}', + documentation: 'After the kernel is unpacked but before it is patched, this task makessure that the machine and metadata branches as specified by theSRCREV variables actually exist on the specifiedbranches. Otherwise, if AUTOREV is not being used, thedo_validate_branches task fails during the build.' + }].map((item) => { + return { + ...item, + insertTextFormat: InsertTextFormat.Snippet, + documentation: { + value: [ + markdownBlock( + `${item.label} (bitbake-language-server)\n\n`, + 'man' + ), + markdownBlock(item.insertText, 'bitbake'), + '---', + `${item.documentation}\n`, + `[Reference](https://docs.yoctoproject.org/singleindex.html#${item.label.replace(/_/, '-')})` + ].join('\n'), + kind: MarkupKind.Markdown + }, + + kind: CompletionItemKind.Snippet + } +}) + +function markdownBlock (text: string, language: string): string { + const tripleQuote = '```' + return [tripleQuote + language, text, tripleQuote].join('\n') +} diff --git a/server/src/connectionHandlers/onCompletion.ts b/server/src/connectionHandlers/onCompletion.ts new file mode 100644 index 00000000..bdc8a8ad --- /dev/null +++ b/server/src/connectionHandlers/onCompletion.ts @@ -0,0 +1,66 @@ +import logger from 'winston' +import { type TextDocumentPositionParams, type CompletionItem, type SymbolInformation, CompletionItemKind } from 'vscode-languageserver/node' +import { symbolKindToCompletionKind } from '../utils/lsp' +import { BITBAKE_VARIABLES } from '../completions/bitbake-variables' +import { RESERVED_KEYWORDS } from '../completions/reserved-keywords' +import { analyzer } from '../tree-sitter/analyzer' +import { SNIPPETS } from '../completions/snippets' + +export function onCompletionHandler (textDocumentPositionParams: TextDocumentPositionParams): CompletionItem[] { + logger.debug('onCompletion') + + const wordPosition = { + line: textDocumentPositionParams.position.line, + // Go one character back to get completion on the current word. This is used as a parameter in descendantForPosition() + character: Math.max(textDocumentPositionParams.position.character - 1, 0) + } + + const word = analyzer.wordAtPointFromTextPosition({ + ...textDocumentPositionParams, + position: wordPosition + }) + + logger.debug(`onCompletion - current word: ${word}`) + + const shouldComplete = analyzer.shouldProvideCompletionItems(textDocumentPositionParams.textDocument.uri, wordPosition.line, wordPosition.character) + // Do not provide completions if it is inside a string but not inside a variable expansion + if (!shouldComplete) { + return [] + } + + let symbolCompletions: CompletionItem[] = [] + if (word !== null) { + const globalDeclarationSymbols = analyzer.getGlobalDeclarationSymbols(textDocumentPositionParams.textDocument.uri) + + symbolCompletions = globalDeclarationSymbols.map((symbol: SymbolInformation) => ( + { + label: symbol.name, + kind: symbolKindToCompletionKind(symbol.kind), + documentation: `${symbol.name}` + } + )) + } + + const reserverdKeywordCompletionItems: CompletionItem[] = RESERVED_KEYWORDS.map(keyword => { + return { + label: keyword, + kind: CompletionItemKind.Keyword + } + }) + + const reserverdVariableCompletionItems: CompletionItem[] = BITBAKE_VARIABLES.map(keyword => { + return { + label: keyword, + kind: CompletionItemKind.Variable + } + }) + + const allCompletions = [ + ...reserverdKeywordCompletionItems, + ...reserverdVariableCompletionItems, + ...SNIPPETS, + ...symbolCompletions + ] + + return allCompletions +} diff --git a/server/src/server.ts b/server/src/server.ts index 188d6ecc..7bbab749 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -10,7 +10,6 @@ import { type CompletionItem, type Definition, type Hover, - type SymbolInformation, createConnection, TextDocuments, ProposedFeatures, @@ -21,10 +20,10 @@ import { BitBakeProjectScanner } from './BitBakeProjectScanner' import { ContextHandler } from './ContextHandler' import { SymbolScanner } from './SymbolScanner' import { TextDocument } from 'vscode-languageserver-textdocument' -import Analyzer from './tree-sitter/analyzer' +import { analyzer } from './tree-sitter/analyzer' import { generateParser } from './tree-sitter/parser' -import { symbolKindToCompletionKind } from './utils/lsp' import logger from 'winston' +import { onCompletionHandler } from './connectionHandlers/onCompletion' // Create a connection for the server. The connection uses Node's IPC as a transport const connection: Connection = createConnection(ProposedFeatures.all) @@ -33,7 +32,6 @@ const documentAsTextMap = new Map() const bitBakeDocScanner = new BitBakeDocScanner() const bitBakeProjectScanner: BitBakeProjectScanner = new BitBakeProjectScanner(connection) const contextHandler: ContextHandler = new ContextHandler(bitBakeProjectScanner) -const analyzer: Analyzer = new Analyzer() connection.onInitialize(async (params): Promise => { const workspaceRoot = params.rootPath ?? '' @@ -93,52 +91,10 @@ connection.onDidChangeWatchedFiles((change) => { bitBakeProjectScanner.rescanProject() }) -connection.onCompletion((textDocumentPositionParams: TextDocumentPositionParams): CompletionItem[] => { - logger.debug('onCompletion') - const documentAsText = documentAsTextMap.get(textDocumentPositionParams.textDocument.uri) - if (documentAsText === undefined) { - return [] - } - - const word = analyzer.wordAtPointFromTextPosition({ - ...textDocumentPositionParams, - position: { - line: textDocumentPositionParams.position.line, - // Go one character back to get completion on the current word. This is used as a parameter in descendantForPosition() - character: Math.max(textDocumentPositionParams.position.character - 1, 0) - } - }) - - logger.debug(`onCompletion - current word: ${word}`) - - let symbolCompletions: CompletionItem[] = [] - if (word !== null) { - const symbols = analyzer.getGlobalDeclarationSymbols(textDocumentPositionParams.textDocument.uri) - - // Covert symbols to completion items - // TODO: remove duplicate symbols - symbolCompletions = symbols.map((symbol: SymbolInformation) => ( - { - label: symbol.name, - kind: symbolKindToCompletionKind(symbol.kind), - documentation: `${symbol.name}` - } - )) - } - - const allCompletions = [ - ...contextHandler.getComletionItems(textDocumentPositionParams, documentAsText), - ...symbolCompletions - ] - - return allCompletions -}) +connection.onCompletion(onCompletionHandler) connection.onCompletionResolve((item: CompletionItem): CompletionItem => { - logger.debug(`onCompletionResolve ${JSON.stringify(item)}`) - - item.insertText = contextHandler.getInsertStringForTheElement(item) - + logger.debug(`onCompletionResolve: ${JSON.stringify(item)}`) return item }) diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index 8da2a5c7..3a6950be 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -6,13 +6,11 @@ import { type TextDocumentPositionParams, type Diagnostic, - type SymbolInformation, - DiagnosticSeverity + type SymbolInformation } from 'vscode-languageserver' import type Parser from 'web-tree-sitter' import type { TextDocument } from 'vscode-languageserver-textdocument' import { getGlobalDeclarations, type GlobalDeclarations } from './declarations' -import { getAllErrorNodes } from './errors' import { debounce } from '../utils/async' import { type Tree } from 'web-tree-sitter' @@ -66,26 +64,7 @@ export default class Analyzer { private executeAnalyzation (document: TextDocument, uri: string, tree: Tree): Diagnostic[] { const diagnostics: Diagnostic[] = [] - const errorNodes = getAllErrorNodes(tree) - - errorNodes.forEach(node => { - const diagnostic: Diagnostic = { - severity: DiagnosticSeverity.Error, - range: { - start: { - line: node.startPosition.row, - character: node.startPosition.column - }, - end: { - line: node.endPosition.row, - character: node.endPosition.column - } - }, - message: `Invalid syntax "${node.text.trim()}" `, - source: 'ex' - } - diagnostics.push(diagnostic) - }) + // It was used to provide diagnostics from tree-sitter, but it is not yet reliable. return diagnostics } @@ -124,6 +103,26 @@ export default class Analyzer { ) } + public shouldProvideCompletionItems ( + uri: string, + line: number, + column: number + ): boolean { + const n = this.nodeAtPoint(uri, line, column) + if (n?.type === 'string_content' || n?.type === 'ERROR') { + return false + } + return true + } + + public hasParser (): boolean { + return this.parser !== undefined + } + + public resetAnalyzedDocuments (): void { + this.uriToAnalyzedDocument = {} + } + /** * Find the node at the given point. */ @@ -146,3 +145,5 @@ export default class Analyzer { return tree.rootNode.descendantForPosition({ row: line, column }) } } + +export const analyzer: Analyzer = new Analyzer() diff --git a/server/src/tree-sitter/declarations.ts b/server/src/tree-sitter/declarations.ts index 939e5fbb..d85b09bb 100644 --- a/server/src/tree-sitter/declarations.ts +++ b/server/src/tree-sitter/declarations.ts @@ -7,6 +7,7 @@ import * as LSP from 'vscode-languageserver/node' import type * as Parser from 'web-tree-sitter' import * as TreeSitterUtil from './utils' +import { BITBAKE_VARIABLES } from '../completions/bitbake-variables' const TREE_SITTER_TYPE_TO_LSP_KIND: Record = { environment_variable_assignment: LSP.SymbolKind.Variable, @@ -49,7 +50,8 @@ export function getGlobalDeclarations ({ const followChildren = !GLOBAL_DECLARATION_NODE_TYPES.has(node.type) const symbol = getDeclarationSymbolFromNode({ node, uri }) - if (symbol !== null) { + // skip if it is a bitbake variable as it is added in BITBAKE_VARIABLES + if (symbol !== null && !(new Set(BITBAKE_VARIABLES).has(symbol.name))) { const word = symbol.name globalDeclarations[word] = symbol } @@ -73,7 +75,7 @@ function nodeToSymbolInformation ({ } const containerName = - TreeSitterUtil.findParent(node, (p) => p.type === 'function_definition') + TreeSitterUtil.findParent(node, (p) => GLOBAL_DECLARATION_NODE_TYPES.has(p.type)) ?.firstNamedChild?.text ?? '' const kind = TREE_SITTER_TYPE_TO_LSP_KIND[node.type]