mirror of
https://github.com/fluencelabs/aqua-vscode
synced 2025-04-25 00:22:15 +00:00
feat(vscode): Retrieve imports from fluence CLI [LNG-252] (#49)
* Fix settings definition * Fix onDidChangeConfiguration event * Implement settings manager * Remove undefined * Refactor
This commit is contained in:
parent
780246c71e
commit
cbff64a0a2
@ -1,5 +1,5 @@
|
||||
import * as path from 'path';
|
||||
import { ExtensionContext, workspace } from 'vscode';
|
||||
import type { ExtensionContext } from 'vscode';
|
||||
|
||||
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node';
|
||||
|
||||
@ -30,10 +30,6 @@ export function activate(context: ExtensionContext) {
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
// Register the server for aqua source files
|
||||
documentSelector: [{ pattern: '**/*.aqua' }],
|
||||
synchronize: {
|
||||
// Notify the server about file changes to '.clientrc files contained in the workspace
|
||||
fileEvents: workspace.createFileSystemWatcher('**/.clientrc'),
|
||||
},
|
||||
};
|
||||
|
||||
// Create the language client and start the client.
|
||||
|
30
package.json
30
package.json
@ -62,19 +62,23 @@
|
||||
"type": "object",
|
||||
"title": "Aqua",
|
||||
"properties": {
|
||||
"aquaSettings": {
|
||||
"imports": {
|
||||
"scope": "resource",
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"description": "Adds imports for aqua file or project"
|
||||
},
|
||||
"enableLegacyAutoImportSearch": {
|
||||
"scope": "resource",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do not look for extra imports"
|
||||
}
|
||||
"aquaSettings.imports": {
|
||||
"scope": "resource",
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"description": "Adds imports for aqua file or project"
|
||||
},
|
||||
"aquaSettings.enableLegacyAutoImportSearch": {
|
||||
"scope": "resource",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do not look for extra imports"
|
||||
},
|
||||
"aquaSettings.fluencePath": {
|
||||
"scope": "resource",
|
||||
"type": ["string", "null"],
|
||||
"default": null,
|
||||
"description": "Path to fluence CLI executable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
45
server/src/cli.ts
Normal file
45
server/src/cli.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
export class FluenceCli {
|
||||
readonly cliPath: string;
|
||||
|
||||
/**
|
||||
* @param cliPath Path to `fluence` executable.
|
||||
* Defaults to `fluence` (i.e. it should be in PATH).
|
||||
*/
|
||||
constructor(cliPath?: string) {
|
||||
this.cliPath = cliPath || 'fluence';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns output of `fluence aqua imports`
|
||||
*/
|
||||
async imports(): Promise<string[]> {
|
||||
const result = await this.runJson(['aqua', 'imports']);
|
||||
if (Array.isArray(result) && result.every((i) => typeof i === 'string')) {
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(`Invalid result: ${JSON.stringify(result)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `fluence` with given arguments and returns its stdout as JSON.
|
||||
*/
|
||||
private async runJson(args: string[]): Promise<JSON> {
|
||||
const cmd = `${this.cliPath} ${args.join(' ')}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(cmd, (err, stdout, _) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else
|
||||
try {
|
||||
const result = JSON.parse(stdout);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import {
|
||||
createConnection,
|
||||
DiagnosticSeverity,
|
||||
DidChangeConfigurationNotification,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
ProposedFeatures,
|
||||
@ -13,6 +15,8 @@ import type { DefinitionParams, Location } from 'vscode-languageserver';
|
||||
import type { TokenLink } from '@fluencelabs/aqua-language-server-api/aqua-lsp-api';
|
||||
|
||||
import { compileAqua } from './validation';
|
||||
import { FluenceCli } from './cli';
|
||||
import { Settings, SettingsManager } from './settings';
|
||||
|
||||
// Create a connection to the server, using Node's IPC as a transport.
|
||||
// Also include all preview / proposed LSP features.
|
||||
@ -25,11 +29,14 @@ let hasConfigurationCapability = false;
|
||||
let hasWorkspaceFolderCapability = false;
|
||||
let folders: WorkspaceFolder[] = [];
|
||||
|
||||
export interface Settings {
|
||||
imports: string[];
|
||||
enableLegacyAutoImportSearch: boolean;
|
||||
function createSettingsManager(cliPath?: string, defaultSettings?: Settings): SettingsManager {
|
||||
const cli = new FluenceCli(cliPath);
|
||||
const configuration = hasConfigurationCapability ? connection.workspace : undefined;
|
||||
return new SettingsManager(cli, configuration, defaultSettings);
|
||||
}
|
||||
|
||||
let documentSettings = createSettingsManager();
|
||||
|
||||
function searchDefinition(position: Position, name: string, locations: TokenLink[]): TokenLink | undefined {
|
||||
return locations.find(
|
||||
(token) =>
|
||||
@ -41,13 +48,6 @@ function searchDefinition(position: Position, name: string, locations: TokenLink
|
||||
);
|
||||
}
|
||||
|
||||
// The global settings, used when the `workspace/configuration` request is not supported by the client.
|
||||
const defaultSettings: Settings = { imports: [], enableLegacyAutoImportSearch: false };
|
||||
let globalSettings: Settings = defaultSettings;
|
||||
|
||||
// Cache the settings of all open documents
|
||||
const documentSettings: Map<string, Settings> = new Map();
|
||||
|
||||
// Cache all locations of all open documents
|
||||
const allLocations: Map<string, TokenLink[]> = new Map();
|
||||
|
||||
@ -89,37 +89,17 @@ async function onDefinition({ textDocument, position }: DefinitionParams): Promi
|
||||
connection.onDefinition(onDefinition);
|
||||
|
||||
connection.onDidChangeConfiguration((change) => {
|
||||
connection.console.log(change.settings);
|
||||
connection.console.log(`onDidChangeConfiguration event ${JSON.stringify(change)}`);
|
||||
|
||||
globalSettings = <Settings>(change.settings.aquaSettings || defaultSettings);
|
||||
documentSettings = createSettingsManager(change.settings.aquaSettings.fluencePath, change.settings.aquaSettings);
|
||||
|
||||
// Revalidate all open text documents
|
||||
documents.all().forEach(validateDocument);
|
||||
});
|
||||
|
||||
async function getDocumentSettings(resource: string): Promise<Settings> {
|
||||
if (!hasConfigurationCapability) {
|
||||
return Promise.resolve(globalSettings);
|
||||
}
|
||||
|
||||
let result = await documentSettings.get(resource);
|
||||
if (!result) {
|
||||
result = await connection.workspace.getConfiguration({
|
||||
scopeUri: resource,
|
||||
section: 'aquaSettings',
|
||||
});
|
||||
if (!result) {
|
||||
result = defaultSettings;
|
||||
}
|
||||
documentSettings.set(resource, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Only keep settings for open documents
|
||||
documents.onDidClose((e) => {
|
||||
documentSettings.delete(e.document.uri);
|
||||
documentSettings.removeDocumentSettings(e.document.uri);
|
||||
});
|
||||
|
||||
connection.onInitialize((params: InitializeParams) => {
|
||||
@ -127,7 +107,6 @@ connection.onInitialize((params: InitializeParams) => {
|
||||
const capabilities = params.capabilities;
|
||||
|
||||
hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
|
||||
|
||||
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
|
||||
|
||||
if (params.workspaceFolders) {
|
||||
@ -140,6 +119,7 @@ connection.onInitialize((params: InitializeParams) => {
|
||||
definitionProvider: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (hasWorkspaceFolderCapability) {
|
||||
result.capabilities.workspace = {
|
||||
workspaceFolders: {
|
||||
@ -147,15 +127,25 @@ connection.onInitialize((params: InitializeParams) => {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
connection.onInitialized(() => {
|
||||
connection.onInitialized(async () => {
|
||||
connection.console.log('onInitialized event');
|
||||
connection.workspace.onDidChangeWorkspaceFolders((event) => {
|
||||
folders = folders.concat(event.added);
|
||||
folders = folders.filter((f) => !event.removed.includes(f));
|
||||
});
|
||||
|
||||
if (hasConfigurationCapability) {
|
||||
connection.client.register(DidChangeConfigurationNotification.type, {
|
||||
section: 'aquaSettings',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWorkspaceFolderCapability) {
|
||||
connection.workspace.onDidChangeWorkspaceFolders((event) => {
|
||||
folders = folders.concat(event.added);
|
||||
folders = folders.filter((f) => !event.removed.includes(f));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
documents.onDidSave(async (change) => {
|
||||
@ -169,7 +159,9 @@ documents.onDidOpen(async (change) => {
|
||||
});
|
||||
|
||||
async function validateDocument(textDocument: TextDocument): Promise<void> {
|
||||
const settings = await getDocumentSettings(textDocument.uri);
|
||||
const settings = await documentSettings.getDocumentSettings(textDocument.uri);
|
||||
|
||||
connection.console.log(`validateDocument ${textDocument.uri} with settings ${JSON.stringify(settings)}`);
|
||||
|
||||
const [diagnostics, locations] = await compileAqua(settings, textDocument, folders, connection.console);
|
||||
|
||||
@ -177,6 +169,11 @@ async function validateDocument(textDocument: TextDocument): Promise<void> {
|
||||
|
||||
// Send the computed diagnostics to VSCode.
|
||||
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
|
||||
|
||||
// Request additional imports update if there are errors
|
||||
if (diagnostics.some((d) => d.severity === DiagnosticSeverity.Error)) {
|
||||
documentSettings.requestImportsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Make the text document manager listen on the connection
|
||||
|
107
server/src/settings.ts
Normal file
107
server/src/settings.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import type { Configuration } from 'vscode-languageserver/lib/common/configuration';
|
||||
|
||||
import type { FluenceCli } from './cli';
|
||||
|
||||
export interface Settings {
|
||||
imports: string[];
|
||||
enableLegacyAutoImportSearch: boolean;
|
||||
}
|
||||
|
||||
function addImports(settings: Settings, imports?: string[]): Settings {
|
||||
return {
|
||||
...settings,
|
||||
imports: [...settings.imports, ...(imports ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compilation settings manager for documents
|
||||
*/
|
||||
export class SettingsManager {
|
||||
private readonly defaultSettings: Settings = {
|
||||
imports: [],
|
||||
enableLegacyAutoImportSearch: false,
|
||||
};
|
||||
private globalSettings: Settings = this.defaultSettings;
|
||||
private documentSettings: Map<string, Settings> = new Map();
|
||||
|
||||
private additionalImports: string[] = [];
|
||||
private additionalImportsLastUpdated = 0;
|
||||
private additionalImportsUpdateRequested = true;
|
||||
|
||||
private readonly cli: FluenceCli;
|
||||
private readonly configuration: Configuration | undefined;
|
||||
|
||||
constructor(cli: FluenceCli, configuration?: Configuration, defaultSettings?: Settings) {
|
||||
this.cli = cli;
|
||||
this.configuration = configuration;
|
||||
if (defaultSettings) {
|
||||
this.defaultSettings = defaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings for document or global settings if document settings are not available
|
||||
*
|
||||
* @param uri Document uri
|
||||
* @returns Settings for the document
|
||||
*/
|
||||
async getDocumentSettings(uri: string): Promise<Settings> {
|
||||
await this.tryUpdateDocumentSettings(uri);
|
||||
|
||||
const settings = this.documentSettings.get(uri) || this.globalSettings;
|
||||
|
||||
await this.tryUpdateAdditionalImports();
|
||||
|
||||
return addImports(settings, this.additionalImports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove document settings from cache
|
||||
*
|
||||
* @param uri Document uri
|
||||
*/
|
||||
removeDocumentSettings(uri: string): void {
|
||||
this.documentSettings.delete(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set flag to request additional imports update.
|
||||
* This flag will be reset after first successful update.
|
||||
* NOTE: Imports are updated no more than once in 5 seconds.
|
||||
*/
|
||||
requestImportsUpdate() {
|
||||
this.additionalImportsUpdateRequested = true;
|
||||
}
|
||||
|
||||
private async tryUpdateDocumentSettings(uri: string): Promise<void> {
|
||||
if (this.configuration && !this.documentSettings.has(uri)) {
|
||||
// TODO: Handle errors
|
||||
const settings = await this.configuration.getConfiguration({
|
||||
scopeUri: uri,
|
||||
section: 'aquaSettings',
|
||||
});
|
||||
if (settings) {
|
||||
this.documentSettings.set(uri, settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async tryUpdateAdditionalImports(): Promise<void> {
|
||||
const now = Date.now();
|
||||
// Update additional imports not more often than once in 5 seconds
|
||||
// and only if there is a request to update
|
||||
if (now - this.additionalImportsLastUpdated < 5000 || !this.additionalImportsUpdateRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.additionalImports = await this.cli.imports();
|
||||
this.additionalImportsLastUpdated = now;
|
||||
this.additionalImportsUpdateRequested = false;
|
||||
} catch (e) {
|
||||
// TODO: Handle this more gracefully
|
||||
console.log('Failed to update additional imports', e);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import type { WorkspaceFolder } from 'vscode-languageserver-protocol';
|
||||
|
||||
import { AquaLSP, ErrorInfo, TokenLink, WarningInfo } from '@fluencelabs/aqua-language-server-api/aqua-lsp-api';
|
||||
|
||||
import type { Settings } from './server';
|
||||
import type { Settings } from './settings';
|
||||
|
||||
function findNearestNodeModules(fileLocation: string, projectLocation: string): string | undefined {
|
||||
const relative = Path.relative(projectLocation, fileLocation);
|
||||
|
Loading…
x
Reference in New Issue
Block a user