Skip to content

Commit 142e8dc

Browse files
authored
Merge pull request #694 from fortran-lang/gnikit/issue693
bug(lsp): fixes relative path resolution in fortls
2 parents 0a64841 + 7117630 commit 142e8dc

File tree

6 files changed

+122
-13
lines changed

6 files changed

+122
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
6969

7070
### Fixed
7171

72+
- Fixed bugs in relative path resolution for `fortls`
73+
([#693](https://github.com/fortran-lang/vscode-fortran-support/issues/693))
7274
- Fixed issues with linter unittests running asynchronously
7375
([#623](https://github.com/fortran-lang/vscode-fortran-support/pull/623))
7476
- Fixed `npm run watch-dev` not syncing changes to spawned Extension Dev Host

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@
722722
"webpack": "webpack --mode production",
723723
"pretest": "npm run compile-dev && tsc -p tsconfig.test.json",
724724
"test": "npm run test:unit && npm run test:integration && npm run test:ui",
725-
"test:ui": "extest setup-and-run -i out/test/ui/*.js -m test/ui/.mocharc.js -s .vscode-test -e .vscode-test/extensions",
725+
"test:ui": "extest setup-and-run -i out/test/ui/*.js -m test/ui/.mocharc.js -s .vscode-test/ui -e .vscode-test/ui/extensions",
726726
"test:unit": "node ./out/test/unitTest/runTest.js",
727727
"test:integration": "node ./out/test/integration/runTest.js",
728728
"test:grammar-free": "vscode-tmgrammar-snap -s source.fortran.free -g ./syntaxes/fortran_free-form.tmLanguage.json \"./test/fortran/syntax/**/*{.f90,F90}\"",

src/lsp/client.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
import * as os from 'os';
34
import * as path from 'path';
45
import * as vscode from 'vscode';
56
import { spawnSync } from 'child_process';
@@ -13,7 +14,6 @@ import {
1314
getOuterMostWorkspaceFolder,
1415
pipInstall,
1516
resolveVariables,
16-
pathRelToAbs,
1717
} from '../lib/tools';
1818
import { Logger } from '../services/logging';
1919
import { RestartLS } from '../features/commands';
@@ -84,9 +84,7 @@ export class FortlsClient {
8484
if (!isFortran(document)) return;
8585

8686
const args: string[] = await this.fortlsArguments();
87-
const fortlsPath = workspace.getConfiguration(EXTENSION_ID).get<string>('fortls.path');
88-
let executablePath = resolveVariables(fortlsPath);
89-
if (fortlsPath !== 'fortls') executablePath = pathRelToAbs(fortlsPath, document.uri);
87+
const executablePath: string = await this.fortlsPath(document);
9088

9189
// Detect language server version and verify selected options
9290
this.version = this.getLSVersion(executablePath, args);
@@ -308,14 +306,7 @@ export class FortlsClient {
308306
*/
309307
private async fortlsDownload(): Promise<boolean> {
310308
const config = workspace.getConfiguration(EXTENSION_ID);
311-
let ls = resolveVariables(config.get<string>('fortls.path'));
312-
// The path can be resolved as a relative path if it's part of a workspace
313-
// AND it does not have the default value `fortls` or is an absolute path
314-
if (workspace.workspaceFolders == undefined && ls !== 'fortls' && !path.isAbsolute(ls)) {
315-
const root = workspace.workspaceFolders[0];
316-
this.logger.debug(`[lsp.client] Assuming relative fortls path is to ${root.uri.fsPath}`);
317-
ls = pathRelToAbs(ls, root.uri);
318-
}
309+
const ls = await this.fortlsPath();
319310

320311
// Check for version, if this fails fortls provided is invalid
321312
const results = spawnSync(ls, ['--version']);
@@ -349,6 +340,50 @@ export class FortlsClient {
349340
});
350341
}
351342

343+
/**
344+
* Try and find the path to the `fortls` executable.
345+
* It will first try and fetch the top-most workspaceFolder from `document`.
346+
* If that fails because the document is standalone and does not belong in a
347+
* workspace it will assume that relative paths are wrt `os.homedir()`.
348+
*
349+
* If the `document` argument is missing, then it will try and find the
350+
* first workspaceFolder and use that as the root. If that fails it will
351+
* revert back to `os.homedir()`.
352+
*
353+
* @param document Optional textdocument
354+
* @returns a promise with the path to the fortls executable
355+
*/
356+
private async fortlsPath(document?: TextDocument): Promise<string> {
357+
// Get the workspace folder that contains the document, this can be undefined
358+
// which means that the document is standalone and not part of any workspace.
359+
let folder: vscode.WorkspaceFolder | undefined;
360+
if (document) {
361+
folder = workspace.getWorkspaceFolder(document.uri);
362+
}
363+
// If the document argument is missing, such as in the case of the Client's
364+
// activation, then try and fetch the first workspace folder to use as a root.
365+
else {
366+
folder = workspace.workspaceFolders[0] ? workspace.workspaceFolders[0] : undefined;
367+
}
368+
369+
// Get the outer most workspace folder to resolve relative paths, but if
370+
// the folder is undefined then use the home directory of the OS
371+
const root = folder ? getOuterMostWorkspaceFolder(folder).uri : vscode.Uri.parse(os.homedir());
372+
373+
const config = workspace.getConfiguration(EXTENSION_ID);
374+
let executablePath = resolveVariables(config.get<string>('fortls.path'));
375+
376+
// The path can be resolved as a relative path if:
377+
// 1. it does not have the default value `fortls` AND
378+
// 2. is not an absolute path
379+
if (executablePath !== 'fortls' && !path.isAbsolute(executablePath)) {
380+
this.logger.debug(`[lsp.client] Assuming relative fortls path is to ${root.fsPath}`);
381+
executablePath = path.join(root.fsPath, executablePath);
382+
}
383+
384+
return executablePath;
385+
}
386+
352387
/**
353388
* Restart the language server
354389
*/

test/fortran/.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"DEBUG": "1",
88
"VAL": ""
99
},
10+
"fortran.fortls.path": "fortls",
1011
"fortran.fortls.disableAutoupdate": true,
1112
"fortran.fortls.preprocessor.definitions": {
1213
"HAVE_ZOLTAN": "",

test/integration/lsp-client.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,32 @@ suite('Language Server integration tests', () => {
4646
const res = server['client']?.initializeResult;
4747
strictEqual(JSON.stringify(ref), JSON.stringify(res));
4848
});
49+
50+
test('Restart the Language Server', async () => {
51+
await server['restartLS']();
52+
await delay(3000); // wait for server to initialize
53+
54+
const ref = {
55+
capabilities: {
56+
completionProvider: {
57+
resolveProvider: false,
58+
triggerCharacters: ['%'],
59+
},
60+
definitionProvider: true,
61+
documentSymbolProvider: true,
62+
referencesProvider: true,
63+
hoverProvider: true,
64+
implementationProvider: true,
65+
renameProvider: true,
66+
workspaceSymbolProvider: true,
67+
textDocumentSync: 2,
68+
signatureHelpProvider: {
69+
triggerCharacters: ['(', ','],
70+
},
71+
codeActionProvider: true,
72+
},
73+
};
74+
const res = server['client']?.initializeResult;
75+
strictEqual(JSON.stringify(ref), JSON.stringify(res));
76+
});
4977
});

test/unitTest/client.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
import { strictEqual } from 'assert';
4+
import { FortlsClient } from '../../src/lsp/client';
5+
import { Logger, LogLevel } from '../../src/services/logging';
6+
import { EXTENSION_ID } from '../../src/lib/tools';
7+
8+
const logger = new Logger(
9+
vscode.window.createOutputChannel('Modern Fortran', 'log'),
10+
LogLevel.DEBUG
11+
);
12+
13+
suite('Language Server integration tests', () => {
14+
const server = new FortlsClient(logger);
15+
const fileUri = vscode.Uri.file(path.resolve(__dirname, '../../../test/fortran/sample.f90'));
16+
const config = vscode.workspace.getConfiguration(EXTENSION_ID);
17+
const oldVal = config.get<string>('fortls.path');
18+
let doc: vscode.TextDocument;
19+
20+
suiteSetup(async () => {
21+
await config.update('fortls.path', './venv/bin/fortls', false);
22+
doc = await vscode.workspace.openTextDocument(fileUri);
23+
});
24+
25+
test('Local path resolution (Document URI)', async () => {
26+
const ls = await server['fortlsPath'](doc);
27+
const root = path.resolve(__dirname, '../../../test/fortran/');
28+
const ref = path.resolve(root, './venv/bin/fortls');
29+
console.log(`Reference: ${ref}`);
30+
strictEqual(ls, ref);
31+
});
32+
33+
test('Local path resolution (Document missing workspaceFolders[0])', async () => {
34+
const ls = await server['fortlsPath']();
35+
const root = path.resolve(__dirname, '../../../test/fortran/');
36+
const ref = path.resolve(root, './venv/bin/fortls');
37+
strictEqual(ls, ref);
38+
});
39+
40+
suiteTeardown(async () => {
41+
await config.update('fortls.path', oldVal, false);
42+
});
43+
});

0 commit comments

Comments
 (0)