From c618d45afd9cbabbbfc4d17fc62706395c8b0e5e Mon Sep 17 00:00:00 2001 From: Giorgi Javakhadze Date: Fri, 9 Oct 2026 18:51:35 +0400 Subject: [PATCH] Fix duplicate problem entries from virtual document schemes (#19) --- package.json | 2 +- src/aspectValidation.ts | 3 +- src/languageClient.ts | 9 +++-- src/test/aspectValidationController.test.ts | 22 ++++++++++++ src/test/languageClient.test.ts | 37 ++++++++++++++++++++- src/test/validationTestHarness.ts | 6 ++-- 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a4ed6f3..300f0bf 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "editor/context": [ { "command": "semantic-models.validateDocumentNow", - "when": "resourceLangId == turtle", + "when": "resourceLangId == turtle && (resourceScheme == file || resourceScheme == untitled)", "group": "1_modification" } ] diff --git a/src/aspectValidation.ts b/src/aspectValidation.ts index d3e7381..2f7bf5d 100644 --- a/src/aspectValidation.ts +++ b/src/aspectValidation.ts @@ -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'; @@ -79,7 +80,7 @@ export class AspectValidationController { document: Pick | undefined, trigger: AspectValidationTrigger, ): Promise { - 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.'); } diff --git a/src/languageClient.ts b/src/languageClient.ts index f1ce3b1..64c45ad 100644 --- a/src/languageClient.ts +++ b/src/languageClient.ts @@ -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 { @@ -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', diff --git a/src/test/aspectValidationController.test.ts b/src/test/aspectValidationController.test.ts index ac55532..5d8785f 100644 --- a/src/test/aspectValidationController.test.ts +++ b/src/test/aspectValidationController.test.ts @@ -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(); diff --git a/src/test/languageClient.test.ts b/src/test/languageClient.test.ts index 2573afe..2422df6 100644 --- a/src/test/languageClient.test.ts +++ b/src/test/languageClient.test.ts @@ -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 () => { diff --git a/src/test/validationTestHarness.ts b/src/test/validationTestHarness.ts index 5643b09..f1bfb0e 100644 --- a/src/test/validationTestHarness.ts +++ b/src/test/validationTestHarness.ts @@ -73,10 +73,12 @@ export function createValidationControllerHarness(options: ValidationHarnessOpti }; } -export function createValidationDocument(filePath: string): Pick { +export function createValidationDocument( + filePathOrUri: string | vscode.Uri, +): Pick { return { languageId: 'turtle', - uri: vscode.Uri.file(filePath), + uri: typeof filePathOrUri === 'string' ? vscode.Uri.file(filePathOrUri) : filePathOrUri, }; }