diff --git a/langium/CHANGELOG.md b/langium/CHANGELOG.md index ce8457b..f5331df 100644 --- a/langium/CHANGELOG.md +++ b/langium/CHANGELOG.md @@ -2,6 +2,13 @@ ### Unreleased +### v0.2.1 (2024-08-06) +- Leader: Cass Braun + - Features: + - Added patterns and compositions to grammar with basic language support + - Added improved completion for justification diagrams and compositions + - Added basic quick fix + ### v0.2.0 (2024-07-19) - Leader: Cass Braun diff --git a/langium/package-lock.json b/langium/package-lock.json index a2c3f1c..0f4891d 100644 --- a/langium/package-lock.json +++ b/langium/package-lock.json @@ -9,8 +9,6 @@ "version": "0.2.0", "license": "MIT", "dependencies": { - "chalk": "~5.3.0", - "commander": "~11.0.0", "langium": "~3.0.0", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" @@ -1277,6 +1275,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1356,6 +1355,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, "engines": { "node": ">=16" } diff --git a/langium/package.json b/langium/package.json index afefe8c..aa0f709 100644 --- a/langium/package.json +++ b/langium/package.json @@ -4,7 +4,7 @@ "description": "Language Support for the jPipe language", "author": "McMaster Centre for Software Certification (McSCert)", "publisher": "mcscert", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "icon": "images/logo.png", "repository": { @@ -26,8 +26,6 @@ "package": "npm run check-types && node esbuild.mjs --production" }, "dependencies": { - "chalk": "~5.3.0", - "commander": "~11.0.0", "langium": "~3.0.0", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" @@ -55,6 +53,12 @@ "configuration": { "title": "jPipe Configuration", "properties": { + "jpipe.developerMode": { + "type": "boolean", + "scope": "window", + "default": false, + "markdownDescription": "When developer mode is turned on, all automatic fixes are disabled" + }, "jpipe.jarFile": { "type": "string", "scope": "window", diff --git a/langium/src/extension/image-generation/image-generator.ts b/langium/src/extension/image-generation/image-generator.ts index e6ceb35..1f0bd8b 100644 --- a/langium/src/extension/image-generation/image-generator.ts +++ b/langium/src/extension/image-generation/image-generator.ts @@ -1,21 +1,28 @@ import * as vscode from 'vscode'; import util from "node:util"; -import { SaveImageCommand } from './save-image-command.js'; import { Command, CommandUser } from '../managers/command-manager.js'; +import { EventSubscriber, isTextEditor } from '../managers/event-manager.js'; +import { ConfigKey, ConfigurationManager } from '../managers/configuration-manager.js'; -export class ImageGenerator implements CommandUser{ +export class ImageGenerator implements CommandUser, EventSubscriber{ // New channel created in vscode terminal for user debugging. private output_channel: vscode.OutputChannel; - // Defines the command needed to execute the extension. - private save_image_command: SaveImageCommand; - + //configuration manager to fetch configurations from + private configuration: ConfigurationManager; + + private editor!: vscode.TextEditor; //current editor + private document!: vscode.TextDocument; //current document + private directory!: vscode.WorkspaceFolder; //current directory + //possible image types and associated commands private types: ImageType[]; - constructor(save_image_command: SaveImageCommand, output_channel: vscode.OutputChannel) { - this.save_image_command = save_image_command; + constructor(configuration: ConfigurationManager, output_channel: vscode.OutputChannel) { this.output_channel = output_channel; + + this.configuration = configuration; + this.types = [ { exe_command: "jpipe.downloadPNG", @@ -26,6 +33,9 @@ export class ImageGenerator implements CommandUser{ format: Format.SVG } ]; + + //automatically registers to start + this.update(vscode.window.activeTextEditor); } //used to register commands with command manager @@ -34,29 +44,152 @@ export class ImageGenerator implements CommandUser{ this.types.forEach( (type) =>{ command_list.push({ command: type.exe_command, - callback: () => {this.saveImage(type.format)} + callback: () => {this.generate({format: type.format, save_image: true})} }); }); return command_list; } - //Generates an image for the selected justification diagram - public async saveImage(format: Format): Promise { + //updater functions + public async update(data: vscode.TextEditor | undefined): Promise{ + if(isTextEditor(data)){ + const {editor, document, directory} = this.updateEditor(data); + + this.editor = editor; + this.document = document; + this.directory = directory; + } + } + + public async generate(command_settings: CommandSettings): Promise<{stdout: any}>{ const { exec } = require('node:child_process'); const execPromise = util.promisify(exec); - // Execute the command, and wait for the result (must be synchronous). - // TODO: Validate that this actually executes synchronously. - try{ - let command = await this.save_image_command.makeCommand({ format: format, save_image: true}); - const {stdout, stderr} = await execPromise(command); + const command = await this.makeCommand(command_settings); + const output: {stdout: any, stderr: any} = await execPromise(command); + + this.output_channel.appendLine(output.stderr.toString()); + + return {stdout: output.stdout}; + } - this.output_channel.appendLine(stdout.toString()); - this.output_channel.appendLine(stderr.toString()); - } catch (error: any){ - this.output_channel.appendLine(error.toString()); + //creates the command based on command settings + private async makeCommand(command_settings: CommandSettings): Promise{ + let jar_file = this.configuration.getConfiguration(ConfigKey.JARFILE); + + let input_file = this.document.uri; + + let diagram_name = this.findDiagramName(this.document,this.editor); + + let format = this.getFormat(command_settings); + + let log_level = this.configuration.getConfiguration(ConfigKey.LOGLEVEL); + + let command = 'java -jar ' + jar_file + ' -i ' + input_file.path + ' -d '+ diagram_name + ' --format ' + format + ' --log-level ' + log_level; + + this.output_channel.appendLine("Made using jar file: " + jar_file.toString()); + if(command_settings.save_image){ + let output_file = await this.makeOutputPath(diagram_name, command_settings); + command += ' -o ' + output_file.path; } + + return command; + } + + //returns the current diagram name + public getDiagramName(): string{ + return this.findDiagramName(this.document, this.editor); + } + + //helper function to get the diagram name from the document + private findDiagramName(document: vscode.TextDocument, editor: vscode.TextEditor): string{ + let diagram_name: string | undefined; + let match: RegExpExecArray | null; + let i = 0; + + let lines = document.getText().split("\n"); + let line_num = editor.selection.active.line + 1; + + while(i < lines.length && (i < line_num || diagram_name===null)){ + match = /justification .*/i.exec(lines[i]) || /pattern .*/i.exec(lines[i]); + + if (match){ + diagram_name = match[0].split(' ')[1]; + } + + i++; + } + + if(diagram_name === undefined){ + throw new Error("Diagram name not found"); + } + + return diagram_name; + } + + //helper function to set default format + private getFormat(command_settings: CommandSettings): Format{ + let format = command_settings.format; + + if(format === undefined){ + format = Format.PNG; + } + + return format; + } + + //creates the output filepath by asking for user input + private async makeOutputPath(diagram_name: string, command_settings: CommandSettings): Promise{ + if(command_settings.format === undefined){ + command_settings.format = Format.PNG; + } + + let default_output_file = vscode.Uri.joinPath(this.directory.uri, diagram_name + "." + command_settings.format.toLowerCase()); + + let save_dialog_options: vscode.SaveDialogOptions = { + defaultUri: default_output_file, + saveLabel: "Save proof model", + filters: { + 'Images': [Format.PNG.toLowerCase(), Format.SVG.toLowerCase()] + } + } + + let output_file = await vscode.window.showSaveDialog(save_dialog_options); + + if(!output_file){ + throw new Error("Please enter a save location"); + } + + return output_file; + } + + //helper function to perform updates related to a new text editor + private updateEditor(editor: vscode.TextEditor | undefined): {editor: vscode.TextEditor, document: vscode.TextDocument, directory: vscode.WorkspaceFolder}{ + let document: vscode.TextDocument; + let directory: vscode.WorkspaceFolder; + + if(!editor){ + editor = this.editor; + document = this.document; + directory = this.directory; + }else{ + document = editor.document; + directory = this.getDirectory(document); + } + + return{editor, document, directory}; + } + + //helper function to get directory for updating + private getDirectory(document: vscode.TextDocument): vscode.WorkspaceFolder{ + let directory = vscode.workspace.getWorkspaceFolder(document.uri); + + if(!directory){ + directory = this.directory; + } + + return directory; } } @@ -70,4 +203,7 @@ export enum Format{ SVG = "SVG", } - +type CommandSettings = { + format?: Format, + save_image: boolean +} \ No newline at end of file diff --git a/langium/src/extension/image-generation/preview-provider.ts b/langium/src/extension/image-generation/preview-provider.ts index a7b9119..baabbd9 100644 --- a/langium/src/extension/image-generation/preview-provider.ts +++ b/langium/src/extension/image-generation/preview-provider.ts @@ -1,10 +1,9 @@ -import * as vscode from 'vscode' -import util from "node:util"; -import { SaveImageCommand } from './save-image-command.js'; -import { Format } from './image-generator.js'; +import * as vscode from 'vscode'; +import { Format, ImageGenerator } from './image-generator.js'; import { Command, CommandUser } from '../managers/command-manager.js'; import { EventSubscriber, isTextEditor, isTextEditorSelectionChangeEvent } from '../managers/event-manager.js'; + //altered from editorReader export class PreviewProvider implements vscode.CustomTextEditorProvider, CommandUser, EventSubscriber, EventSubscriber { @@ -31,15 +30,15 @@ export class PreviewProvider implements vscode.CustomTextEditorProvider, Command // Global text panel used to display the jd code. private static textPanel: Thenable; - private static save_image_command: SaveImageCommand; + private static image_generator: ImageGenerator; - constructor(save_image_command: SaveImageCommand, output_channel: vscode.OutputChannel) { + constructor(image_generator: ImageGenerator, output_channel: vscode.OutputChannel) { // Without any initial data, must be empty string to prevent null error. PreviewProvider.svg_data = ""; this.output_channel = output_channel; PreviewProvider.updating = false; PreviewProvider.webviewDisposed = true; - PreviewProvider.save_image_command = save_image_command; + PreviewProvider.image_generator = image_generator; vscode.window.registerCustomEditorProvider(PreviewProvider.ext_command, this); } @@ -51,16 +50,14 @@ export class PreviewProvider implements vscode.CustomTextEditorProvider, Command public async update(editor: vscode.TextEditor | undefined): Promise; public async update(changes: vscode.TextEditorSelectionChangeEvent): Promise; public async update(data: (vscode.TextEditor | undefined) | vscode.TextEditorSelectionChangeEvent): Promise{ - if(PreviewProvider.webviewDisposed){ - return; + if(!PreviewProvider.webviewDisposed){ + if(isTextEditorSelectionChangeEvent(data)){ + this.updateTextSelection(data); + } + else if(isTextEditor(data)){ + this.updateEditor(data); + } } - - if(isTextEditorSelectionChangeEvent(data)){ - this.updateTextSelection(data); - } - else if(isTextEditor(data)){ - this.updateEditor(data); - } } private async createWebview(): Promise{ @@ -149,15 +146,10 @@ export class PreviewProvider implements vscode.CustomTextEditorProvider, Command // Executes the jar file for updated SVG public async updateSVG(): Promise { - const { exec } = require('node:child_process'); - const execPromise = util.promisify(exec); - try{ - let command = await PreviewProvider.save_image_command.makeCommand({format: Format.SVG, save_image: false}); - PreviewProvider.webviewPanel.title = PreviewProvider.save_image_command.getDiagramName(); - - const {stdout, stderr} = await execPromise(command); - this.output_channel.appendLine(stderr.toString()); + const {stdout} = await PreviewProvider.image_generator.generate({format: Format.SVG, save_image: false}) + + PreviewProvider.webviewPanel.title = PreviewProvider.image_generator.getDiagramName(); PreviewProvider.svg_data = stdout; }catch (error: any){ this.output_channel.appendLine(error.toString()); @@ -192,7 +184,7 @@ export class PreviewProvider implements vscode.CustomTextEditorProvider, Command //helper function to update text selection private async updateTextSelection(event: vscode.TextEditorSelectionChangeEvent){ if (event !== undefined && event.textEditor.document.languageId=="jpipe" && !PreviewProvider.webviewDisposed){ - let new_diagram = PreviewProvider.save_image_command.getDiagramName(); + let new_diagram = PreviewProvider.image_generator.getDiagramName(); let token : vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); diff --git a/langium/src/extension/image-generation/save-image-command.ts b/langium/src/extension/image-generation/save-image-command.ts deleted file mode 100644 index 4394a23..0000000 --- a/langium/src/extension/image-generation/save-image-command.ts +++ /dev/null @@ -1,219 +0,0 @@ -import * as vscode from 'vscode'; -import { Format } from './image-generator.js'; -import { EventSubscriber, isConfigurationChangeEvent, isTextEditor } from '../managers/event-manager.js'; - -//creates the command required to run to generate the image -export class SaveImageCommand implements EventSubscriber, EventSubscriber{ - private output_channel: vscode.OutputChannel - - private jar_file: string; //location of the jar file - private log_level: string; //log level setting - - private editor!: vscode.TextEditor; //current editor - private document!: vscode.TextDocument; //current document - private directory!: vscode.WorkspaceFolder; //current directory - - constructor(private readonly context: vscode.ExtensionContext, editor: vscode.TextEditor | undefined, output_channel: vscode.OutputChannel){ - this.output_channel = output_channel; - - //Finding the associated compiler - this.jar_file = this.getJarFile(); - - //the log level should only change on configuration change - this.log_level = this.getLogLevel(); - - //automatically registers to start - this.update(editor); - } - - //updater functions - public async update(change: vscode.ConfigurationChangeEvent): Promise; - public async update(editor: vscode.TextEditor | undefined): Promise; - public async update(data: vscode.ConfigurationChangeEvent | vscode.TextEditor | undefined): Promise{ - if(isConfigurationChangeEvent(data)){ - if(data.affectsConfiguration("jpipe.logLevel")){ - this.log_level = this.getLogLevel(); - } - else if(data.affectsConfiguration("jpipe.jarFile")){ - this.jar_file = this.getJarFile(); - } - }else if(isTextEditor(data)){ - const {editor, document, directory} = this.updateEditor(data); - - this.editor = editor; - this.document = document; - this.directory = directory; - } - } - - //creates the command based on command settings - public async makeCommand(command_settings: CommandSettings): Promise{ - let input_file = this.document.uri; - let diagram_name = this.findDiagramName(this.document,this.editor); - let format = this.getFormat(command_settings); - - let command = 'java -jar ' + this.jar_file + ' -i ' + input_file.path + ' -d '+ diagram_name + ' --format ' + format + ' --log-level ' + this.log_level; - - if(command_settings.save_image){ - let output_file = await this.makeOutputPath(diagram_name, command_settings); - command += ' -o ' + output_file.path; - } - - return command; - } - - //returns the current diagram name - public getDiagramName(): string{ - return this.findDiagramName(this.document, this.editor); - } - - //helper function to get the diagram name from the document - private findDiagramName(document: vscode.TextDocument, editor: vscode.TextEditor): string{ - let diagram_name: string | undefined; - let match: RegExpExecArray | null; - let i = 0; - - let lines = document.getText().split("\n"); - let line_num = editor.selection.active.line + 1; - - while(i < lines.length && (i < line_num || diagram_name===null)){ - match = /justification .*/i.exec(lines[i]) || /pattern .*/i.exec(lines[i]); - - if (match){ - diagram_name = match[0].split(' ')[1]; - } - - i++; - } - - if(diagram_name === undefined){ - throw new Error("Diagram name not found"); - } - - return diagram_name; - } - - //helper function to set default format - private getFormat(command_settings: CommandSettings): Format{ - let format = command_settings.format; - - if(format === undefined){ - format = Format.PNG; - } - - return format; - } - - //creates the output filepath by asking for user input - private async makeOutputPath(diagram_name: string, command_settings: CommandSettings): Promise{ - if(command_settings.format === undefined){ - command_settings.format = Format.PNG; - } - - let default_output_file = vscode.Uri.joinPath(this.directory.uri, diagram_name + "." + command_settings.format.toLowerCase()); - - let save_dialog_options: vscode.SaveDialogOptions = { - defaultUri: default_output_file, - saveLabel: "Save proof model", - filters: { - 'Images': [Format.PNG.toLowerCase(), Format.SVG.toLowerCase()] - } - } - - let output_file = await vscode.window.showSaveDialog(save_dialog_options); - - if(!output_file){ - throw new Error("Please enter a save location"); - } - - return output_file; - } - - //helper function to perform updates related to a new text editor - private updateEditor(editor: vscode.TextEditor | undefined): {editor: vscode.TextEditor, document: vscode.TextDocument, directory: vscode.WorkspaceFolder}{ - let document: vscode.TextDocument; - let directory: vscode.WorkspaceFolder; - - if(!editor){ - editor = this.editor; - document = this.document; - directory = this.directory; - }else{ - document = editor.document; - directory = this.getDirectory(document); - } - - return{editor, document, directory}; - } - - //helper function to get directory for updating - private getDirectory(document: vscode.TextDocument): vscode.WorkspaceFolder{ - let directory = vscode.workspace.getWorkspaceFolder(document.uri); - - if(!directory){ - directory = this.directory; - } - - return directory; - } - - //helper function to fetch the log level on configuration change - private getLogLevel(): string{ - let log_level: string; - let configuration = vscode.workspace.getConfiguration().inspect("jpipe.logLevel")?.globalValue; - - if(typeof configuration === "string"){ - log_level = configuration; - }else{ - log_level = "error"; - } - - return log_level; - } - - //helper function to fetch the input jar file path - private getJarFile(): string{ - let jar_file: string; - let default_value = "";//must be kept in sync with the actual default value manually - let configuration = vscode.workspace.getConfiguration().inspect("jpipe.jarFile")?.globalValue; - - if(typeof configuration === "string"){ - jar_file = configuration; - }else{ - jar_file = default_value; - } - - if(jar_file === default_value){ - jar_file = vscode.Uri.joinPath(this.context.extensionUri, 'jar', 'jpipe.jar').path; - vscode.workspace.getConfiguration().update("jpipe.jarFile", jar_file); - }else if(!this.jarPathExists(jar_file)){ - throw new Error("Specified jar path does not exist"); - } - - return jar_file; - } - - private jarPathExists(file_path: string): boolean{ - let jar_path_exists: boolean; - - try{ - let file = vscode.Uri.file(file_path); - let new_uri = vscode.Uri.joinPath(this.directory.uri, "example.jar"); - - vscode.workspace.fs.copy(file, new_uri); - vscode.workspace.fs.delete(new_uri); - - jar_path_exists = true; - }catch(error: any){ - this.output_channel.appendLine(error.toString()); - - jar_path_exists = false; - } - return jar_path_exists; - } -} - -type CommandSettings = { - format?: Format, - save_image: boolean -} diff --git a/langium/src/extension/main.ts b/langium/src/extension/main.ts index 395e347..7eaf3fb 100644 --- a/langium/src/extension/main.ts +++ b/langium/src/extension/main.ts @@ -3,30 +3,30 @@ import * as vscode from 'vscode'; import {window} from 'vscode'; import * as path from 'node:path'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; -import { SaveImageCommand } from './image-generation/save-image-command.js'; + import { ImageGenerator } from './image-generation/image-generator.js'; import { ContextManager } from './managers/context-manager.js'; import { PreviewProvider } from './image-generation/preview-provider.js'; import { CommandManager } from './managers/command-manager.js'; import { EventManager, EventRunner } from './managers/event-manager.js'; +import { ConfigurationManager } from './managers/configuration-manager.js'; let client: LanguageClient; // This function is called when the extension is activated. export function activate(context: vscode.ExtensionContext): void { client = startLanguageClient(context); + //create universal output channel + const output_channel = vscode.window.createOutputChannel("jpipe_console"); //managers for updating and registration const command_manager = new CommandManager(context); const event_manager = new EventManager(); const context_manager = new ContextManager(window.activeTextEditor); - - //create universal output channel - const output_channel = vscode.window.createOutputChannel("jpipe_console"); + const configuration_manager = new ConfigurationManager(context, output_channel); //create needs for image generation - const save_image_command = new SaveImageCommand(context, window.activeTextEditor, output_channel); - const image_generator = new ImageGenerator(save_image_command, output_channel); - const preview_provider = new PreviewProvider(save_image_command, output_channel); + const image_generator = new ImageGenerator(configuration_manager, output_channel); + const preview_provider = new PreviewProvider(image_generator, output_channel); //register commands from classes command_manager.register( @@ -36,12 +36,8 @@ export function activate(context: vscode.ExtensionContext): void { //register subscribers for events that need to monitor changes event_manager.register(new EventRunner(window.onDidChangeTextEditorSelection), context_manager, preview_provider); - event_manager.register(new EventRunner(window.onDidChangeActiveTextEditor), context_manager, save_image_command, preview_provider); - event_manager.register(new EventRunner(vscode.workspace.onDidChangeConfiguration), save_image_command); - - vscode.workspace.onDidChangeConfiguration(() =>{ - output_channel.appendLine("configuration changed"); - }); + event_manager.register(new EventRunner(window.onDidChangeActiveTextEditor), context_manager, image_generator, preview_provider); + event_manager.register(new EventRunner(vscode.workspace.onDidChangeConfiguration), configuration_manager); //activate listening for events event_manager.listen(); diff --git a/langium/src/extension/managers/configuration-manager.ts b/langium/src/extension/managers/configuration-manager.ts new file mode 100644 index 0000000..6ec5301 --- /dev/null +++ b/langium/src/extension/managers/configuration-manager.ts @@ -0,0 +1,204 @@ +import * as vscode from 'vscode'; +import { EventSubscriber, isConfigurationChangeEvent, isTextEditor } from './event-manager.js'; + +type Configuration = { + readonly key: ConfigKey, + readonly update_function: () => T, + value: T +} + +//keeps track of values of configuration settings +export class ConfigurationManager implements EventSubscriber, EventSubscriber{ + // Output channel used for debugging + private output_channel: vscode.OutputChannel; + + //list of all configurations including their key, update function, and current associated value + private configurations: Configuration[]; + + //map to reference from key, the location of the configuration values in configurations array + private configuration_indices: Map; + + //current directory + private directory!: vscode.WorkspaceFolder; + + constructor(private readonly context: vscode.ExtensionContext, output_channel: vscode.OutputChannel){ + this.output_channel = output_channel; + + this.update(vscode.window.activeTextEditor); + + this.configurations = [ + { + key: ConfigKey.LOGLEVEL, + update_function: this.updateLogLevel, + value: this.updateLogLevel() + }, + { + key: ConfigKey.JARFILE, + update_function: this.updateJarFile, + value: this.updateJarFile() + }, + { + key: ConfigKey.DEVMODE, + update_function: this.updateDeveloperMode, + value: this.updateDeveloperMode() + } + ] + + this.configuration_indices = this.setIndices(this.configurations); + } + + + //updater functions + public async update(change: vscode.ConfigurationChangeEvent): Promise; + public async update(editor: vscode.TextEditor | undefined): Promise; + public async update(data: vscode.ConfigurationChangeEvent | vscode.TextEditor | undefined): Promise{ + if(isTextEditor(data)){ + this.updateEditor(data); + }else if(isConfigurationChangeEvent(data)){ + this.updateConfiguration(data); + } + } + + //helper function to manage configuration updates + private updateConfiguration(configuration_change: vscode.ConfigurationChangeEvent): void{ + this.configurations.forEach((configuration)=>{ + if(configuration_change.affectsConfiguration(configuration.key)){ + try{ + configuration.value = configuration.update_function.call(this); + }catch(error: any){ + this.output_channel.appendLine(error); + } + + } + }); + } + + //helper function to manage editor updates + private updateEditor(editor: vscode.TextEditor | undefined): void{ + if(editor){ + let document = editor.document + let directory = vscode.workspace.getWorkspaceFolder(document.uri); + + if(directory){ + this.directory = directory; + } + } + } + + //helper function to fetch the current log level + private updateLogLevel(): string{ + let log_level: string; + let configuration = vscode.workspace.getConfiguration().inspect(ConfigKey.LOGLEVEL)?.globalValue; + + if(typeof configuration === "string"){ + log_level = configuration; + }else{ + log_level = "error"; + } + + return log_level; + } + + //helper function to fetch the current developer mode setting + private updateDeveloperMode(): boolean { + let developer_mode: boolean; + let configuration = vscode.workspace.getConfiguration().inspect(ConfigKey.DEVMODE)?.globalValue; + + if(typeof configuration === "boolean"){ + developer_mode = configuration; + }else{ + developer_mode = false; + } + + return developer_mode; + } + + //helper function to fetch and verify the input jar file path + private updateJarFile(): string{ + let jar_file: string; + let default_path: string; + + let default_value = "";//must be kept in sync with the actual default value manually + let configuration = vscode.workspace.getConfiguration().inspect(ConfigKey.JARFILE)?.globalValue; + + if(typeof configuration === "string"){ + jar_file = configuration; + }else{ + jar_file = default_value; + } + + default_path = vscode.Uri.joinPath(this.context.extensionUri, 'jar', 'jpipe.jar').path; + + if(jar_file === default_value){ + jar_file = default_path; + vscode.workspace.getConfiguration().update(ConfigKey.JARFILE, jar_file); + }else if(!this.jarPathExists(jar_file)){ + let developer_mode = this.getConfiguration(ConfigKey.DEVMODE); + if(developer_mode){ + throw new Error("This file does not exist, please try again"); + }else{ + jar_file = vscode.Uri.joinPath(this.context.extensionUri, 'jar', 'jpipe.jar').path; + } + + } + + return jar_file; + } + + + //getter function to return the current value of any configuration being monitored + public getConfiguration(configuration_key: ConfigKey): any{ + let target_config: any; + + let config_index = this.configuration_indices.get(configuration_key); + + if(config_index !== undefined){ + target_config = this.configurations[config_index].value; + }else{ + throw new Error("Configuration: " + configuration_key + " cannot be found in configuration key list"); + } + + return target_config; + } + + //helper function to verify jar file path + private jarPathExists(file_path: string): boolean{ + let jar_path_exists: boolean; + + try{ + let file = vscode.Uri.file(file_path); + let new_uri = vscode.Uri.joinPath(this.directory.uri, "example.jar"); + + vscode.workspace.fs.copy(file, new_uri); + vscode.workspace.fs.delete(new_uri); + + jar_path_exists = true; + }catch(error: any){ + this.output_channel.appendLine(error.toString()); + + jar_path_exists = false; + } + return jar_path_exists; + } + + //helper function to parse through the configuration array, and set a search location based on configuration key + private setIndices(array: Configuration[]): Map{ + let map: Map = new Map(); + + let counter = 0; + + array.forEach(configuration =>{ + map.set(configuration.key, counter); + + counter++; + }) + + return map; + } +} + +export enum ConfigKey{ + LOGLEVEL = "jpipe.logLevel", + JARFILE = "jpipe.jarFile", + DEVMODE = "jpipe.developerMode" +} \ No newline at end of file diff --git a/langium/src/language/jpipe-module.ts b/langium/src/language/jpipe-module.ts index 20692a5..69570a9 100644 --- a/langium/src/language/jpipe-module.ts +++ b/langium/src/language/jpipe-module.ts @@ -1,15 +1,18 @@ import { type Module, inject, } from 'langium'; import { createDefaultModule, createDefaultSharedModule, type DefaultSharedModuleContext, type LangiumServices, type LangiumSharedServices, type PartialLangiumServices } from 'langium/lsp'; import { JpipeGeneratedModule, JpipeGeneratedSharedModule } from './generated/module.js'; -import { JpipeValidator, registerValidationChecks } from './services/jpipe-validator.js'; import { JpipeHoverProvider } from './services/jpipe-hover-provider.js'; -import { JpipeCompletionProvider } from './services/jpipe-completion-provider.js'; +import { JpipeCompletionProvider } from './services/completion/jpipe-completion-provider.js'; +import { JpipeValidator, registerValidationChecks } from './services/validation/main-validation.js'; +import { JpipeScopeProvider } from './services/jpipe-scope-provider.js'; +import { JpipeCodeActionProvider } from './services/jpipe-code-actions.js'; + /** * Declaration of custom services - add your own service classes here. */ export type JpipeAddedServices = { validation: { - JpipeValidator: JpipeValidator + validator: JpipeValidator } } @@ -26,17 +29,18 @@ export type JpipeServices = LangiumServices & JpipeAddedServices */ export const JpipeModule: Module = { validation: { - JpipeValidator: () => new JpipeValidator() + validator: () => new JpipeValidator() }, lsp:{ CompletionProvider: (services) => new JpipeCompletionProvider(services), HoverProvider: (services) => new JpipeHoverProvider(services), - - } + CodeActionProvider: () => new JpipeCodeActionProvider() + }, + references:{ + ScopeProvider: (services) => new JpipeScopeProvider(services) + } }; - - /** * Create the full set of services required by Langium. * @@ -67,9 +71,9 @@ export function createJpipeServices(context: DefaultSharedModuleContext): { JpipeModule ); shared.ServiceRegistry.register(Jpipe); + registerValidationChecks(Jpipe); + return { shared, Jpipe }; -} - - +} \ No newline at end of file diff --git a/langium/src/language/jpipe.langium b/langium/src/language/jpipe.langium index 769a69c..9d873cd 100644 --- a/langium/src/language/jpipe.langium +++ b/langium/src/language/jpipe.langium @@ -1,25 +1,47 @@ grammar Jpipe entry Model: - (entries+=Body)*; + (loads+=(Load))* (class_kinds+=Class )*; -Body: - Justification; +Load: + "load" name=STRING; -Justification: - kind=('justification') name=ID '{' (variables+=Variable | instructions+=Instruction | supports += Support )* '}'; +interface Class{ + kind: ('justification' | 'pattern' | 'composition') + name: string + implemented?: @Class +} + +Class returns Class: + kind=('justification' | 'pattern' | 'composition') (name=ID | name=ID "implements" implemented=[Class:ID]) (JustificationPattern | Composition); + +JustificationPattern: + '{' (variables+=Variable | instructions+=Instruction | supports += Support)* '}'; + +Composition: + '{' (variables+=CompositionVariable | instructions+=CompositionInstruction)* '}'; Variable: - kind=('evidence' | 'strategy' | 'sub-conclusion' | 'conclusion') name=ID; + kind=('evidence' | 'strategy' | 'sub-conclusion' | 'conclusion' | '@support') name=ID Instruction; Instruction: - Variable 'is' information=STRING; + 'is' information=STRING; Support: left=[Variable:ID] 'supports' right=[Variable:ID]; +CompositionVariable: + kind='justification' name=ID; + +CompositionInstruction: + CompositionVariable 'is' information=CompositionInformation; + +CompositionInformation: + left=[Class:ID] 'with' right=[Class:ID]; + + hidden terminal WS: /\s+/; terminal ID: /[_a-zA-Z][\w_]*/; terminal STRING: /"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/; hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//; -hidden terminal SL_COMMENT: /\/\/[^\n\r]*/; +hidden terminal SL_COMMENT: /\/\/[^\n\r]*/; \ No newline at end of file diff --git a/langium/src/language/services/completion/class-completion.ts b/langium/src/language/services/completion/class-completion.ts new file mode 100644 index 0000000..d22a858 --- /dev/null +++ b/langium/src/language/services/completion/class-completion.ts @@ -0,0 +1,40 @@ +import { AstNodeDescription, ReferenceInfo } from "langium"; +import { JpipeCompletion } from "./jpipe-completion-provider.js"; +import { isClass } from "../../generated/ast.js"; + +//Provides completion for class references +export class ClassCompletionProvider implements JpipeCompletion{ + //provides reference candidates + getCandidates(potential_references: Set, refInfo: ReferenceInfo): Set { + console.log("Getting candidates") + let candidates = new Set(); + let verification_function: ((potential_references: Set) => Set) | undefined; + + if(refInfo.property === "implemented"){ + verification_function = (potential_references: Set) => this.filterReferencesByKind("pattern", potential_references); + }else if(refInfo.property === "left" || refInfo.property === "right"){ + verification_function = (potential_references: Set) => this.filterReferencesByKind("justification", potential_references); + } + + if(verification_function){ + candidates = verification_function.call(this, potential_references); + } + + return candidates; + } + + //helper function to filter class references by kind out of a list of nodes + private filterReferencesByKind(property: string, potential_references: Set): Set{ + let candidates = new Set(); + + potential_references.forEach(ref =>{ + if(isClass(ref.node)){ + if(ref.node.kind === property){ + candidates.add(ref); + } + } + }); + + return candidates; + } +} \ No newline at end of file diff --git a/langium/src/language/services/completion/jpipe-completion-provider.ts b/langium/src/language/services/completion/jpipe-completion-provider.ts new file mode 100644 index 0000000..7e2d36a --- /dev/null +++ b/langium/src/language/services/completion/jpipe-completion-provider.ts @@ -0,0 +1,54 @@ +import { AstNodeDescription, ReferenceInfo, Stream } from "langium"; +import { CompletionContext, DefaultCompletionProvider } from "langium/lsp"; +import { isClass, isCompositionInformation, isSupport } from "../../generated/ast.js"; +import { stream } from "../../../../node_modules/langium/src/utils/stream.js" +import { SupportCompletionProvider } from "./support-completion.js"; +import { ClassCompletionProvider } from "./class-completion.js"; + +//implement interface when adding new complettion provider +export interface JpipeCompletion{ + getCandidates(potential_references: Set, refInfo: ReferenceInfo): Set; +} + +//provides additional completion support for the jpipe language +export class JpipeCompletionProvider extends DefaultCompletionProvider{ + + //filters reference candidates for variables in support statements for autocompletion + protected override getReferenceCandidates(refInfo: ReferenceInfo, _context: CompletionContext): Stream { + let completion_provider: JpipeCompletion | undefined; + let addtional_references: Set | undefined; + + let references = new Set(); + + let potential_references = this.scopeProvider.getScope(refInfo).getAllElements().toSet(); + + if(isSupport(_context.node)){ + //if the current context is of a supporting statement, determines which variables should appear for autocomplete + completion_provider = new SupportCompletionProvider(); + }else if(isClass(_context.node) || isCompositionInformation(_context.node)){ + //provides completion for class references + completion_provider = new ClassCompletionProvider() + } + + addtional_references = completion_provider?.getCandidates(potential_references, refInfo); + + addtional_references?.forEach(variable =>{ + references.add(variable); + }); + + return stream(references); + } + + // //helper function for finding non-variable keywords + // private findKeywords(potential_references: Set): Set{ + // let keywords = new Set(); + + // potential_references.forEach(potential =>{ + // if(!isVariable(potential.node)){ + // keywords.add(potential); + // } + // }); + + // return keywords; + // } +} \ No newline at end of file diff --git a/langium/src/language/services/completion/support-completion.ts b/langium/src/language/services/completion/support-completion.ts new file mode 100644 index 0000000..232efe7 --- /dev/null +++ b/langium/src/language/services/completion/support-completion.ts @@ -0,0 +1,138 @@ +import { AstNodeDescription, ReferenceInfo } from "langium"; +import { isSupport, isVariable, Variable } from "../../generated/ast.js"; +import { JpipeCompletion } from "./jpipe-completion-provider.js"; +import { possible_supports } from "../validation/main-validation.js"; + + +export class SupportCompletionProvider implements JpipeCompletion{ + //returns all candidates from the given reference type + public getCandidates(potential_references: Set, refInfo: ReferenceInfo): Set { + let strict_left_filtering = true; + let support_variables = new Set(); + + let variables = this.findVariables(potential_references); + if(refInfo.property === "right"){ + if(isSupport(refInfo.container)){ + if(isVariable(refInfo.container.left.ref)){ + support_variables = this.getRightVariables(variables, refInfo.container.left.ref); + } + } + }else{ + support_variables = this.getLeftVariables(variables, strict_left_filtering); + } + + return support_variables; + } + + //helper function for filtering references + private findVariables(potential_references: Set): Set{ + let variables = new Set(); + + potential_references.forEach((potential) =>{ + if(isVariable(potential.node)){ + variables.add(potential); + } + }); + + return variables; + } + + //autocompletes right-side variables so that only those which fit the format are shown + //ex. if your JD defines evidence 'e1', strategy 'e2' and conclusion e3', when starting the statement: + //e2 supports ___ auto-completion will only show e3 as an option + private getRightVariables(variables: Set, node: Variable): Set{ + let rightVariables = new Set(); + + let supporter_kind = node.kind; + let allowable_types = possible_supports.get(supporter_kind); + + rightVariables = this.findRightVariables(variables, allowable_types); + + return rightVariables; + } + + + //helperFunction for getRightVariables + private findRightVariables(variables: Set, allowable_types: string[] | undefined): Set{ + let right_variables = new Set(); + + variables.forEach((variable)=>{ + if(isVariable(variable.node)){ + if(allowable_types?.includes(variable.node.kind)){ + right_variables.add(variable); + } + } + }); + + return right_variables; + } + + //only gives variables which can actually support other variables for autocompletion + //ex. if your current document only defines evidence 'e1', strategy 'e2', + //the autocompletion will only show evidence e1 as an autocompletion for the left support element + private getLeftVariables(variables: Set, strict_left_filtering: boolean): Set{ + let left_variables: Set; + + if(strict_left_filtering){ + left_variables = this.filterLeftProbableVariables(variables); + } + else{ + left_variables = this.filterLeftPossibleVariables(variables); + } + + return left_variables; + } + + //helper function for getLeftVariables: checks if the variable + //could have a potential matching right variable, regardless if this variable + //has already been defined or not + private filterLeftPossibleVariables(variables: Set): Set{ + let left_variables = new Set(); + + variables.forEach(variable =>{ + if(isVariable(variable.node)){ + let variable_kind = variable.node.kind; + let allowable_types = possible_supports.get(variable_kind); + + if (!(allowable_types === undefined || allowable_types.length === 0)){ + left_variables.add(variable); + } + } + }); + + return left_variables; + } + + //helper function for getLeftVariables: checks if the variable could have + //a potential matching right variable from the existing defined variables + private filterLeftProbableVariables(variables: Set): Set{ + let left_variables = new Set(); + + variables.forEach((variable) =>{ + if(isVariable(variable.node)){ + let allowable_types = possible_supports.get(variable.node.kind); + + if (this.hasRightVariableDefined(variables, allowable_types)){ + left_variables.add(variable); + } + } + }); + + return left_variables; + } + + //helper function for filtering left probable variables + private hasRightVariableDefined(variables: Set, allowable_types: string[] | undefined): boolean{ + let result:boolean = false; + + variables.forEach(possible_variable=>{ + if(isVariable(possible_variable.node)){ + if(allowable_types?.includes(possible_variable.node.kind)){ + result = true; + } + } + }); + + return result; + } +} \ No newline at end of file diff --git a/langium/src/language/services/jpipe-code-actions.ts b/langium/src/language/services/jpipe-code-actions.ts new file mode 100644 index 0000000..a560e8c --- /dev/null +++ b/langium/src/language/services/jpipe-code-actions.ts @@ -0,0 +1,71 @@ +import { LangiumDocument, MaybePromise, URI } from "langium"; +import { CodeActionProvider } from "langium/lsp"; +import { CodeActionParams, CancellationToken, Command, CodeAction, CodeActionKind, Range, WorkspaceEdit, Diagnostic, TextEdit } from "vscode-languageserver"; + + +export class JpipeCodeActionProvider implements CodeActionProvider{ + getCodeActions(document: LangiumDocument, params: CodeActionParams, cancelToken?: CancellationToken): MaybePromise | undefined> { + if(cancelToken){ + if(cancelToken.isCancellationRequested){ + return undefined; + } + } + + let code_actions = new Array(); + + params.context.diagnostics.forEach(diagnostic =>{ + if(this.hasCode(diagnostic, "supportInJustification")){ + code_actions.push( + new RemoveLine(document.uri, params.range, diagnostic, "supportInJustification") + ); + } + }) + + return code_actions; + } + + private hasCode(diagnostic: Diagnostic, code: string): boolean{ + let hasCode = false; + + if(diagnostic.data){ + if(diagnostic.data.code === code){ + hasCode = true; + } + } + + return hasCode; + } + +} + +class RemoveLine implements CodeAction{ + public title = "Remove line"; + public kind = CodeActionKind.QuickFix; + public edit: WorkspaceEdit; + public data: string; + public diagnostics?: Diagnostic[] | undefined; + + constructor(uri: URI, range: Range, diagnostic: Diagnostic, data: string){ + let text_edit = TextEdit.del(this.getLine(range)); + this.edit = { + changes: { + [uri.toString()]: [text_edit] + } + } + this.diagnostics = [diagnostic]; + this.data = data; + } + + private getLine(range: Range): Range{ + return { + start: { + line: range.start.line, + character: 0 + }, + end: { + line: range.end.line, + character: 100000 + } + }; + } +} \ No newline at end of file diff --git a/langium/src/language/services/jpipe-completion-provider.ts b/langium/src/language/services/jpipe-completion-provider.ts deleted file mode 100644 index 1ece361..0000000 --- a/langium/src/language/services/jpipe-completion-provider.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { AstNodeDescription, ReferenceInfo, Stream } from "langium"; -import { CompletionContext, DefaultCompletionProvider } from "langium/lsp"; -import { Support, isSupport, isVariable } from "../generated/ast.js"; -import { stream } from "../../../node_modules/langium/src/utils/stream.js" - -export class JpipeCompletionProvider extends DefaultCompletionProvider{ - - //track which variable types can support other variable types - readonly typeMap: Map = new Map([ - ['evidence', ['strategy']], - ['strategy', ['sub-conclusion', 'conclusion']], - ['sub-conclusion', ['strategy', 'conclusion']], - ['conclusion', []] - ]); - - //filters reference candidates for variables in support statements for autocompletion - protected override getReferenceCandidates(refInfo: ReferenceInfo, _context: CompletionContext): Stream { - let strict_left_filtering = false; //to be added as a configuration option - - let potential_references = this.scopeProvider.getScope(refInfo).getAllElements(); - - let references = this.findKeywords(potential_references); - let variables = this.findVariables(potential_references); - - //if the current context is of a supporting statement, determines which variables should appear for autocomplete - if(isSupport(_context.node)){ - let support_variables: AstNodeDescription[]; - - if(this.onRightSide(_context.node)){ - support_variables = this.getRightVariables(variables, _context); - } - else{ - support_variables = this.getLeftVariables(variables, strict_left_filtering); - } - - support_variables.forEach(v =>{ - references.push(v); - }); - } - - return stream(references); - } - - //helper function for filtering references - findVariables(potential_references: Stream): AstNodeDescription[]{ - let variables: AstNodeDescription[]=[]; - - potential_references.forEach((potential) =>{ - if(isVariable(potential.node)){ - variables.push(potential); - } - }); - - return variables; - } - - //helper function for finding non-variable keywords - findKeywords(potential_references: Stream): AstNodeDescription[]{ - let keywords: AstNodeDescription[] = []; - - potential_references.forEach(potential =>{ - if(!isVariable(potential.node)){ - keywords.push(potential); - } - }); - - return keywords; - } - - //helper functino to determine which side of support statement we are on - onRightSide(context_node: Support){ - return context_node.left.ref !== undefined; - } - - - //autocompletes right-side variables so that only those which fit the format are shown - //ex. if your JD defines evidence 'e1', strategy 'e2' and conclusion e3', when starting the statement: - //e2 supports ___ auto-completion will only show e3 as an option - getRightVariables(variables: AstNodeDescription[], _context: CompletionContext): AstNodeDescription[]{ - let rightVariables: AstNodeDescription[] = []; - - if(isSupport(_context.node)){ - if(_context.node.left.ref !== undefined){ - let supporter_kind = _context.node.left.ref.kind; - let allowable_types = this.typeMap.get(supporter_kind); - - rightVariables = this.findRightVariables(variables, allowable_types); - } - } - - return rightVariables; - } - - //helperFunction for getRightVariables - findRightVariables(variables: AstNodeDescription[], allowable_types: string[] | undefined){ - let right_variables: AstNodeDescription[] = []; - - variables.forEach((variable)=>{ - if(isVariable(variable.node)){ - if(allowable_types?.includes(variable.node.kind)){ - right_variables.push(variable); - } - } - }); - - return right_variables; - } - - //only gives variables which can actually support other variables for autocompletion - //ex. if your current document only defines evidence 'e1', strategy 'e2', - //the autocompletion will only show evidence e1 as an autocompletion for the left support element - getLeftVariables(variables: AstNodeDescription[], strict_left_filtering: boolean): AstNodeDescription[]{ - let left_variables: AstNodeDescription[]; - - if(strict_left_filtering){ - left_variables = this.filterLeftProbableVariables(variables); - } - else{ - left_variables = this.filterLeftPossibleVariables(variables); - } - - return left_variables; - } - - //helper function for getLeftVariables: checks if the variable - //could have a potential matching right variable, regardless if this variable - //has already been defined or not - filterLeftPossibleVariables(variables: AstNodeDescription[]): AstNodeDescription[]{ - let left_variables: AstNodeDescription[] = []; - - variables.forEach(variable =>{ - if(isVariable(variable.node)){ - let variable_kind = variable.node.kind; - let allowable_types = this.typeMap.get(variable_kind); - - if (!(allowable_types === undefined || allowable_types.length === 0)){ - left_variables.push(variable); - } - } - }); - - return left_variables; - } - - //helper function for getLeftVariables: checks if the variable could have - //a potential matching right variable from the existing defined variables - filterLeftProbableVariables(variables: AstNodeDescription[]): AstNodeDescription[]{ - let left_variables: AstNodeDescription[] = []; - - variables.forEach((variable) =>{ - if(isVariable(variable.node)){ - let allowable_types = this.typeMap.get(variable.node.kind); - - if (this.hasRightVariableDefined(variables,allowable_types)){ - left_variables.push(variable); - } - } - }); - - return left_variables; - } - - //helper function for filtering left probable variables - hasRightVariableDefined(variables: AstNodeDescription[], allowable_types: string[] | undefined): boolean{ - let result:boolean = false; - - variables.forEach(possible_variable=>{ - if(isVariable(possible_variable.node)){ - if(allowable_types?.includes(possible_variable.node.kind)){ - result = true; - } - } - }); - - return result; - } -} diff --git a/langium/src/language/services/jpipe-hover-provider.ts b/langium/src/language/services/jpipe-hover-provider.ts index 94b798a..0de898b 100644 --- a/langium/src/language/services/jpipe-hover-provider.ts +++ b/langium/src/language/services/jpipe-hover-provider.ts @@ -1,24 +1,22 @@ import { type AstNode, type MaybePromise, } from 'langium'; - import { AstNodeHoverProvider } from 'langium/lsp'; import { Hover } from 'vscode-languageserver'; -import { isJustification, isVariable } from '../generated/ast.js'; - +import { isClass, isInstruction } from '../generated/ast.js'; //provides hover for variables and class types export class JpipeHoverProvider extends AstNodeHoverProvider{ protected getAstNodeHoverContent(node: AstNode): MaybePromise { - if(isVariable(node)){ + if(isInstruction(node)){ return { contents: { kind: 'markdown', language: 'Jpipe', - value: `${node.kind}: ${node.information}` + value: `${node.kind}: ${node.information.toString()}` } } } - if(isJustification(node)){ + if(isClass(node)){ return { contents: { kind: 'markdown', @@ -27,7 +25,7 @@ export class JpipeHoverProvider extends AstNodeHoverProvider{ } } } - - return undefined; + + return undefined; } -} +} \ No newline at end of file diff --git a/langium/src/language/services/jpipe-scope-provider.ts b/langium/src/language/services/jpipe-scope-provider.ts new file mode 100644 index 0000000..6cd4f7f --- /dev/null +++ b/langium/src/language/services/jpipe-scope-provider.ts @@ -0,0 +1,129 @@ +import { AstNode, DefaultScopeProvider, LangiumCoreServices, LangiumDocuments, MapScope, ReferenceInfo, Scope, URI } from "langium"; +import { isModel, Load, Model } from "../generated/ast.js"; + +export class JpipeScopeProvider extends DefaultScopeProvider{ + private langiumDocuments: () => LangiumDocuments; + + constructor(services: LangiumCoreServices){ + super(services) + this.langiumDocuments = () => services.shared.workspace.LangiumDocuments; + } + + //gets the global scope for a given reference + protected override getGlobalScope(referenceType: string, _context: ReferenceInfo): Scope { + let included_URIs: Set = new Set(); + + let current_URI = getCurrentURI(_context.container); + + if(current_URI){ + included_URIs = this.getImports(current_URI, new Set(), this.langiumDocuments()); + } + + + return this.globalScopeCache.get(referenceType, () => new MapScope(this.indexManager.allElements(referenceType, toString(included_URIs)))); + } + + //gets all imports from the current document recursively + private getImports(current_URI: URI, all_imports: Set, doc_provider: LangiumDocuments): Set{ + if(all_imports.has(current_URI)){ + return new Set(); + }else{ + let load_URIs: Set | undefined; + let node = this.getNode(current_URI); + + all_imports.add(current_URI); + + if(node){ + load_URIs = getURIs(node); + } + + if(load_URIs){ + load_URIs.forEach(load_URI =>{ + this.getImports(load_URI, all_imports, doc_provider); + }); + } + return all_imports; + } + } + + //gets any node from a uri + private getNode(doc_import: URI): AstNode | undefined{ + let node: AstNode | undefined; + let model = this.indexManager.allElements(undefined, new Set([doc_import.toString()])); + let head = model.head(); + + if(head){ + if(head.node){ + node = head.node; + } + } + + return node; + } +} + +//turns a set of URIs into a set of strings +function toString(URIs: Set): Set{ + let strings = new Set(); + + URIs.forEach(uri =>{ + strings.add(uri.toString()); + }); + + return strings; +} + +//gets the current uri from a given node +function getCurrentURI(node: AstNode): URI | undefined{ + let current_URI: URI | undefined; + let model = getModelNode(node); + + if(model){ + if(model.$document){ + current_URI = model.$document.uri; + } + } + + return current_URI; +} + +//gets all URIs within a certain document from a node +function getURIs(node: AstNode): Set{ + let links = new Set; + + let load_statements = findLoadStatements(node); + + load_statements.forEach(load_statement =>{ + links.add(URI.file(load_statement.name)); + }); + + return links; +} + +//finds all load statements in a document from a node +function findLoadStatements(node: AstNode): Set{ + let load_statements = new Set(); + let model = getModelNode(node); + + if(model){ + model.loads.forEach(load_statement =>{ + load_statements.add(load_statement); + }); + } + + return load_statements; +} + +//gets the model node of a given node +function getModelNode(node: AstNode): Model | undefined{ + let container = node.$container; + while(container !== undefined && !isModel(container)){ + container = container.$container; + } + + if(isModel(container)){ + return container; + }else{ + return undefined; + } +} \ No newline at end of file diff --git a/langium/src/language/services/jpipe-validator.ts b/langium/src/language/services/jpipe-validator.ts deleted file mode 100644 index ba2c5b0..0000000 --- a/langium/src/language/services/jpipe-validator.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Reference, type ValidationAcceptor, type ValidationChecks } from 'langium'; -import type { JpipeAstType, Model, Support, Variable} from '../generated/ast.js'; -import type { JpipeServices } from '../jpipe-module.js'; - - -/** - * Register custom validation checks. - */ -export function registerValidationChecks(services: JpipeServices) { - const registry = services.validation.ValidationRegistry; - const validator = services.validation.JpipeValidator; - const checks: ValidationChecks = { - Model: [validator.allChecks] - }; - registry.register(checks, validator); -} - -export class JpipeValidator { - - //edit to implement validation hierarchy (no duplicate statements) - allChecks(model: Model, accept: ValidationAcceptor): void{ - //this.checkNaming(model, accept); - this.checkVariables(model, accept); - this.checkSupportingStatements(model, accept); - } - - //Checks that variables are defined - private checkVariables(model: Model, accept: ValidationAcceptor): void{ - model.entries.forEach( (entry) =>{ - entry.supports.forEach( (support) =>{ - this.checkSupport(support, accept); - }); - }); - } - - //helper function to test if variables are defined - private checkSupport(support: Support, accept: ValidationAcceptor): void{ - if(this.hasError(support.left, support.right)){ - let errorStatement = this.getErrorStatement(support.left, support.right); - accept("error", errorStatement, {node: support}); - } - } - - //helper function to determine if there is an error in a support statement - private hasError(leftSupport: Reference, rightSupport: Reference): boolean{ - let hasError: boolean; - - let leftKind = leftSupport.ref?.kind; - let rightKind = rightSupport.ref?.kind; - - if(leftKind === undefined || rightKind === undefined){ - hasError = true; - }else{ - hasError = false; - } - - return hasError; - } - - //helper function to determine the necessary error statement - private getErrorStatement(leftSupport: Reference, rightSupport: Reference): string{ - let errorStatement: string - - let leftKind = leftSupport.ref?.kind; - let rightKind = rightSupport.ref?.kind; - - if(leftKind === undefined && rightKind === undefined){ - errorStatement = `Variables ${leftSupport.$refText} and ${rightSupport.$refText} are undefined.` - }else if(leftKind === undefined){ - errorStatement = `Variable ${leftSupport.$refText} is undefined.` - }else{ - errorStatement = `Variable ${rightSupport.$refText} is undefined.` - } - - return errorStatement; - } - - //checks if support statements follow proper typing convention - private checkSupportingStatements(model: Model, accept: ValidationAcceptor): void{ - let possibleSupports: Map = new Map([ - ['evidence', ['strategy']], - ['strategy', ['sub-conclusion', 'conclusion']], - ['sub-conclusion', ['strategy', 'conclusion']], - ['conclusion', []] - ]); - - model.entries.forEach( (entry) =>{ - entry.supports.forEach( (support) =>{ - if(support.left.ref !== undefined && support.right.ref !==undefined){ - let leftKind = support.left.ref?.kind; - let rightKind = support.right.ref?.kind; - let possibleRights: string[] | undefined = possibleSupports.get(leftKind); - - if(rightKind !== undefined){ - if (possibleRights?.includes(rightKind)){ - return; - } - } - - accept("error", `It is not possible to have ${leftKind} support ${rightKind}.`, { - node:support - }); - } - - }); - }); - } -} diff --git a/langium/src/language/services/old/jpipe-command-handler.ts-old b/langium/src/language/services/old/jpipe-command-handler.ts-old deleted file mode 100644 index 36b15d4..0000000 --- a/langium/src/language/services/old/jpipe-command-handler.ts-old +++ /dev/null @@ -1,8 +0,0 @@ -import { AbstractExecuteCommandHandler, ExecuteCommandAcceptor } from "langium/lsp"; -export class JpipeExecuteCommandHandler extends AbstractExecuteCommandHandler{ - override registerCommands(acceptor: ExecuteCommandAcceptor): void { - - acceptor("jpipe.sayHello",args => console.log("Jpipe says hello!")); - } -} - diff --git a/langium/src/language/services/old/jpipe-scope-provider.ts-old b/langium/src/language/services/old/jpipe-scope-provider.ts-old deleted file mode 100644 index b06294c..0000000 --- a/langium/src/language/services/old/jpipe-scope-provider.ts-old +++ /dev/null @@ -1,46 +0,0 @@ - -/* -import { AstUtils, AstNodeDescriptionProvider, Scope, LangiumCoreServices, ReferenceInfo, ScopeProvider, EMPTY_SCOPE, MapScope } from "langium"; -import { Model, isModel, isSupportingStatement } from "../generated/ast.js"; -export class JpipeScopeProvider implements ScopeProvider { - private astNodeDescriptionProvider: AstNodeDescriptionProvider; - - constructor(services: LangiumCoreServices) { - //get some helper services - this.astNodeDescriptionProvider = services.workspace.AstNodeDescriptionProvider; - } - - getScope(context: ReferenceInfo): Scope { - if(isSupportingStatement(context.container) && (context.property === 'supporter' || context.property === 'supportee')){ - const model = AstUtils.getContainerOfType(context.container, isModel); - if( model !== undefined){ - const variables = model.variables; - - //might have to map to variable.description idk - const descriptions = variables.map(variable => this.astNodeDescriptionProvider.createDescription(variable,variable.name)); - return new MapScope(descriptions); - } - } - return EMPTY_SCOPE; - } - - makeSupporterArray(model: Model | undefined){ - let arr: any[] = []; - if(model !== undefined){ - model.supports.forEach((support) =>{ - arr.push(support.supporter); - }); - } - return arr; - } - - makeSupporteArray(model: Model): any[]{ - let arr: any[] = []; - model.supports.forEach((support) =>{ - arr.push(support.supportee); - }); - return arr; - } - -} -*/ \ No newline at end of file diff --git a/langium/src/language/services/validation/main-validation.ts b/langium/src/language/services/validation/main-validation.ts new file mode 100644 index 0000000..8ecddc8 --- /dev/null +++ b/langium/src/language/services/validation/main-validation.ts @@ -0,0 +1,43 @@ +import { ValidationAcceptor, ValidationChecks } from 'langium'; +import type { JpipeServices } from '../../jpipe-module.js'; +import { SupportValidator } from './support-validator.js'; +import { JpipeAstType } from '../../generated/ast.js'; +import { JustificationVariableValidator } from './variable-validator.js'; +import { PatternValidator } from './pattern-validator.js'; + +/** + * Register custom validation checks. + */ +export function registerValidationChecks(services: JpipeServices) { + const registry = services.validation.ValidationRegistry; + const validator = services.validation.validator; + + registry.register(validator.checks, validator); +} + +//Register additional validation here +export class JpipeValidator{ + public static support_validator = new SupportValidator(); + public static justification_validator = new JustificationVariableValidator(); + public static pattern_validator = new PatternValidator(); + public readonly checks: ValidationChecks = { + Variable: JpipeValidator.justification_validator.validate, + Support: JpipeValidator.support_validator.validate, + JustificationPattern: JpipeValidator.pattern_validator.validate + } +} + +//when creating a validator, implement this interface +export interface Validator{ + //function which actually validates the given information + validate(model: T, accept: ValidationAcceptor): void; +} + +//data structure to represent which elements can support which, format is ['a', ['b', 'c']], where "a supports b" or "a supports c" +export var possible_supports = new Map([ + ['evidence', ['strategy', '@support']], + ['strategy', ['sub-conclusion', 'conclusion', '@support']], + ['sub-conclusion', ['strategy', 'conclusion', '@support']], + ['conclusion', []] , + ['@support', ['strategy', 'sub-conclusion', 'conclusion']] +]); \ No newline at end of file diff --git a/langium/src/language/services/validation/pattern-validator.ts b/langium/src/language/services/validation/pattern-validator.ts new file mode 100644 index 0000000..108006c --- /dev/null +++ b/langium/src/language/services/validation/pattern-validator.ts @@ -0,0 +1,23 @@ +import { diagnosticData, ValidationAcceptor } from "langium"; +import { JustificationPattern } from "../../generated/ast.js"; +import { Validator } from "./main-validation.js"; + + +export class PatternValidator implements Validator{ + validate(model: JustificationPattern, accept: ValidationAcceptor): void { + if(model.kind === "pattern"){ + let support_found = false; + + model.variables.forEach( variable => { + if(variable.kind === "@support"){ + support_found = true; + } + }); + + if(!support_found){ + accept("warning", "No @support variables found in pattern", {node: model, property: "name", data: diagnosticData("noSupportInPattern")}) + } + } + } + +} \ No newline at end of file diff --git a/langium/src/language/services/validation/support-validator.ts b/langium/src/language/services/validation/support-validator.ts new file mode 100644 index 0000000..d07b1a8 --- /dev/null +++ b/langium/src/language/services/validation/support-validator.ts @@ -0,0 +1,130 @@ +import { Reference, ValidationAcceptor } from "langium"; +import { Support, Variable } from "../../generated/ast.js"; +import { possible_supports, Validator } from "./main-validation.js"; + +//class to validate supporting statements found in the document +export class SupportValidator implements Validator{ + //validator function + public validate(support: Support, accept: ValidationAcceptor): void { + if(SupportValidator.referencesCorrect(support, accept)){ + SupportValidator.checkSupportRelations(support, accept); + } + } + + //helper function to test if variables are defined + private static referencesCorrect(support: Support, accept: ValidationAcceptor): boolean{ + let symbolNamesCorrect: boolean; + + let error = SupportValidator.getErrorType(support.left, support.right); + + if(error === ErrorType.NoError){ + symbolNamesCorrect = true; + }else{ + symbolNamesCorrect = false; + + let errorStatements = this.getError(error).call(this,support); + + errorStatements.forEach(statement =>{ + accept("error", statement, {node: support}); + }); + } + + return symbolNamesCorrect; + } + + + //helper function to determine if there is an error in a support statement + private static getErrorType(left: Reference, right: Reference): ErrorType{ + let errorType: ErrorType; + + if(left.ref === undefined || right.ref === undefined){ + errorType = ErrorType.ReferenceError; + }else{ + errorType = ErrorType.NoError; + } + + return errorType; + } + + //helper function to determine the necessary error statement + private static getError(errorType: ErrorType): (support: Support) => string[]{ + let errorFunction: (support: Support) => string[]; + + let errors = new Map string[]>([ + [ErrorType.ReferenceError, SupportValidator.getReferenceError] + ]); + + let returnFunction = errors.get(errorType); + + if(returnFunction){ + errorFunction = returnFunction; + }else{ + errorFunction = () => []; + } + + return errorFunction; + } + + //helper function which gets the text(s) for a reference error (variable undefined etc.) + private static getReferenceError = (support: Support): string[] => { + let errorStatement: string[]; + + let left = support.left; + let right = support.right; + + if(left.ref === undefined && right.ref === undefined){ + errorStatement =[ `Variables ${left.$refText} and ${right.$refText} are undefined.`]; + }else if(left.ref === undefined){ + errorStatement = [`Variable ${left.$refText} is undefined.`]; + }else{ + errorStatement = [`Variable ${right.$refText}is undefined.`]; + } + + return errorStatement; + }; + + //checks if support statements follow proper typing convention + private static checkSupportRelations(support: Support, accept: ValidationAcceptor): void{ + if(!this.supportRelationCorrect(support)){ + let error_message = this.getRelationErrorMessage(support); + accept("error", error_message, {node:support}); + } + } + + //helper function to check if the left and the right variable are of the right kind + private static supportRelationCorrect(support: Support): boolean{ + let supportCorrect: boolean; + + if(support.left.ref !== undefined && support.right.ref !==undefined){ + let rightKind = support.right.ref.kind; + let possibleRights = possible_supports.get(support.left.ref.kind); + + if(possibleRights?.includes(rightKind)){ + supportCorrect = true; + }else{ + supportCorrect = false; + } + }else{ + supportCorrect = false; + } + + return supportCorrect; + } + + //helper function to get the error message on incorrect relation + private static getRelationErrorMessage(support: Support): string{ + let error_message: string = ""; + + if(support.left.ref !== undefined && support.right.ref !== undefined){ + error_message = `It is not possible to have ${support.left.ref.kind} support ${support.right.ref.kind}.` + } + + return error_message; + } +} + +//enum to represent error types +enum ErrorType{ + NoError, + ReferenceError +} \ No newline at end of file diff --git a/langium/src/language/services/validation/variable-validator.ts b/langium/src/language/services/validation/variable-validator.ts new file mode 100644 index 0000000..c0ac023 --- /dev/null +++ b/langium/src/language/services/validation/variable-validator.ts @@ -0,0 +1,39 @@ +import { diagnosticData, ValidationAcceptor } from "langium"; +import { Variable } from "../../generated/ast.js"; +import { Validator } from "./main-validation.js"; + +//class to validate variables in justification diagrams +export class JustificationVariableValidator implements Validator{ + //actual function to validate the variable + public validate(variable: Variable, accept: ValidationAcceptor): void { + let class_kind = variable.$container.kind; + if(class_kind === 'justification'){ + JustificationVariableValidator.validateJustificationVariables(variable, accept); + } + + } + + //helper function to validate variables within justification diagrams + private static validateJustificationVariables(variable: Variable, accept: ValidationAcceptor){ + if(JustificationVariableValidator.isJustificationVariableAcceptable(variable)){ + let error_message = "Variable kind: " + variable.kind + " is not included in a " + variable.$container.kind + " diagram"; + + accept("error", error_message, {node: variable, property: "kind", data: diagnosticData("supportInJustification")}); + } + } + + //helper function to determine if a variable is acceptable within a justification diagram + private static isJustificationVariableAcceptable(variable: Variable): boolean{ + let variable_acceptable: boolean; + + let acceptable_kinds = ["evidence", "strategy", "sub-conclusion", "conclusion"]; + + if(!acceptable_kinds.includes(variable.kind)){ + variable_acceptable = true; + }else{ + variable_acceptable = false; + } + + return variable_acceptable; + } +} \ No newline at end of file diff --git a/langium/tsconfig.json b/langium/tsconfig.json index ae02089..840966b 100644 --- a/langium/tsconfig.json +++ b/langium/tsconfig.json @@ -21,7 +21,7 @@ "include": [ "src/**/*.ts", "test/**/*.ts" -, "src/language/services/old/jpipe-command-handler.ts-old" ], + ], "exclude": [ "out", "node_modules"