Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3a35707
Add graphical view functionality and enhance language client integration
e-filchenko-bosh Aug 26, 2026
4438a2f
Enhance graphical view functionality and add webview assets
e-filchenko-bosh Aug 27, 2026
b406d54
Add support for attribute navigation in graphical view
e-filchenko-bosh Aug 27, 2026
4a841f7
Update SVG references in graphical view tests to use full URLs
e-filchenko-bosh Aug 27, 2026
56bfeb1
refactoring
e-filchenko-bosh Sep 2, 2026
9555968
refactoring
e-filchenko-bosh Sep 2, 2026
c53c451
Merge remote-tracking branch 'origin/main'
e-filchenko-bosh Sep 2, 2026
0d3691d
resolve merge conflicts
e-filchenko-bosh Sep 3, 2026
89af6c1
Enhance graphical view with accessible controls and status indicators
e-filchenko-bosh Sep 10, 2026
f520453
improve graphical view open tab management
e-filchenko-bosh Sep 16, 2026
91d79b4
remove graphical SVG sanitizer
e-filchenko-bosh Sep 16, 2026
f48f58d
improve graphical viewport controls
e-filchenko-bosh Sep 17, 2026
9a8e971
stabilize graphical viewport layout
e-filchenko-bosh Sep 17, 2026
9b28684
stabilize graphical viewport layout
e-filchenko-bosh Sep 17, 2026
60ed732
resolve merge conflict
e-filchenko-bosh Sep 21, 2026
4c6ce68
Merge remote-tracking branch 'origin/main' into add-graphical-model-view
e-filchenko-bosh Sep 23, 2026
cae2296
Merge branch 'main' into add-graphical-model-view
atextor Oct 12, 2026
2dd6e18
Adjust 'temporarilyUnresolvable' to 'unresolvable'
atextor Oct 12, 2026
cfbe83b
Fix copyright headers
atextor Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions .vscode-test.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { defineConfig } from '@vscode/test-cli';
import {defineConfig} from '@vscode/test-cli';

const runId = process.env.VSCODE_TEST_RUN_ID ?? 'default';
const vscodeExecutablePath = process.env.VSCODE_EXECUTABLE_PATH;
const runId = (process.env.VSCODE_TEST_RUN_ID ?? 'default').replace(/[^a-zA-Z0-9_-]/g, '-');

export default defineConfig({
files: 'out/test/**/*.test.js',
useInstallation: process.env.VSCODE_EXECUTABLE_PATH
? { fromPath: process.env.VSCODE_EXECUTABLE_PATH }
: undefined,
launchArgs: [
`--user-data-dir=/tmp/extension-vscode-test-user-data-${runId}`,
`--extensions-dir=/tmp/extension-vscode-test-extensions-${runId}`
],
files: 'out/test/**/*.test.js',
...(vscodeExecutablePath ? {useInstallation: {fromPath: vscodeExecutablePath}} : {}),
launchArgs: [
`--user-data-dir=/tmp/extension-vscode-test-user-data-${runId}`,
`--extensions-dir=/tmp/extension-vscode-test-extensions-${runId}`,
],
});
2 changes: 2 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.vscode/**
.vscode-test/**
scripts/
src/**
out/test/**
.gitignore
.yarnrc
.prettierrc
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ Use the command `Semantic Models: Select SAMM CLI Executable` to choose either:
- Manual validation command:
- `Semantic Models: Validate Document Now`

## Graphical View

Use `Semantic Models: Open Graphical View` (`semantic-models.openGraphicalView`) from the Turtle editor-title button, the editor context menu, or the Command Palette to open a read-only SVG snapshot in a separate panel. One panel is reused per Turtle document.

The view renders on initial open, manual Refresh, and Save while the main Turtle document is visible. It includes unsaved text from the main document but uses the persisted versions of imported files. Typing, import saves, hidden-document saves, and revealing an existing panel do not trigger a render.

Element headers can navigate to definitions in local files. Eligible attribute rows whose owner is a named model element navigate as a whole row; language-qualified and wrapped rows retain their source mapping, and aggregated rows navigate to the start of the corresponding predicate. Rows with anonymous or otherwise non-deterministic owners remain inert. Navigation to remote URIs and arbitrary referenced values is not supported.

If rendering fails, the last successful diagram remains visible with an error or warning. Rendering is limited to 1,000 boxes and a 30-second request timeout. The graphical view is not an editor, does not update live while typing, and does not claim visual parity with the Aspect Model Editor.

Graphical View supports SVG only from the shipped/current trusted SAMM language server and generator contract. The extension strict-parses each SVG as XML but does not sanitize or filter its elements and attributes. Its restrictive webview CSP and extension-side message, sidecar, and local-file navigation validation remain enforced. External, substituted, incompatible, or compromised language servers are outside the supported security model; future Graphper or generator changes must preserve the tested style-free passive-output invariant.

## Running the Server and Extension Together

1. In this extension project, install the dependencies using `npm install`.
Expand Down
Binary file added media/aspect-model-editor-targetsize-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 25 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
"command": "semantic-models.selectSammCliExecutable",
"title": "Select SAMM CLI Executable",
"category": "Semantic Models"
},
{
"command": "semantic-models.openGraphicalView",
"title": "Open Graphical View",
"category": "Semantic Models",
"enablement": "editorLangId == turtle",
"icon": {
"light": "media/aspect-model-editor-targetsize-192.png",
"dark": "media/aspect-model-editor-targetsize-192.png"
}
}
],
"languages": [
Expand Down Expand Up @@ -143,11 +153,23 @@
}
},
"menus": {
"editor/title": [
{
"command": "semantic-models.openGraphicalView",
"when": "resourceLangId == turtle",
"group": "navigation@10"
}
],
"editor/context": [
{
"command": "semantic-models.validateDocumentNow",
"when": "resourceLangId == turtle && (resourceScheme == file || resourceScheme == untitled)",
"group": "1_modification"
},
{
"command": "semantic-models.openGraphicalView",
"when": "resourceLangId == turtle",
"group": "1_modification@2"
}
]
},
Expand Down Expand Up @@ -183,14 +205,15 @@
"vscode:prepublish": "npm run build",
"watch": "tsc -watch -p ./",
"pretest": "npm run build && npm run lint",
"build": "tsc -p tsconfig.json",
"build": "tsc -p tsconfig.json && node scripts/copy-webview-assets.mjs",
"build-watch": "tsc -p tsconfig.json --watch",
"prettier": "prettier --config .prettierrc --write './src/**/*{.ts,.js,.json}'",
"test": "vscode-test --config .vscode-test.mjs",
"test:prettier": "prettier --config .prettierrc --list-different './src/**/*{.ts,.js,.json}'",
"test:coverage": "vscode-test --config .vscode-test.mjs --coverage --coverage-reporter text --coverage-reporter lcov",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix"
"lint:fix": "eslint src --ext .ts --fix",
"test:webview-assets": "node scripts/verify-webview-assets.mjs"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
Expand Down
34 changes: 34 additions & 0 deletions scripts/copy-webview-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
*
* See the AUTHORS file(s) distributed with this work for additional
* information regarding authorship.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

