Skip to content

Commit

Permalink
Merge pull request #17 from lfortran/dylon/rename-symbol
Browse files Browse the repository at this point in the history
Adds initial support for renaming symbols
  • Loading branch information
certik authored Dec 1, 2024
2 parents 0f5b933 + c59388b commit 95aa8f1
Show file tree
Hide file tree
Showing 7 changed files with 657 additions and 41 deletions.
103 changes: 103 additions & 0 deletions integ/spec/function_call1.f90.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,66 @@ async function triggerHoverAndGetText() : Promise<string> {
return text;
}

async function renameSymbol(newName: string): Promise<void> {
// await editor.sendKeys(Key.F2);
await driver.actions().clear(); // necessary for the key-down event
await driver.actions().sendKeys(Key.F2).perform();
const renameInput: WebElement =
driver.wait(
until.elementLocated(
By.css('input.rename-input')),
timeout);
let oldName: string = await renameInput.getAttribute("value");
for (let i = 0, k = oldName.length;
(oldName.length > 0) && (i < k);
i++) {
await renameInput.sendKeys(Key.BACK_SPACE);
oldName = await renameInput.getAttribute("value");
}
oldName = await renameInput.getAttribute("value");
assert.isEmpty(
oldName,
"Failed to clear all characters from .rename-input");
await renameInput.sendKeys(newName);
await renameInput.sendKeys(Key.ENTER);
}

async function getErrorAlert(): Promise<string> {
await driver.actions().clear();
await driver.actions().sendKeys(Key.F8).perform();
// NOTE: k=10 might be excessive, but I don't like failing due to a lack
// of retries ...
// ---------------------------------------------------------------------
for (let i = 0, k = 10; i < k; i++) {
try {
const outerElement: WebElement =
await driver.wait(
until.elementLocated(
By.css('div.message[role="alert"][aria-label^="error"] div')),
timeout);
const innerElement: WebElement =
await outerElement.findElement(
By.css(':first-child'));
const outerMessage: string = await outerElement.getText();
const innerMessage: string = await innerElement.getText();
// NOTE: Drop the inner message from the error message since it contains
// the name of the plugin:
// ---------------------------------------------------------------------
const errorMessage: string =
outerMessage.substring(0, outerMessage.length - innerMessage.length);
return errorMessage;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
const retryMessage: string =
"stale element reference: stale element not found in the current frame";
if (!error.message.startsWith(retryMessage) || ((i + 1) == k)) {
throw error;
}
}
}
throw new Error("This should not be reached!");
}

