Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
"editor/context": [
{
"command": "semantic-models.validateDocumentNow",
"when": "resourceLangId == turtle",
"when": "resourceLangId == turtle && (resourceScheme == file || resourceScheme == untitled)",
"group": "1_modification"
}
]
Expand Down
3 changes: 2 additions & 1 deletion src/aspectValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {ExtensionLogger} from './outputChannel';

export const VALIDATE_DOCUMENT_REQUEST = 'turtle/aspectValidation/validateDocument';
export const VALIDATE_DOCUMENT_COMMAND = 'semantic-models.validateDocumentNow';
export const SUPPORTED_DOCUMENT_SCHEMES = ['file', 'untitled'];
const STATUS_MESSAGE_TIMEOUT_MS = 5000;

export type AspectValidationTrigger = 'manual' | 'save';
Expand Down Expand Up @@ -79,7 +80,7 @@ export class AspectValidationController {
document: Pick<vscode.TextDocument, 'languageId' | 'uri'> | undefined,
trigger: AspectValidationTrigger,
): Promise<DiagnosticReport | undefined> {
if (!document || document.languageId !== 'turtle') {
if (!document || document.languageId !== 'turtle' || !SUPPORTED_DOCUMENT_SCHEMES.includes(document.uri.scheme)) {
if (trigger === 'manual') {
await this.window.showWarningMessage('Open a Turtle file before running aspect validation.');
}
Expand Down
9 changes: 7 additions & 2 deletions src/languageClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@
import {Trace} from 'vscode-jsonrpc';
import * as net from 'node:net';
import * as vscode from 'vscode';
import {CloseAction, ErrorAction, ErrorHandler, LanguageClient, LanguageClientOptions, State, StreamInfo} from 'vscode-languageclient/node';
import {CloseAction, DocumentFilter, ErrorAction, ErrorHandler, LanguageClient, LanguageClientOptions, State, StreamInfo} from 'vscode-languageclient/node';
import type {RequestClient} from './aspectValidation';
import type {ExtensionLogger} from './outputChannel';
import type {DisposableLike} from './languageServicesSupervisor';

const CLIENT_START_TIMEOUT_MS = 60_000;

export const TURTLE_DOCUMENT_SELECTOR: DocumentFilter[] = [
{language: 'turtle', scheme: 'file'},
{language: 'turtle', scheme: 'untitled'},
];

export function createDoNotRestartErrorHandler(onUnexpectedClose: () => void): ErrorHandler {
let closeReported = false;
return {
Expand Down Expand Up @@ -77,7 +82,7 @@ export class TurtleLanguageClient implements RequestClient {
});

const clientOptions: LanguageClientOptions = {
documentSelector: ['turtle'],
documentSelector: TURTLE_DOCUMENT_SELECTOR,
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.ttl'),
configurationSection: 'semantic-models.modelResolution',
Expand Down
22 changes: 22 additions & 0 deletions src/test/aspectValidationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ suite('AspectValidationController', () => {
assert.deepStrictEqual(harness.window.errorMessages, ['Aspect validation request failed: connection failed']);
});

test('does not validate documents with an unsupported scheme', async () => {
const harness = createValidationControllerHarness({response: {diagnostics: []}});
const virtualDoc = createValidationDocument(vscode.Uri.parse('chat-editing-text-model:/tmp/Model.ttl'));

const result = await harness.controller.validateDocument(virtualDoc, 'manual');

assert.strictEqual(result, undefined);
assert.strictEqual(harness.sentRequests.length, 0);
assert.strictEqual(harness.window.warningMessages.length, 1);
assert.deepStrictEqual(harness.window.warningMessages, ['Open a Turtle file before running aspect validation.']);
});

test('validates untitled documents', async () => {
const harness = createValidationControllerHarness({response: {diagnostics: []}});
const untitledDoc = createValidationDocument(vscode.Uri.parse('untitled:Untitled-1'));

const result = await harness.controller.validateDocument(untitledDoc, 'manual');

assert.deepStrictEqual(result, {diagnostics: []});
assert.strictEqual(harness.sentRequests.length, 1);
});

test('register remains compatible with the VS Code command surface', async () => {
await withStubbedRegisterCommand(() => {
const harness = createValidationControllerHarness();
Expand Down
37 changes: 36 additions & 1 deletion src/test/languageClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,43 @@
*/

import * as assert from 'assert';
import * as vscode from 'vscode';
import {CloseAction, State} from 'vscode-languageclient/node';
import {createDoNotRestartErrorHandler, TurtleLanguageClient} from '../languageClient';
import {createDoNotRestartErrorHandler, TURTLE_DOCUMENT_SELECTOR, TurtleLanguageClient} from '../languageClient';

suite('TurtleLanguageClient document selector', () => {
test('document selector only matches editable turtle documents', () => {
const matches = (scheme: string) =>
TURTLE_DOCUMENT_SELECTOR.some(
filter => typeof filter === 'object' && 'scheme' in filter && filter.language === 'turtle' && filter.scheme === scheme,
);

assert.ok(matches('file'));
assert.ok(matches('untitled'));
assert.ok(!matches('chat-editing-text-model'));
assert.ok(!matches('git'));
});

test('vscode matching rejects virtual turtle documents', () => {
const virtualDoc = {languageId: 'turtle', uri: vscode.Uri.parse('chat-editing-text-model:/tmp/Model.ttl?x=1')};
assert.strictEqual(
vscode.languages.match(TURTLE_DOCUMENT_SELECTOR as vscode.DocumentSelector, virtualDoc as vscode.TextDocument),
0,
);
});

test('vscode matching accepts file and untitled turtle documents', () => {
const fileDoc = {languageId: 'turtle', uri: vscode.Uri.file('/tmp/Model.ttl')};
assert.ok(
vscode.languages.match(TURTLE_DOCUMENT_SELECTOR as vscode.DocumentSelector, fileDoc as vscode.TextDocument) > 0,
);

const untitledDoc = {languageId: 'turtle', uri: vscode.Uri.parse('untitled:Untitled-1')};
assert.ok(
vscode.languages.match(TURTLE_DOCUMENT_SELECTOR as vscode.DocumentSelector, untitledDoc as vscode.TextDocument) > 0,
);
});
});

suite('TurtleLanguageClient lifecycle', () => {
test('uses DoNotRestart and reports a duplicate close only once', async () => {
Expand Down
6 changes: 4 additions & 2 deletions src/test/validationTestHarness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ export function createValidationControllerHarness(options: ValidationHarnessOpti
};
}

export function createValidationDocument(filePath: string): Pick<vscode.TextDocument, 'languageId' | 'uri'> {
export function createValidationDocument(
filePathOrUri: string | vscode.Uri,
): Pick<vscode.TextDocument, 'languageId' | 'uri'> {
return {
languageId: 'turtle',
uri: vscode.Uri.file(filePath),
uri: typeof filePathOrUri === 'string' ? vscode.Uri.file(filePathOrUri) : filePathOrUri,
};
}

Expand Down
Loading