import {copyFileSync, mkdirSync, readdirSync, rmSync} from 'node:fs';
import {join} from 'node:path';
import {outputDirectory, sha256, webviewAssets} from './webview-assets.mjs';

for (const asset of webviewAssets) {
if (asset.expectedSha256 && sha256(asset.source) !== asset.expectedSha256) {
throw new Error(`Pinned webview asset hash mismatch: ${asset.destination}`);
}
}

rmSync(outputDirectory, {recursive: true, force: true});
mkdirSync(outputDirectory, {recursive: true});
for (const asset of webviewAssets) {
copyFileSync(asset.source, join(outputDirectory, asset.destination));
}

const actual = readdirSync(outputDirectory).sort();
const expected = webviewAssets.map(asset => asset.destination).sort();
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error('Webview asset copy produced an unexpected inventory.');
}
37 changes: 37 additions & 0 deletions scripts/verify-webview-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
*
* See the AUTHORS file(s) distributed with this work for additional
* information regarding authorship.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

import {readdirSync, statSync} from 'node:fs';
import {join} from 'node:path';
import {outputDirectory, sha256, webviewAssets} from './webview-assets.mjs';

const actual = readdirSync(outputDirectory).sort();
const expected = webviewAssets.map(asset => asset.destination).sort();
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`Unexpected out/webview inventory: ${actual.join(', ')}`);
}

for (const asset of webviewAssets) {
const output = join(outputDirectory, asset.destination);
if (!statSync(output).isFile() || statSync(output).size === 0) {
throw new Error(`Missing or empty webview asset: ${asset.destination}`);
}
if (sha256(asset.source) !== sha256(output)) {
throw new Error(`Copied webview asset differs from its source: ${asset.destination}`);
}
if (asset.expectedSha256 && sha256(output) !== asset.expectedSha256) {
throw new Error(`Pinned webview asset hash mismatch: ${asset.destination}`);
}
}

process.stdout.write(`Verified ${webviewAssets.length} deterministic webview assets.\n`);
48 changes: 48 additions & 0 deletions scripts/webview-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
*
* See the AUTHORS file(s) distributed with this work for additional
* information regarding authorship.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

