Skip to content

Commit 8e86d44

Browse files
feat: look for fortls in the user Scripts folder on Windows
pip installs fortls in the %appdata%\Roaming\Python\Python311\Scripts\ folder, which is typically not in PATH, so the extension wouldn't find fortls after installing it. Now it also looks for fortls in this folder. Other changes: - The user configured path to fortls must now be absolute. This simplified a lot of things and it doesn't make sense to me to have multiple versions of fortls on the system, per workspace. Please let me know if this is not OK. - The fortls.disabled config value now gets stored in the USER settings instead of workspace. Similar reasons as above, it seems easier to find.
1 parent 68f4b3b commit 8e86d44

File tree

1 file changed

+110
-103
lines changed

1 file changed

+110
-103
lines changed

src/lsp/client.ts

Lines changed: 110 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,63 @@ export class FortlsClient {
3737
}
3838

3939
private client: LanguageClient | undefined;
40-
private version: string | undefined;
40+
private path: string | undefined; // path to the forls binary
41+
private version: string | undefined; // fortls version
4142
private readonly name: string = 'Fortran Language Server';
4243

4344
public async activate() {
44-
// Detect if fortls is present, download if missing or disable LS functionality
45-
// Do not allow activating the LS functionality if no fortls is detected
46-
await this.fortlsDownload().then(fortlsDisabled => {
47-
if (fortlsDisabled) return;
48-
workspace.onDidOpenTextDocument(this.didOpenTextDocument, this);
49-
workspace.textDocuments.forEach(this.didOpenTextDocument, this);
50-
workspace.onDidChangeWorkspaceFolders(event => {
51-
for (const folder of event.removed) {
52-
const client = clients.get(folder.uri.toString());
53-
if (client) {
54-
clients.delete(folder.uri.toString());
55-
client.stop();
45+
const config = workspace.getConfiguration(EXTENSION_ID);
46+
47+
if (!config.get['fortls.disabled']) {
48+
// Detect if fortls is present, download if missing or disable LS functionality
49+
// Do not allow activating the LS functionality if no fortls is detected
50+
const fortlsFound = this.getLSPath();
51+
52+
if (!fortlsFound) {
53+
const msg = `Forlts wasn't found on your system.
54+
It is highly recommended to use the fortls to enable IDE features like hover, peeking, GoTos and many more.
55+
For a full list of features the language server adds see: https://fortls.fortran-lang.org`;
56+
57+
const selection = window.showInformationMessage(msg, 'Install', 'Disable');
58+
selection.then(async opt => {
59+
if (opt === 'Install') {
60+
try {
61+
this.logger.info(`[lsp.client] Downloading ${LS_NAME}`);
62+
const msg = await pipInstall(LS_NAME);
63+
window.showInformationMessage(msg);
64+
this.logger.info(`[lsp.client] ${LS_NAME} installed`);
65+
66+
// restart this class
67+
this.deactivate();
68+
this.activate();
69+
70+
} catch (error) {
71+
this.logger.error(`[lsp.client] Error installing ${LS_NAME}: ${error}`);
72+
window.showErrorMessage(error);
73+
}
74+
} else if (opt == 'Disable') {
75+
config.update('fortls.disabled', true, vscode.ConfigurationTarget.Global);
76+
this.logger.info(`[lsp.client] ${LS_NAME} disabled in settings`);
5677
}
57-
}
58-
});
59-
});
78+
});
79+
80+
} else {
81+
workspace.onDidOpenTextDocument(this.didOpenTextDocument, this);
82+
workspace.textDocuments.forEach(this.didOpenTextDocument, this);
83+
workspace.onDidChangeWorkspaceFolders(event => {
84+
for (const folder of event.removed) {
85+
const client = clients.get(folder.uri.toString());
86+
if (client) {
87+
clients.delete(folder.uri.toString());
88+
client.stop();
89+
}
90+
}
91+
});
92+
}
93+
}
94+
6095
return;
96+
6197
}
6298

6399
public async deactivate(): Promise<void> {
@@ -84,7 +120,7 @@ export class FortlsClient {
84120
if (!isFortran(document)) return;
85121

86122
const args: string[] = await this.fortlsArguments();
87-
const executablePath: string = await this.fortlsPath(document);
123+
const executablePath: string = this.path;
88124

89125
// Detect language server version and verify selected options
90126
this.version = this.getLSVersion(executablePath, args);
@@ -251,6 +287,63 @@ export class FortlsClient {
251287
return args;
252288
}
253289

290+
/**
291+
* Tries to find fortls and saves its path to this.path.
292+
*
293+
* If a user path is configured, then only use this.
294+
* If not, try running fortls globally, or from python user scripts folder on Windows.
295+
*
296+
* @returns true if fortls found, false if not
297+
*/
298+
private getLSPath(): boolean {
299+
const config = workspace.getConfiguration(EXTENSION_ID);
300+
const configuredPath = resolveVariables(config.get<string>('fortls.path'));
301+
302+
const pathsToCheck: string[] = [];
303+
304+
// if there's a user configured path to the executable, check if it's absolute
305+
if (configuredPath !== '') {
306+
if (!path.isAbsolute(configuredPath)) {
307+
throw Error("The path to fortls (fortls.path) must be absolute.");
308+
}
309+
310+
pathsToCheck.push(configuredPath);
311+
312+
} else { // no user configured path => perform standard search for fortls
313+
314+
pathsToCheck.push('fortls');
315+
316+
// On Windows, `pip install fortls --user` installs fortls to the userbase\PythonXY\Scripts path,
317+
// so we want to look for it in this path as well.
318+
if (os.platform() == 'win32') {
319+
const result = spawnSync('python', ['-c', 'import site; print(site.getusersitepackages())']);
320+
const userSitePackagesStr = result.stdout.toString().trim();
321+
322+
// check if the call above returned something, in case the site module in python ever changes...
323+
if (userSitePackagesStr) {
324+
const userScriptsPath = path.resolve(userSitePackagesStr, '../Scripts/fortls');
325+
pathsToCheck.push(userScriptsPath);
326+
}
327+
}
328+
329+
}
330+
331+
// try to run `fortls --version` for all the given paths
332+
// if any succeed, save it to this.path and stop.
333+
for (const pathToCheck of pathsToCheck) {
334+
const result = spawnSync(pathToCheck, ['--version']);
335+
if (result.status == 0) {
336+
this.path = pathToCheck;
337+
this.logger.info('Successfully spawned fortls with path ' + pathToCheck);
338+
return true;
339+
} else {
340+
this.logger.info('Failed to spawn fortls with path ' + pathToCheck);
341+
}
342+
}
343+
344+
return false; // fortls not found
345+
}
346+
254347
/**
255348
* Check if `fortls` is present and the arguments being passed are correct
256349
* The presence check has already been done in the extension activate call
@@ -299,92 +392,6 @@ export class FortlsClient {
299392
return results.stdout.toString().trim();
300393
}
301394

302-
/**
303-
* Check if fortls is present in the system, if not show prompt to install/disable.
304-
* If disabling or erroring the function will return true.
305-
* For all normal cases it should return false.
306-
*
307-
* @returns false if the fortls has been detected or installed successfully
308-
*/
309-
private async fortlsDownload(): Promise<boolean> {
310-
const config = workspace.getConfiguration(EXTENSION_ID);
311-
const ls = await this.fortlsPath();
312-
313-
// Check for version, if this fails fortls provided is invalid
314-
const results = spawnSync(ls, ['--version']);
315-
const msg = `It is highly recommended to use the fortls to enable IDE features like hover, peeking, GoTos and many more.
316-
For a full list of features the language server adds see: https://fortls.fortran-lang.org`;
317-
return new Promise<boolean>(resolve => {
318-
if (results.error) {
319-
const selection = window.showInformationMessage(msg, 'Install', 'Disable');
320-
selection.then(async opt => {
321-
if (opt === 'Install') {
322-
try {
323-
this.logger.info(`[lsp.client] Downloading ${LS_NAME}`);
324-
const msg = await pipInstall(LS_NAME);
325-
window.showInformationMessage(msg);
326-
this.logger.info(`[lsp.client] ${LS_NAME} installed`);
327-
resolve(false);
328-
} catch (error) {
329-
this.logger.error(`[lsp.client] Error installing ${LS_NAME}: ${error}`);
330-
window.showErrorMessage(error);
331-
resolve(true);
332-
}
333-
} else if (opt == 'Disable') {
334-
config.update('fortls.disabled', true);
335-
this.logger.info(`[lsp.client] ${LS_NAME} disabled in settings`);
336-
resolve(true);
337-
}
338-
});
339-
} else {
340-
resolve(false);
341-
}
342-
});
343-
}
344-
345-
/**
346-
* Try and find the path to the `fortls` executable.
347-
* It will first try and fetch the top-most workspaceFolder from `document`.
348-
* If that fails because the document is standalone and does not belong in a
349-
* workspace it will assume that relative paths are wrt `os.homedir()`.
350-
*
351-
* If the `document` argument is missing, then it will try and find the
352-
* first workspaceFolder and use that as the root. If that fails it will
353-
* revert back to `os.homedir()`.
354-
*
355-
* @param document Optional textdocument
356-
* @returns a promise with the path to the fortls executable
357-
*/
358-
private async fortlsPath(document?: TextDocument): Promise<string> {
359-
// Get the workspace folder that contains the document, this can be undefined
360-
// which means that the document is standalone and not part of any workspace.
361-
let folder: vscode.WorkspaceFolder | undefined;
362-
if (document) {
363-
folder = workspace.getWorkspaceFolder(document.uri);
364-
}
365-
// If the document argument is missing, such as in the case of the Client's
366-
// activation, then try and fetch the first workspace folder to use as a root.
367-
else {
368-
folder = workspace.workspaceFolders[0] ? workspace.workspaceFolders[0] : undefined;
369-
}
370-
371-
// Get the outer most workspace folder to resolve relative paths, but if
372-
// the folder is undefined then use the home directory of the OS
373-
const root = folder ? getOuterMostWorkspaceFolder(folder).uri : vscode.Uri.parse(os.homedir());
374-
375-
const config = workspace.getConfiguration(EXTENSION_ID);
376-
let executablePath = resolveVariables(config.get<string>('fortls.path'));
377-
378-
// The path can be resolved as a relative path if:
379-
// 1. it does not have the default value `fortls` AND
380-
// 2. is not an absolute path
381-
if (executablePath !== 'fortls' && !path.isAbsolute(executablePath)) {
382-
this.logger.debug(`[lsp.client] Assuming relative fortls path is to ${root.fsPath}`);
383-
executablePath = path.join(root.fsPath, executablePath);
384-
}
385-
386-
return executablePath;
387-
}
388395

389396
/**
390397
* Restart the language server

0 commit comments

Comments
 (0)