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

fix lookup and automatic installation of fortls on Windows #772

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@
"properties": {
"fortran.fortls.path": {
"type": "string",
"default": "fortls",
"markdownDescription": "Path to the Fortran language server (`fortls`).",
"default": "",
"markdownDescription": "Path to the Fortran language server (`fortls`) (must be absolute).",
"order": 10
},
"fortran.fortls.configure": {
Expand Down Expand Up @@ -742,7 +742,7 @@
"posttest": "npm run format",
"lint": "eslint . --ext .ts,.tsx",
"lint-fix": "npm run lint -- --fix",
"format": "prettier --write 'src/**/*.{ts,json}' 'test/**/*.ts' 'syntaxes/**/*.json' 'snippets/**/*.json' './**/*.{md,json,yaml,yml}'",
"format": "prettier --write --end-of-line auto src/**/*.{ts,json} test/**/*.ts syntaxes/**/*.json snippets/**/*.json ./**/*.{md,json,yaml,yml}",
"prepare": "husky install",
"pre-commit": "lint-staged",
"coverage": "c8 --clean npm run test"
Expand Down
2 changes: 1 addition & 1 deletion schemas/fortls.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,4 @@
"type": "boolean"
}
}
}
}
43 changes: 41 additions & 2 deletions src/lib/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,48 @@ export async function promptForMissingTool(
* @param pyPackage name of python package in PyPi
*/
export async function pipInstall(pyPackage: string): Promise<string> {
const py = 'python3'; // Fetches the top-most python in the Shell
const py = await checkPython();

const args = ['-m', 'pip', 'install', '--user', '--upgrade', pyPackage];
return await shellTask(py, args, `pip: ${pyPackage}`);
return await shellTask(py, args, `python3 -m pip install ${pyPackage}`);
}

/**
* Checks whether python can be called from the shell.
*
* Tries `python` on Windows and `python3` on other platforms.
*
* TODO: this could also check for python version, which has to be > 3.7 for fortls.
*
* @returns name of the command to run python on the current platform
*/
export async function checkPython(): Promise<string> {
let py = '';
if (os.platform() == 'win32') {
py = 'python';
} else {
py = 'python3';
}
const args = ['--version'];

try {
await shellTask(py, args, 'getting python version');
return py;
} catch (e) {
let errMsg = '';
if (os.platform() == 'win32') {
errMsg =
py +
" isn't callable from the shell. " +
'Please make sure python is installed and added to the PATH.';
} else {
errMsg = py + " isn't callable from the shell. Please make sure python is installed";
}

return await new Promise<string>((result, reject) => {
reject(errMsg);
});
}
}

export async function shellTask(command: string, args: string[], name: string): Promise<string> {
Expand Down
221 changes: 117 additions & 104 deletions src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,66 @@ export class FortlsClient {
}

private client: LanguageClient | undefined;
private version: string | undefined;
private path: string | undefined; // path to the forls binary
private version: string | undefined; // fortls version
private readonly name: string = 'Fortran Language Server';

public async activate() {
// Detect if fortls is present, download if missing or disable LS functionality
// Do not allow activating the LS functionality if no fortls is detected
await this.fortlsDownload().then(fortlsDisabled => {
if (fortlsDisabled) return;
workspace.onDidOpenTextDocument(this.didOpenTextDocument, this);
workspace.textDocuments.forEach(this.didOpenTextDocument, this);
workspace.onDidChangeWorkspaceFolders(event => {
for (const folder of event.removed) {
const client = clients.get(folder.uri.toString());
if (client) {
clients.delete(folder.uri.toString());
client.stop();
const config = workspace.getConfiguration(EXTENSION_ID);

if (!config.get['fortls.disabled']) {
// Detect if fortls is present, download if missing or disable LS functionality
// Do not allow activating the LS functionality if no fortls is detected
const fortlsFound = this.getLSPath();

const configuredPath = resolveVariables(config.get<string>('fortls.path'));
if (configuredPath) {
const msg = `Failed to run fortls from user configured path '` + configuredPath + `'`;
await window.showErrorMessage(msg);
return;
}

if (!fortlsFound) {
const msg = `Forlts wasn't found on your system.
It is highly recommended to use the fortls to enable IDE features like hover, peeking, GoTos and many more.
For a full list of features the language server adds see: https://fortls.fortran-lang.org`;

const selection = window.showInformationMessage(msg, 'Install', 'Disable');
selection.then(async opt => {
if (opt === 'Install') {
try {
this.logger.info(`[lsp.client] Downloading ${LS_NAME}`);
const msg = await pipInstall(LS_NAME);
window.showInformationMessage(msg);
this.logger.info(`[lsp.client] ${LS_NAME} installed`);

// restart this class
this.deactivate();
this.activate();
} catch (error) {
this.logger.error(`[lsp.client] Error installing ${LS_NAME}: ${error}`);
window.showErrorMessage(error);
}
} else if (opt == 'Disable') {
config.update('fortls.disabled', true, vscode.ConfigurationTarget.Global);
this.logger.info(`[lsp.client] ${LS_NAME} disabled in settings`);
}
}
});
});
});
} else {
workspace.onDidOpenTextDocument(this.didOpenTextDocument, this);
workspace.textDocuments.forEach(this.didOpenTextDocument, this);
workspace.onDidChangeWorkspaceFolders(event => {
for (const folder of event.removed) {
const client = clients.get(folder.uri.toString());
if (client) {
clients.delete(folder.uri.toString());
client.stop();
}
}
});
}
}

return;
}

Expand Down Expand Up @@ -84,7 +124,7 @@ export class FortlsClient {
if (!isFortran(document)) return;

const args: string[] = await this.fortlsArguments();
const executablePath: string = await this.fortlsPath(document);
const executablePath: string = this.path;

// Detect language server version and verify selected options
this.version = this.getLSVersion(executablePath, args);
Expand Down Expand Up @@ -251,6 +291,66 @@ export class FortlsClient {
return args;
}

/**
* Tries to find fortls and saves its path to this.path.
*
* If a user path is configured, then only use this.
* If not, try running fortls globally, or from python user scripts folder on Windows.
*
* @returns true if fortls found, false if not
*/
private getLSPath(): boolean {
const config = workspace.getConfiguration(EXTENSION_ID);
const configuredPath = resolveVariables(config.get<string>('fortls.path'));

const pathsToCheck: string[] = [];

// if there's a user configured path to the executable, check if it's absolute
if (configuredPath !== '') {
if (!path.isAbsolute(configuredPath)) {
window.showErrorMessage('The path to fortls (fortran.fortls.path) must be absolute.');
return false;
}

pathsToCheck.push(configuredPath);
} else {
// no user configured path => perform standard search for fortls

pathsToCheck.push('fortls');

// On Windows, `pip install fortls --user` installs fortls to the userbase\PythonXY\Scripts path,
// so we want to look for it in this path as well.
if (os.platform() == 'win32') {
const result = spawnSync('python', [
'-c',
'import site; print(site.getusersitepackages())',
]);
const userSitePackagesStr = result.stdout.toString().trim();

// check if the call above returned something, in case the site module in python ever changes...
if (userSitePackagesStr) {
const userScriptsPath = path.resolve(userSitePackagesStr, '../Scripts/fortls');
pathsToCheck.push(userScriptsPath);
}
}
}

// try to run `fortls --version` for all the given paths
// if any succeed, save it to this.path and stop.
for (const pathToCheck of pathsToCheck) {
const result = spawnSync(pathToCheck, ['--version']);
if (result.status == 0) {
this.path = pathToCheck;
this.logger.info('Successfully spawned fortls with path ' + pathToCheck);
return true;
} else {
this.logger.info('Failed to spawn fortls with path ' + pathToCheck);
}
}

return false; // fortls not found
}

/**
* Check if `fortls` is present and the arguments being passed are correct
* The presence check has already been done in the extension activate call
Expand Down Expand Up @@ -299,93 +399,6 @@ export class FortlsClient {
return results.stdout.toString().trim();
}

/**
* Check if fortls is present in the system, if not show prompt to install/disable.
* If disabling or erroring the function will return true.
* For all normal cases it should return false.
*
* @returns false if the fortls has been detected or installed successfully
*/
private async fortlsDownload(): Promise<boolean> {
const config = workspace.getConfiguration(EXTENSION_ID);
const ls = await this.fortlsPath();

// Check for version, if this fails fortls provided is invalid
const results = spawnSync(ls, ['--version']);
const msg = `It is highly recommended to use the fortls to enable IDE features like hover, peeking, GoTos and many more.
For a full list of features the language server adds see: https://fortls.fortran-lang.org`;
return new Promise<boolean>(resolve => {
if (results.error) {
const selection = window.showInformationMessage(msg, 'Install', 'Disable');
selection.then(async opt => {
if (opt === 'Install') {
try {
this.logger.info(`[lsp.client] Downloading ${LS_NAME}`);
const msg = await pipInstall(LS_NAME);
window.showInformationMessage(msg);
this.logger.info(`[lsp.client] ${LS_NAME} installed`);
resolve(false);
} catch (error) {
this.logger.error(`[lsp.client] Error installing ${LS_NAME}: ${error}`);
window.showErrorMessage(error);
resolve(true);
}
} else if (opt == 'Disable') {
config.update('fortls.disabled', true);
this.logger.info(`[lsp.client] ${LS_NAME} disabled in settings`);
resolve(true);
}
});
} else {
resolve(false);
}
});
}

/**
* Try and find the path to the `fortls` executable.
* It will first try and fetch the top-most workspaceFolder from `document`.
* If that fails because the document is standalone and does not belong in a
* workspace it will assume that relative paths are wrt `os.homedir()`.
*
* If the `document` argument is missing, then it will try and find the
* first workspaceFolder and use that as the root. If that fails it will
* revert back to `os.homedir()`.
*
* @param document Optional textdocument
* @returns a promise with the path to the fortls executable
*/
private async fortlsPath(document?: TextDocument): Promise<string> {
// Get the workspace folder that contains the document, this can be undefined
// which means that the document is standalone and not part of any workspace.
let folder: vscode.WorkspaceFolder | undefined;
if (document) {
folder = workspace.getWorkspaceFolder(document.uri);
}
// If the document argument is missing, such as in the case of the Client's
// activation, then try and fetch the first workspace folder to use as a root.
else {
folder = workspace.workspaceFolders[0] ? workspace.workspaceFolders[0] : undefined;
}

// Get the outer most workspace folder to resolve relative paths, but if
// the folder is undefined then use the home directory of the OS
const root = folder ? getOuterMostWorkspaceFolder(folder).uri : vscode.Uri.parse(os.homedir());

const config = workspace.getConfiguration(EXTENSION_ID);
let executablePath = resolveVariables(config.get<string>('fortls.path'));

// The path can be resolved as a relative path if:
// 1. it does not have the default value `fortls` AND
// 2. is not an absolute path
if (executablePath !== 'fortls' && !path.isAbsolute(executablePath)) {
this.logger.debug(`[lsp.client] Assuming relative fortls path is to ${root.fsPath}`);
executablePath = path.join(root.fsPath, executablePath);
}

return executablePath;
}

/**
* Restart the language server
*/
Expand Down