From 31d63355193e3d27e7e7301b2b50a33846daa9e6 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Fri, 13 Oct 2023 15:31:41 -0400 Subject: [PATCH 01/16] Feat: Add common completion items 1. Add reserved variables 2. Add reserved keywords (currently they include python and shell keywords) 3. Add simple snippets for bitbake recipe tasks --- server/src/completions/reserved-keywords.ts | 42 ++++++ server/src/completions/reserved-variables.ts | 136 +++++++++++++++++++ server/src/completions/snippets.ts | 130 ++++++++++++++++++ server/src/server.ts | 36 +++-- 4 files changed, 334 insertions(+), 10 deletions(-) create mode 100644 server/src/completions/reserved-keywords.ts create mode 100644 server/src/completions/reserved-variables.ts create mode 100644 server/src/completions/snippets.ts diff --git a/server/src/completions/reserved-keywords.ts b/server/src/completions/reserved-keywords.ts new file mode 100644 index 00000000..858c32e4 --- /dev/null +++ b/server/src/completions/reserved-keywords.ts @@ -0,0 +1,42 @@ +export const RESERVED_KEYWORDS = [ + 'python', + 'def', + 'include', + 'from', + 'import', + 'require', + 'inherit', + 'addtask', + 'deltask', + 'after', + 'before', + 'export', + 'echo', + 'if', + 'fi', + 'else', + 'return', + 'unset', + 'print', + 'or', + 'fakeroot', + 'EXPORT_FUNCTIONS', + 'INHERIT', + 'elif', + 'for', + 'while', + 'break', + 'continue', + 'yield', + 'try', + 'except', + 'finally', + 'raise', + 'assert', + 'as', + 'pass', + 'del', + 'with', + 'async', + 'await' +] diff --git a/server/src/completions/reserved-variables.ts b/server/src/completions/reserved-variables.ts new file mode 100644 index 00000000..253d691d --- /dev/null +++ b/server/src/completions/reserved-variables.ts @@ -0,0 +1,136 @@ +export const RESERVED_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/snippets.ts b/server/src/completions/snippets.ts new file mode 100644 index 00000000..580fac83 --- /dev/null +++ b/server/src/completions/snippets.ts @@ -0,0 +1,130 @@ +/** + * Inspired by bash-language-sever + * Repo: https://github.com/bash-lsp/bash-language-server + */ + +import { InsertTextFormat, type CompletionItem, CompletionItemKind, MarkupKind } from 'vscode-languageserver' + +export const SNIPPETS: CompletionItem[] = [ + { + documentation: 'Fetch something', + label: 'do_fetch', + insertText: [ + 'def do_fetch():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Unpack something', + label: 'do_unpack', + insertText: [ + 'def do_unpack():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Patch something', + label: 'do_patch', + insertText: [ + 'def do_patch():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Configure something', + label: 'do_configure', + insertText: [ + 'def do_configure():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Compile something', + label: 'do_compile', + insertText: [ + 'def do_compile():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Install something', + label: 'do_install', + insertText: [ + 'def do_install():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Package something', + label: 'do_package', + insertText: [ + 'def do_package():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Rootfs', + label: 'do_rootfs', + insertText: [ + 'def do_rootfs():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Populate sysroot', + label: 'do_populate_sysroot', + insertText: [ + 'def do_populate_sysroot():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + }, + { + documentation: 'Deploy something', + label: 'do_deploy', + insertText: [ + 'def do_deploy():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n') + } +].map((item) => { + return { + ...item, + insertTextFormat: InsertTextFormat.Snippet, + documentation: { + value: [ + markdownBlock( + `${item.documentation ?? item.label} (bitbake-language-server)\n\n`, + 'man' + ), + markdownBlock(item.insertText, 'bitbake') + ].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/server.ts b/server/src/server.ts index 188d6ecc..7381a2a4 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -14,7 +14,8 @@ import { createConnection, TextDocuments, ProposedFeatures, - TextDocumentSyncKind + TextDocumentSyncKind, + CompletionItemKind } from 'vscode-languageserver/node' import { BitBakeDocScanner } from './BitBakeDocScanner' import { BitBakeProjectScanner } from './BitBakeProjectScanner' @@ -25,6 +26,9 @@ import Analyzer from './tree-sitter/analyzer' import { generateParser } from './tree-sitter/parser' import { symbolKindToCompletionKind } from './utils/lsp' import logger from 'winston' +import { RESERVED_KEYWORDS } from './completions/reserved-keywords' +import { RESERVED_VARIABLES } from './completions/reserved-variables' +import { SNIPPETS } from './completions/snippets' // Create a connection for the server. The connection uses Node's IPC as a transport const connection: Connection = createConnection(ProposedFeatures.all) @@ -113,11 +117,9 @@ connection.onCompletion((textDocumentPositionParams: TextDocumentPositionParams) let symbolCompletions: CompletionItem[] = [] if (word !== null) { - const symbols = analyzer.getGlobalDeclarationSymbols(textDocumentPositionParams.textDocument.uri) + const globalDeclarationSymbols = analyzer.getGlobalDeclarationSymbols(textDocumentPositionParams.textDocument.uri) - // Covert symbols to completion items - // TODO: remove duplicate symbols - symbolCompletions = symbols.map((symbol: SymbolInformation) => ( + symbolCompletions = globalDeclarationSymbols.map((symbol: SymbolInformation) => ( { label: symbol.name, kind: symbolKindToCompletionKind(symbol.kind), @@ -126,8 +128,24 @@ connection.onCompletion((textDocumentPositionParams: TextDocumentPositionParams) )) } + const reserverdKeywordCompletionItems: CompletionItem[] = RESERVED_KEYWORDS.map(keyword => { + return { + label: keyword, + kind: CompletionItemKind.Keyword + } + }) + + const reserverdVariableCompletionItems: CompletionItem[] = RESERVED_VARIABLES.map(keyword => { + return { + label: keyword, + kind: CompletionItemKind.Variable + } + }) + const allCompletions = [ - ...contextHandler.getComletionItems(textDocumentPositionParams, documentAsText), + ...reserverdKeywordCompletionItems, + ...reserverdVariableCompletionItems, + ...SNIPPETS, ...symbolCompletions ] @@ -135,10 +153,8 @@ connection.onCompletion((textDocumentPositionParams: TextDocumentPositionParams) }) connection.onCompletionResolve((item: CompletionItem): CompletionItem => { - logger.debug(`onCompletionResolve ${JSON.stringify(item)}`) - - item.insertText = contextHandler.getInsertStringForTheElement(item) - + logger.debug(`onCompletionResolve: ${JSON.stringify(item)}`) + // TODO: get docs for reserved variables here return item }) From d3ed3799fcef154f31ab76cb29dca60a58ee9a73 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Fri, 13 Oct 2023 15:36:29 -0400 Subject: [PATCH 02/16] Chore: avoid adding reserved variable as global declaration; predicate condition correction --- server/src/tree-sitter/declarations.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/tree-sitter/declarations.ts b/server/src/tree-sitter/declarations.ts index 939e5fbb..d2ac661b 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 { RESERVED_VARIABLES } from '../completions/reserved-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 reserved variable as it is added in RESERVED_VARIABLES + if (symbol !== null && !(new Set(RESERVED_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] From a50ff43c5ba73744e6cad57c1d437f0d5ce65b64 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Mon, 16 Oct 2023 11:57:31 -0400 Subject: [PATCH 03/16] Chore: Temporarily disable the prompting of error nodes as it is not yet reliable --- server/src/tree-sitter/analyzer.ts | 48 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index 8da2a5c7..537d9fd8 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -6,13 +6,13 @@ import { type TextDocumentPositionParams, type Diagnostic, - type SymbolInformation, - DiagnosticSeverity + type SymbolInformation + // DiagnosticSeverity } 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 { getAllErrorNodes } from './errors' import { debounce } from '../utils/async' import { type Tree } from 'web-tree-sitter' @@ -66,26 +66,28 @@ 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) - }) + // Temporarily disable checking the error from tree-sitter as it is not yet reliable + + // 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) + // }) return diagnostics } From aeefe12dd945b5591a54c09668d4b549aa53a966 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Mon, 16 Oct 2023 16:30:13 -0400 Subject: [PATCH 04/16] Chore: Move handler for onCompletion to its own file 1. Export an instance of analyzer from the source file and use this instance through out the program 2. Move the handler function for onCompletion event to its own file for the convenience of testing --- server/src/connectionHandlers/onCompletion.ts | 62 +++++++++++++++++ server/src/server.ts | 68 ++----------------- server/src/tree-sitter/analyzer.ts | 2 + 3 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 server/src/connectionHandlers/onCompletion.ts diff --git a/server/src/connectionHandlers/onCompletion.ts b/server/src/connectionHandlers/onCompletion.ts new file mode 100644 index 00000000..a7233314 --- /dev/null +++ b/server/src/connectionHandlers/onCompletion.ts @@ -0,0 +1,62 @@ +import logger from 'winston' +import { type TextDocumentPositionParams, type CompletionItem, type SymbolInformation, CompletionItemKind } from 'vscode-languageserver/node' +import { symbolKindToCompletionKind } from '../utils/lsp' +import { RESERVED_VARIABLES } from '../completions/reserved-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 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 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[] = RESERVED_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 7381a2a4..2f448741 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -10,25 +10,20 @@ import { type CompletionItem, type Definition, type Hover, - type SymbolInformation, createConnection, TextDocuments, ProposedFeatures, - TextDocumentSyncKind, - CompletionItemKind + TextDocumentSyncKind } from 'vscode-languageserver/node' import { BitBakeDocScanner } from './BitBakeDocScanner' 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 { RESERVED_KEYWORDS } from './completions/reserved-keywords' -import { RESERVED_VARIABLES } from './completions/reserved-variables' -import { SNIPPETS } from './completions/snippets' +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) @@ -37,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 ?? '' @@ -97,64 +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 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[] = RESERVED_VARIABLES.map(keyword => { - return { - label: keyword, - kind: CompletionItemKind.Variable - } - }) - - const allCompletions = [ - ...reserverdKeywordCompletionItems, - ...reserverdVariableCompletionItems, - ...SNIPPETS, - ...symbolCompletions - ] - - return allCompletions -}) +connection.onCompletion(onCompletionHandler.bind(this)) connection.onCompletionResolve((item: CompletionItem): CompletionItem => { logger.debug(`onCompletionResolve: ${JSON.stringify(item)}`) - // TODO: get docs for reserved variables here return item }) diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index 537d9fd8..d712c7e8 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -148,3 +148,5 @@ export default class Analyzer { return tree.rootNode.descendantForPosition({ row: line, column }) } } + +export const analyzer: Analyzer = new Analyzer() From 8581a792bc4559b87ce223169c80d1669e337c74 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Mon, 16 Oct 2023 16:36:47 -0400 Subject: [PATCH 05/16] Chore: Initial tests --- server/src/__tests__/analyzer.test.ts | 106 ++++++++++++++++++ server/src/__tests__/completions.test.ts | 58 ++++++++++ server/src/__tests__/fixtures/correct.bb | 5 + server/src/__tests__/fixtures/declarations.bb | 2 + server/src/__tests__/fixtures/fixtures.ts | 33 ++++++ 5 files changed, 204 insertions(+) create mode 100644 server/src/__tests__/analyzer.test.ts create mode 100644 server/src/__tests__/completions.test.ts create mode 100644 server/src/__tests__/fixtures/correct.bb create mode 100644 server/src/__tests__/fixtures/declarations.bb create mode 100644 server/src/__tests__/fixtures/fixtures.ts 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..e9637c23 --- /dev/null +++ b/server/src/__tests__/completions.test.ts @@ -0,0 +1,58 @@ +import { onCompletionHandler } from '../connectionHandlers/onCompletion' + +const DUMMY_URI = 'dummy_uri' + +describe('On Completion', () => { + it('expects reserved variables, keywords and snippets in completion item lists', async () => { + 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( + expect.arrayContaining([ + { + documentation: { + /* eslint-disable no-template-curly-in-string */ + value: '```man\nDeploy something (bitbake-language-server)\n\n\n```\n```bitbake\ndef do_deploy():\n\t# Your code here\n\t${1:pass}\n```', + kind: 'markdown' + }, + insertText: [ + 'def do_deploy():', + '\t# Your code here', + /* eslint-disable no-template-curly-in-string */ + '\t${1:pass}' + ].join('\n'), + insertTextFormat: 2, + label: 'do_deploy', + kind: 15 + } + ]) + ) + }) +}) 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..c9241ec3 --- /dev/null +++ b/server/src/__tests__/fixtures/fixtures.ts @@ -0,0 +1,33 @@ +/** + * 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')}` +} + +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 +}, {}) From 4e9bb843304c7d8b6752d9efe64f6168f5e989a5 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Tue, 17 Oct 2023 11:16:19 -0400 Subject: [PATCH 06/16] Chore: Separate keywords as per their language --- server/src/completions/reserved-keywords.ts | 57 +++++++++++++++++---- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/server/src/completions/reserved-keywords.ts b/server/src/completions/reserved-keywords.ts index 858c32e4..6b8b9591 100644 --- a/server/src/completions/reserved-keywords.ts +++ b/server/src/completions/reserved-keywords.ts @@ -1,8 +1,7 @@ -export const RESERVED_KEYWORDS = [ +const BITBAKE_KEYWORDS = [ 'python', 'def', 'include', - 'from', 'import', 'require', 'inherit', @@ -11,17 +10,19 @@ export const RESERVED_KEYWORDS = [ 'after', 'before', 'export', - 'echo', + 'fakeroot', + 'EXPORT_FUNCTIONS', + 'INHERIT' +] + +const PYTHON_VARIABLES = [ + 'def', + 'from', + 'import', 'if', - 'fi', 'else', 'return', - 'unset', - 'print', 'or', - 'fakeroot', - 'EXPORT_FUNCTIONS', - 'INHERIT', 'elif', 'for', 'while', @@ -40,3 +41,41 @@ export const RESERVED_KEYWORDS = [ '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_VARIABLES, + ...SHELL_KEYWORDS +])] From e2189d530b6bf89754164e34cfffc218712c39c6 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Tue, 17 Oct 2023 11:44:23 -0400 Subject: [PATCH 07/16] Chore: Clean code --- server/src/connectionHandlers/onCompletion.ts | 4 --- server/src/tree-sitter/analyzer.ts | 25 +------------------ 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/server/src/connectionHandlers/onCompletion.ts b/server/src/connectionHandlers/onCompletion.ts index a7233314..a2ec016a 100644 --- a/server/src/connectionHandlers/onCompletion.ts +++ b/server/src/connectionHandlers/onCompletion.ts @@ -8,10 +8,6 @@ import { SNIPPETS } from '../completions/snippets' export function onCompletionHandler (textDocumentPositionParams: TextDocumentPositionParams): CompletionItem[] { logger.debug('onCompletion') - // const documentAsText = documentAsTextMap.get(textDocumentPositionParams.textDocument.uri) - // if (documentAsText === undefined) { - // return [] - // } const word = analyzer.wordAtPointFromTextPosition({ ...textDocumentPositionParams, diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index d712c7e8..e3b77c30 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -7,12 +7,10 @@ import { type TextDocumentPositionParams, type Diagnostic, type SymbolInformation - // DiagnosticSeverity } 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,28 +64,7 @@ export default class Analyzer { private executeAnalyzation (document: TextDocument, uri: string, tree: Tree): Diagnostic[] { const diagnostics: Diagnostic[] = [] - // Temporarily disable checking the error from tree-sitter as it is not yet reliable - - // 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 } From 73cfd1f0f0f9910cf64292a43f3645e318880c5e Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Tue, 17 Oct 2023 12:18:07 -0400 Subject: [PATCH 08/16] Chore: Remove dummy documentation content --- server/src/completions/snippets.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/server/src/completions/snippets.ts b/server/src/completions/snippets.ts index 580fac83..0e62f8e7 100644 --- a/server/src/completions/snippets.ts +++ b/server/src/completions/snippets.ts @@ -7,7 +7,6 @@ import { InsertTextFormat, type CompletionItem, CompletionItemKind, MarkupKind } export const SNIPPETS: CompletionItem[] = [ { - documentation: 'Fetch something', label: 'do_fetch', insertText: [ 'def do_fetch():', @@ -17,7 +16,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Unpack something', label: 'do_unpack', insertText: [ 'def do_unpack():', @@ -27,7 +25,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Patch something', label: 'do_patch', insertText: [ 'def do_patch():', @@ -37,7 +34,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Configure something', label: 'do_configure', insertText: [ 'def do_configure():', @@ -47,7 +43,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Compile something', label: 'do_compile', insertText: [ 'def do_compile():', @@ -57,7 +52,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Install something', label: 'do_install', insertText: [ 'def do_install():', @@ -67,7 +61,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Package something', label: 'do_package', insertText: [ 'def do_package():', @@ -77,7 +70,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Rootfs', label: 'do_rootfs', insertText: [ 'def do_rootfs():', @@ -87,7 +79,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Populate sysroot', label: 'do_populate_sysroot', insertText: [ 'def do_populate_sysroot():', @@ -97,7 +88,6 @@ export const SNIPPETS: CompletionItem[] = [ ].join('\n') }, { - documentation: 'Deploy something', label: 'do_deploy', insertText: [ 'def do_deploy():', @@ -113,7 +103,7 @@ export const SNIPPETS: CompletionItem[] = [ documentation: { value: [ markdownBlock( - `${item.documentation ?? item.label} (bitbake-language-server)\n\n`, + `${item.label} (bitbake-language-server)\n\n`, 'man' ), markdownBlock(item.insertText, 'bitbake') From 640dd78fc89dac9cfdc47084d6c6c7f040c27a69 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Wed, 18 Oct 2023 10:58:27 -0400 Subject: [PATCH 09/16] Chore: Rename --- .../{reserved-variables.ts => bitbake-variables.ts} | 2 +- server/src/connectionHandlers/onCompletion.ts | 4 ++-- server/src/tree-sitter/declarations.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename server/src/completions/{reserved-variables.ts => bitbake-variables.ts} (98%) diff --git a/server/src/completions/reserved-variables.ts b/server/src/completions/bitbake-variables.ts similarity index 98% rename from server/src/completions/reserved-variables.ts rename to server/src/completions/bitbake-variables.ts index 253d691d..366220ae 100644 --- a/server/src/completions/reserved-variables.ts +++ b/server/src/completions/bitbake-variables.ts @@ -1,4 +1,4 @@ -export const RESERVED_VARIABLES = [ +export const BITBAKE_VARIABLES = [ 'ASSUME_PROVIDED', 'AZ_SAS', 'B', diff --git a/server/src/connectionHandlers/onCompletion.ts b/server/src/connectionHandlers/onCompletion.ts index a2ec016a..8bfc82de 100644 --- a/server/src/connectionHandlers/onCompletion.ts +++ b/server/src/connectionHandlers/onCompletion.ts @@ -1,7 +1,7 @@ import logger from 'winston' import { type TextDocumentPositionParams, type CompletionItem, type SymbolInformation, CompletionItemKind } from 'vscode-languageserver/node' import { symbolKindToCompletionKind } from '../utils/lsp' -import { RESERVED_VARIABLES } from '../completions/reserved-variables' +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' @@ -40,7 +40,7 @@ export function onCompletionHandler (textDocumentPositionParams: TextDocumentPos } }) - const reserverdVariableCompletionItems: CompletionItem[] = RESERVED_VARIABLES.map(keyword => { + const reserverdVariableCompletionItems: CompletionItem[] = BITBAKE_VARIABLES.map(keyword => { return { label: keyword, kind: CompletionItemKind.Variable diff --git a/server/src/tree-sitter/declarations.ts b/server/src/tree-sitter/declarations.ts index d2ac661b..d85b09bb 100644 --- a/server/src/tree-sitter/declarations.ts +++ b/server/src/tree-sitter/declarations.ts @@ -7,7 +7,7 @@ import * as LSP from 'vscode-languageserver/node' import type * as Parser from 'web-tree-sitter' import * as TreeSitterUtil from './utils' -import { RESERVED_VARIABLES } from '../completions/reserved-variables' +import { BITBAKE_VARIABLES } from '../completions/bitbake-variables' const TREE_SITTER_TYPE_TO_LSP_KIND: Record = { environment_variable_assignment: LSP.SymbolKind.Variable, @@ -50,8 +50,8 @@ export function getGlobalDeclarations ({ const followChildren = !GLOBAL_DECLARATION_NODE_TYPES.has(node.type) const symbol = getDeclarationSymbolFromNode({ node, uri }) - // skip if it is a reserved variable as it is added in RESERVED_VARIABLES - if (symbol !== null && !(new Set(RESERVED_VARIABLES).has(symbol.name))) { + // 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 } From 4ac9b586d721ef921886d5d34cf3893cf67b2e2f Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Wed, 18 Oct 2023 15:18:34 -0400 Subject: [PATCH 10/16] Chore: Add more tasks and documentations from Yocto --- server/src/completions/snippets.ts | 311 ++++++++++++++++++++++------- 1 file changed, 240 insertions(+), 71 deletions(-) diff --git a/server/src/completions/snippets.ts b/server/src/completions/snippets.ts index 0e62f8e7..71110546 100644 --- a/server/src/completions/snippets.ts +++ b/server/src/completions/snippets.ts @@ -5,111 +5,280 @@ import { InsertTextFormat, type CompletionItem, CompletionItemKind, MarkupKind } from 'vscode-languageserver' +/* eslint-disable no-template-curly-in-string */ export const SNIPPETS: CompletionItem[] = [ { - label: 'do_fetch', - insertText: [ - 'def do_fetch():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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_unpack', - insertText: [ - 'def do_unpack():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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_patch', - insertText: [ - 'def do_patch():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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_compile', - insertText: [ - 'def do_compile():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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_rootfs', - insertText: [ - 'def do_rootfs():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') + 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_deploy', - insertText: [ - 'def do_deploy():', - '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ - '\t${1:pass}' - ].join('\n') - } -].map((item) => { + 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' + `${item.label} (bitbake-language-server)\n\n`, + 'man' ), - markdownBlock(item.insertText, 'bitbake') + 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 } }) From fbe314e9a979d77cd4ff2f376827929b0155836c Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Wed, 18 Oct 2023 15:19:04 -0400 Subject: [PATCH 11/16] Chore: Update tests --- server/src/__tests__/completions.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/src/__tests__/completions.test.ts b/server/src/__tests__/completions.test.ts index e9637c23..81eeafec 100644 --- a/server/src/__tests__/completions.test.ts +++ b/server/src/__tests__/completions.test.ts @@ -35,21 +35,20 @@ describe('On Completion', () => { ) expect(result).toEqual( + /* eslint-disable no-template-curly-in-string */ expect.arrayContaining([ { documentation: { - /* eslint-disable no-template-curly-in-string */ - value: '```man\nDeploy something (bitbake-language-server)\n\n\n```\n```bitbake\ndef do_deploy():\n\t# Your code here\n\t${1:pass}\n```', + 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_deploy():', + 'def do_bootimg():', '\t# Your code here', - /* eslint-disable no-template-curly-in-string */ '\t${1:pass}' ].join('\n'), insertTextFormat: 2, - label: 'do_deploy', + label: 'do_bootimg', kind: 15 } ]) From b63a3e6658fc91a087409dcd058dca5a13c6952f Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Wed, 18 Oct 2023 17:29:43 -0400 Subject: [PATCH 12/16] Chore: Enable completions when in variable expansion --- client/src/language/languageClient.ts | 15 +++++++++++++-- server/src/connectionHandlers/onCompletion.ts | 19 ++++++++++++++----- server/src/tree-sitter/analyzer.ts | 12 ++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) 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/connectionHandlers/onCompletion.ts b/server/src/connectionHandlers/onCompletion.ts index 8bfc82de..5dc90b42 100644 --- a/server/src/connectionHandlers/onCompletion.ts +++ b/server/src/connectionHandlers/onCompletion.ts @@ -9,17 +9,26 @@ 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: { - 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) - } + position: wordPosition }) logger.debug(`onCompletion - current word: ${word}`) + const shouldComplete = analyzer.shouldProvideCompletionItems(textDocumentPositionParams.textDocument.uri, wordPosition.line, wordPosition.character) + logger.debug(`isString: ${shouldComplete}`) + // 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) diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index e3b77c30..b9eb625f 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -103,6 +103,18 @@ export default class Analyzer { ) } + public shouldProvideCompletionItems ( + uri: string, + line: number, + column: number + ): boolean { + const n = this.nodeAtPoint(uri, line, column) + if (n !== null && (n.type === 'string_content' || n.type === 'ERROR')) { + return false + } + return true + } + /** * Find the node at the given point. */ From 48c3bff0cb692110e5a245a5f85826e69e543a58 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Thu, 19 Oct 2023 10:46:52 -0400 Subject: [PATCH 13/16] Refactor: various refactors and code cleaning --- server/src/completions/reserved-keywords.ts | 4 ++-- server/src/connectionHandlers/onCompletion.ts | 1 - server/src/server.ts | 2 +- server/src/tree-sitter/analyzer.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/server/src/completions/reserved-keywords.ts b/server/src/completions/reserved-keywords.ts index 6b8b9591..15ac8245 100644 --- a/server/src/completions/reserved-keywords.ts +++ b/server/src/completions/reserved-keywords.ts @@ -15,7 +15,7 @@ const BITBAKE_KEYWORDS = [ 'INHERIT' ] -const PYTHON_VARIABLES = [ +const PYTHON_KEYWORDS = [ 'def', 'from', 'import', @@ -76,6 +76,6 @@ const SHELL_KEYWORDS = [ export const RESERVED_KEYWORDS = [...new Set([ ...BITBAKE_KEYWORDS, - ...PYTHON_VARIABLES, + ...PYTHON_KEYWORDS, ...SHELL_KEYWORDS ])] diff --git a/server/src/connectionHandlers/onCompletion.ts b/server/src/connectionHandlers/onCompletion.ts index 5dc90b42..bdc8a8ad 100644 --- a/server/src/connectionHandlers/onCompletion.ts +++ b/server/src/connectionHandlers/onCompletion.ts @@ -23,7 +23,6 @@ export function onCompletionHandler (textDocumentPositionParams: TextDocumentPos logger.debug(`onCompletion - current word: ${word}`) const shouldComplete = analyzer.shouldProvideCompletionItems(textDocumentPositionParams.textDocument.uri, wordPosition.line, wordPosition.character) - logger.debug(`isString: ${shouldComplete}`) // Do not provide completions if it is inside a string but not inside a variable expansion if (!shouldComplete) { return [] diff --git a/server/src/server.ts b/server/src/server.ts index 2f448741..7bbab749 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -91,7 +91,7 @@ connection.onDidChangeWatchedFiles((change) => { bitBakeProjectScanner.rescanProject() }) -connection.onCompletion(onCompletionHandler.bind(this)) +connection.onCompletion(onCompletionHandler) connection.onCompletionResolve((item: CompletionItem): CompletionItem => { logger.debug(`onCompletionResolve: ${JSON.stringify(item)}`) diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index b9eb625f..316239cd 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -109,7 +109,7 @@ export default class Analyzer { column: number ): boolean { const n = this.nodeAtPoint(uri, line, column) - if (n !== null && (n.type === 'string_content' || n.type === 'ERROR')) { + if (n?.type === 'string_content' || n?.type === 'ERROR') { return false } return true From 8f7e69a3910d79c28f0c459b4a1f716ce7060b30 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Thu, 19 Oct 2023 12:47:17 -0400 Subject: [PATCH 14/16] Chore: Add public functions to check parse's existence and reset analyzed documents --- server/src/tree-sitter/analyzer.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index 316239cd..3a6950be 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -115,6 +115,14 @@ export default class Analyzer { return true } + public hasParser (): boolean { + return this.parser !== undefined + } + + public resetAnalyzedDocuments (): void { + this.uriToAnalyzedDocument = {} + } + /** * Find the node at the given point. */ From 96e8151624124e0da55942da43f8f979f9c5b0a3 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Thu, 19 Oct 2023 12:47:58 -0400 Subject: [PATCH 15/16] Chore: Add fixture for completion tests --- server/src/__tests__/fixtures/completion.bb | 2 ++ server/src/__tests__/fixtures/fixtures.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 server/src/__tests__/fixtures/completion.bb 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/fixtures.ts b/server/src/__tests__/fixtures/fixtures.ts index c9241ec3..2bd49a14 100644 --- a/server/src/__tests__/fixtures/fixtures.ts +++ b/server/src/__tests__/fixtures/fixtures.ts @@ -22,7 +22,9 @@ function getDocument (uri: string): TextDocument { export const FIXTURE_URI = { CORRECT: `file://${path.join(FIXTURE_FOLDER, 'correct.bb')}`, - DECLARATION: `file://${path.join(FIXTURE_FOLDER, 'declarations.bb')}` + DECLARATION: `file://${path.join(FIXTURE_FOLDER, 'declarations.bb')}`, + COMPLETION: `file://${path.join(FIXTURE_FOLDER, 'completion.bb')}` + } export const FIXTURE_DOCUMENT: Record = ( From 359c0c0399a90a084c0491b75a2bfc50abb6ebf5 Mon Sep 17 00:00:00 2001 From: Ziwei Wang Date: Thu, 19 Oct 2023 12:49:01 -0400 Subject: [PATCH 16/16] Chore: Add tests for suggestions in string quotations --- server/src/__tests__/completions.test.ts | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/server/src/__tests__/completions.test.ts b/server/src/__tests__/completions.test.ts index 81eeafec..ee28cdcc 100644 --- a/server/src/__tests__/completions.test.ts +++ b/server/src/__tests__/completions.test.ts @@ -1,9 +1,30 @@ 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 @@ -54,4 +75,42 @@ describe('On Completion', () => { ]) ) }) + + 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([]) + }) })