Skip to content

Gnikit/issue446 #448

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

Merged
merged 3 commits into from
Apr 29, 2022
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Changed

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

### Added

- Added single file and multiple workspace folder support for the Language Server
([#446](https://github.com/fortran-lang/vscode-fortran-support/issues/446))
- Added file synchronization with VS Code settings and `.fortls` against the Language Server
- Added unittests for the formatting providers
([#423](https://github.com/fortran-lang/vscode-fortran-support/issues/423))
- Added GitHub Actions environment to dependabot
Expand Down
59 changes: 53 additions & 6 deletions src/lib/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ export const envDelimiter: string = process.platform === 'win32' ? ';' : ':';
*
* @warning this should match the value on the package.json otherwise the extension
* won't work at all
*
* @param folder `optional` WorkspaceFolder to search
* @param path `optional` string for `pattern` path to include
* @returns tuple of DocumentSelector
*/
export function FortranDocumentSelector(folder?: vscode.WorkspaceFolder) {
if (folder) {
export function FortranDocumentSelector(path?: string) {
if (path) {
return [
{ scheme: 'file', language: 'FortranFreeForm', pattern: `${folder.uri.fsPath}/**/*` },
{ scheme: 'file', language: 'FortranFixedForm', pattern: `${folder.uri.fsPath}/**/*` },
{ scheme: 'file', language: 'FortranFreeForm', pattern: `${path}/**/*` },
{ scheme: 'file', language: 'FortranFixedForm', pattern: `${path}/**/*` },
];
} else {
return [
Expand All @@ -38,6 +37,54 @@ export function FortranDocumentSelector(folder?: vscode.WorkspaceFolder) {
}
}

export function isFortran(document: vscode.TextDocument): boolean {
return (
FortranDocumentSelector().some(e => e.scheme === document.uri.scheme) &&
FortranDocumentSelector().some(e => e.language === document.languageId)
);
}

//
// Taken with minimal alterations from lsp-multi-server-sample
//

/**
* Return in ascending order the workspace folders in an array of strings
* @returns sorted workspace folders
*/
export function sortedWorkspaceFolders(): string[] | undefined {
const workspaceFolders = vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders
.map(folder => {
let result = folder.uri.toString();
if (result.charAt(result.length - 1) !== '/') result = result + '/';
return result;
})
.sort((a, b) => {
return a.length - b.length;
})
: [];
return workspaceFolders;
}

/**
* Locate the top most workspace folder for a given file
* @param folder workspace folder
* @returns outer most workspace folder
*/
export function getOuterMostWorkspaceFolder(
folder: vscode.WorkspaceFolder
): vscode.WorkspaceFolder {
const sorted = sortedWorkspaceFolders();
for (const element of sorted) {
let uri = folder.uri.toString();
if (uri.charAt(uri.length - 1) !== '/') uri = uri + '/';
if (uri.startsWith(element))
return vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(element))!;
}
return folder;
}

/**
* Install a package either a Python pip package or a VS Marketplace Extension.
*
Expand Down
133 changes: 77 additions & 56 deletions src/lsp/client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// Modified version of Chris Hansen's Fortran Intellisense

'use strict';

import * as path from 'path';
import * as vscode from 'vscode';
import { spawnSync } from 'child_process';
import { commands, window, workspace, TextDocument, WorkspaceFolder } from 'vscode';
import { commands, window, workspace, TextDocument } from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
import { EXTENSION_ID, FortranDocumentSelector, LS_NAME } from '../lib/tools';
import {
EXTENSION_ID,
FortranDocumentSelector,
LS_NAME,
isFortran,
getOuterMostWorkspaceFolder,
} from '../lib/tools';
import { LoggingService } from '../services/logging-service';
import { RestartLS } from '../features/commands';

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

/**
* Checks if the Language Server should run in the current workspace and return
* the workspace folder if it should else return undefined.
* @param document the active VS Code editor
* @returns the root workspace folder or undefined
*/
export function checkLanguageServerActivation(document: TextDocument): WorkspaceFolder | undefined {
// We are only interested in Fortran files
if (
!FortranDocumentSelector().some(e => e.scheme === document.uri.scheme) ||
!FortranDocumentSelector().some(e => e.language === document.languageId)
) {
return undefined;
}
const uri = document.uri;
const folder = workspace.getWorkspaceFolder(uri);
// Files outside a folder can't be handled. This might depend on the language.
// Single file languages like JSON might handle files outside the workspace folders.
// This will be undefined if the file does not belong to the workspace
if (!folder) return undefined;
if (clients.has(folder.uri.toString())) return undefined;

return folder;
}

export class FortlsClient {
constructor(private logger: LoggingService, private context?: vscode.ExtensionContext) {
this.logger.logInfo('Fortran Language Server');
Expand All @@ -54,7 +34,8 @@ export class FortlsClient {
}

private client: LanguageClient | undefined;
private _fortlsVersion: string | undefined;
private version: string | undefined;
private readonly name: string = 'Fortran Language Server';

public async activate() {
// Detect if fortls is present, download if missing or disable LS functionality
Expand All @@ -63,6 +44,15 @@ export class FortlsClient {
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();
}
}
});
});
return;
}
Expand All @@ -88,43 +78,74 @@ export class FortlsClient {
* @returns
*/
private async didOpenTextDocument(document: TextDocument): Promise<void> {
const folder = checkLanguageServerActivation(document);
if (!folder) return;

this.logger.logInfo('Initialising the Fortran Language Server');
if (!isFortran(document)) return;

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

// Detect language server version and verify selected options
this._fortlsVersion = this.getLSVersion(executablePath, args);
if (this._fortlsVersion) {
const serverOptions: ServerOptions = {
command: executablePath,
args: args,
};
this.version = this.getLSVersion(executablePath, args);
if (!this.version) return;
const serverOptions: ServerOptions = {
command: executablePath,
args: args,
};

let folder = workspace.getWorkspaceFolder(document.uri);

/**
* The strategy for registering the language server is to register an individual
* server for the top-most workspace folder. If we are outside of a workspace
* then we register the server for folder the standalone Fortran file is located.
* This has to be done in order for standalone server to NOT interfere with
* the workspace server.
*
* We also set the log channel to the Modern Fortran log output and add
* system watchers for the default configuration file .fortls and the
* configuration settings for the entire extension.
*/

// If the document is part of a standalone file and not part of a workspace
if (!folder) {
const fileRoot: string = path.dirname(document.uri.fsPath);
if (clients.has(fileRoot)) return; // already registered
this.logger.logInfo('Initialising Language Server for file: ' + document.uri.fsPath);
// Options to control the language client
const clientOptions: LanguageClientOptions = {
// Register the server for all Fortran documents in workspace
// NOTE: remove the folder args and workspaceFolder to ge single file language servers
// will also have to change the checkLanguageServerActivation method
// we want fortran documents but not just in the workspace
// if in the workspace do provide
documentSelector: FortranDocumentSelector(folder),
documentSelector: FortranDocumentSelector(fileRoot),
outputChannel: this.logger.getOutputChannel(),
synchronize: {
// Synchronize all configuration settings to the server
configurationSection: EXTENSION_ID,
// Notify the server about file changes to '.fortls files contained in the workspace
// fileEvents: workspace.createFileSystemWatcher('**/.fortls'),
},
};
this.client = new LanguageClient(LS_NAME, this.name, serverOptions, clientOptions);
this.client.start();
clients.set(fileRoot, this.client); // Add the client to the global map
return;
}
// The document is part of a workspace folder
if (!clients.has(folder.uri.toString())) {
folder = getOuterMostWorkspaceFolder(folder);
if (clients.has(folder.uri.toString())) return; // already registered
this.logger.logInfo('Initialising Language Server for workspace: ' + folder.uri.fsPath);
// Options to control the language client
const clientOptions: LanguageClientOptions = {
documentSelector: FortranDocumentSelector(folder.uri.fsPath),
workspaceFolder: folder,
outputChannel: this.logger.getOutputChannel(),
synchronize: {
// Synchronize all configuration settings to the server
configurationSection: EXTENSION_ID,
// Notify the server about file changes to '.fortls files contained in the workspace
// fileEvents: workspace.createFileSystemWatcher('**/.fortls'),
},
};

// Create the language client, start the client and add it to the registry
this.client = new LanguageClient(
LS_NAME,
'Fortran Language Server',
serverOptions,
clientOptions
);
this.client = new LanguageClient(LS_NAME, this.name, serverOptions, clientOptions);
this.client.start();
// Add the Language Client to the global map
clients.set(folder.uri.toString(), this.client);
clients.set(folder.uri.toString(), this.client); // Add the client to the global map
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/services/logging-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { window } from 'vscode';
import { OutputChannel, window } from 'vscode';

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

Expand All @@ -11,6 +11,14 @@ export class LoggingService {
this.logLevel = logLevel;
}

public getOutputLevel(): LogLevel {
return this.logLevel;
}

public getOutputChannel(): OutputChannel {
return this.outputChannel;
}

/**
* Append messages to the output channel and format it with a title
*
Expand Down