diff --git a/client/README.md b/client/README.md index 78e23ce5..2af89b68 100644 --- a/client/README.md +++ b/client/README.md @@ -73,6 +73,10 @@ user settings. } ``` +Note that the extension deactivates the "files.trimTrailingWhitespace" setting for +Python and Shell script, as it interferes with the functioning of features related +to embedded languages. + ## Features ### Syntax highlighting @@ -191,6 +195,9 @@ You can also set up the SDK for the recipe by running the `Bitbake: Devtool: Con If your recipe's class is not supported, or you have an older version of poky, the `Bitbake: Devtool: Configure devtool fallback` command will add tasks to build and deploy the package through `devtool build/deploy-target`. Linting, debugging, testing and other advanced features will not be available in this mode. +## Troubleshooting +See the [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) file. + ## Contributing ### Reporting issues diff --git a/client/TROUBLESHOOTING.md b/client/TROUBLESHOOTING.md new file mode 100644 index 00000000..93dcffbc --- /dev/null +++ b/client/TROUBLESHOOTING.md @@ -0,0 +1,22 @@ +# Troubleshooting + +## Known Issues + +### Problems from Unknown Files Appear in the Problems Tab +Errors and warnings appear twice in the "PROBLEMS" tab. They first appear for the BitBake files to which they belong, and then again for Bash or Python files with UUID names (ex. 9ad23ed5-9278-41e0-98cd-349750c1e2c0.py). As the first one is expected and desired, the second is not. This issue arises from the way we handle diagnostics (errors and warnings). See [Trade-offs on Diagnostics](TROUBLESHOOTING.md#trade-offs-on-diagnostics). + +Unfortunately, the VS Code API does not offer a way to programmatically filter the "PROBLEMS" tab. However, manual filtering is still possible. Typing `!workspaceStorage` or `!workspaceStorage/**/yocto-project.yocto-bitbake/embedded-documents` (if more precision is needed) should filter out all these unwanted problems. + +### Tabs from Unknown Files Open and Close Quickly +While typing, tabs with UUID names (ex. aa2a3ba2-769c-4900-8f5f-31ea16bfbc8f.sh) might occasionally open in the tabs bar and then close shortly thereafter. This occurs as a result of our method for handling diagnostics (errors and warnings). See [Trade-offs on Diagnostics](TROUBLESHOOTING.md#trade-offs-on-diagnostics). + +We haven't found a way to prevent these tabs from opening, but we try to close them as quickly as possible. + +## Trade-offs + +### Trade-offs on Diagnostics +Some functionalities for [embedded Bash and Python code](https://code.visualstudio.com/api/language-extensions/embedded-languages) are provided by other VS Code extensions, such as ms-python.python or timonwong.shellcheck. To achieve this, we extract the Bash and Python content from the BitBake files and create temporary Bash or Python files that can be analyzed by Bash or Python extensions. We adapt the results of these extensions, and present them to the user. It works well for Completion, Definition and Hover. Unfortunately VS Code [does not yet offer such functionality for diagnostics](https://github.com/yoctoproject/vscode-bitbake/pull/18). We still achieve it by some homemade hacky technique that consists of opening the temporary documents in the background, in a way to trigger the generation of diagnostics. It brings a couple of issues, but we hope it is still worth having the diagnostics. + +The related issues: +- [Problems from Unknown Files Appear in the Problems Tab](TROUBLESHOOTING.md#problems-from-unknown-files-appear-in-the-problems-tab) +- [Tabs from Unknown Files Open and Close Quickly](TROUBLESHOOTING.md#tabs-from-unknown-files-open-and-close-quickly) \ No newline at end of file diff --git a/client/src/extension.ts b/client/src/extension.ts index 3c46cfb3..a5ea2f25 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -59,6 +59,16 @@ function updatePythonPath (): void { } } +async function disableInteferingSettings (): Promise { + const config = vscode.workspace.getConfiguration() + for (const languageKey of ['[python]', '[shellscript]']) { + const languageConfig = config.get>(languageKey) ?? {} + // 'files.trimTrailingWhitespace' modifies the embedded languages documents and breaks the mapping of the positions + languageConfig['files.trimTrailingWhitespace'] = false + await config.update(languageKey, languageConfig, vscode.ConfigurationTarget.Workspace) + } +} + export async function activate (context: vscode.ExtensionContext): Promise { logger.outputChannel = vscode.window.createOutputChannel('BitBake') @@ -69,6 +79,7 @@ export async function activate (context: vscode.ExtensionContext): Promise bitbakeDriver.loadSettings(vscode.workspace.getConfiguration('bitbake'), vscode.workspace.workspaceFolders?.[0].uri.fsPath) const bitBakeProjectScanner: BitBakeProjectScanner = new BitBakeProjectScanner(bitbakeDriver) updatePythonPath() + await disableInteferingSettings() bitbakeWorkspace.loadBitbakeWorkspace(context.workspaceState) bitbakeTaskProvider = new BitbakeTaskProvider(bitbakeDriver) client = await activateLanguageServer(context) diff --git a/server/src/embedded-languages/documents-manager.ts b/client/src/language/EmbeddedLanguageDocsManager.ts similarity index 54% rename from server/src/embedded-languages/documents-manager.ts rename to client/src/language/EmbeddedLanguageDocsManager.ts index 71620f86..da6a66f9 100644 --- a/server/src/embedded-languages/documents-manager.ts +++ b/client/src/language/EmbeddedLanguageDocsManager.ts @@ -7,10 +7,9 @@ import { randomUUID } from 'crypto' import path from 'path' import fs from 'fs' +import { type EmbeddedLanguageDoc, type EmbeddedLanguageType } from '../lib/src/types/embedded-languages' import { logger } from '../lib/src/utils/OutputLogger' - -import { type EmbeddedLanguageDocInfos, type EmbeddedLanguageType } from '../lib/src/types/embedded-languages' -import { type EmbeddedLanguageDoc } from './utils' +import { Range, Uri, WorkspaceEdit, workspace } from 'vscode' const EMBEDDED_DOCUMENTS_FOLDER = 'embedded-documents' @@ -19,16 +18,30 @@ const fileExtensionsMap = { python: '.py' } +export interface EmbeddedLanguageDocInfos { + uri: Uri + language: EmbeddedLanguageType + characterIndexes: number[] +} + type EmbeddedLanguageDocsRecord = Partial> export default class EmbeddedLanguageDocsManager { private readonly embeddedLanguageDocsInfos = new Map() // map of original uri to embedded documents infos private _storagePath: string | undefined + private readonly filesWaitingToUpdate = new Map() get storagePath (): string | undefined { return this._storagePath } + get embeddedLanguageDocsFolder (): string | undefined { + if (this._storagePath === undefined) { + return + } + return path.join(this._storagePath, EMBEDDED_DOCUMENTS_FOLDER) + } + async setStoragePath (newStoragePath: string | undefined): Promise { logger.debug(`Set embedded language documents storage path. New: ${newStoragePath}. Old: ${this._storagePath}`) if (this._storagePath === newStoragePath) { @@ -45,7 +58,7 @@ export default class EmbeddedLanguageDocsManager { const newPathToEmbeddedLanguageDocsFolder = path.join(newStoragePath, EMBEDDED_DOCUMENTS_FOLDER) fs.mkdir(newPathToEmbeddedLanguageDocsFolder, { recursive: true }, (err) => { if (err !== null) { - logger.error(`Failed to create embedded language documents folder: ${JSON.stringify(err)}`) + logger.error(`Failed to create embedded language documents folder: ${err as any}`) } resolve() }) @@ -58,7 +71,7 @@ export default class EmbeddedLanguageDocsManager { const oldPathToEmbeddedLanguageDocsFolder = path.join(this._storagePath, EMBEDDED_DOCUMENTS_FOLDER) fs.rmdir(oldPathToEmbeddedLanguageDocsFolder, { recursive: true }, (err) => { if (err !== null) { - logger.error(`Failed to remove embedded language documents folder: ${JSON.stringify(err)}`) + logger.error(`Failed to remove embedded language documents folder: ${err as any}`) } resolve() }) @@ -67,10 +80,14 @@ export default class EmbeddedLanguageDocsManager { this._storagePath = newStoragePath } - private registerEmbeddedLanguageDocInfos (originalUriString: string, embeddedLanguageDocInfos: EmbeddedLanguageDocInfos): void { - const embeddedLanguageDocs = this.embeddedLanguageDocsInfos.get(originalUriString) ?? {} - embeddedLanguageDocs[embeddedLanguageDocInfos.language] = embeddedLanguageDocInfos - this.embeddedLanguageDocsInfos.set(originalUriString, embeddedLanguageDocs) + private registerEmbeddedLanguageDocInfos (embeddedLanguageDoc: EmbeddedLanguageDoc, uri: Uri): void { + const embeddedLanguageDocInfos: EmbeddedLanguageDocInfos = { + ...embeddedLanguageDoc, + uri + } + const embeddedLanguageDocs = this.embeddedLanguageDocsInfos.get(embeddedLanguageDoc.originalUri) ?? {} + embeddedLanguageDocs[embeddedLanguageDoc.language] = embeddedLanguageDocInfos + this.embeddedLanguageDocsInfos.set(embeddedLanguageDoc.originalUri, embeddedLanguageDocs) } getEmbeddedLanguageDocInfos ( @@ -81,57 +98,94 @@ export default class EmbeddedLanguageDocsManager { return embeddedLanguageDocs?.[languageType] } - private getPathToEmbeddedLanguageDoc (embeddedLanguageDoc: EmbeddedLanguageDoc): string | undefined { - if (this.storagePath === undefined) { + getOriginalUri (embeddedLanguageDocUri: Uri): Uri | undefined { + let originalUri: Uri | undefined + this.embeddedLanguageDocsInfos.forEach((embeddedLanguageDocs, stringUri) => { + if ( + embeddedLanguageDocs.bash?.uri.toString() === embeddedLanguageDocUri.toString() || + embeddedLanguageDocs.python?.uri.toString() === embeddedLanguageDocUri.toString() + ) { + originalUri = Uri.parse(stringUri) + } + }) + return originalUri + } + + private createEmbeddedLanguageDocUri (embeddedLanguageDoc: EmbeddedLanguageDoc): Uri | undefined { + if (this.embeddedLanguageDocsFolder === undefined) { return undefined } - const embeddedLanguageDocInfos = this.getEmbeddedLanguageDocInfos( - embeddedLanguageDoc.originalUri, - embeddedLanguageDoc.language - ) - if (embeddedLanguageDocInfos !== undefined) { - return embeddedLanguageDocInfos.uri.replace('file://', '') - } const randomName = randomUUID() const fileExtension = fileExtensionsMap[embeddedLanguageDoc.language] const embeddedLanguageDocFilename = randomName + fileExtension - const pathToEmbeddedLanguageDocsFolder = path.join(this.storagePath, EMBEDDED_DOCUMENTS_FOLDER) - return `${pathToEmbeddedLanguageDocsFolder}/${embeddedLanguageDocFilename}` + const pathToEmbeddedLanguageDocsFolder = this.embeddedLanguageDocsFolder + return Uri.parse(`file://${pathToEmbeddedLanguageDocsFolder}/${embeddedLanguageDocFilename}`) + } + + async saveEmbeddedLanguageDocs ( + embeddedLanguageDocs: EmbeddedLanguageDoc[] + ): Promise { + await Promise.all(embeddedLanguageDocs.map(async (embeddedLanguageDoc) => { + await this.saveEmbeddedLanguageDoc(embeddedLanguageDoc) + })) + } + + private async updateEmbeddedLanguageDocFile (embeddedLanguageDoc: EmbeddedLanguageDoc, uri: Uri): Promise { + const document = await workspace.openTextDocument(uri) + if (document.isDirty) { + this.filesWaitingToUpdate.set(uri.toString(), embeddedLanguageDoc) + return + } + const fullRange = new Range( + document.positionAt(0), + document.positionAt(document.getText().length) + ) + const workspaceEdit = new WorkspaceEdit() + workspaceEdit.replace(uri, fullRange, embeddedLanguageDoc.content) + await workspace.applyEdit(workspaceEdit) + await document.save() + this.registerEmbeddedLanguageDocInfos(embeddedLanguageDoc, uri) + const fileWaitingToUpdate = this.filesWaitingToUpdate.get(uri.toString()) + if (fileWaitingToUpdate !== undefined) { + this.filesWaitingToUpdate.delete(uri.toString()) + await this.updateEmbeddedLanguageDocFile(fileWaitingToUpdate, uri) + } + } + + private async createEmbeddedLanguageDocFile (embeddedLanguageDoc: EmbeddedLanguageDoc): Promise { + const uri = this.createEmbeddedLanguageDocUri(embeddedLanguageDoc) + if (uri === undefined) { + return undefined + } + try { + await workspace.fs.writeFile(uri, Buffer.from(embeddedLanguageDoc.content)) + await workspace.openTextDocument(uri) + } catch (err) { + logger.error(`Failed to create embedded document: ${err as any}`) + } + this.registerEmbeddedLanguageDocInfos(embeddedLanguageDoc, uri) } async saveEmbeddedLanguageDoc ( embeddedLanguageDoc: EmbeddedLanguageDoc ): Promise { logger.debug(`Save embedded document (${embeddedLanguageDoc.language}) for ${embeddedLanguageDoc.originalUri}`) - const pathToEmbeddedLanguageDoc = this.getPathToEmbeddedLanguageDoc(embeddedLanguageDoc) - if (pathToEmbeddedLanguageDoc === undefined) { - return + const embeddedLanguageDocInfos = this.getEmbeddedLanguageDocInfos( + embeddedLanguageDoc.originalUri, + embeddedLanguageDoc.language + ) + if (embeddedLanguageDocInfos !== undefined) { + await this.updateEmbeddedLanguageDocFile(embeddedLanguageDoc, embeddedLanguageDocInfos.uri) + } else { + await this.createEmbeddedLanguageDocFile(embeddedLanguageDoc) } - await new Promise((resolve, reject) => { - fs.writeFile(pathToEmbeddedLanguageDoc, embeddedLanguageDoc.content, (err) => { - err !== null ? reject(err) : resolve() - }) - }).then(() => { - const embeddedLanguageDocInfos: EmbeddedLanguageDocInfos = { - ...embeddedLanguageDoc, - uri: `file://${pathToEmbeddedLanguageDoc}` - } - this.registerEmbeddedLanguageDocInfos(embeddedLanguageDoc.originalUri, embeddedLanguageDocInfos) - }).catch((err) => { - logger.error(`Failed to create embedded document: ${err}`) - }) } async deleteEmbeddedLanguageDocs (originalUriString: string): Promise { logger.debug(`Delete embedded documents for ${originalUriString}`) const embeddedLanguageDocs = this.embeddedLanguageDocsInfos.get(originalUriString) ?? {} await Promise.all(Object.values(embeddedLanguageDocs).map(async ({ uri }) => { - await new Promise((resolve, reject) => { - const pathToEmbeddedLanguageDoc = uri.replace('file://', '') - fs.unlink(pathToEmbeddedLanguageDoc, (err) => { - err !== null ? reject(err) : resolve() - }) - }) + await workspace.fs.delete(uri) })).then(() => { this.embeddedLanguageDocsInfos.delete(originalUriString) }).catch((err) => { diff --git a/client/src/language/RequestManager.ts b/client/src/language/RequestManager.ts index 1bd1fe25..1d97966d 100644 --- a/client/src/language/RequestManager.ts +++ b/client/src/language/RequestManager.ts @@ -11,12 +11,12 @@ import { RequestMethod, type RequestParams, type RequestResult } from '../lib/sr export class RequestManager { client: LanguageClient | undefined - getEmbeddedLanguageDocInfos = async ( + getEmbeddedLanguageTypeOnPosition = async ( uriString: string, position: Position - ): RequestResult['EmbeddedLanguageDocInfos'] => { - const params: RequestParams['EmbeddedLanguageDocInfos'] = { uriString, position } - return await this.client?.sendRequest(RequestMethod.EmbeddedLanguageDocInfos, params) + ): RequestResult['EmbeddedLanguageTypeOnPosition'] => { + const params: RequestParams['EmbeddedLanguageTypeOnPosition'] = { uriString, position } + return await this.client?.sendRequest(RequestMethod.EmbeddedLanguageTypeOnPosition, params) } } diff --git a/client/src/language/diagnosticsSupport.ts b/client/src/language/diagnosticsSupport.ts new file mode 100644 index 00000000..f487c3e6 --- /dev/null +++ b/client/src/language/diagnosticsSupport.ts @@ -0,0 +1,67 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2023 Savoir-faire Linux. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as vscode from 'vscode' + +import { getOriginalDocRange } from './utils' +import { embeddedLanguageDocsManager } from './EmbeddedLanguageDocsManager' +import { type EmbeddedLanguageType } from '../lib/src/types/embedded-languages' + +const diagnosticCollections = { + bash: vscode.languages.createDiagnosticCollection('bitbake-bash'), + python: vscode.languages.createDiagnosticCollection('bitbake-python') +} + +export const updateDiagnostics = async (uri: vscode.Uri): Promise => { + if (!uri.path.endsWith('.py') && !uri.path.endsWith('.sh')) { + return + } + const originalUri = embeddedLanguageDocsManager.getOriginalUri(uri) + if (originalUri === undefined) { + return + } + const originalTextDocument = await vscode.workspace.openTextDocument(originalUri) + await Promise.all([ + setEmbeddedLanguageDocDiagnostics(originalTextDocument, 'bash'), + setEmbeddedLanguageDocDiagnostics(originalTextDocument, 'python') + ]) +} + +const setEmbeddedLanguageDocDiagnostics = async ( + originalTextDocument: vscode.TextDocument, + embeddedLanguageType: EmbeddedLanguageType +): Promise => { + const embeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos( + originalTextDocument.uri.toString(), + embeddedLanguageType + ) + if (embeddedLanguageDocInfos?.uri === undefined) { + return + } + const embeddedLanguageDoc = await vscode.workspace.openTextDocument(embeddedLanguageDocInfos.uri.fsPath) + const dirtyDiagnostics = vscode.languages.getDiagnostics(embeddedLanguageDocInfos.uri) + const cleanDiagnostics: vscode.Diagnostic[] = [] + dirtyDiagnostics.forEach((diagnostic) => { + if (diagnostic.range === undefined) { + cleanDiagnostics.push(diagnostic) + } + const newRange = getOriginalDocRange( + originalTextDocument, + embeddedLanguageDoc, + embeddedLanguageDocInfos.characterIndexes, + diagnostic.range + ) + if (newRange === undefined) { + return + } + const newDiagnostic = { + ...diagnostic, + range: newRange + } + cleanDiagnostics.push(newDiagnostic) + }) + const diagnosticCollection = diagnosticCollections[embeddedLanguageType] + diagnosticCollection.set(originalTextDocument.uri, cleanDiagnostics) +} diff --git a/client/src/language/languageClient.ts b/client/src/language/languageClient.ts index 91890ef2..41d99322 100644 --- a/client/src/language/languageClient.ts +++ b/client/src/language/languageClient.ts @@ -9,7 +9,9 @@ import { workspace, type ExtensionContext, window, - commands + commands, + languages, + TabInputText } from 'vscode' import { @@ -18,20 +20,14 @@ import { TransportKind, type ServerOptions } from 'vscode-languageclient/node' -import { NotificationMethod, type NotificationParams } from '../lib/src/types/notifications' import { middlewareProvideCompletion } from './middlewareCompletion' import { middlewareProvideHover } from './middlewareHover' import { requestsManager } from './RequestManager' import { middlewareProvideDefinition } from './middlewareDefinition' - -const notifyFileRenameChanged = async ( - client: LanguageClient, - oldUriString: string, - newUriString: string -): Promise => { - const params: NotificationParams['FilenameChanged'] = { oldUriString, newUriString } - await client.sendNotification(NotificationMethod.FilenameChanged, params) -} +import { embeddedLanguageDocsManager } from './EmbeddedLanguageDocsManager' +import { logger } from '../lib/src/utils/OutputLogger' +import { NotificationMethod, type NotificationParams } from '../lib/src/types/notifications' +import { updateDiagnostics } from './diagnosticsSupport' export async function activateLanguageServer (context: ExtensionContext): Promise { const serverModule = context.asAbsolutePath(path.join('server', 'server.js')) @@ -48,7 +44,13 @@ export async function activateLanguageServer (context: ExtensionContext): Promis workspace.onDidRenameFiles((params) => { params.files.forEach((file) => { - void notifyFileRenameChanged(client, file.oldUri.toString(), file.newUri.toString()) + embeddedLanguageDocsManager.renameEmbeddedLanguageDocs(file.oldUri.toString(), file.newUri.toString()) + }) + }) + + workspace.onDidDeleteFiles((params) => { + params.files.forEach((file) => { + void embeddedLanguageDocsManager.deleteEmbeddedLanguageDocs(file.toString()) }) }) @@ -58,7 +60,6 @@ export async function activateLanguageServer (context: ExtensionContext): Promis // TODO: check new documentSelector documentSelector: [{ scheme: 'file', language: 'bitbake' }], initializationOptions: { - storagePath: context.storageUri?.fsPath, extensionPath: context.extensionPath }, middleware: { @@ -68,6 +69,18 @@ export async function activateLanguageServer (context: ExtensionContext): Promis } } + languages.onDidChangeDiagnostics(e => { + e.uris.forEach(uri => { + void updateDiagnostics(uri) + }) + }) + + if (context.storageUri?.fsPath === undefined) { + logger.error('Failed to get storage path') + } else { + void embeddedLanguageDocsManager.setStoragePath(context.storageUri.fsPath) + } + // Create the language client and start the client. const client: LanguageClient = new LanguageClient('bitbake', 'Bitbake Language Server', serverOptions, clientOptions) requestsManager.client = client @@ -98,6 +111,32 @@ export async function activateLanguageServer (context: ExtensionContext): Promis return await commands.executeCommand('bitbake.rescan-project') }) + client.onNotification(NotificationMethod.EmbeddedLanguageDocs, (embeddedLanguageDocs: NotificationParams['EmbeddedLanguageDocs']) => { + void embeddedLanguageDocsManager.saveEmbeddedLanguageDocs(embeddedLanguageDocs) + }) + + window.tabGroups.onDidChangeTabs((event) => { + [...event.opened, ...event.changed].forEach((tab) => { + if (tab.input instanceof TabInputText) { + const uri = tab.input.uri + if (embeddedLanguageDocsManager.embeddedLanguageDocsFolder === undefined) { + return + } + // Close embedded document tabs when they open automatically + if (uri.fsPath.includes(embeddedLanguageDocsManager.embeddedLanguageDocsFolder)) { + if ( + // Prevent prompt to appear on unsaved files + !tab.isDirty && + // Make possible to open embedded documents in a tab + !tab.isPreview && !tab.isActive && !tab.isPinned + ) { + void window.tabGroups.close(tab, false) + } + } + } + }) + }) + // Start the client and launch the server await client.start() diff --git a/client/src/language/middlewareCompletion.ts b/client/src/language/middlewareCompletion.ts index 9f79aaa0..3b839898 100644 --- a/client/src/language/middlewareCompletion.ts +++ b/client/src/language/middlewareCompletion.ts @@ -3,29 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { type CompletionList, Uri, commands, Range } from 'vscode' +import { type CompletionList, commands, Range, workspace } from 'vscode' import { type CompletionMiddleware } from 'vscode-languageclient/node' import { requestsManager } from './RequestManager' import { getEmbeddedLanguageDocPosition, getOriginalDocRange } from './utils' -import { getFileContent } from '../lib/src/utils/files' +import { embeddedLanguageDocsManager } from './EmbeddedLanguageDocsManager' export const middlewareProvideCompletion: CompletionMiddleware['provideCompletionItem'] = async (document, position, context, token, next) => { - const embeddedLanguageDocInfos = await requestsManager.getEmbeddedLanguageDocInfos(document.uri.toString(), position) - if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) { + const embeddedLanguageType = await requestsManager.getEmbeddedLanguageTypeOnPosition(document.uri.toString(), position) + if (embeddedLanguageType === undefined || embeddedLanguageType === null) { return await next(document, position, context, token) } - const embeddedLanguageDocContent = await getFileContent(Uri.parse(embeddedLanguageDocInfos.uri).fsPath) - if (embeddedLanguageDocContent === undefined) { + const embeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(document.uri.toString(), embeddedLanguageType) + if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) { return } + const embeddedLanguageTextDocument = await workspace.openTextDocument(embeddedLanguageDocInfos.uri) const adjustedPosition = getEmbeddedLanguageDocPosition( document, - embeddedLanguageDocContent, + embeddedLanguageTextDocument, embeddedLanguageDocInfos.characterIndexes, position ) - const vdocUri = Uri.parse(embeddedLanguageDocInfos.uri) + const vdocUri = embeddedLanguageTextDocument.uri const result = await commands.executeCommand( 'vscode.executeCompletionItemProvider', vdocUri, @@ -36,10 +37,10 @@ export const middlewareProvideCompletion: CompletionMiddleware['provideCompletio if (item.range === undefined) { // pass } else if (item.range instanceof Range) { - item.range = getOriginalDocRange(document, embeddedLanguageDocContent, embeddedLanguageDocInfos.characterIndexes, item.range) + item.range = getOriginalDocRange(document, embeddedLanguageTextDocument, embeddedLanguageDocInfos.characterIndexes, item.range) } else { - const inserting = getOriginalDocRange(document, embeddedLanguageDocContent, embeddedLanguageDocInfos.characterIndexes, item.range.inserting) - const replacing = getOriginalDocRange(document, embeddedLanguageDocContent, embeddedLanguageDocInfos.characterIndexes, item.range.replacing) + const inserting = getOriginalDocRange(document, embeddedLanguageTextDocument, embeddedLanguageDocInfos.characterIndexes, item.range.inserting) + const replacing = getOriginalDocRange(document, embeddedLanguageTextDocument, embeddedLanguageDocInfos.characterIndexes, item.range.replacing) if (inserting === undefined || replacing === undefined) { return } diff --git a/client/src/language/middlewareDefinition.ts b/client/src/language/middlewareDefinition.ts index 06d64d61..d90bd1c8 100644 --- a/client/src/language/middlewareDefinition.ts +++ b/client/src/language/middlewareDefinition.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { Location, Position, Range, Uri, commands, type LocationLink, type TextDocument } from 'vscode' +import { Location, Position, Range, commands, type LocationLink, type TextDocument, workspace } from 'vscode' import { type DefinitionMiddleware } from 'vscode-languageclient' -import { getFileContent } from '../lib/src/utils/files' import { requestsManager } from './RequestManager' import { changeDefinitionUri, checkIsDefinitionRangeEqual, checkIsDefinitionUriEqual, convertToSameDefinitionType, getDefinitionUri, getEmbeddedLanguageDocPosition, getOriginalDocRange } from './utils' -import { type EmbeddedLanguageDocInfos } from '../lib/src/types/embedded-languages' import { logger } from '../lib/src/utils/OutputLogger' +import { type EmbeddedLanguageDocInfos, embeddedLanguageDocsManager } from './EmbeddedLanguageDocsManager' export const middlewareProvideDefinition: DefinitionMiddleware['provideDefinition'] = async (document, position, token, next) => { logger.debug(`[middlewareProvideDefinition] ${document.uri.toString()}, line ${position.line}, character ${position.character}`) @@ -19,34 +18,34 @@ export const middlewareProvideDefinition: DefinitionMiddleware['provideDefinitio logger.debug('[middlewareProvideDefinition] returning nextResult') return nextResult } - const embeddedLanguageDocInfos = await requestsManager.getEmbeddedLanguageDocInfos(document.uri.toString(), position) - logger.debug(`[middlewareProvideDefinition] embeddedLanguageDoc ${embeddedLanguageDocInfos?.uri}`) - if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) { + const embeddedLanguageType = await requestsManager.getEmbeddedLanguageTypeOnPosition(document.uri.toString(), position) + if (embeddedLanguageType === undefined || embeddedLanguageType === null) { return } - const embeddedLanguageDocContent = await getFileContent(Uri.parse(embeddedLanguageDocInfos.uri).fsPath) - if (embeddedLanguageDocContent === undefined) { + const embeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(document.uri.toString(), embeddedLanguageType) + logger.debug(`[middlewareProvideDefinition] embeddedLanguageDoc ${embeddedLanguageDocInfos?.uri as any}`) + if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) { return } + const embeddedLanguageTextDocument = await workspace.openTextDocument(embeddedLanguageDocInfos.uri) const adjustedPosition = getEmbeddedLanguageDocPosition( document, - embeddedLanguageDocContent, + embeddedLanguageTextDocument, embeddedLanguageDocInfos.characterIndexes, position ) - const vdocUri = Uri.parse(embeddedLanguageDocInfos.uri) const tempResult = await commands.executeCommand( 'vscode.executeDefinitionProvider', - vdocUri, + embeddedLanguageDocInfos.uri, adjustedPosition ) // This check's purpose is only to please TypeScript. // We'd rather have a pointless check than losing the type assurance provided by TypeScript. if (checkIsArrayLocation(tempResult)) { - return await processDefinitions(tempResult, document, embeddedLanguageDocContent, embeddedLanguageDocInfos) + return await processDefinitions(tempResult, document, embeddedLanguageTextDocument, embeddedLanguageDocInfos) } else { - return await processDefinitions(tempResult, document, embeddedLanguageDocContent, embeddedLanguageDocInfos) + return await processDefinitions(tempResult, document, embeddedLanguageTextDocument, embeddedLanguageDocInfos) } } @@ -57,12 +56,12 @@ const checkIsArrayLocation = (array: Location[] | LocationLink[]): array is Loca const processDefinitions = async ( definitions: DefinitionType[], originalTextDocument: TextDocument, - embeddedLanguageDocContent: string, + embeddedLanguageTextDocument: TextDocument, embeddedLanguageDocInfos: EmbeddedLanguageDocInfos ): Promise => { const result: DefinitionType[] = [] await Promise.all(definitions.map(async (definition) => { - if (!checkIsDefinitionUriEqual(definition, Uri.parse(embeddedLanguageDocInfos.uri))) { + if (!checkIsDefinitionUriEqual(definition, embeddedLanguageDocInfos.uri)) { result.push(definition) // only definitions located on the embedded language documents need ajustments return } @@ -76,7 +75,7 @@ const processDefinitions = async { if (definition instanceof Location) { - const newRange = getOriginalDocRange(originalTextDocument, embeddedLanguageDocContent, characterIndexes, definition.range) + const newRange = getOriginalDocRange(originalTextDocument, embeddedLanguageTextDocument, characterIndexes, definition.range) if (newRange !== undefined) { definition.range = newRange } } else { - const newTargetRange = getOriginalDocRange(originalTextDocument, embeddedLanguageDocContent, characterIndexes, definition.targetRange) + const newTargetRange = getOriginalDocRange(originalTextDocument, embeddedLanguageTextDocument, characterIndexes, definition.targetRange) if (newTargetRange !== undefined) { definition.targetRange = newTargetRange } if (definition.targetSelectionRange !== undefined) { - const newTargetSelectionRange = getOriginalDocRange(originalTextDocument, embeddedLanguageDocContent, characterIndexes, definition.targetSelectionRange) + const newTargetSelectionRange = getOriginalDocRange(originalTextDocument, embeddedLanguageTextDocument, characterIndexes, definition.targetSelectionRange) if (newTargetSelectionRange !== undefined) { definition.targetSelectionRange = newTargetRange } diff --git a/client/src/language/middlewareHover.ts b/client/src/language/middlewareHover.ts index 7852800a..b660ccb6 100644 --- a/client/src/language/middlewareHover.ts +++ b/client/src/language/middlewareHover.ts @@ -4,35 +4,35 @@ * ------------------------------------------------------------------------------------------ */ import { type HoverMiddleware } from 'vscode-languageclient' -import { type Hover, Uri, commands } from 'vscode' +import { type Hover, commands, workspace } from 'vscode' -import { requestsManager } from './RequestManager' import { getEmbeddedLanguageDocPosition } from './utils' -import { getFileContent } from '../lib/src/utils/files' +import { embeddedLanguageDocsManager } from './EmbeddedLanguageDocsManager' +import { requestsManager } from './RequestManager' export const middlewareProvideHover: HoverMiddleware['provideHover'] = async (document, position, token, next) => { const nextResult = await next(document, position, token) if (nextResult !== undefined) { return nextResult } - const embeddedLanguageDocInfos = await requestsManager.getEmbeddedLanguageDocInfos(document.uri.toString(), position) - if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) { + const embeddedLanguageType = await requestsManager.getEmbeddedLanguageTypeOnPosition(document.uri.toString(), position) + if (embeddedLanguageType === undefined || embeddedLanguageType === null) { return } - const embeddedLanguageDocContent = await getFileContent(Uri.parse(embeddedLanguageDocInfos.uri).fsPath) - if (embeddedLanguageDocContent === undefined) { + const embeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(document.uri.toString(), embeddedLanguageType) + if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) { return } + const embeddedLanguageTextDocument = await workspace.openTextDocument(embeddedLanguageDocInfos.uri) const adjustedPosition = getEmbeddedLanguageDocPosition( document, - embeddedLanguageDocContent, + embeddedLanguageTextDocument, embeddedLanguageDocInfos.characterIndexes, position ) - const vdocUri = Uri.parse(embeddedLanguageDocInfos.uri) const result = await commands.executeCommand( 'vscode.executeHoverProvider', - vdocUri, + embeddedLanguageDocInfos.uri, adjustedPosition ) return result[0] diff --git a/client/src/language/utils.ts b/client/src/language/utils.ts index 52753fd9..1ea1a080 100644 --- a/client/src/language/utils.ts +++ b/client/src/language/utils.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { Location, type LocationLink, Position, Range, type Uri, type TextDocument } from 'vscode' +import { Location, type LocationLink, type Position, Range, type Uri, type TextDocument } from 'vscode' export const getOriginalDocRange = ( originalTextDocument: TextDocument, - embeddedLanguageDocContent: string, + embeddedLanguageTextDocument: TextDocument, characterIndexes: number[], embeddedRange: Range ): Range | undefined => { - const start = getOriginalDocPosition(originalTextDocument, embeddedLanguageDocContent, characterIndexes, embeddedRange.start) - const end = getOriginalDocPosition(originalTextDocument, embeddedLanguageDocContent, characterIndexes, embeddedRange.end) + const start = getOriginalDocPosition(originalTextDocument, embeddedLanguageTextDocument, characterIndexes, embeddedRange.start) + const end = getOriginalDocPosition(originalTextDocument, embeddedLanguageTextDocument, characterIndexes, embeddedRange.end) if (start === undefined || end === undefined) { return } @@ -21,11 +21,11 @@ export const getOriginalDocRange = ( const getOriginalDocPosition = ( originalTextDocument: TextDocument, - embeddedLanguageDocContent: string, + embeddedLanguageTextDocument: TextDocument, characterIndexes: number[], embeddedPosition: Position ): Position | undefined => { - const embeddedLanguageOffset = getOffset(embeddedLanguageDocContent, embeddedPosition) + const embeddedLanguageOffset = embeddedLanguageTextDocument.offsetAt(embeddedPosition) const originalOffset = characterIndexes.findIndex(index => index === embeddedLanguageOffset) if (originalOffset === -1) { return @@ -35,36 +35,13 @@ const getOriginalDocPosition = ( export const getEmbeddedLanguageDocPosition = ( originalTextDocument: TextDocument, - embeddedLanguageDocContent: string, + embeddedLanguageTextDocument: TextDocument, characterIndexes: number[], originalPosition: Position ): Position => { const originalOffset = originalTextDocument.offsetAt(originalPosition) const embeddedLanguageDocOffset = characterIndexes[originalOffset] - return getPosition(embeddedLanguageDocContent, embeddedLanguageDocOffset) -} - -const getPosition = (documentContent: string, offset: number): Position => { - let line = 0 - let character = 0 - for (let i = 0; i < offset; i++) { - if (documentContent[i] === '\n') { - line++ - character = 0 - } else { - character++ - } - } - return new Position(line, character) -} - -const getOffset = (documentContent: string, position: Position): number => { - let offset = 0 - for (let i = 0; i < position.line; i++) { - offset = documentContent.indexOf('\n', offset) + 1 - } - offset += position.character - return offset + return embeddedLanguageTextDocument.positionAt(embeddedLanguageDocOffset) } export const checkIsPositionEqual = (position1: Position, position2: Position): boolean => { diff --git a/client/src/lib/src/types/embedded-languages.ts b/client/src/lib/src/types/embedded-languages.ts index fec375fd..dad480b9 100644 --- a/client/src/lib/src/types/embedded-languages.ts +++ b/client/src/lib/src/types/embedded-languages.ts @@ -5,8 +5,9 @@ export type EmbeddedLanguageType = 'bash' | 'python' -export interface EmbeddedLanguageDocInfos { - uri: string +export interface EmbeddedLanguageDoc { + originalUri: string language: EmbeddedLanguageType + content: string characterIndexes: number[] } diff --git a/client/src/lib/src/types/notifications.ts b/client/src/lib/src/types/notifications.ts index 53c52e08..143870f2 100644 --- a/client/src/lib/src/types/notifications.ts +++ b/client/src/lib/src/types/notifications.ts @@ -3,19 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ +import { type EmbeddedLanguageDoc } from './embedded-languages' + enum NotificationType { - FilenameChanged = 'FilenameChanged' + EmbeddedLanguageDocs = 'EmbeddedLanguageDocs' } export const NotificationMethod: Record = { - [NotificationType.FilenameChanged]: 'custom/fileNameChanged' + [NotificationType.EmbeddedLanguageDocs]: 'custom/EmbeddedLanguageDocs' } export interface NotificationParams { - [NotificationType.FilenameChanged]: NotifyFileNameChangeParams -} - -interface NotifyFileNameChangeParams { - oldUriString: string - newUriString: string + [NotificationType.EmbeddedLanguageDocs]: EmbeddedLanguageDoc[] } diff --git a/client/src/lib/src/types/requests.ts b/client/src/lib/src/types/requests.ts index 3533dc03..e07f64e5 100644 --- a/client/src/lib/src/types/requests.ts +++ b/client/src/lib/src/types/requests.ts @@ -4,29 +4,24 @@ * ------------------------------------------------------------------------------------------ */ import { type Range, type Position } from 'vscode' -import { type EmbeddedLanguageDocInfos } from './embedded-languages' +import { type EmbeddedLanguageType } from './embedded-languages' export enum RequestType { - EmbeddedLanguageDocInfos = 'EmbeddedLanguageDocInfos', + EmbeddedLanguageTypeOnPosition = 'EmbeddedLanguageTypeOnPosition', getLinksInDocument = 'getLinksInDocument' } export const RequestMethod: Record = { - [RequestType.EmbeddedLanguageDocInfos]: 'custom/requestEmbeddedLanguageDocInfos', + [RequestType.EmbeddedLanguageTypeOnPosition]: 'custom/requestEmbeddedLanguageTypeOnPosition', [RequestType.getLinksInDocument]: 'custom/getLinksInDocument' } export interface RequestParams { - [RequestType.EmbeddedLanguageDocInfos]: RequestEmbeddedLanguageDocInfosParams + [RequestType.EmbeddedLanguageTypeOnPosition]: { uriString: string, position: Position } [RequestType.getLinksInDocument]: { documentUri: string } } export interface RequestResult { - [RequestType.EmbeddedLanguageDocInfos]: Promise // for unknown reasons, the client receives null instead of undefined + [RequestType.EmbeddedLanguageTypeOnPosition]: Promise // for unknown reasons, the client receives null instead of undefined [RequestType.getLinksInDocument]: Promise> } - -interface RequestEmbeddedLanguageDocInfosParams { - uriString: string - position: Position -} diff --git a/client/src/lib/src/utils/files.ts b/client/src/lib/src/utils/files.ts index 504aa1cf..7478d5ad 100644 --- a/client/src/lib/src/utils/files.ts +++ b/client/src/lib/src/utils/files.ts @@ -3,23 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import fs from 'fs' import path from 'path' -import { logger } from './OutputLogger' - -export const getFileContent = async (path: string): Promise => { - const fileContent = await new Promise((resolve, reject) => { - fs.readFile(path, { encoding: 'utf-8' }, - (error, data) => { error !== null ? reject(error) : resolve(data) } - ) - }).catch(err => { - logger.error(`Could not open file: ${err}`) - return undefined - }) - return fileContent -} - export function extractRecipeName (filename: string | undefined): string | undefined { if (filename === undefined) { return undefined } return path.basename(filename).split('.')[0].split('_')[0] diff --git a/integration-tests/project-folder/sources/meta-fixtures/diagnostics.bb b/integration-tests/project-folder/sources/meta-fixtures/diagnostics.bb new file mode 100644 index 00000000..c4d9f0d9 --- /dev/null +++ b/integration-tests/project-folder/sources/meta-fixtures/diagnostics.bb @@ -0,0 +1,3 @@ +python () { + error() +} \ No newline at end of file diff --git a/integration-tests/src/tests/diagnostics.test.ts b/integration-tests/src/tests/diagnostics.test.ts new file mode 100644 index 00000000..ac33e4ba --- /dev/null +++ b/integration-tests/src/tests/diagnostics.test.ts @@ -0,0 +1,55 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2023 Savoir-faire Linux. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as assert from 'assert' +import * as vscode from 'vscode' +import path from 'path' +import { afterEach } from 'mocha' + +suite('Bitbake Diagnostics Test Suite', () => { + const filePath = path.resolve(__dirname, '../../project-folder/sources/meta-fixtures/diagnostics.bb') + const docUri = vscode.Uri.parse(`file://${filePath}`) + + let disposables: vscode.Disposable[] = [] + + suiteSetup(async function (this: Mocha.Context) { + this.timeout(100000) + const vscodeBitbake = vscode.extensions.getExtension('yocto-project.yocto-bitbake') + if (vscodeBitbake === undefined) { + assert.fail('Bitbake extension is not available') + } + await vscodeBitbake.activate() + }) + + afterEach(function () { + for (const disposable of disposables) { + disposable.dispose() + } + disposables = [] + }) + + test('Diagnostics', async () => { + void vscode.workspace.openTextDocument(docUri) + await new Promise((resolve) => { + let nbChanges = 0 + const disposable = vscode.languages.onDidChangeDiagnostics((e) => { + if (e.uris.some((uri) => uri.toString() === docUri.toString())) { + nbChanges++ + } + const diagnostics = vscode.languages.getDiagnostics(docUri) + if (nbChanges === 3) { + resolve(diagnostics) + } + }) + disposables.push(disposable) + }).then((diagnostics) => { + assert.strictEqual(diagnostics.length, 1) + assert.strictEqual(diagnostics[0].source, 'Pylance') + assert.deepEqual(diagnostics[0].range, new vscode.Range(1, 4, 1, 9)) + }).catch((err) => { + assert.fail(err) + }) + }).timeout(300000) +}) diff --git a/server/src/__tests__/.gitignore b/server/src/__tests__/.gitignore deleted file mode 100644 index 3b47d161..00000000 --- a/server/src/__tests__/.gitignore +++ /dev/null @@ -1 +0,0 @@ -embedded-documents diff --git a/server/src/__tests__/embedded-languages.test.ts b/server/src/__tests__/embedded-languages.test.ts index fb6ff74b..4c7e173d 100644 --- a/server/src/__tests__/embedded-languages.test.ts +++ b/server/src/__tests__/embedded-languages.test.ts @@ -3,104 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import fs from 'fs' import { randomUUID } from 'crypto' -import { embeddedLanguageDocsManager } from '../embedded-languages/documents-manager' -import { generateEmbeddedLanguageDocs, getEmbeddedLanguageDocInfosOnPosition } from '../embedded-languages/general-support' +import { generateEmbeddedLanguageDocs } from '../embedded-languages/general-support' import { analyzer } from '../tree-sitter/analyzer' import { generateParser } from '../tree-sitter/parser' -import { FIXTURE_DOCUMENT } from './fixtures/fixtures' import { TextDocument } from 'vscode-languageserver-textdocument' import { type EmbeddedLanguageType } from '../lib/src/types/embedded-languages' import { imports } from '../embedded-languages/python-support' +import { shebang } from '../embedded-languages/bash-support' -describe('Embedded Language Documents file management', () => { +describe('Create basic embedded bash documents', () => { beforeAll(async () => { if (!analyzer.hasParser()) { const parser = await generateParser() analyzer.initialize(parser) } analyzer.resetAnalyzedDocuments() - await embeddedLanguageDocsManager.setStoragePath(__dirname) }) - beforeEach(() => { - analyzer.resetAnalyzedDocuments() - }) - - it('generate, rename and delete embedded language documents', async () => { - // Setup - analyzer.analyze({ - uri: FIXTURE_DOCUMENT.EMBEDDED.uri, - document: FIXTURE_DOCUMENT.EMBEDDED - }) - - await generateEmbeddedLanguageDocs(FIXTURE_DOCUMENT.EMBEDDED) - - // Test embedded documents infos - const bashEmbeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(FIXTURE_DOCUMENT.EMBEDDED.uri, 'bash') - if (bashEmbeddedLanguageDocInfos === undefined) { - expect(bashEmbeddedLanguageDocInfos).not.toBeUndefined() - return - } - expect(bashEmbeddedLanguageDocInfos.language).toEqual('bash') - - const pythonEmbeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(FIXTURE_DOCUMENT.EMBEDDED.uri, 'python') - if (pythonEmbeddedLanguageDocInfos === undefined) { - expect(bashEmbeddedLanguageDocInfos).not.toBeUndefined() - return - } - expect(pythonEmbeddedLanguageDocInfos?.language).toEqual('python') - - // Test embedded documents contents - const bashEmbeddedLanguageDocPath = bashEmbeddedLanguageDocInfos.uri.replace('file://', '') - const bashEmbeddedLanguageDoc = fs.readFileSync(bashEmbeddedLanguageDocPath, 'utf8') - expect(bashEmbeddedLanguageDoc).toEqual(expectedBashEmbeddedLanguageDoc) - - const pythonEmbeddedLanguageDocPath = pythonEmbeddedLanguageDocInfos.uri.replace('file://', '') - const pythonEmbeddedLanguageDoc = fs.readFileSync(pythonEmbeddedLanguageDocPath, 'utf8') - expect(pythonEmbeddedLanguageDoc).toEqual(expectedPythonEmbeddedLanguageDoc) - - // Test returned embedded documents for positions - const undefinedDocumentUri = getEmbeddedLanguageDocInfosOnPosition( - FIXTURE_DOCUMENT.EMBEDDED.uri, - { line: 0, character: 0 } - ) - expect(undefinedDocumentUri).toBeUndefined() - - const bashEmbeddedLanguageDocInfosOnPosition = getEmbeddedLanguageDocInfosOnPosition( - FIXTURE_DOCUMENT.EMBEDDED.uri, - { line: 8, character: 0 } - ) - expect(bashEmbeddedLanguageDocInfosOnPosition).toEqual(bashEmbeddedLanguageDocInfos) - - const pythonEmbeddedLanguageDocInfosOnPosition = getEmbeddedLanguageDocInfosOnPosition( - FIXTURE_DOCUMENT.EMBEDDED.uri, - { line: 3, character: 0 } - ) - expect(pythonEmbeddedLanguageDocInfosOnPosition).toEqual(pythonEmbeddedLanguageDocInfos) - - // Test saving the document a second time does not create a new file - await generateEmbeddedLanguageDocs(FIXTURE_DOCUMENT.EMBEDDED) - const bashEmbeddedLanguageDocInfos2 = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(FIXTURE_DOCUMENT.EMBEDDED.uri, 'bash') - expect(bashEmbeddedLanguageDocInfos2?.uri).toEqual(bashEmbeddedLanguageDocInfos.uri) - - const pythonEmbeddedLanguageDocInfos2 = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(FIXTURE_DOCUMENT.EMBEDDED.uri, 'python') - expect(pythonEmbeddedLanguageDocInfos2?.uri).toEqual(pythonEmbeddedLanguageDocInfos.uri) - - // Test moving embedded documents - const newUri = 'dummy' - embeddedLanguageDocsManager.renameEmbeddedLanguageDocs(FIXTURE_DOCUMENT.EMBEDDED.uri, newUri) - expect(embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(FIXTURE_DOCUMENT.EMBEDDED.uri, 'bash')).toBeUndefined() - expect(embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(FIXTURE_DOCUMENT.EMBEDDED.uri, 'python')).toBeUndefined() - expect(embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(newUri, 'bash')).toEqual(bashEmbeddedLanguageDocInfos) - expect(embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(newUri, 'python')).toEqual(pythonEmbeddedLanguageDocInfos) - - // Test deletion - await embeddedLanguageDocsManager.deleteEmbeddedLanguageDocs(newUri) - expect(fs.existsSync(bashEmbeddedLanguageDocPath)).toBeFalsy() - expect(fs.existsSync(pythonEmbeddedLanguageDocPath)).toBeFalsy() + test.each([ + [ + 'basic', + 'foo(){\nBAR=""\n}', + `${shebang}foo(){\nBAR=""\n}` + ], + [ + 'with override', + 'foo:append(){\nBAR=""\n}', + `${shebang}foo (){\nBAR=""\n}` + ], + [ + 'with inline python', + // eslint-disable-next-line no-template-curly-in-string + 'foo(){\n${@FOO}\n}', + `${shebang}foo(){\n\${? }\n}` + ] + ])('%s', async (description, input, result) => { + const embeddedContent = await createEmbeddedContent(input, 'bash') + expect(embeddedContent).toEqual(result) }) }) @@ -111,7 +52,6 @@ describe('Create various basic embedded python documents', () => { analyzer.initialize(parser) } analyzer.resetAnalyzedDocuments() - await embeddedLanguageDocsManager.setStoragePath(__dirname) }) test.each([ @@ -148,7 +88,6 @@ describe('Create Python embedded language content with inline Python', () => { analyzer.initialize(parser) } analyzer.resetAnalyzedDocuments() - await embeddedLanguageDocsManager.setStoragePath(__dirname) }) test.each([ @@ -181,14 +120,13 @@ describe('Create Python embedded language content with inline Python', () => { // eslint-disable-next-line no-template-curly-in-string 'inherit ${@"test"}', `${imports} \n\n"test"\n` - ] - /* // This is not yet supported by tree-sitter + ], [ 'inside bash function', // eslint-disable-next-line no-template-curly-in-string - 'foo(){\necho ${@"bar"}\n}\n', - ' \n \n"bar"\n\n \n' - ] */ + 'foo(){\n${@FOO}\n}', + `${imports} \n \n\nFOO\n\n ` + ] ])('%s', async (description, input, result) => { const embeddedContent = await createEmbeddedContent(input, 'python') expect(embeddedContent).toEqual(result) @@ -199,38 +137,7 @@ const createEmbeddedContent = async (content: string, language: EmbeddedLanguage const uri = randomUUID() const document = TextDocument.create(uri, 'bitbake', 1, content) analyzer.analyze({ document, uri }) - await generateEmbeddedLanguageDocs(document) - const pythonEmbeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(uri, language) - if (pythonEmbeddedLanguageDocInfos === undefined) { - return - } - const pythonEmbeddedLanguageDocPath = pythonEmbeddedLanguageDocInfos.uri.replace('file://', '') - const embeddedContent = fs.readFileSync(pythonEmbeddedLanguageDocPath, 'utf8') - void embeddedLanguageDocsManager.deleteEmbeddedLanguageDocs(uri) + const embeddedLanguageDocs = generateEmbeddedLanguageDocs(document) analyzer.resetAnalyzedDocuments() - return embeddedContent -} - -const expectedPythonEmbeddedLanguageDoc = -`${imports} - -def do_foo(): - print('123') - - - - - -` - -const expectedBashEmbeddedLanguageDoc = -` - - - - - -do_bar(){ - echo '123' + return embeddedLanguageDocs?.find((embeddedLanguageDoc) => embeddedLanguageDoc.language === language)?.content } -` diff --git a/server/src/embedded-languages/bash-support.ts b/server/src/embedded-languages/bash-support.ts index a41f867d..d79f1a00 100644 --- a/server/src/embedded-languages/bash-support.ts +++ b/server/src/embedded-languages/bash-support.ts @@ -3,29 +3,67 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { type TextDocument } from 'vscode-languageserver-textdocument' - -import { analyzer } from '../tree-sitter/analyzer' +import { type AnalyzedDocument } from '../tree-sitter/analyzer' import * as TreeSitterUtils from '../tree-sitter/utils' -import { embeddedLanguageDocsManager } from './documents-manager' import { initEmbeddedLanguageDoc, insertTextIntoEmbeddedLanguageDoc } from './utils' +import { type EmbeddedLanguageDoc } from '../lib/src/types/embedded-languages' +import { type SyntaxNode } from 'web-tree-sitter' +import { logger } from '../lib/src/utils/OutputLogger' -export const generateBashEmbeddedLanguageDoc = async (textDocument: TextDocument): Promise => { - const analyzedDocument = analyzer.getAnalyzedDocument(textDocument.uri) - if (analyzedDocument === undefined) { - return - } - const embeddedLanguageDoc = initEmbeddedLanguageDoc(textDocument, 'bash') +export const shebang = '#!/bin/sh\n' + +export const generateBashEmbeddedLanguageDoc = (analyzedDocument: AnalyzedDocument): EmbeddedLanguageDoc => { + const embeddedLanguageDoc = initEmbeddedLanguageDoc(analyzedDocument.document, 'bash') TreeSitterUtils.forEach(analyzedDocument.tree.rootNode, (node) => { switch (node.type) { case 'recipe': return true case 'function_definition': - insertTextIntoEmbeddedLanguageDoc(embeddedLanguageDoc, node.startIndex, node.endIndex, node.text) + handleFunctionDefinitionNode(node, embeddedLanguageDoc) return false default: return false } }) - await embeddedLanguageDocsManager.saveEmbeddedLanguageDoc(embeddedLanguageDoc) + insertTextIntoEmbeddedLanguageDoc(embeddedLanguageDoc, 0, 0, shebang) + return embeddedLanguageDoc +} + +const handleFunctionDefinitionNode = (node: SyntaxNode, embeddedLanguageDoc: EmbeddedLanguageDoc): void => { + insertTextIntoEmbeddedLanguageDoc(embeddedLanguageDoc, node.startIndex, node.endIndex, node.text) + node.children.forEach((child) => { + switch (child.type) { + case 'override': + handleOverrideNode(child, embeddedLanguageDoc) + break + case 'inline_python': + handleInlinePythonNode(child, embeddedLanguageDoc) + break + default: + break + } + }) +} + +const handleInlinePythonNode = (inlinePythonNode: SyntaxNode, embeddedLanguageDoc: EmbeddedLanguageDoc): void => { + // Example: + // if [ "${@d.getVar('FOO')}" = "0" ] ; + // will become + // if [ "${? }" = "0" ] ; + // Replacing the whole inline_python by spaces would create a constant string and might trigger a warning if the spellcheck + // extension is activated, since the comparison with "0" would always give the same result + // ${?} is an arbitrary value that is expected not to cause any trouble. + const trailingSpacesLength = inlinePythonNode.text.length - 4 + if (trailingSpacesLength <= 0) { + // This is expected to never happen + logger.error(`[handleInlinePythonNode (Bash)] Invalid string length for node ${inlinePythonNode.toString()}`) + return + } + const replacement = `\${?${' '.repeat(trailingSpacesLength)}}` + insertTextIntoEmbeddedLanguageDoc(embeddedLanguageDoc, inlinePythonNode.startIndex, inlinePythonNode.endIndex, replacement) +} + +const handleOverrideNode = (overrideNode: SyntaxNode, embeddedLanguageDoc: EmbeddedLanguageDoc): void => { + // Remove it + insertTextIntoEmbeddedLanguageDoc(embeddedLanguageDoc, overrideNode.startIndex, overrideNode.endIndex, ' '.repeat(overrideNode.text.length)) } diff --git a/server/src/embedded-languages/general-support.ts b/server/src/embedded-languages/general-support.ts index 2ca225e8..3f72b275 100644 --- a/server/src/embedded-languages/general-support.ts +++ b/server/src/embedded-languages/general-support.ts @@ -8,25 +8,27 @@ import { type Position } from 'vscode-languageserver' import { generateBashEmbeddedLanguageDoc } from './bash-support' import { generatePythonEmbeddedLanguageDoc } from './python-support' -import { embeddedLanguageDocsManager } from './documents-manager' import { isInsideBashRegion, isInsidePythonRegion } from './utils' -import { type EmbeddedLanguageDocInfos } from '../lib/src/types/embedded-languages' +import { type EmbeddedLanguageDoc, type EmbeddedLanguageType } from '../lib/src/types/embedded-languages' +import { analyzer } from '../tree-sitter/analyzer' -export const generateEmbeddedLanguageDocs = async (textDocument: TextDocument): Promise => { - await Promise.all([ - generateBashEmbeddedLanguageDoc(textDocument), - generatePythonEmbeddedLanguageDoc(textDocument) - ]) +export const generateEmbeddedLanguageDocs = (textDocument: TextDocument): EmbeddedLanguageDoc[] | undefined => { + const analyzedDocument = analyzer.getAnalyzedDocument(textDocument.uri) + if (analyzedDocument === undefined) { + return + } + return [ + generateBashEmbeddedLanguageDoc(analyzedDocument), + generatePythonEmbeddedLanguageDoc(analyzedDocument) + ] } -export const getEmbeddedLanguageDocInfosOnPosition = (uriString: string, position: Position): EmbeddedLanguageDocInfos | undefined => { +export const getEmbeddedLanguageTypeOnPosition = (uriString: string, position: Position): EmbeddedLanguageType | undefined => { if (isInsideBashRegion(uriString, position)) { - const documentInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(uriString, 'bash') - return documentInfos + return 'bash' } if (isInsidePythonRegion(uriString, position)) { - const documentInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(uriString, 'python') - return documentInfos + return 'python' } return undefined } diff --git a/server/src/embedded-languages/python-support.ts b/server/src/embedded-languages/python-support.ts index a5e44df3..86f47f78 100644 --- a/server/src/embedded-languages/python-support.ts +++ b/server/src/embedded-languages/python-support.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { type TextDocument } from 'vscode-languageserver-textdocument' import { type SyntaxNode } from 'web-tree-sitter' -import { analyzer } from '../tree-sitter/analyzer' +import { type AnalyzedDocument } from '../tree-sitter/analyzer' import * as TreeSitterUtils from '../tree-sitter/utils' -import { embeddedLanguageDocsManager } from './documents-manager' -import { type EmbeddedLanguageDoc, insertTextIntoEmbeddedLanguageDoc, initEmbeddedLanguageDoc } from './utils' +import { insertTextIntoEmbeddedLanguageDoc, initEmbeddedLanguageDoc } from './utils' +import { type EmbeddedLanguageDoc } from '../lib/src/types/embedded-languages' export const imports = [ 'import bb', @@ -23,12 +22,8 @@ export const imports = [ '' ].join('\n') -export const generatePythonEmbeddedLanguageDoc = async (textDocument: TextDocument): Promise => { - const analyzedDocument = analyzer.getAnalyzedDocument(textDocument.uri) - if (analyzedDocument === undefined) { - return - } - const embeddedLanguageDoc = initEmbeddedLanguageDoc(textDocument, 'python') +export const generatePythonEmbeddedLanguageDoc = (analyzedDocument: AnalyzedDocument): EmbeddedLanguageDoc => { + const embeddedLanguageDoc = initEmbeddedLanguageDoc(analyzedDocument.document, 'python') TreeSitterUtils.forEach(analyzedDocument.tree.rootNode, (node) => { switch (node.type) { case 'python_function_definition': @@ -45,7 +40,7 @@ export const generatePythonEmbeddedLanguageDoc = async (textDocument: TextDocume } }) insertTextIntoEmbeddedLanguageDoc(embeddedLanguageDoc, 0, 0, imports) - await embeddedLanguageDocsManager.saveEmbeddedLanguageDoc(embeddedLanguageDoc) + return embeddedLanguageDoc } const handlePythonFunctionDefinition = (node: SyntaxNode, embeddedLanguageDoc: EmbeddedLanguageDoc): void => { diff --git a/server/src/embedded-languages/utils.ts b/server/src/embedded-languages/utils.ts index a4f4ba28..2d26e870 100644 --- a/server/src/embedded-languages/utils.ts +++ b/server/src/embedded-languages/utils.ts @@ -9,14 +9,7 @@ import { type TextDocument } from 'vscode-languageserver-textdocument' import { analyzer } from '../tree-sitter/analyzer' import { positionIsWithinRange } from '../utils/range' -import { type EmbeddedLanguageType } from '../lib/src/types/embedded-languages' - -export interface EmbeddedLanguageDoc { - originalUri: string - language: EmbeddedLanguageType - content: string - characterIndexes: number[] -} +import { type EmbeddedLanguageDoc, type EmbeddedLanguageType } from '../lib/src/types/embedded-languages' const isInsideRegion = (position: Position, region: LSP.SymbolInformation): boolean => { return positionIsWithinRange(position, region.location.range) diff --git a/server/src/server.ts b/server/src/server.ts index d42e2a00..5fac6d78 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -22,12 +22,11 @@ import { logger } from './lib/src/utils/OutputLogger' import { onCompletionHandler } from './connectionHandlers/onCompletion' import { onDefinitionHandler } from './connectionHandlers/onDefinition' import { onHoverHandler } from './connectionHandlers/onHover' -import { generateEmbeddedLanguageDocs, getEmbeddedLanguageDocInfosOnPosition } from './embedded-languages/general-support' -import { embeddedLanguageDocsManager } from './embedded-languages/documents-manager' -import { RequestMethod, type RequestParams, type RequestResult } from './lib/src/types/requests' -import { NotificationMethod, type NotificationParams } from './lib/src/types/notifications' +import { generateEmbeddedLanguageDocs, getEmbeddedLanguageTypeOnPosition } from './embedded-languages/general-support' import { getSemanticTokens, legend } from './semanticTokens' import { bitBakeProjectScannerClient } from './BitbakeProjectScannerClient' +import { RequestMethod, type RequestParams, type RequestResult } from './lib/src/types/requests' +import { NotificationMethod, type NotificationParams } from './lib/src/types/notifications' // Create a connection for the server. The connection uses Node's IPC as a transport const connection: Connection = createConnection(ProposedFeatures.all) @@ -47,15 +46,11 @@ let currentActiveTextDocument: TextDocument = TextDocument.create( connection.onInitialize(async (params: InitializeParams): Promise => { logger.level = 'debug' logger.info('[onInitialize] Initializing connection') - bitBakeProjectScannerClient.setConnection(connection) disposables.push(...bitBakeProjectScannerClient.buildHandlers()) - const storagePath = params.initializationOptions.storagePath as string const extensionPath = params.initializationOptions.extensionPath as string - await embeddedLanguageDocsManager.setStoragePath(storagePath) - logger.info('[onInitialize] Setting yocto doc path and parsing doc files') bitBakeDocScanner.setDocPathAndParse(extensionPath) @@ -108,9 +103,9 @@ connection.onDefinition(onDefinitionHandler) connection.onHover(onHoverHandler) connection.onRequest( - RequestMethod.EmbeddedLanguageDocInfos, - async ({ uriString, position }: RequestParams['EmbeddedLanguageDocInfos']): RequestResult['EmbeddedLanguageDocInfos'] => { - return getEmbeddedLanguageDocInfosOnPosition(uriString, position) + RequestMethod.EmbeddedLanguageTypeOnPosition, + async ({ uriString, position }: RequestParams['EmbeddedLanguageTypeOnPosition']): RequestResult['EmbeddedLanguageTypeOnPosition'] => { + return getEmbeddedLanguageTypeOnPosition(uriString, position) } ) // This request method 'textDocument/semanticTokens' will be sent when semanticTokensProvider capability is enabled @@ -123,13 +118,6 @@ connection.onRequest(RequestMethod.getLinksInDocument, (params: RequestParams['g return analyzer.getLinksInStringContent(params.documentUri) }) -connection.onNotification( - NotificationMethod.FilenameChanged, - ({ oldUriString, newUriString }: NotificationParams['FilenameChanged']): void => { - embeddedLanguageDocsManager.renameEmbeddedLanguageDocs(oldUriString, newUriString) - } -) - connection.listen() documents.onDidChangeContent(async (event) => { @@ -137,7 +125,10 @@ documents.onDidChangeContent(async (event) => { if (textDocument.getText().length > 0) { const diagnostics = analyzer.analyze({ document: textDocument, uri: textDocument.uri }) - void generateEmbeddedLanguageDocs(event.document) + const embeddedLanguageDocs: NotificationParams['EmbeddedLanguageDocs'] | undefined = generateEmbeddedLanguageDocs(event.document) + if (embeddedLanguageDocs !== undefined) { + void connection.sendNotification(NotificationMethod.EmbeddedLanguageDocs, embeddedLanguageDocs) + } void connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) } diff --git a/server/src/tree-sitter/analyzer.ts b/server/src/tree-sitter/analyzer.ts index ee436ffd..fc4eca55 100644 --- a/server/src/tree-sitter/analyzer.ts +++ b/server/src/tree-sitter/analyzer.ts @@ -28,7 +28,7 @@ import path from 'path' import { bitBakeProjectScannerClient } from '../BitbakeProjectScannerClient' import { bitBakeDocScanner } from '../BitBakeDocScanner' -interface AnalyzedDocument { +export interface AnalyzedDocument { document: TextDocument globalDeclarations: GlobalDeclarations globalSymbolComments: GlobalSymbolComments