Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
58b9955
Update user documentation with information about graphical view
atextor Oct 23, 2026
9c60288
Merge branch 'main' into add-graphical-model-view
atextor Oct 23, 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
11 changes: 11 additions & 0 deletions documentation/vs-code-plugin/modules/ROOT/pages/features.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
* Document outline for navigating the current file.
* Automatic syntax validation while typing, with diagnostics in the editor and the *Problems* view.

== Graphical View

When editing a SAMM Aspect Model, you can right-click the document and select "Open Graphical View"
to open a graphical representation of the model. Alternatively, you can select "Semantic Models:
Open Graphical View" from the Command Palette.

The graphical view shows a read-only diagram representation of the model. It is updated when the
document is saved, or when you click the "Refresh" button in the panel. You can click element
headers in the graphical view to directly jump to the definition of the respective element in the
corresponding `.ttl` file.

== Working with Aspect Models

* *Go to Definition* for referenced elements in the current file, workspace, and SAMM vocabulary.
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 @@ -24,7 +24,7 @@ export type AspectValidationTrigger = 'manual' | 'save';
export type ValidationResult = 'validation_successful' | 'validation_failed';

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