Skip to content

Commit

Permalink
Feat: Add rename on local Python and Bash variables
Browse files Browse the repository at this point in the history
  • Loading branch information
idillon-sfl committed May 31, 2024
1 parent efdaad3 commit d3c5508
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 1 deletion.
5 changes: 4 additions & 1 deletion client/src/language/languageClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import * as vscode from 'vscode'
import { middlewareProvideReferences } from './middlewareReferences'
import { RequestMethod, type RequestParams, type RequestResult } from '../lib/src/types/requests'
import { BitbakeDocumentLinkProvider } from '../documentLinkProvider'
import { middlewarePrepareRename, middlewareProvideRenameEdits } from './middlewareRename'

export async function activateLanguageServer (context: ExtensionContext, bitBakeProjectScanner: BitBakeProjectScanner): Promise<LanguageClient> {
const serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'))
Expand Down Expand Up @@ -64,7 +65,9 @@ export async function activateLanguageServer (context: ExtensionContext, bitBake
provideCompletionItem: middlewareProvideCompletion,
provideDefinition: middlewareProvideDefinition,
provideHover: middlewareProvideHover,
provideReferences: middlewareProvideReferences
provideReferences: middlewareProvideReferences,
prepareRename: middlewarePrepareRename,
provideRenameEdits: middlewareProvideRenameEdits
}
}

Expand Down
126 changes: 126 additions & 0 deletions client/src/language/middlewareRename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) 2024 Savoir-faire Linux. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import { type RenameMiddleware } from 'vscode-languageclient'
import { commands, workspace, WorkspaceEdit, type Range, type TextEdit } from 'vscode'

import { getEmbeddedLanguageDocPosition, getOriginalDocRange } from './utils/embeddedLanguagesUtils'
import { embeddedLanguageDocsManager } from './EmbeddedLanguageDocsManager'
import { requestsManager } from './RequestManager'

export const middlewareProvideRenameEdits: RenameMiddleware['provideRenameEdits'] = async (document, position, newName, token, next) => {
const nextResult = await next(document, position, newName, token)
if (nextResult !== undefined && nextResult !== null) {
return nextResult
}
const embeddedLanguageType = await requestsManager.getEmbeddedLanguageTypeOnPosition(document.uri.toString(), position)
if (embeddedLanguageType === undefined || embeddedLanguageType === null) {
return
}
const embeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(document.uri, embeddedLanguageType)
if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) {
return
}
const embeddedLanguageTextDocument = await workspace.openTextDocument(embeddedLanguageDocInfos.uri)
const adjustedPosition = getEmbeddedLanguageDocPosition(
document,
embeddedLanguageTextDocument,
embeddedLanguageDocInfos.characterIndexes,
position
)
const tempWorkspaceEdit = await commands.executeCommand<WorkspaceEdit>(
'vscode.executeDocumentRenameProvider',
embeddedLanguageDocInfos.uri,
adjustedPosition,
newName
)

const workspaceEdit = new WorkspaceEdit()

tempWorkspaceEdit.entries().forEach(([tempUri, tempTextEdits]) => {
const textEdits: TextEdit[] = []
const originalUri = embeddedLanguageDocsManager.getOriginalUri(tempUri)
if (originalUri === undefined) {
return
}
tempTextEdits.forEach((tempTextEdit) => {
const range = getOriginalDocRange(
document,
embeddedLanguageTextDocument,
embeddedLanguageDocInfos.characterIndexes,
tempTextEdit.range
)
if (range === undefined) {
return
}
textEdits.push({
range,
newText: tempTextEdit.newText
})
})
workspaceEdit.set(originalUri, textEdits)
})

return workspaceEdit
}

// It seems RenameMiddleware['prepareRename'] expects to throw an error when rename is not possible.
const invalidRenameError = new Error("The element can't be renamed.")

export const middlewarePrepareRename: RenameMiddleware['prepareRename'] = async (document, position, token, next) => {
let nextResult: Awaited<ReturnType<typeof next>> | undefined
try {
nextResult = await next(document, position, token)
} catch (error) {
// pass
}

if (nextResult !== undefined && nextResult !== null) {
return nextResult
}

const embeddedLanguageType = await requestsManager.getEmbeddedLanguageTypeOnPosition(document.uri.toString(), position)

if (embeddedLanguageType === undefined || embeddedLanguageType === null) {
throw invalidRenameError
}
const embeddedLanguageDocInfos = embeddedLanguageDocsManager.getEmbeddedLanguageDocInfos(document.uri, embeddedLanguageType)

if (embeddedLanguageDocInfos === undefined || embeddedLanguageDocInfos === null) {
throw invalidRenameError
}
const embeddedLanguageTextDocument = await workspace.openTextDocument(embeddedLanguageDocInfos.uri)
const adjustedPosition = getEmbeddedLanguageDocPosition(
document,
embeddedLanguageTextDocument,
embeddedLanguageDocInfos.characterIndexes,
position
)
const tempPrepareRename = await commands.executeCommand<{ range: Range, placeholder: string } | undefined>(
'vscode.prepareRename',
embeddedLanguageDocInfos.uri,
adjustedPosition
)

if (tempPrepareRename === undefined) {
throw invalidRenameError
}

const range = getOriginalDocRange(
document,
embeddedLanguageTextDocument,
embeddedLanguageDocInfos.characterIndexes,
tempPrepareRename.range
)

if (range === undefined) {
throw invalidRenameError
}

return {
range,
placeholder: tempPrepareRename.placeholder
}
}
13 changes: 13 additions & 0 deletions integration-tests/project-folder/sources/meta-fixtures/rename.bb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
foo='foo'