import {createHash} from 'node:crypto';
import {readFileSync} from 'node:fs';
import {dirname, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';

export const extensionRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export const outputDirectory = resolve(extensionRoot, 'out', 'webview');

export const webviewAssets = Object.freeze([
asset(
'src/webview/RobotoCondensed-LICENSE-Apache-2.0.txt',
'RobotoCondensed-LICENSE-Apache-2.0.txt',
'8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90',
),
asset('src/webview/RobotoCondensed-NOTICE.txt', 'RobotoCondensed-NOTICE.txt'),
asset(
'src/webview/RobotoCondensed-Regular.ttf',
'RobotoCondensed-Regular.ttf',
'4a7c36df4318fee50a8159c3a0ebde4572abab65447ae4a651c2fe87212302b5',
),
asset('src/webview/webview.css', 'webview.css'),
asset('src/webview/webview.js', 'webview.js'),
]);

export function sha256(file) {
return createHash('sha256').update(readFileSync(file)).digest('hex');
}

function asset(source, destination, expectedSha256) {
return Object.freeze({
source: resolve(extensionRoot, source),
destination,
...(expectedSha256 ? {expectedSha256} : {}),
});
}
2 changes: 1 addition & 1 deletion src/aspectValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface DiagnosticReport {
}

export interface RequestClient {
sendRequest<R>(method: string, params?: unknown): Thenable<R>;
sendRequest<R>(method: string, params?: unknown, token?: vscode.CancellationToken): Thenable<R>;
}

type RequestClientBinding = {
Expand Down
53 changes: 48 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@

import * as vscode from 'vscode';
import {AspectValidationController, RequestClient} from './aspectValidation';
import {GraphicalViewController} from './graphicalView';
import {LspGraphicalViewClient} from './graphicalViewClient';
import {VscodeGraphicalViewPanelFactory} from './graphicalViewPanel';
import {GitHubRepositoryValidator} from './githubRepositoryValidator';
import {TurtleLanguageClient} from './languageClient';
import {TurtleLanguageServer} from './languageServer';
import {LanguageServicesMode, LanguageServicesSupervisor, TerminalAction} from './languageServicesSupervisor';
import type {ExtensionLogger} from './outputChannel';
import {SammCliDownloader} from './sammCliDownloader';
import {TurtleExtensionSettings} from './settings';
import {TurtleLanguageClient} from './languageClient';
import {GitHubRepositoryValidator} from './githubRepositoryValidator';
import type {ExtensionLogger} from './outputChannel';
import {LanguageServicesSupervisor, LanguageServicesMode, TerminalAction} from './languageServicesSupervisor';

const SELECT_EXECUTABLE_COMMAND = 'semantic-models.selectSammCliExecutable';
const SELECT_EXECUTABLE_TITLE = 'Select SAMM CLI Executable';
Expand All @@ -28,6 +31,7 @@ const GITHUB_REPOSITORY_VALIDATION_DEBOUNCE_MS = 2000;

let settings: TurtleExtensionSettings;
let aspectValidationController: AspectValidationController;
let graphicalViewController: GraphicalViewController;
let sammCliDownloader: SammCliDownloader;
let gitHubRepositoryValidator: GitHubRepositoryValidator;

Expand All @@ -47,6 +51,40 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
gitHubRepositoryValidator = new GitHubRepositoryValidator(outputChannel);
aspectValidationController = new AspectValidationController(createUnavailableClient(), vscode.window, outputChannel);
aspectValidationController.register(context);
graphicalViewController = new GraphicalViewController(
undefined,
new VscodeGraphicalViewPanelFactory(context.extensionUri),
vscode.commands,
vscode.window,
{
onDidSaveTextDocument: listener => vscode.workspace.onDidSaveTextDocument(listener),
onDidChangeDocumentAvailability: listener =>
vscode.Disposable.from(
vscode.workspace.onDidOpenTextDocument(document => listener(document.uri.toString(), true)),
vscode.workspace.onDidCloseTextDocument(document => listener(document.uri.toString(), false)),
vscode.window.tabGroups.onDidChangeTabs(event => {
for (const tab of event.closed) {
if (tab.input instanceof vscode.TabInputText) {
const sourceUri = tab.input.uri.toString();
const remainsOpen = vscode.window.tabGroups.all.some(group =>
group.tabs.some(
candidate =>
candidate.input instanceof vscode.TabInputText &&
candidate.input.uri.toString() === sourceUri,
),
);
if (!remainsOpen) {
listener(sourceUri, false);
}
}
}
}),
),
isDocumentAvailable: uri => vscode.workspace.textDocuments.some(document => document.uri.toString() === uri),
},
outputChannel,
);
graphicalViewController.register(context);
languageServicesSupervisor = createLanguageServicesSupervisor();

context.subscriptions.push(
Expand Down Expand Up @@ -137,7 +175,12 @@ function createLanguageServicesSupervisor(): LanguageServicesSupervisor {
logOutputChannel.logLevel,
),
createClient: configuration => new TurtleLanguageClient(outputChannel, configuration.port, logOutputChannel.logLevel),
setRequestClient: (client, generation) => aspectValidationController.setClient(client, generation),
setRequestClient: (client, generation) => {
aspectValidationController.setClient(client, generation);
graphicalViewController.setClient(
client instanceof TurtleLanguageClient ? new LspGraphicalViewClient(client) : undefined,
);
},
unavailableClient: createUnavailableClient,
logger: outputChannel,
notifyTerminal: notifyTerminalRecoveryFailure,
Expand Down
Loading
Loading