Skip to content

Commit e261767

Browse files
authored
Merge pull request #448 from fortran-lang/gnikit/issue446
Gnikit/issue446
2 parents ab61821 + 58cf3df commit e261767

File tree

4 files changed

+143
-63
lines changed

4 files changed

+143
-63
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3434

3535
### Changed
3636

37+
- Merged Language Server's log channel to Modern Fortran's log channel
3738
- Merged all Fortran intrinsics into a single `json` file
3839
([#424](https://github.com/fortran-lang/vscode-fortran-support/issues/424))
3940
- Updates `README` text and animations, changes `SECURITY` and updates `package.json`
@@ -46,6 +47,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
4647

4748
### Added
4849

50+
- Added single file and multiple workspace folder support for the Language Server
51+
([#446](https://github.com/fortran-lang/vscode-fortran-support/issues/446))
52+
- Added file synchronization with VS Code settings and `.fortls` against the Language Server
4953
- Added unittests for the formatting providers
5054
([#423](https://github.com/fortran-lang/vscode-fortran-support/issues/423))
5155
- Added GitHub Actions environment to dependabot

Diff for: src/lib/tools.ts

+53-6
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@ export const envDelimiter: string = process.platform === 'win32' ? ';' : ':';
2020
*
2121
* @warning this should match the value on the package.json otherwise the extension
2222
* won't work at all
23-
*
24-
* @param folder `optional` WorkspaceFolder to search
23+
* @param path `optional` string for `pattern` path to include
2524
* @returns tuple of DocumentSelector
2625
*/
27-
export function FortranDocumentSelector(folder?: vscode.WorkspaceFolder) {
28-
if (folder) {
26+
export function FortranDocumentSelector(path?: string) {
27+
if (path) {
2928
return [
30-
{ scheme: 'file', language: 'FortranFreeForm', pattern: `${folder.uri.fsPath}/**/*` },
31-
{ scheme: 'file', language: 'FortranFixedForm', pattern: `${folder.uri.fsPath}/**/*` },
29+
{ scheme: 'file', language: 'FortranFreeForm', pattern: `${path}/**/*` },
30+
{ scheme: 'file', language: 'FortranFixedForm', pattern: `${path}/**/*` },
3231
];
3332
} else {
3433
return [
@@ -38,6 +37,54 @@ export function FortranDocumentSelector(folder?: vscode.WorkspaceFolder) {
3837
}
3938
}
4039

40+
export function isFortran(document: vscode.TextDocument): boolean {
41+
return (
42+
FortranDocumentSelector().some(e => e.scheme === document.uri.scheme) &&
43+
FortranDocumentSelector().some(e => e.language === document.languageId)
44+
);
45+
}
46+
47+
//
48+
// Taken with minimal alterations from lsp-multi-server-sample
49+
//
50+
51+
/**
52+
* Return in ascending order the workspace folders in an array of strings
53+
* @returns sorted workspace folders
54+
*/
55+
export function sortedWorkspaceFolders(): string[] | undefined {
56+
const workspaceFolders = vscode.workspace.workspaceFolders
57+
? vscode.workspace.workspaceFolders
58+
.map(folder => {
59+
let result = folder.uri.toString();
60+
if (result.charAt(result.length - 1) !== '/') result = result + '/';
61+
return result;
62+
})
63+
.sort((a, b) => {
64+
return a.length - b.length;
65+
})
66+
: [];
67+
return workspaceFolders;
68+
}
69+
70+
/**
71+
* Locate the top most workspace folder for a given file
72+
* @param folder workspace folder
73+
* @returns outer most workspace folder
74+
*/
75+
export function getOuterMostWorkspaceFolder(
76+
folder: vscode.WorkspaceFolder
77+
): vscode.WorkspaceFolder {
78+
const sorted = sortedWorkspaceFolders();
79+
for (const element of sorted) {
80+
let uri = folder.uri.toString();
81+
if (uri.charAt(uri.length - 1) !== '/') uri = uri + '/';
82+
if (uri.startsWith(element))
83+
return vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(element))!;
84+
}
85+
return folder;
86+
}
87+
4188
/**
4289
* Install a package either a Python pip package or a VS Marketplace Extension.
4390
*

Diff for: src/lsp/client.ts

+77-56
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
// Modified version of Chris Hansen's Fortran Intellisense
2-
31
'use strict';
42

3+
import * as path from 'path';
54
import * as vscode from 'vscode';
65
import { spawnSync } from 'child_process';
7-
import { commands, window, workspace, TextDocument, WorkspaceFolder } from 'vscode';
6+
import { commands, window, workspace, TextDocument } from 'vscode';
87
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
9-
import { EXTENSION_ID, FortranDocumentSelector, LS_NAME } from '../lib/tools';
8+
import {
9+
EXTENSION_ID,
10+
FortranDocumentSelector,
11+
LS_NAME,
12+
isFortran,
13+
getOuterMostWorkspaceFolder,
14+
} from '../lib/tools';
1015
import { LoggingService } from '../services/logging-service';
1116
import { RestartLS } from '../features/commands';
1217

@@ -15,31 +20,6 @@ import { RestartLS } from '../features/commands';
1520
// the server
1621
export const clients: Map<string, LanguageClient> = new Map();
1722

18-
/**
19-
* Checks if the Language Server should run in the current workspace and return
20-
* the workspace folder if it should else return undefined.
21-
* @param document the active VS Code editor
22-
* @returns the root workspace folder or undefined
23-
*/
24-
export function checkLanguageServerActivation(document: TextDocument): WorkspaceFolder | undefined {
25-
// We are only interested in Fortran files
26-
if (
27-
!FortranDocumentSelector().some(e => e.scheme === document.uri.scheme) ||
28-
!FortranDocumentSelector().some(e => e.language === document.languageId)
29-
) {
30-
return undefined;
31-
}
32-
const uri = document.uri;
33-
const folder = workspace.getWorkspaceFolder(uri);
34-
// Files outside a folder can't be handled. This might depend on the language.
35-
// Single file languages like JSON might handle files outside the workspace folders.
36-
// This will be undefined if the file does not belong to the workspace
37-
if (!folder) return undefined;
38-
if (clients.has(folder.uri.toString())) return undefined;
39-
40-
return folder;
41-
}
42-
4323
export class FortlsClient {
4424
constructor(private logger: LoggingService, private context?: vscode.ExtensionContext) {
4525
this.logger.logInfo('Fortran Language Server');
@@ -54,7 +34,8 @@ export class FortlsClient {
5434
}
5535

5636
private client: LanguageClient | undefined;
57-
private _fortlsVersion: string | undefined;
37+
private version: string | undefined;
38+
private readonly name: string = 'Fortran Language Server';
5839

5940
public async activate() {
6041
// Detect if fortls is present, download if missing or disable LS functionality
@@ -63,6 +44,15 @@ export class FortlsClient {
6344
if (fortlsDisabled) return;
6445
workspace.onDidOpenTextDocument(this.didOpenTextDocument, this);
6546
workspace.textDocuments.forEach(this.didOpenTextDocument, this);
47+
workspace.onDidChangeWorkspaceFolders(event => {
48+
for (const folder of event.removed) {
49+
const client = clients.get(folder.uri.toString());
50+
if (client) {
51+
clients.delete(folder.uri.toString());
52+
client.stop();
53+
}
54+
}
55+
});
6656
});
6757
return;
6858
}
@@ -88,43 +78,74 @@ export class FortlsClient {
8878
* @returns
8979
*/
9080
private async didOpenTextDocument(document: TextDocument): Promise<void> {
91-
const folder = checkLanguageServerActivation(document);
92-
if (!folder) return;
93-
94-
this.logger.logInfo('Initialising the Fortran Language Server');
81+
if (!isFortran(document)) return;
9582

9683
const args: string[] = await this.fortlsArguments();
9784
const executablePath = workspace.getConfiguration(EXTENSION_ID).get<string>('fortls.path');
9885

9986
// Detect language server version and verify selected options
100-
this._fortlsVersion = this.getLSVersion(executablePath, args);
101-
if (this._fortlsVersion) {
102-
const serverOptions: ServerOptions = {
103-
command: executablePath,
104-
args: args,
105-
};
87+
this.version = this.getLSVersion(executablePath, args);
88+
if (!this.version) return;
89+
const serverOptions: ServerOptions = {
90+
command: executablePath,
91+
args: args,
92+
};
10693

94+
let folder = workspace.getWorkspaceFolder(document.uri);
95+
96+
/**
97+
* The strategy for registering the language server is to register an individual
98+
* server for the top-most workspace folder. If we are outside of a workspace
99+
* then we register the server for folder the standalone Fortran file is located.
100+
* This has to be done in order for standalone server to NOT interfere with
101+
* the workspace server.
102+
*
103+
* We also set the log channel to the Modern Fortran log output and add
104+
* system watchers for the default configuration file .fortls and the
105+
* configuration settings for the entire extension.
106+
*/
107+
108+
// If the document is part of a standalone file and not part of a workspace
109+
if (!folder) {
110+
const fileRoot: string = path.dirname(document.uri.fsPath);
111+
if (clients.has(fileRoot)) return; // already registered
112+
this.logger.logInfo('Initialising Language Server for file: ' + document.uri.fsPath);
107113
// Options to control the language client
108114
const clientOptions: LanguageClientOptions = {
109-
// Register the server for all Fortran documents in workspace
110-
// NOTE: remove the folder args and workspaceFolder to ge single file language servers
111-
// will also have to change the checkLanguageServerActivation method
112-
// we want fortran documents but not just in the workspace
113-
// if in the workspace do provide
114-
documentSelector: FortranDocumentSelector(folder),
115+
documentSelector: FortranDocumentSelector(fileRoot),
116+
outputChannel: this.logger.getOutputChannel(),
117+
synchronize: {
118+
// Synchronize all configuration settings to the server
119+
configurationSection: EXTENSION_ID,
120+
// Notify the server about file changes to '.fortls files contained in the workspace
121+
// fileEvents: workspace.createFileSystemWatcher('**/.fortls'),
122+
},
123+
};
124+
this.client = new LanguageClient(LS_NAME, this.name, serverOptions, clientOptions);
125+
this.client.start();
126+
clients.set(fileRoot, this.client); // Add the client to the global map
127+
return;
128+
}
129+
// The document is part of a workspace folder
130+
if (!clients.has(folder.uri.toString())) {
131+
folder = getOuterMostWorkspaceFolder(folder);
132+
if (clients.has(folder.uri.toString())) return; // already registered
133+
this.logger.logInfo('Initialising Language Server for workspace: ' + folder.uri.fsPath);
134+
// Options to control the language client
135+
const clientOptions: LanguageClientOptions = {
136+
documentSelector: FortranDocumentSelector(folder.uri.fsPath),
115137
workspaceFolder: folder,
138+
outputChannel: this.logger.getOutputChannel(),
139+
synchronize: {
140+
// Synchronize all configuration settings to the server
141+
configurationSection: EXTENSION_ID,
142+
// Notify the server about file changes to '.fortls files contained in the workspace
143+
// fileEvents: workspace.createFileSystemWatcher('**/.fortls'),
144+
},
116145
};
117-
118-
// Create the language client, start the client and add it to the registry
119-
this.client = new LanguageClient(
120-
LS_NAME,
121-
'Fortran Language Server',
122-
serverOptions,
123-
clientOptions
124-
);
146+
this.client = new LanguageClient(LS_NAME, this.name, serverOptions, clientOptions);
125147
this.client.start();
126-
// Add the Language Client to the global map
127-
clients.set(folder.uri.toString(), this.client);
148+
clients.set(folder.uri.toString(), this.client); // Add the client to the global map
128149
}
129150
}
130151

Diff for: src/services/logging-service.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { window } from 'vscode';
1+
import { OutputChannel, window } from 'vscode';
22

33
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'NONE';
44

@@ -11,6 +11,14 @@ export class LoggingService {
1111
this.logLevel = logLevel;
1212
}
1313

14+
public getOutputLevel(): LogLevel {
15+
return this.logLevel;
16+
}
17+
18+
public getOutputChannel(): OutputChannel {
19+
return this.outputChannel;
20+
}
21+
1422
/**
1523
* Append messages to the output channel and format it with a title
1624
*

0 commit comments

Comments
 (0)