diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..be6f4b5 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,36 @@ +name: Build and deploy MkDocs to GitHub pages + +on: + push: + branches: + - cosimstudio_rework + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + - run: pip install mkdocs + - name: Build static files + id: build + run: | + mkdocs build --config-file docs/mkdocs.yml + - name: Upload static files as artifact + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: docs/site/ + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/package-vsix.yml b/.github/workflows/package-vsix.yml new file mode 100644 index 0000000..a0839ef --- /dev/null +++ b/.github/workflows/package-vsix.yml @@ -0,0 +1,39 @@ +name: Build and Test VS Code Extension + +on: + push: + branches: + - cosimstudio_rework + pull_request: + +jobs: + build-and-test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [20] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm run test:unit + + - name: Build the extension + run: npx vsce package + + - name: Upload .vsix artifact + uses: actions/upload-artifact@v4 + with: + name: vscode-extension-${{ matrix.os }} + path: '*.vsix' \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e6605a..76c4589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.1.3] - 2024-11-09 + +### Changed + +- Variable completion items in cosimulation configuration files are now context-aware, only showing the relevant variables, i.e. either inputs, outputs or parameters. + ## [0.1.2] - 2024-10-03 ### Fixed diff --git a/README.md b/README.md index 155f8c9..35f0d9d 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,13 @@ The editor will generate an error if an FMU definition contains a reference to a Defining connections with incorrect causality will be detected as an error. -![An animation illustrating the linting feature of the extension and how it ensure correct causality](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1/dark/fmu_causality_linting.gif) +![An animation illustrating the linting feature of the extension and how it ensure correct causality](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1.3/dark/fmu_causality_linting.gif) ### Autocompletion When the configuration contains references to FMUs that the extension can resolve, the editor provides smart completions of input and output variables. -![An animation illustrating the autocompletion feature of the extension](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1/dark/fmu_auto_completion.gif) +![An animation illustrating the autocompletion feature of the extension](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1.3/dark/fmu_auto_completion.gif) ### Integration with Maestro diff --git a/TODOS.md b/TODOS.md index 2440643..e1e2717 100644 --- a/TODOS.md +++ b/TODOS.md @@ -7,15 +7,15 @@ - [x] Weird inconsistent spacing/tabs - [x] The cosim file is not linted when it is opened or when the extension starts. Meaning when the extension first loads it won't catch errors until the file has been edited. Also, if an FMU is ever deleted, it won't show up as an error in the configuration file. - [x] Remove dangling period in Axios error message. -- [ ] Filter autocompletion items for connections to only show input/output/parameters depending on context. -- [ ] Setup Actions to build extension package +- [x] Filter autocompletion items for connections to only show input/output/parameters depending on context. +- [x] Setup Actions to build extension package - [ ] Additional testing - increase coverage in unit tests -- [ ] Demo video showing basic functionality of extension. +- [x] Demo video showing basic functionality of extension. - [ ] Documentation - MkDocs, for reference: ## v0.2.0 development -1. (later) cosim_studio.json --> to be used for one cosimulation workspace. It can refer to multiple cosimulation configs - maestro host and port is moved to here. +1. (later) cosim_studio.json --> to be used for one cosimulation workspace. It can refer to multiple cosimulation configs - maestro host and port is moved to here. 2. (later) extension configuration, i.e. where is Maestro running 3. The project could benefit from a DI framework - consider this for a future release. 4. Look at activation events and extension configuration with a fixed name. diff --git a/docs/docs/developing.md b/docs/docs/developing.md new file mode 100644 index 0000000..f2d779d --- /dev/null +++ b/docs/docs/developing.md @@ -0,0 +1,105 @@ +# Developer guide for Cosimulation Studio VS Code extension + +This guide aims to boil everything you need to know to develop on the extension down to a few short steps. The guide covers how to set up your development environment, running tests, bundling the extension, debugging the extension in a VS Code instance, and finally how to publish the extension when it's ready for release. + +## Setting up the development environment + +### Using Dev Containers + +The easiest way(*) to get started with development is by using the Dev Container environment that is set up for the project. It comes with batteries included: + +1. VS Code extensions required to develop, e.g. eslint and prettier. +2. Node and npm. +3. The newest version of maestro-webapi and a compatible version of the JRE. + +If you prefer to use your own Maestro JAR and Java version, such as when working with a cutting-edge development version, there's also a `Basic` Dev Container available that doesn't include Maestro or Java. + +(*) *if you already have Docker installed, otherwise it can be slightly involved*. + +#### Prerequisites + +Before you can use dev containers, you need to install Docker Desktop, and if you're on Windows enable the WSL 2 backend, so you can run Linux containers. These great guides walk you through the entire process: + +1. Installing Docker Desktop: [Windows](https://docs.docker.com/desktop/install/windows-install/), [Mac](https://docs.docker.com/desktop/install/windows-install/), [Linux](https://docs.docker.com/desktop/install/windows-install/) +2. Enabling the WSL 2 backend (skip of not on Windows): [guide](https://docs.docker.com/desktop/wsl/) + +⚠️ **NOTE:** When running Docker on a Windows host, it's very important to configure Docker Desktop to use Linux containers, otherwise the dev containers will simply not start. + +Now install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension from the VS Code Marketplace. + +#### Cloning the repo + +Run the VS Code command ```Dev Containers: Clone Repository in Named Container Volume...``` and go through the steps of the setup wizard. + +#### Running Maestro + +Maestro is automatically started when you launch the Extension Host and use the `Run Extension with Maestro` launch task. To avoid this behavior, use the other launch task instead: `Run Extension`. + +### Alternative approach (without Dev Containers) + +With this approach, you don't have to install Docker. + +1. Clone the repo: `git clone https://github.com/INTO-CPS-Association/Co-Simulation-Studio.git` +2. Open your extensions view in VS Code, search for `@recommended` and install the `Workspace Recommendations`. +3. Install node and NPM. Instructions for your OS are found on the web. +4. [Optional] Downloading Maestro + 1. Install at least Java 11 (JRE). + 2. Download the latest release of `maestro-webapi` from [here](https://github.com/INTO-CPS-Association/maestro/releases). + 3. Set the environment variable `MAESTRO_WEBAPI_PATH` to the path pointing to the Maestro JAR. This allows the dev tools to automatically find and start Maestro when you're debugging the extension. + +### Finishing steps (applies to both approaches) + +Standing at the root of the cloned repository, install the project dependencies: + +```bash +npm install +``` + +## Running tests + +Running tests and generating coverage reports is as simple as running a few npm scripts: + +### Units tests + +```bash +npm run test:unit +``` + +### Integration tests + +```bash +npm run test:integration +``` + +## Launching the extension for debugging + +When you've made modifications to the extension that you'd like to try out, the fastest way to poke around in a custom build of the extension is to launch the VS Code "Extension Host". There are two configurations to choose from: one that starts Maestro alongside the Extension Host, and one that doesn't (default). You can pick from one of these two configurations and start the Extension Host in the `Run and Debug` view. + +For future reference, when you've selected your preferred debug configuration, the shortcut F5 can be used to launch the most recently used configuration. + +### A note on code updates + +Although `esbuild` is running in watch mode and bundles the project on every detected change, the VS Code Extension Host does not support hot reloading. Thus, your changes will not immediately reflect in the Extension Host, which will require a reload. The easiest way to do this is to run `Developer: Reload Window` from the VS Code command palette. + +## Bundling the extension + +If a pre-bundled version of the extension hasn't been published to the Visual Studio Code Marketplace, or if you require a build with cutting-edge features, you can bundle the extension locally. + +First install the CLI to package VS Code extension: + +```bash +# The VS Code Extension Manager, used to bundle the extension +npm install -g @vscode/vsce +``` + +The extension can then be bundled by running: + +```bash +vsce package +``` + +This will output a `.vsix`-file which can be installed by following the guide [here](https://code.visualstudio.com/docs/editor/extension-marketplace#_install-from-a-vsix). The filename of the `.vsix`-file will reflect the version defined in the `package.json`, so it's a good idea to set the version to something that clearly reflects that the bundle is pre-release if you haven't finished development yet. + +## Publishing the extension + +With a `.vsix` in hand, it's time to publish the extension to the Marketplace. At the moment, this is done manually through the [browser-based interface](https://marketplace.visualstudio.com/manage/). diff --git a/docs/docs/index.md b/docs/docs/index.md new file mode 100644 index 0000000..d000196 --- /dev/null +++ b/docs/docs/index.md @@ -0,0 +1,89 @@ +# Cosimulation Studio VS Code Extension + +[![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/intocps.cosimulation-studio)](https://marketplace.visualstudio.com/items?itemName=intocps.cosimulation-studio) +![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/intocps.cosimulation-studio) + +The Cosimulation Studio VS Code Extension seamlessly integrates with the INTO-CPS Maestro cosimulation orchestration engine and provides FMU-aware autocompletion and linting, enhancing the experience of authoring cosimulation configurations within Visual Studio Code. + +## Quickstart guide + +To quickly get the extension up and running, begin here. + +### Installing the extension + +The most recent stable version of the extension is always available in the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=intocps.cosimulation-studio) as a one-click installation. + +### Setting up Maestro + +The extension does not automatically install or orchestrate the Maestro application. If you plan on using the Maestro integration, download the latest release of `maestro-webapi--bundle.jar` from the Maestro repository's release page. It's crucial to install the JAR with the Web API, as the extension cannot communicate with Maestro otherwise. Once that’s done, you can start Maestro by running the command: + +```bash +java -jar maestro-webapi--bundle.jar +``` + +This will expose the API on port `8082` by default, which is the port expected by the extension. At this point, you should be able to launch simulations from within VS Code, as demonstrated in the [features overview](#integration-with-maestro). + +### Editing cosimulation configuration files + +With the extension installed, you're ready to create your first cosimulation configuration file. By default, any file named `cosim.json` within a workspace is considered a cosim configuration file by the tool and triggers IntelliSense features. The trigger path is configurable via the VS Code settings: Ctrl + Shift + P > `Preferences: Open Settings (UI)` > `Cosimstudio: Cosim Path`. + +FMUs definitions can either use absolute paths or relative paths to reference FMU files. Relative paths are relative to the workspace root. + +⚠️ NOTE: For maximum portability of projects, it is generally recommended to use relative paths. However, you can choose to use absolute paths if your specific situation warrants it. + +> #### Example +> +> Consider a VS Code workspace folder located at: `/home/user/cosim_workspace` that is structured as follows +> +>```text +>📦 cosim_workspace/ +>├─ fmus/ +>│  ├─ fmu1.fmu +>│  └─ fmu2.fmu +>└─ cosim.json +>``` +> +> The `cosim.json` file can then reference the fmus using either relative or absolute paths +> +> ```json +> { +> ... +> "fmus": { +> "{fmu1}": "./fmus/fmu1.fmu", +> "{fmu2}": "/home/user/cosim_workspace/fmus/fmu2.fmu" +> } +> ... +> } +>``` +> +> Note that using relative paths for `{fmu1}` is more succinct and will not break if another developer clones the project into a different directory, such as `/home/another_user/cosim_workspace`. + +## Features + +### Linting + +The linter catches errors in cosimulation files as you're writing them, and importantly before they reach the Maestro engine. + +The editor will generate an error if an FMU definition contains a reference to a file that doesn't exist. + +![An animation illustrating the autocompletion feature of the extension](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1/dark/fmu_file_linting.gif) + +Defining connections with incorrect causality will be detected as an error. + +![An animation illustrating the linting feature of the extension and how it ensure correct causality](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1/dark/fmu_causality_linting.gif) + +### Autocompletion + +When the configuration contains references to FMUs that the extension can resolve, the editor provides smart completions of input and output variables. + +![An animation illustrating the autocompletion feature of the extension](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1/dark/fmu_auto_completion.gif) + +### Integration with Maestro + +Assuming there's a Maestro instance running, launching simulations directly from within VS Code is very easy. Whenever a cosimulation configuration file is open in the editor, a button to run simulations will appear in the editor toolbar. Pressing the button will send the configuration currently being edited to Maestro, and upon completion, the results will be populated in a new CSV file. + +![An animation illustrating the autocompletion feature of the extension](https://odin.cps.digit.au.dk/into-cps/cosim-studio/v0.1/dark/maestro_integration.gif) + +## Developing the extension + +For a more in-depth guide on setting up the development environment and building and installing a development version of the extension, refer to the [developer guide](./developing.md). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 0000000..aec9b1a --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,6 @@ +site_name: Cosimulation Studio Documentation +theme: + name: readthedocs +nav: + - Home: index.md + - Developing: developing.md \ No newline at end of file diff --git a/package.json b/package.json index 6bc73e6..2a57ebc 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "SEE LICENSE IN LICENSE.md", "description": "Co-simulation in VS Code", "repository": "https://github.com/INTO-CPS-Association/Co-Simulation-Studio", - "version": "0.1.2", + "version": "0.1.3", "icon": "into_cps_logo.png", "engines": { "vscode": "^1.82.0" diff --git a/src/fmu.ts b/src/fmu.ts index 131ea0e..0c4c392 100644 --- a/src/fmu.ts +++ b/src/fmu.ts @@ -6,17 +6,14 @@ import { getLogger } from 'logging' const logger = getLogger() -interface ModelInput { - name: string -} - -interface ModelOutput { +export interface ModelVariable { name: string } export interface FMUModel { - inputs: ModelInput[] - outputs: ModelOutput[] + inputs: ModelVariable[] + outputs: ModelVariable[] + parameters: ModelVariable[] } export interface FMUSource { @@ -97,8 +94,9 @@ export async function extractFMUModelFromPath( } const modelDescriptionObject = zipFile.file('modelDescription.xml') - const modelDescriptionContents = - await modelDescriptionObject?.async('nodebuffer') + const modelDescriptionContents = await modelDescriptionObject?.async( + 'nodebuffer' + ) if (modelDescriptionContents) { return parseXMLModelDescription(modelDescriptionContents) @@ -119,8 +117,9 @@ export function parseXMLModelDescription(source: string | Buffer): FMUModel { throw new Error('Failed to parse XML model description.') } - const inputs: ModelInput[] = [] - const outputs: ModelOutput[] = [] + const inputs: ModelVariable[] = [] + const outputs: ModelVariable[] = [] + const parameters: ModelVariable[] = [] // TODO: update this code to use Zod schemas instead of optional chaining and nullish coalescing const modelVariables = @@ -138,11 +137,16 @@ export function parseXMLModelDescription(source: string | Buffer): FMUModel { outputs.push({ name: mVar['@_name'], }) + } else if (varCausality === 'parameter') { + parameters.push({ + name: mVar['@_name'], + }) } } return { inputs, outputs, + parameters, } } diff --git a/src/language-features/completion-items.ts b/src/language-features/completion-items.ts index d63852b..6c228bf 100644 --- a/src/language-features/completion-items.ts +++ b/src/language-features/completion-items.ts @@ -1,11 +1,12 @@ import * as vscode from 'vscode' -import { getNodePath } from 'jsonc-parser' +import { getNodePath, Node } from 'jsonc-parser' import { CosimulationConfiguration, getFMUIdentifierFromConnectionString, getStringContentRange, isNodeString, } from './utils' +import { ModelVariable } from 'fmu' export class SimulationConfigCompletionItemProvider implements vscode.CompletionItemProvider @@ -101,15 +102,38 @@ export class SimulationConfigCompletionItemProvider return [] } - const validVariables = - await cosimConfig.getAllVariablesFromIdentifier(fmuIdentifier) + const fmuModel = await cosimConfig.getFMUModel(fmuIdentifier) + + console.log(fmuModel) + + if (!fmuModel) { + return [] + } + + const completionContext = this.getCompletionContext(completionNode) + + console.log('Completion context', completionContext) + + const completionVariables: ModelVariable[] = [] + + if (completionContext === 'input') { + completionVariables.push(...fmuModel.inputs) + } else if (completionContext === 'output') { + completionVariables.push(...fmuModel.outputs) + } else if (completionContext === 'parameter') { + completionVariables.push(...fmuModel.parameters) + } + + const completionStrings = completionVariables.map( + (variable) => variable.name + ) // Get range of the nearest word following a period const range = cosimConfig .getDocument() .getWordRangeAtPosition(position, /(?<=\.)\w+/) - const suggestions = validVariables.map((variable) => { + const suggestions = completionStrings.map((variable) => { const completionItem = new vscode.CompletionItem( variable, vscode.CompletionItemKind.Property @@ -121,4 +145,25 @@ export class SimulationConfigCompletionItemProvider return suggestions } + + getCompletionContext( + completionNode: Node + ): 'input' | 'output' | 'parameter' | null { + const nodePath = getNodePath(completionNode) + + console.log('Node path:', nodePath) + + if (nodePath.length === 2 && nodePath[0] === 'parameters') { + return 'parameter' + } else if (nodePath.length === 2 && nodePath[0] === 'connections') { + return 'output' + } else if ( + nodePath.length === 3 && + nodePath[0] === 'connections' && + typeof nodePath[2] === 'number' + ) { + return 'input' + } + return null + } } diff --git a/test/fmu.unit.test.ts b/test/fmu.unit.test.ts index 014b710..5206bb7 100644 --- a/test/fmu.unit.test.ts +++ b/test/fmu.unit.test.ts @@ -27,10 +27,33 @@ const dummyModelDescription = ` + + ` +const dummyModel: FMUModel = { + inputs: [ + { + name: 'fk', + }, + ], + outputs: [ + { + name: 'x1', + }, + { + name: 'v1', + }, + ], + parameters: [ + { + name: 'c1', + }, + ], +} + describe('FMU Parsing', () => { afterEach(() => { jest.clearAllMocks() @@ -40,21 +63,7 @@ describe('FMU Parsing', () => { it('parses XML model description correctly', async () => { const result = parseXMLModelDescription(dummyModelDescription) - expect(result).toEqual({ - inputs: [ - { - name: 'fk', - }, - ], - outputs: [ - { - name: 'x1', - }, - { - name: 'v1', - }, - ], - } satisfies FMUModel) + expect(result).toEqual(dummyModel) }) it('throws when parsing invalid XML model description', async () => { @@ -105,21 +114,7 @@ describe('FMU Parsing', () => { const result = await extractFMUModelFromPath(Uri.file('file/path')) - expect(result).toEqual({ - inputs: [ - { - name: 'fk', - }, - ], - outputs: [ - { - name: 'x1', - }, - { - name: 'v1', - }, - ], - } satisfies FMUModel) + expect(result).toEqual(dummyModel) }) }) describe('getFMUModelFromPath', () => { @@ -172,21 +167,7 @@ describe('FMU Parsing', () => { 'file/path' ) - expect(result).toEqual({ - inputs: [ - { - name: 'fk', - }, - ], - outputs: [ - { - name: 'x1', - }, - { - name: 'v1', - }, - ], - } satisfies FMUModel) + expect(result).toEqual(dummyModel) expect(vscode.workspace.fs.stat).toHaveBeenCalledWith( Uri.file('/data/file/path') ) @@ -239,21 +220,7 @@ describe('FMU Parsing', () => { 'file/path' ) - expect(result).toEqual({ - inputs: [ - { - name: 'fk', - }, - ], - outputs: [ - { - name: 'x1', - }, - { - name: 'v1', - }, - ], - } satisfies FMUModel) + expect(result).toEqual(dummyModel) const secondResult = await getFMUModelFromPath( workspaceFolder, @@ -289,21 +256,7 @@ describe('FMU Parsing', () => { 'file/path' ) - expect(result).toEqual({ - inputs: [ - { - name: 'fk', - }, - ], - outputs: [ - { - name: 'x1', - }, - { - name: 'v1', - }, - ], - } satisfies FMUModel) + expect(result).toEqual(dummyModel) ;(vscode.workspace.fs.stat as jest.Mock).mockResolvedValue({ ctime: 1, }) diff --git a/test/language-features/completion-items.unit.test.ts b/test/language-features/completion-items.unit.test.ts index d9cb0ce..aa9a126 100644 --- a/test/language-features/completion-items.unit.test.ts +++ b/test/language-features/completion-items.unit.test.ts @@ -1,35 +1,95 @@ +import { FMUModel } from 'fmu' import { createTextDocument } from 'jest-mock-vscode' import { SimulationConfigCompletionItemProvider } from 'language-features/completion-items' import { CosimulationConfiguration } from 'language-features/utils' -import { Position, Uri } from 'vscode' +import { Position, TextDocument, Uri } from 'vscode' const workspaceUri = Uri.file('/data') -const dummyCosimConfig = ` +const dummyCosimConfigTemplate = ` { "fmus": { "{fmu1}": "${Uri.joinPath(workspaceUri, 'fmu1.fmu').path}", "{fmu2}": "${Uri.joinPath(workspaceUri, 'fmu2.fmu').path}" }, "connections": { - "": [""], - "{fmu1}.fmui1.": [""] + "$": [""], + "{fmu1}.fmui1.$": ["{fmu1}.fmui1.$"] }, + "parameters": { + "{fmu1}.fmui1.$" : 1.0 + } } ` -const dummyConfigDocument = createTextDocument( - Uri.joinPath(workspaceUri, 'custom_cosim.json'), - dummyCosimConfig, - 'json' -) +const dummyModel: FMUModel = { + inputs: [ + { + name: 'fk', + }, + ], + outputs: [ + { + name: 'x1', + }, + { + name: 'v1', + }, + ], + parameters: [ + { + name: 'c1', + }, + ], +} + +function getCompletionPosition( + text: string, + completionChar: string, + offset: number +): Position { + const completionCharCount = text.split(completionChar).length - 1 + + if (completionCharCount - 1 < offset) { + throw Error( + 'Offset exceeds the number of completion characters present in the completion template.' + ) + } + + let cleanedText = text + for (let i = 0; i < offset; i++) { + cleanedText = cleanedText.replace(completionChar, '') + } + + const parts = cleanedText.split(completionChar, 1) + const preCompletionLines = parts[0].split('\n') + const completionLine = preCompletionLines.length - 1 + const completionColumn = preCompletionLines[completionLine].length + + return new Position(completionLine, completionColumn) +} + +function constructCompletionExample( + template: string, + offset: number +): [TextDocument, Position] { + const completionPosition = getCompletionPosition(template, '$', offset) + const cosimConfig = template.replaceAll('$', '') + + const dummyConfigDocument = createTextDocument( + Uri.joinPath(workspaceUri, 'custom_cosim.json'), + cosimConfig, + 'json' + ) + + return [dummyConfigDocument, completionPosition] +} describe('SimulationConfigCompletionItemProvider', () => { let cosimConfig: CosimulationConfiguration let simulationConfigCIP: SimulationConfigCompletionItemProvider beforeEach(() => { - cosimConfig = new CosimulationConfiguration(dummyConfigDocument) simulationConfigCIP = new SimulationConfigCompletionItemProvider() }) @@ -40,7 +100,11 @@ describe('SimulationConfigCompletionItemProvider', () => { describe('getFMUIdentifierCompletionItems', () => { it('should return the correct completion items', async () => { // The position inside the empty string of `dummyCosimConfig`, where the completion was triggered. - const pos = new Position(7, 9) + const [dummyConfigDocument, pos] = constructCompletionExample( + dummyCosimConfigTemplate, + 0 + ) + cosimConfig = new CosimulationConfiguration(dummyConfigDocument) const suggestions = await simulationConfigCIP.getFMUIdentifierCompletionItems( @@ -56,22 +120,77 @@ describe('SimulationConfigCompletionItemProvider', () => { }) describe('getFMUVariableCompletionItems', () => { - it('should return the correct completion items', async () => { - // The position inside the connection string right after the final period of `dummyCosimConfig`, where the completion was triggered. - const pos = new Position(8, 22) - const getAllVariablesFromIdentifierSpy = jest.spyOn( - cosimConfig, - 'getAllVariablesFromIdentifier' + it('should return the correct output completion items', async () => { + // The position inside the connection string right after the period, where the completion was triggered. + const [dummyConfigDocument, pos] = constructCompletionExample( + dummyCosimConfigTemplate, + 1 ) - getAllVariablesFromIdentifierSpy.mockImplementation( - async (identifier: string) => { - if (identifier === '{fmu1}') { - return ['v1', 'v2'] - } + cosimConfig = new CosimulationConfiguration(dummyConfigDocument) + const getFMUModelSpy = jest.spyOn(cosimConfig, 'getFMUModel') + getFMUModelSpy.mockImplementation(async (identifier: string) => { + if (identifier === '{fmu1}') { + return dummyModel + } + + return undefined + }) + + const suggestions = + await simulationConfigCIP.getFMUVariableCompletionItems( + cosimConfig, + pos + ) - return [] + expect(suggestions).toHaveLength(2) + + const suggestionLabels = suggestions.map((sug) => sug.label) + expect(suggestionLabels).toEqual(['x1', 'v1']) + }) + + it('should return the correct input completion items', async () => { + // The position inside the string in the inputs array of connections right after the period, where the completion was triggered. + const [dummyConfigDocument, pos] = constructCompletionExample( + dummyCosimConfigTemplate, + 2 + ) + cosimConfig = new CosimulationConfiguration(dummyConfigDocument) + const getFMUModelSpy = jest.spyOn(cosimConfig, 'getFMUModel') + getFMUModelSpy.mockImplementation(async (identifier: string) => { + if (identifier === '{fmu1}') { + return dummyModel } + + return undefined + }) + + const suggestions = + await simulationConfigCIP.getFMUVariableCompletionItems( + cosimConfig, + pos + ) + + expect(suggestions).toHaveLength(1) + + const suggestionLabels = suggestions.map((sug) => sug.label) + expect(suggestionLabels).toEqual(['fk']) + }) + + it('should return the correct parameter completion items', async () => { + // The position inside the string in the parameters mapping right after the period, where the completion was triggered. + const [dummyConfigDocument, pos] = constructCompletionExample( + dummyCosimConfigTemplate, + 3 ) + cosimConfig = new CosimulationConfiguration(dummyConfigDocument) + const getFMUModelSpy = jest.spyOn(cosimConfig, 'getFMUModel') + getFMUModelSpy.mockImplementation(async (identifier: string) => { + if (identifier === '{fmu1}') { + return dummyModel + } + + return undefined + }) const suggestions = await simulationConfigCIP.getFMUVariableCompletionItems( @@ -79,10 +198,10 @@ describe('SimulationConfigCompletionItemProvider', () => { pos ) - expect(suggestions).toHaveLength(2) + expect(suggestions).toHaveLength(1) const suggestionLabels = suggestions.map((sug) => sug.label) - expect(suggestionLabels).toEqual(['v1', 'v2']) + expect(suggestionLabels).toEqual(['c1']) }) }) }) diff --git a/test/language-features/utils.unit.test.ts b/test/language-features/utils.unit.test.ts index 9c3d081..a8d3701 100644 --- a/test/language-features/utils.unit.test.ts +++ b/test/language-features/utils.unit.test.ts @@ -59,6 +59,11 @@ const fmuModel1: FMUModel = { name: 'vo1', }, ], + parameters: [ + { + name: 'vp1', + }, + ], } const fmuSource1: FMUSource = { @@ -77,6 +82,11 @@ const fmuModel2: FMUModel = { name: 'vo2', }, ], + parameters: [ + { + name: 'vp2', + }, + ], } const fmuSource2: FMUSource = {