@@ -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