Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: Use VS Code API to manage embedded languages files #14

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import { randomUUID } from 'crypto'
import path from 'path'
import fs from 'fs'

import logger from 'winston'

import { type EmbeddedLanguageDocInfos, type EmbeddedLanguageType } from '../lib/src/types/embedded-languages'
import { type EmbeddedLanguageDoc } from './utils'
import { type EmbeddedLanguageDoc, type EmbeddedLanguageType } from '../lib/src/types/embedded-languages'
import { logger } from '../lib/src/utils/OutputLogger'
import { Range, Uri, WorkspaceEdit, workspace } from 'vscode'

const EMBEDDED_DOCUMENTS_FOLDER = 'embedded-documents'

Expand All @@ -19,6 +18,12 @@ const fileExtensionsMap = {
python: '.py'
}

export interface EmbeddedLanguageDocInfos {
uri: Uri
language: EmbeddedLanguageType
characterIndexes: number[]
}

type EmbeddedLanguageDocsRecord = Partial<Record<EmbeddedLanguageType, EmbeddedLanguageDocInfos>>

export default class EmbeddedLanguageDocsManager {
Expand All @@ -45,7 +50,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:', err)
logger.error(`Failed to create embedded language documents folder: ${err as any}`)
}
resolve()
})
Expand All @@ -58,7 +63,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:', err)
logger.error(`Failed to remove embedded language documents folder: ${err as any}`)
}
resolve()
})
Expand All @@ -67,10 +72,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 (
Expand All @@ -81,61 +90,75 @@ export default class EmbeddedLanguageDocsManager {
return embeddedLanguageDocs?.[languageType]
}

private getPathToEmbeddedLanguageDoc (embeddedLanguageDoc: EmbeddedLanguageDoc): string | undefined {
private createEmbeddedLanguageDocUri (embeddedLanguageDoc: EmbeddedLanguageDoc): Uri | undefined {
if (this.storagePath === 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}`
return Uri.parse(`file://${pathToEmbeddedLanguageDocsFolder}/${embeddedLanguageDocFilename}`)
}

async saveEmbeddedLanguageDocs (
embeddedLanguageDocs: EmbeddedLanguageDoc[]
): Promise<void> {
await Promise.all(embeddedLanguageDocs.map(async (embeddedLanguageDoc) => {
await this.saveEmbeddedLanguageDoc(embeddedLanguageDoc)
}))
}

private async updateEmbeddedLanguageDocFile (embeddedLanguageDoc: EmbeddedLanguageDoc, uri: Uri): Promise<void> {
const document = await workspace.openTextDocument(uri)
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)
}

private async createEmbeddedLanguageDocFile (embeddedLanguageDoc: EmbeddedLanguageDoc): Promise<void> {
const uri = this.createEmbeddedLanguageDocUri(embeddedLanguageDoc)
if (uri === undefined) {
return undefined
}
try {
await workspace.fs.writeFile(uri, Buffer.from(embeddedLanguageDoc.content))
} catch (err) {
logger.error(`Failed to create embedded document: ${err as any}`)
}
this.registerEmbeddedLanguageDocInfos(embeddedLanguageDoc, uri)
}

async saveEmbeddedLanguageDoc (
embeddedLanguageDoc: EmbeddedLanguageDoc
): Promise<void> {
logger.debug(`Save embedded document (${embeddedLanguageDoc.language}) for`, embeddedLanguageDoc.originalUri)
const pathToEmbeddedLanguageDoc = this.getPathToEmbeddedLanguageDoc(embeddedLanguageDoc)
if (pathToEmbeddedLanguageDoc === undefined) {
return
logger.debug(`Save embedded document (${embeddedLanguageDoc.language}) for ${embeddedLanguageDoc.originalUri}`)
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<void>((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<void> {
logger.debug('Delete embedded documents for', originalUriString)
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<void>((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) => {
logger.error('Failed to delete embedded document:', err)
logger.error(`Failed to delete embedded document: ${err}`)
})
}

Expand Down
8 changes: 4 additions & 4 deletions client/src/language/RequestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
32 changes: 20 additions & 12 deletions client/src/language/languageClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,12 @@ 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'

const notifyFileRenameChanged = async (
client: LanguageClient,
oldUriString: string,
newUriString: string
): Promise<void> => {
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'

export async function activateLanguageServer (context: ExtensionContext): Promise<LanguageClient> {
const serverModule = context.asAbsolutePath(path.join('server', 'server.js'))
Expand All @@ -46,7 +39,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())
})
})

Expand All @@ -67,7 +66,6 @@ export async function activateLanguageServer (context: ExtensionContext): Promis
]
},
initializationOptions: {
storagePath: context.storageUri?.fsPath,
extensionPath: context.extensionPath
},
middleware: {
Expand All @@ -76,6 +74,12 @@ export async function activateLanguageServer (context: ExtensionContext): Promis
}
}

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
Expand All @@ -101,6 +105,10 @@ 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)
})

// Start the client and launch the server
await client.start()

Expand Down
23 changes: 12 additions & 11 deletions client/src/language/middlewareCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CompletionList>(
'vscode.executeCompletionItemProvider',
vdocUri,
Expand All @@ -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
}
Expand Down
20 changes: 10 additions & 10 deletions client/src/language/middlewareHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hover[]>(
'vscode.executeHoverProvider',
vdocUri,
embeddedLanguageDocInfos.uri,
adjustedPosition
)
return result[0]
Expand Down
Loading
Loading