// initialize the browser and webdriver
before(async () => {
browser = VSBrowser.instance;
Expand Down Expand Up @@ -143,6 +203,7 @@ describe(fileName, () => {
afterEach(async () => {
await workbench.executeCommand("revert file");
await editorView.closeEditor(fileName);
await driver.actions().clear();
});

describe('When I type "m"', () => {
Expand Down Expand Up @@ -251,4 +312,46 @@ describe(fileName, () => {
assert.equal(column, 5);
});
});

describe('When I rename eval_1d to foo', () => {
it('should rename all the respective symbols', async () => {
await editor.setCursor(18, 22); // hover over "eval_1d"
await renameSymbol("foo");
const text: string = await editor.getText();
assert.equal(text, [
"module module_function_call1",
" type :: softmax",
" contains",
" procedure :: foo",
" end type softmax",
" contains",
" ",
" pure function foo(self, x) result(res)",
" class(softmax), intent(in) :: self",
" real, intent(in) :: x(:)",
" real :: res(size(x))",
" end function foo",
" ",
" pure function eval_1d_prime(self, x) result(res)",
" class(softmax), intent(in) :: self",
" real, intent(in) :: x(:)",
" real :: res(size(x))",
" res = self%foo(x)",
" end function eval_1d_prime",
"end module module_function_call1",
"",
].join("\n"));
});
});

describe('When I introduce an error', () => {
it('should notify me of the issue.', async () => {
await editor.setCursor(21, 1);
await editor.typeText("error");
const errorMessage: string = await getErrorAlert();
assert.equal(
errorMessage,
"Statement or Declaration expected inside program, found Variable name");
});
});
});
12 changes: 10 additions & 2 deletions scripts/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ function activate-conda() {
pandoc \
gcc \
gxx \
libcxx
libcxx \
rapidjson
RETURN_CODE=$?
if (( RETURN_CODE != EXIT_SUCCESS )); then
echo "`conda create -n $CONDA_ENV` failed with status $RETURN_CODE" 1>&2
Expand Down Expand Up @@ -143,7 +144,12 @@ function build-lfortran() {
return $EXIT_BUILD_FAILED
fi

cmake --fresh -DCMAKE_BUILD_TYPE=Debug -DWITH_LSP=yes -DWITH_LLVM=yes -DCMAKE_INSTALL_PREFIX=`pwd`/inst .
cmake --fresh \
-DCMAKE_BUILD_TYPE=Debug \
-DWITH_LSP=yes \
-DWITH_LLVM=yes \
-DWITH_JSON=yes \
-DCMAKE_INSTALL_PREFIX=`pwd`/inst .
RETURN_CODE=$?
if (( RETURN_CODE != EXIT_SUCCESS )); then
echo "cmake failed with status $RETURN_CODE" 1>&2
Expand Down Expand Up @@ -191,6 +197,8 @@ Usage: ./scripts/e2e.sh [OPTIONS]
Options:
-h|--help Print this help text.
-u|--update-lfortran Whether to update LFortran before running the end-to-end tests.
--headless Whether to run the tests in an XVFB framebuffer
(render off-screen; no visible window).
EOF
}

Expand Down
107 changes: 90 additions & 17 deletions server/src/lfortran-accessors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Range,
SymbolInformation,
SymbolKind,
TextEdit,
} from 'vscode-languageserver/node';

import {
Expand Down Expand Up @@ -55,6 +56,13 @@ export interface LFortranAccessor {
showErrors(uri: string,
text: string,
settings: ExampleSettings): Promise<Diagnostic[]>;

renameSymbol(uri: string,
text: string,
line: number,
column: number,
newName: string,
settings: ExampleSettings): Promise<TextEdit[]>;
}

/**
Expand Down Expand Up @@ -172,8 +180,8 @@ export class LFortranCLIAccessor implements LFortranAccessor {
}

async showDocumentSymbols(uri: string,
text: string,
settings: ExampleSettings): Promise<SymbolInformation[]> {
text: string,
settings: ExampleSettings): Promise<SymbolInformation[]> {
const flags = ["--show-document-symbols"];
const stdout = await this.runCompiler(settings, flags, text);
let results;
Expand Down Expand Up @@ -208,10 +216,10 @@ export class LFortranCLIAccessor implements LFortranAccessor {
}

async lookupName(uri: string,
text: string,
line: number,
column: number,
settings: ExampleSettings): Promise<DefinitionLink[]> {
text: string,
line: number,
column: number,
settings: ExampleSettings): Promise<DefinitionLink[]> {
try {
const flags = [
"--lookup-name",
Expand Down Expand Up @@ -247,27 +255,92 @@ export class LFortranCLIAccessor implements LFortranAccessor {
}

async showErrors(uri: string,
text: string,
settings: ExampleSettings): Promise<Diagnostic[]> {
text: string,
settings: ExampleSettings): Promise<Diagnostic[]> {
const diagnostics: Diagnostic[] = [];
let stdout: string | null = null;
try {
const flags = ["--show-errors"];
const stdout = await this.runCompiler(settings, flags, text);
const results: ErrorDiagnostics = JSON.parse(stdout);
if (results?.diagnostics) {
const k = Math.min(results.diagnostics.length, settings.maxNumberOfProblems);
for (let i = 0; i < k; i++) {
const diagnostic: Diagnostic = results.diagnostics[i];
diagnostic.severity = DiagnosticSeverity.Warning;
diagnostic.source = "lfortran-lsp";
diagnostics.push(diagnostic);
stdout = await this.runCompiler(settings, flags, text);
if (stdout.length > 0) {
let results: ErrorDiagnostics;
try {
results = JSON.parse(stdout);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// FIXME: Remove this repair logic once the respective bug has been
// fixed (lfortran/lfortran issue #5525)
// ----------------------------------------------------------------
console.warn("Failed to parse response, attempting to repair and re-parse it.");
const repaired: string = stdout.substring(0, 28) + "{" + stdout.substring(28);
try {
results = JSON.parse(repaired);
console.log("Repair succeeded, see: https://github.com/lfortran/lfortran/issues/5525");
} catch {
console.error("Failed to repair response");
throw error;
}
}
if (results?.diagnostics) {
const k = Math.min(results.diagnostics.length, settings.maxNumberOfProblems);
for (let i = 0; i < k; i++) {
const diagnostic: Diagnostic = results.diagnostics[i];
diagnostic.severity = DiagnosticSeverity.Warning;
diagnostic.source = "lfortran-lsp";
diagnostics.push(diagnostic);
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Failed to show errors");
if (stdout !== null) {
console.error("Failed to parse response: %s", stdout);
}
console.error(error);
}
return diagnostics;
}

async renameSymbol(uri: string,
text: string,
line: number,
column: number,
newName: string,
settings: ExampleSettings): Promise<TextEdit[]> {
const edits: TextEdit[] = [];
try {
const flags = [
"--rename-symbol",
"--line=" + (line + 1),
"--column=" + (column + 1)
];
const stdout = await this.runCompiler(settings, flags, text);
const obj = JSON.parse(stdout);
for (let i = 0, k = obj.length; i < k; i++) {
const location = obj[i].location;
if (location) {
const range: Range = location.range;

const start: Position = range.start;
start.character--;

const end: Position = range.end;
end.character--;

const edit: TextEdit = {
range: range,
newText: newName,
};

edits.push(edit);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Failed to rename symbol at line=%d, column=%d", line, column);
console.error(error);
}
return edits;
}
}
Loading

0 comments on commit 95aa8f1

Please sign in to comment.