python() {
foo='foo'
print(foo)
d.getVar('foo') # should be included in global variables, but it does not work in integration tests for unknown reasons
}

do_stuff() {
echo "${foo}"
local foo='foo'
echo "$foo"
}
173 changes: 173 additions & 0 deletions integration-tests/src/tests/rename.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/* --------------------------------------------------------------------------------------------
* 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 { assertWillComeTrue } from '../utils/async'
import { BITBAKE_TIMEOUT } from '../utils/bitbake'

suite('Bitbake Rename Test Suite', () => {
const filePath = path.resolve(__dirname, '../../project-folder/sources/meta-fixtures/rename.bb')
const docUri = vscode.Uri.parse(`file://${filePath}`)

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()
await vscode.workspace.openTextDocument(docUri)
})

const testRename = async (
position: vscode.Position,
newName: string,
expected: ReturnType<vscode.WorkspaceEdit['entries']>
): Promise<void> => {
let result: vscode.WorkspaceEdit | undefined

await assertWillComeTrue(async () => {
result = await vscode.commands.executeCommand<vscode.WorkspaceEdit>(
'vscode.executeDocumentRenameProvider',
docUri,
position,
newName
)
return result.entries().length === expected.length
})
result?.entries().forEach(([uri, edits], index) => {
const [expectedUri, expectedTextEdits] = expected[index]
assert.strictEqual(uri.toString() === expectedUri.toString(), true)
assert.strictEqual(edits.length === expectedTextEdits.length, true)
edits.forEach((edit, editIndex) => {
const expectedEdit = expectedTextEdits[editIndex]
assert.strictEqual(edit.newText === expectedEdit.newText, true)
assert.strictEqual(edit.range.isEqual(expectedEdit.range), true)
})
})
}

const testPrepareRename = async (
position: vscode.Position,
expected: { range: vscode.Range, placeholder: string }
): Promise<void> => {
let result: { range: vscode.Range, placeholder: string } | undefined

await assertWillComeTrue(async () => {
result = await vscode.commands.executeCommand<{ range: vscode.Range, placeholder: string } | undefined>(
'vscode.prepareRename',
docUri,
position
)
return result !== undefined
})

assert.strictEqual(result?.range.isEqual(expected.range), true)
assert.strictEqual(result?.placeholder === expected.placeholder, true)
}

const testInvalidRename = async (
position: vscode.Position
): Promise<void> => {
try {
await vscode.commands.executeCommand<{ range: vscode.Range, placeholder: string } | undefined>(
'vscode.prepareRename',
docUri,
position
)
} catch (error) {
if (error instanceof Error) {
assert.strictEqual(error.message === "The element can't be renamed.", true)
return
}
}
assert.fail()
}

test('Rename properly on global variable', async () => {
const position = new vscode.Position(0, 2)
const expectedPrepareRename = {
range: new vscode.Range(0, 0, 0, 3),
placeholder: 'foo'
}
await testPrepareRename(position, expectedPrepareRename)
const newName = 'bar'
const expectedRename: ReturnType<vscode.WorkspaceEdit['entries']> = [
[
docUri,
[
new vscode.TextEdit(
new vscode.Range(0, 0, 0, 3),
newName
),
new vscode.TextEdit(
new vscode.Range(9, 12, 9, 15),
newName
)
]
]
]
await testRename(position, newName, expectedRename)
}).timeout(BITBAKE_TIMEOUT)

test('Rename properly on local Python variable', async () => {
const position = new vscode.Position(4, 12)
const expectedPrepareRename = {
range: new vscode.Range(4, 10, 4, 13),
placeholder: 'foo'
}
await testPrepareRename(position, expectedPrepareRename)
const newName = 'bar'
const expectedRename: ReturnType<vscode.WorkspaceEdit['entries']> = [
[
docUri,
[
new vscode.TextEdit(
new vscode.Range(3, 4, 3, 7),
newName
),
new vscode.TextEdit(
new vscode.Range(4, 10, 4, 13),
newName
)
]
]
]
await testRename(position, newName, expectedRename)
}).timeout(BITBAKE_TIMEOUT)

test('Rename properly on local Bash variable', async () => {
const position = new vscode.Position(10, 12)
const expectedPrepareRename = {
range: new vscode.Range(10, 10, 10, 13),
placeholder: 'foo'
}
await testPrepareRename(position, expectedPrepareRename)
const newName = 'bar'
const expectedRename: ReturnType<vscode.WorkspaceEdit['entries']> = [
[
docUri,
[
new vscode.TextEdit(
new vscode.Range(10, 10, 10, 13),
newName
),
new vscode.TextEdit(
new vscode.Range(11, 11, 11, 14),
newName
)
]
]
]
await testRename(position, newName, expectedRename)
}).timeout(BITBAKE_TIMEOUT)

test('Rename shows proper message where renaming is not possible', async () => {
const position = new vscode.Position(0, 7)
await testInvalidRename(position)
}).timeout(BITBAKE_TIMEOUT)
})

0 comments on commit d3c5508

Please sign in to comment.