From a2f0697abc3c6fe93db199bbdcc760b68a1301cf Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Mon, 14 Sep 2026 10:07:21 +0200 Subject: [PATCH 01/10] Add dependencies Signed-off-by: Fabrizio Antonangeli --- packages/language-service/package.json | 4 ++- packages/language-service/vite.config.ts | 2 +- pnpm-lock.yaml | 40 ++++++++++++++++++++++++ pnpm-workspace.yaml | 1 + 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/language-service/package.json b/packages/language-service/package.json index bfe35b1a..f244b830 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -36,7 +36,9 @@ "test": "vitest run" }, "dependencies": { - "@volar/language-service": "catalog:" + "@openworkflowspec/sdk": "catalog:", + "@volar/language-service": "catalog:", + "volar-service-json": "catalog:" }, "devDependencies": { "@types/node": "catalog:", diff --git a/packages/language-service/vite.config.ts b/packages/language-service/vite.config.ts index 5dbee9dd..c741739d 100644 --- a/packages/language-service/vite.config.ts +++ b/packages/language-service/vite.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ formats: ["es"], }, rollupOptions: { - external: [/^@volar\//], + external: [/^@volar\//, /^volar-service-/, /^@openworkflowspec\//], }, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2e0d863..1a2b91d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,9 @@ catalogs: vitest: specifier: ^5.0.0 version: 5.0.0 + volar-service-json: + specifier: 0.0.71 + version: 0.0.71 importers: @@ -276,9 +279,15 @@ importers: packages/language-service: dependencies: + '@openworkflowspec/sdk': + specifier: 'catalog:' + version: 1.0.3-alpha8 '@volar/language-service': specifier: 'catalog:' version: 2.4.28 + volar-service-json: + specifier: 'catalog:' + version: 0.0.71(@volar/language-service@2.4.28) devDependencies: '@types/node': specifier: 'catalog:' @@ -2801,6 +2810,9 @@ packages: '@volar/source-map@2.4.28': resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@webcontainer/env@1.1.1': resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} @@ -4042,6 +4054,17 @@ packages: jsdom: optional: true + volar-service-json@0.0.71: + resolution: {integrity: sha512-behyNhAG5UEXfvbIOWdE62XsPkp/e5W0V3sz3AW6zy7vgWLzgvElR77YSlcu708GwKu7OOFl5m9sUGVfqLIriQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-json-languageservice@5.7.2: + resolution: {integrity: sha512-WtKRDtJfFEmLrgtu+ODexOHm/6/krRF0k6t+uvkKIKW1Jh9ZIyxZQwJJwB3qhrEgvAxa37zbUg+vn+UyUK/U2w==} + vscode-jsonrpc@9.0.2: resolution: {integrity: sha512-SbQSV9yRemARxeXw6LU5sS6Zq0e9/DgCCX5yelH263ZQWukbTk8EF8fjTrr1dziasf4GwlJbvTwFnTrnQFWZXQ==} engines: {node: '>=14.0.0'} @@ -6180,6 +6203,8 @@ snapshots: '@volar/source-map@2.4.28': {} + '@vscode/l10n@0.0.18': {} + '@webcontainer/env@1.1.1': {} '@xyflow/react@12.11.6(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': @@ -7342,6 +7367,21 @@ snapshots: transitivePeerDependencies: - msw + volar-service-json@0.0.71(@volar/language-service@2.4.28): + dependencies: + vscode-json-languageservice: 5.7.2 + vscode-uri: 3.2.0 + optionalDependencies: + '@volar/language-service': 2.4.28 + + vscode-json-languageservice@5.7.2: + dependencies: + '@vscode/l10n': 0.0.18 + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.14 + vscode-languageserver-types: 3.18.3 + vscode-uri: 3.2.0 + vscode-jsonrpc@9.0.2: {} vscode-languageserver-protocol@3.18.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8f90becf..3a1f1af6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: catalog: "monaco-editor": "0.56.0" "@volar/language-service": ^2.4.28 + "volar-service-json": "0.0.71" "@changesets/changelog-github": ^1.0.1 "@changesets/cli": ^3.0.2 "@chromatic-com/storybook": ^5.3.1 From c580d23c95c39271686c1d4b45206c472e5c00ec Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Mon, 14 Sep 2026 17:33:22 +0200 Subject: [PATCH 02/10] Json plugin creation with first schema driven completions Signed-off-by: Fabrizio Antonangeli --- packages/language-service/README.md | 24 +++++++- packages/language-service/package.json | 3 +- packages/language-service/src/volar/index.ts | 4 ++ .../src/volar/plugins/json.ts | 36 ++++++++++++ .../tests/plugins/json.test.ts | 58 +++++++++++++++++++ packages/language-service/tests/testUtils.ts | 42 ++++++++++++++ pnpm-lock.yaml | 6 ++ pnpm-workspace.yaml | 1 + 8 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 packages/language-service/src/volar/plugins/json.ts create mode 100644 packages/language-service/tests/plugins/json.test.ts create mode 100644 packages/language-service/tests/testUtils.ts diff --git a/packages/language-service/README.md b/packages/language-service/README.md index 0b421811..b14e821e 100644 --- a/packages/language-service/README.md +++ b/packages/language-service/README.md @@ -22,15 +22,35 @@ The package provides the common infrastructure for Open Workflow language featur ## Architecture -Volar-specific code is isolated under `src/volar/`. Imports from `@volar/*` outside this directory are prevented by Oxlint. +Volar-specific code is isolated under `src/volar/`. Imports from `@volar/*` and `volar-service-*` outside this directory are prevented by Oxlint. ```text src/ ├── index.ts └── volar/ - └── index.ts + ├── index.ts + └── plugins/ + └── json.ts (JSON schema-driven completion via volar-service-json) ``` +## API + +### `createJsonLanguageServicePlugin()` + +Creates a Volar `LanguageServicePlugin` for JSON using [`volar-service-json`](https://github.com/volarjs/services/tree/master/packages/json) and the Open Workflow schema exported by `@openworkflowspec/sdk`. + +The plugin provides schema-driven JSON completion and can be composed by the host with other Volar language service plugins. + +```ts +import { createJsonLanguageServicePlugin } from "@openworkflowspec/language-service"; + +const jsonPlugin = createJsonLanguageServicePlugin(); +``` + +Host integration is responsible for composing this plugin with the other Volar services it needs. See the official [Volar Services documentation](https://volarjs.dev/reference/services/) for the service/plugin model. + +`volar-service-json` also exposes its standard JSON diagnostics through the returned plugin. This package does not add custom Open Workflow diagnostics at this stage. + ## Development ```bash diff --git a/packages/language-service/package.json b/packages/language-service/package.json index f244b830..3f2da5e8 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -47,6 +47,7 @@ "rimraf": "catalog:", "typescript": "catalog:", "vite": "catalog:", - "vitest": "catalog:" + "vitest": "catalog:", + "vscode-languageserver-textdocument": "catalog:" } } diff --git a/packages/language-service/src/volar/index.ts b/packages/language-service/src/volar/index.ts index 55a55f19..d73996e5 100644 --- a/packages/language-service/src/volar/index.ts +++ b/packages/language-service/src/volar/index.ts @@ -18,7 +18,11 @@ export { createLanguageService } from "@volar/language-service"; export type { LanguageService, + LanguageServiceContext, LanguageServiceEnvironment, LanguageServicePlugin, + LanguageServicePluginInstance, ProjectContext, } from "@volar/language-service"; + +export { createJsonLanguageServicePlugin } from "./plugins/json"; diff --git a/packages/language-service/src/volar/plugins/json.ts b/packages/language-service/src/volar/plugins/json.ts new file mode 100644 index 00000000..db147885 --- /dev/null +++ b/packages/language-service/src/volar/plugins/json.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { create } from "volar-service-json"; +import { workflowSchema } from "@openworkflowspec/sdk"; + +const DEFAULT_SCHEMA_URI = "urn:open-workflow-specification:workflow-schema"; + +export function createJsonLanguageServicePlugin() { + return create({ + getLanguageSettings() { + return { + schemas: [ + { + uri: workflowSchema.$id || DEFAULT_SCHEMA_URI, + fileMatch: ["*.json"], + schema: workflowSchema, + }, + ], + }; + }, + }); +} diff --git a/packages/language-service/tests/plugins/json.test.ts b/packages/language-service/tests/plugins/json.test.ts new file mode 100644 index 00000000..e4418c2d --- /dev/null +++ b/packages/language-service/tests/plugins/json.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach } from "vitest"; +import { createJsonLanguageServicePlugin } from "../../src/index"; +import type { + LanguageServiceContext, + LanguageServicePlugin, + LanguageServicePluginInstance, +} from "../../src/index"; +import { treat } from "../testUtils"; + +describe("createJsonLanguageServicePlugin", () => { + let plugin: LanguageServicePlugin; + let instance: LanguageServicePluginInstance; + + beforeEach(() => { + const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; + plugin = createJsonLanguageServicePlugin(); + instance = plugin.create(minimalContext); + }); + + it("returns a LanguageServicePlugin", () => { + expect(typeof plugin.create).toBe("function"); + }); + + describe("provideCompletionItems", () => { + it("suggests top-level OWS properties on an empty object", async () => { + const { doc, cursorPosition } = treat("{🎯}"); + const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); + const labels = result?.items.map((i) => i.label) ?? []; + expect(labels).toContain("document"); + expect(labels).toContain("do"); + }); + + it("suggests document nested properties", async () => { + const { doc, cursorPosition } = treat('{ "document": { 🎯 }, "do": [] }'); + const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); + const labels = result?.items.map((i) => i.label) ?? []; + expect(labels).toContain("dsl"); + expect(labels).toContain("namespace"); + expect(labels).toContain("name"); + }); + }); +}); diff --git a/packages/language-service/tests/testUtils.ts b/packages/language-service/tests/testUtils.ts new file mode 100644 index 00000000..4dc4682a --- /dev/null +++ b/packages/language-service/tests/testUtils.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TextDocument } from "vscode-languageserver-textdocument"; + +const CURSOR = "🎯"; + +/** + * Parses a content string containing a 🎯 cursor marker and returns: + * - `content`: the content with the cursor marker removed. + * - `doc`: a TextDocument created from the trimmed content (with cursor marker). + * - `cursorPosition`: the LSP Position of the cursor in the document. + * - `cursorOffset`: the character offset of the cursor in the document. + * + * @example + * const { content, doc, cursorPosition } = treat(`{ "document": { 🎯 } }`); + */ +export function treat(content: string, languageId: "json" | "yaml" = "json") { + const trimmedContent = content.trim(); + const treatedContent = trimmedContent.replace(CURSOR, ""); + const doc = TextDocument.create("file:///test.json", languageId, 1, trimmedContent); + const cursorOffset = trimmedContent.indexOf(CURSOR); + return { + content: treatedContent, + doc: TextDocument.create("file:///test.json", languageId, 1, treatedContent), + cursorPosition: doc.positionAt(cursorOffset), + cursorOffset, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a2b91d2..10f16393 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,6 +159,9 @@ catalogs: volar-service-json: specifier: 0.0.71 version: 0.0.71 + vscode-languageserver-textdocument: + specifier: ^1.0.14 + version: 1.0.14 importers: @@ -310,6 +313,9 @@ importers: vitest: specifier: 'catalog:' version: 5.0.0(@types/node@26.4.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + vscode-languageserver-textdocument: + specifier: 'catalog:' + version: 1.0.14 packages/open-workflow-diagram-editor: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3a1f1af6..53d26738 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ catalog: "monaco-editor": "0.56.0" "@volar/language-service": ^2.4.28 "volar-service-json": "0.0.71" + "vscode-languageserver-textdocument": "^1.0.14" "@changesets/changelog-github": ^1.0.1 "@changesets/cli": ^3.0.2 "@chromatic-com/storybook": ^5.3.1 From 093ab61fab9830cb3053d4061f302a02430f00d3 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Tue, 15 Sep 2026 18:07:55 +0200 Subject: [PATCH 03/10] Created foundation for OWS completions and code lenses Signed-off-by: Fabrizio Antonangeli --- packages/language-service/README.md | 30 ++++++---- .../src/samples/hello-world.ts | 35 ++++++++++++ packages/language-service/src/utils.ts | 24 ++++++++ packages/language-service/src/volar/index.ts | 18 +++++- .../src/volar/json/code-lenses.ts | 48 ++++++++++++++++ .../src/volar/json/completions.ts | 57 +++++++++++++++++++ .../volar/{plugins/json.ts => json/schema.ts} | 3 +- packages/language-service/tests/testUtils.ts | 12 ++++ .../tests/volar/json/code-lenses.test.ts | 56 ++++++++++++++++++ .../json/completions.test.ts} | 36 ++++++------ .../tests/volar/json/schema.test.ts | 47 +++++++++++++++ 11 files changed, 333 insertions(+), 33 deletions(-) create mode 100644 packages/language-service/src/samples/hello-world.ts create mode 100644 packages/language-service/src/utils.ts create mode 100644 packages/language-service/src/volar/json/code-lenses.ts create mode 100644 packages/language-service/src/volar/json/completions.ts rename packages/language-service/src/volar/{plugins/json.ts => json/schema.ts} (87%) create mode 100644 packages/language-service/tests/volar/json/code-lenses.test.ts rename packages/language-service/tests/{plugins/json.test.ts => volar/json/completions.test.ts} (57%) create mode 100644 packages/language-service/tests/volar/json/schema.test.ts diff --git a/packages/language-service/README.md b/packages/language-service/README.md index b14e821e..82f87681 100644 --- a/packages/language-service/README.md +++ b/packages/language-service/README.md @@ -26,30 +26,36 @@ Volar-specific code is isolated under `src/volar/`. Imports from `@volar/*` and ```text src/ -├── index.ts -└── volar/ - ├── index.ts - └── plugins/ - └── json.ts (JSON schema-driven completion via volar-service-json) +├── samples/ (OWS sample documents) +├── utils.ts (shared OWS utilities) +└── volar/ (Volar adapters) ``` ## API -### `createJsonLanguageServicePlugin()` +### `createJsonLanguageServicePlugins()` -Creates a Volar `LanguageServicePlugin` for JSON using [`volar-service-json`](https://github.com/volarjs/services/tree/master/packages/json) and the Open Workflow schema exported by `@openworkflowspec/sdk`. +Creates the default set of Volar `LanguageServicePlugin`s for Open Workflow JSON support. -The plugin provides schema-driven JSON completion and can be composed by the host with other Volar language service plugins. +It combines the schema, completion, and CodeLens plugins provided by this package. ```ts -import { createJsonLanguageServicePlugin } from "@openworkflowspec/language-service"; +import { createJsonLanguageServicePlugins } from "@openworkflowspec/language-service"; -const jsonPlugin = createJsonLanguageServicePlugin(); +const plugins = createJsonLanguageServicePlugins(); ``` -Host integration is responsible for composing this plugin with the other Volar services it needs. See the official [Volar Services documentation](https://volarjs.dev/reference/services/) for the service/plugin model. +### `createJsonSchemaLanguageServicePlugin()` -`volar-service-json` also exposes its standard JSON diagnostics through the returned plugin. This package does not add custom Open Workflow diagnostics at this stage. +Creates the JSON language service plugin based on `volar-service-json` and the Open Workflow schema. + +### `createJsonCompletionsPlugin()` + +Creates the Open Workflow-specific JSON completion plugin. + +### `createJsonCodeLensesPlugin()` + +Creates the Open Workflow JSON CodeLens plugin. ## Development diff --git a/packages/language-service/src/samples/hello-world.ts b/packages/language-service/src/samples/hello-world.ts new file mode 100644 index 00000000..74b2fa74 --- /dev/null +++ b/packages/language-service/src/samples/hello-world.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const HELLO_WORLD_SAMPLE = `{ + "document": { + "dsl": "1.0.3", + "namespace": "examples", + "name": "hello-world", + "version": "0.1.0" + }, + "do": [ + { + "greet": { + "call": "http", + "with": { + "method": "GET", + "endpoint": "https://httpbin.org/get" + } + } + } + ] +}`; diff --git a/packages/language-service/src/utils.ts b/packages/language-service/src/utils.ts new file mode 100644 index 00000000..52d8b719 --- /dev/null +++ b/packages/language-service/src/utils.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Returns true if the document content is considered an empty Open Workflow — + * i.e. the trimmed content is an empty string. + * Any other content, including bare `{}`, returns false. + */ +export function isEmptyWorkflow(text: string): boolean { + return text.trim().length === 0; +} diff --git a/packages/language-service/src/volar/index.ts b/packages/language-service/src/volar/index.ts index d73996e5..d9e6d3c7 100644 --- a/packages/language-service/src/volar/index.ts +++ b/packages/language-service/src/volar/index.ts @@ -25,4 +25,20 @@ export type { ProjectContext, } from "@volar/language-service"; -export { createJsonLanguageServicePlugin } from "./plugins/json"; +import { createJsonSchemaLanguageServicePlugin } from "./json/schema"; +import { createJsonCompletionsPlugin } from "./json/completions"; +import { createJsonCodeLensesPlugin } from "./json/code-lenses"; + +export { + createJsonSchemaLanguageServicePlugin, + createJsonCompletionsPlugin, + createJsonCodeLensesPlugin, +}; + +export function createJsonLanguageServicePlugins() { + return [ + createJsonSchemaLanguageServicePlugin(), + createJsonCompletionsPlugin(), + createJsonCodeLensesPlugin(), + ]; +} diff --git a/packages/language-service/src/volar/json/code-lenses.ts b/packages/language-service/src/volar/json/code-lenses.ts new file mode 100644 index 00000000..667f7142 --- /dev/null +++ b/packages/language-service/src/volar/json/code-lenses.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { LanguageServicePlugin } from "@volar/language-service"; +import { isEmptyWorkflow } from "../../utils"; + +export function createJsonCodeLensesPlugin(): LanguageServicePlugin { + return { + capabilities: { + codeLensProvider: {}, + }, + create() { + return { + provideCodeLenses(document) { + if (!isEmptyWorkflow(document.getText())) { + return null; + } + + return [ + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + command: { + title: "Create an Open Workflow", + command: "openworkflow.insertHelloWorld", + }, + }, + ]; + }, + }; + }, + }; +} diff --git a/packages/language-service/src/volar/json/completions.ts b/packages/language-service/src/volar/json/completions.ts new file mode 100644 index 00000000..864b219e --- /dev/null +++ b/packages/language-service/src/volar/json/completions.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { LanguageServicePlugin, LanguageServicePluginInstance } from "@volar/language-service"; +import { HELLO_WORLD_SAMPLE } from "../../samples/hello-world"; +import { isEmptyWorkflow } from "../../utils"; + +// CompletionItemKind.Snippet = 15 (LSP spec, cast needed because kind is a const enum) +const SNIPPET_KIND = 15; + +const EMPTY_RANGE = { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, +}; + +export function createJsonCompletionsPlugin(): LanguageServicePlugin { + return { + capabilities: { + completionProvider: {}, + }, + create(): LanguageServicePluginInstance { + return { + isAdditionalCompletion: true, + provideCompletionItems(document) { + if (!isEmptyWorkflow(document.getText())) { + return null; + } + + return { + isIncomplete: false, + items: [ + { + label: "Insert Hello World workflow", + kind: SNIPPET_KIND as 15, + detail: "Insert a Hello World Open Workflow document", + textEdit: { range: EMPTY_RANGE, newText: HELLO_WORLD_SAMPLE }, + }, + ], + }; + }, + }; + }, + }; +} diff --git a/packages/language-service/src/volar/plugins/json.ts b/packages/language-service/src/volar/json/schema.ts similarity index 87% rename from packages/language-service/src/volar/plugins/json.ts rename to packages/language-service/src/volar/json/schema.ts index db147885..524fd99e 100644 --- a/packages/language-service/src/volar/plugins/json.ts +++ b/packages/language-service/src/volar/json/schema.ts @@ -14,12 +14,13 @@ * limitations under the License. */ +import type { LanguageServicePlugin } from "@volar/language-service"; import { create } from "volar-service-json"; import { workflowSchema } from "@openworkflowspec/sdk"; const DEFAULT_SCHEMA_URI = "urn:open-workflow-specification:workflow-schema"; -export function createJsonLanguageServicePlugin() { +export function createJsonSchemaLanguageServicePlugin(): LanguageServicePlugin { return create({ getLanguageSettings() { return { diff --git a/packages/language-service/tests/testUtils.ts b/packages/language-service/tests/testUtils.ts index 4dc4682a..3cad7ce3 100644 --- a/packages/language-service/tests/testUtils.ts +++ b/packages/language-service/tests/testUtils.ts @@ -15,6 +15,7 @@ */ import { TextDocument } from "vscode-languageserver-textdocument"; +import type { CompletionList } from "@volar/language-service"; const CURSOR = "🎯"; @@ -40,3 +41,14 @@ export function treat(content: string, languageId: "json" | "yaml" = "json") { cursorOffset, }; } + +/** + * Extracts the labels from a completion result for use in test assertions. + * + * @example + * const labels = completionLabels(result); + * expect(labels).toContain("document"); + */ +export function completionLabels(result: CompletionList | null | undefined): string[] { + return result?.items.map((i) => i.label) ?? []; +} diff --git a/packages/language-service/tests/volar/json/code-lenses.test.ts b/packages/language-service/tests/volar/json/code-lenses.test.ts new file mode 100644 index 00000000..ec0f0791 --- /dev/null +++ b/packages/language-service/tests/volar/json/code-lenses.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach } from "vitest"; +import { createJsonCodeLensesPlugin } from "../../../src/index"; +import type { + LanguageServiceContext, + LanguageServicePlugin, + LanguageServicePluginInstance, +} from "../../../src/index"; +import { treat } from "../../testUtils"; + +describe("createJsonCodeLensesPlugin", () => { + let plugin: LanguageServicePlugin; + let instance: LanguageServicePluginInstance; + + beforeEach(() => { + const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; + plugin = createJsonCodeLensesPlugin(); + instance = plugin.create(minimalContext); + }); + + describe("provideCodeLenses", () => { + it("provides a code lens on an empty document", () => { + const { doc } = treat("🎯"); + const result = instance.provideCodeLenses!(doc, {}); + expect(result).toHaveLength(1); + expect(result![0].command?.title).toBe("Create an Open Workflow"); + }); + + it("does not provide a code lens on bare {}", () => { + const { doc } = treat("{🎯}"); + const result = instance.provideCodeLenses!(doc, {}); + expect(result).toBeNull(); + }); + + it("does not provide a code lens on partial OWS content", () => { + const { doc } = treat('{ "document": { 🎯 }, "do": [] }'); + const result = instance.provideCodeLenses!(doc, {}); + expect(result).toBeNull(); + }); + }); +}); diff --git a/packages/language-service/tests/plugins/json.test.ts b/packages/language-service/tests/volar/json/completions.test.ts similarity index 57% rename from packages/language-service/tests/plugins/json.test.ts rename to packages/language-service/tests/volar/json/completions.test.ts index e4418c2d..50ff7f73 100644 --- a/packages/language-service/tests/plugins/json.test.ts +++ b/packages/language-service/tests/volar/json/completions.test.ts @@ -15,44 +15,42 @@ */ import { describe, it, expect, beforeEach } from "vitest"; -import { createJsonLanguageServicePlugin } from "../../src/index"; +import { createJsonCompletionsPlugin } from "../../../src/index"; import type { LanguageServiceContext, LanguageServicePlugin, LanguageServicePluginInstance, -} from "../../src/index"; -import { treat } from "../testUtils"; +} from "../../../src/index"; +import { treat, completionLabels } from "../../testUtils"; -describe("createJsonLanguageServicePlugin", () => { +const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; + +describe("createJsonCompletionsPlugin", () => { let plugin: LanguageServicePlugin; let instance: LanguageServicePluginInstance; beforeEach(() => { - const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; - plugin = createJsonLanguageServicePlugin(); + plugin = createJsonCompletionsPlugin(); instance = plugin.create(minimalContext); }); - it("returns a LanguageServicePlugin", () => { - expect(typeof plugin.create).toBe("function"); - }); - describe("provideCompletionItems", () => { - it("suggests top-level OWS properties on an empty object", async () => { + it("proposes Hello World completion on empty document", async () => { + const { doc, cursorPosition } = treat("🎯"); + const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); + expect(completionLabels(result)).toContain("Insert Hello World workflow"); + }); + + it("does not propose Hello World completion on bare {}", async () => { const { doc, cursorPosition } = treat("{🎯}"); const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); - const labels = result?.items.map((i) => i.label) ?? []; - expect(labels).toContain("document"); - expect(labels).toContain("do"); + expect(completionLabels(result)).not.toContain("Insert Hello World workflow"); }); - it("suggests document nested properties", async () => { + it("does not propose Hello World completion on partial OWS content", async () => { const { doc, cursorPosition } = treat('{ "document": { 🎯 }, "do": [] }'); const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); - const labels = result?.items.map((i) => i.label) ?? []; - expect(labels).toContain("dsl"); - expect(labels).toContain("namespace"); - expect(labels).toContain("name"); + expect(completionLabels(result)).not.toContain("Insert Hello World workflow"); }); }); }); diff --git a/packages/language-service/tests/volar/json/schema.test.ts b/packages/language-service/tests/volar/json/schema.test.ts new file mode 100644 index 00000000..ce82311f --- /dev/null +++ b/packages/language-service/tests/volar/json/schema.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach } from "vitest"; +import { createJsonSchemaLanguageServicePlugin } from "../../../src/index"; +import type { LanguageServiceContext, LanguageServicePluginInstance } from "../../../src/index"; +import { treat, completionLabels } from "../../testUtils"; + +const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; + +describe("createJsonSchemaLanguageServicePlugin", () => { + let instance: LanguageServicePluginInstance; + + beforeEach(() => { + instance = createJsonSchemaLanguageServicePlugin().create(minimalContext); + }); + + describe("provideCompletionItems", () => { + it("suggests top-level OWS properties on an empty object", async () => { + const { doc, cursorPosition } = treat("{🎯}"); + const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); + expect(completionLabels(result)).toContain("document"); + expect(completionLabels(result)).toContain("do"); + }); + + it("suggests document nested properties", async () => { + const { doc, cursorPosition } = treat('{ "document": { 🎯 }, "do": [] }'); + const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); + expect(completionLabels(result)).toContain("dsl"); + expect(completionLabels(result)).toContain("namespace"); + expect(completionLabels(result)).toContain("name"); + }); + }); +}); From ed4de38ff263044693493093c85fe45c70942862 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Wed, 16 Sep 2026 09:56:17 +0200 Subject: [PATCH 04/10] Add Changeset file Signed-off-by: Fabrizio Antonangeli --- .changeset/json-language-service-completions.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/json-language-service-completions.md diff --git a/.changeset/json-language-service-completions.md b/.changeset/json-language-service-completions.md new file mode 100644 index 00000000..c945ea33 --- /dev/null +++ b/.changeset/json-language-service-completions.md @@ -0,0 +1,5 @@ +--- +"@openworkflowspec/language-service": minor +--- + +Add JSON language service plugins foundation for Open Workflow Spec From a4ca56fb1fdfdffa3a7d4f0a7b57ef46da027e1a Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Wed, 16 Sep 2026 10:44:33 +0200 Subject: [PATCH 05/10] Add createJsonLanguageServicePlugins tests Signed-off-by: Fabrizio Antonangeli --- packages/language-service/tests/testUtils.ts | 37 +++++++++++++- ....ts => createJsonCodeLensesPlugin.test.ts} | 11 ++--- ...ts => createJsonCompletionsPlugin.test.ts} | 24 ++++----- .../createJsonLanguageServicePlugins.test.ts | 49 +++++++++++++++++++ ...teJsonSchemaLanguageServicePlugin.test.ts} | 22 ++++----- 5 files changed, 106 insertions(+), 37 deletions(-) rename packages/language-service/tests/volar/json/{code-lenses.test.ts => createJsonCodeLensesPlugin.test.ts} (85%) rename packages/language-service/tests/volar/json/{completions.test.ts => createJsonCompletionsPlugin.test.ts} (63%) create mode 100644 packages/language-service/tests/volar/json/createJsonLanguageServicePlugins.test.ts rename packages/language-service/tests/volar/json/{schema.test.ts => createJsonSchemaLanguageServicePlugin.test.ts} (61%) diff --git a/packages/language-service/tests/testUtils.ts b/packages/language-service/tests/testUtils.ts index 3cad7ce3..3d59c469 100644 --- a/packages/language-service/tests/testUtils.ts +++ b/packages/language-service/tests/testUtils.ts @@ -15,10 +15,20 @@ */ import { TextDocument } from "vscode-languageserver-textdocument"; -import type { CompletionList } from "@volar/language-service"; +import type { + CompletionList, + LanguageServiceContext, + LanguageServicePluginInstance, + Position, +} from "@volar/language-service"; const CURSOR = "🎯"; +/** + * A minimal Volar LanguageServiceContext suitable for unit tests. + */ +export const MINIMAL_CONTEXT = { env: { workspaceFolders: [] } } as LanguageServiceContext; + /** * Parses a content string containing a 🎯 cursor marker and returns: * - `content`: the content with the cursor marker removed. @@ -49,6 +59,29 @@ export function treat(content: string, languageId: "json" | "yaml" = "json") { * const labels = completionLabels(result); * expect(labels).toContain("document"); */ -export function completionLabels(result: CompletionList | null | undefined): string[] { +export function getCompletionLabels(result: CompletionList | null | undefined): string[] { return result?.items.map((i) => i.label) ?? []; } + +/** + * Collects completion labels from one or more plugin instances into a single flat array. + * + * @example + * // single instance + * const labels = await getAllCompletionLabels([instance], doc, cursorPosition); + * expect(labels).toContain("document"); + * + * // composed plugins + * const labels = await getAllCompletionLabels(instances, doc, cursorPosition); + * expect(labels).toContain("document"); + */ +export async function getAllCompletionLabels( + instances: LanguageServicePluginInstance[], + document: TextDocument, + position: Position, +): Promise { + const results = await Promise.all( + instances.map((instance) => instance.provideCompletionItems?.(document, position, {})), + ); + return results.flatMap(getCompletionLabels); +} diff --git a/packages/language-service/tests/volar/json/code-lenses.test.ts b/packages/language-service/tests/volar/json/createJsonCodeLensesPlugin.test.ts similarity index 85% rename from packages/language-service/tests/volar/json/code-lenses.test.ts rename to packages/language-service/tests/volar/json/createJsonCodeLensesPlugin.test.ts index ec0f0791..81cc2aeb 100644 --- a/packages/language-service/tests/volar/json/code-lenses.test.ts +++ b/packages/language-service/tests/volar/json/createJsonCodeLensesPlugin.test.ts @@ -16,21 +16,16 @@ import { describe, it, expect, beforeEach } from "vitest"; import { createJsonCodeLensesPlugin } from "../../../src/index"; -import type { - LanguageServiceContext, - LanguageServicePlugin, - LanguageServicePluginInstance, -} from "../../../src/index"; -import { treat } from "../../testUtils"; +import type { LanguageServicePlugin, LanguageServicePluginInstance } from "../../../src/index"; +import { treat, MINIMAL_CONTEXT } from "../../testUtils"; describe("createJsonCodeLensesPlugin", () => { let plugin: LanguageServicePlugin; let instance: LanguageServicePluginInstance; beforeEach(() => { - const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; plugin = createJsonCodeLensesPlugin(); - instance = plugin.create(minimalContext); + instance = plugin.create(MINIMAL_CONTEXT); }); describe("provideCodeLenses", () => { diff --git a/packages/language-service/tests/volar/json/completions.test.ts b/packages/language-service/tests/volar/json/createJsonCompletionsPlugin.test.ts similarity index 63% rename from packages/language-service/tests/volar/json/completions.test.ts rename to packages/language-service/tests/volar/json/createJsonCompletionsPlugin.test.ts index 50ff7f73..015c736f 100644 --- a/packages/language-service/tests/volar/json/completions.test.ts +++ b/packages/language-service/tests/volar/json/createJsonCompletionsPlugin.test.ts @@ -16,14 +16,8 @@ import { describe, it, expect, beforeEach } from "vitest"; import { createJsonCompletionsPlugin } from "../../../src/index"; -import type { - LanguageServiceContext, - LanguageServicePlugin, - LanguageServicePluginInstance, -} from "../../../src/index"; -import { treat, completionLabels } from "../../testUtils"; - -const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; +import type { LanguageServicePlugin, LanguageServicePluginInstance } from "../../../src/index"; +import { treat, getAllCompletionLabels, MINIMAL_CONTEXT } from "../../testUtils"; describe("createJsonCompletionsPlugin", () => { let plugin: LanguageServicePlugin; @@ -31,26 +25,26 @@ describe("createJsonCompletionsPlugin", () => { beforeEach(() => { plugin = createJsonCompletionsPlugin(); - instance = plugin.create(minimalContext); + instance = plugin.create(MINIMAL_CONTEXT); }); describe("provideCompletionItems", () => { it("proposes Hello World completion on empty document", async () => { const { doc, cursorPosition } = treat("🎯"); - const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); - expect(completionLabels(result)).toContain("Insert Hello World workflow"); + const labels = await getAllCompletionLabels([instance], doc, cursorPosition); + expect(labels).toContain("Insert Hello World workflow"); }); it("does not propose Hello World completion on bare {}", async () => { const { doc, cursorPosition } = treat("{🎯}"); - const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); - expect(completionLabels(result)).not.toContain("Insert Hello World workflow"); + const labels = await getAllCompletionLabels([instance], doc, cursorPosition); + expect(labels).not.toContain("Insert Hello World workflow"); }); it("does not propose Hello World completion on partial OWS content", async () => { const { doc, cursorPosition } = treat('{ "document": { 🎯 }, "do": [] }'); - const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); - expect(completionLabels(result)).not.toContain("Insert Hello World workflow"); + const labels = await getAllCompletionLabels([instance], doc, cursorPosition); + expect(labels).not.toContain("Insert Hello World workflow"); }); }); }); diff --git a/packages/language-service/tests/volar/json/createJsonLanguageServicePlugins.test.ts b/packages/language-service/tests/volar/json/createJsonLanguageServicePlugins.test.ts new file mode 100644 index 00000000..d981cfcc --- /dev/null +++ b/packages/language-service/tests/volar/json/createJsonLanguageServicePlugins.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach } from "vitest"; +import { createJsonLanguageServicePlugins, createJsonCompletionsPlugin } from "../../../src/index"; +import type { LanguageServicePluginInstance } from "../../../src/index"; +import { treat, getAllCompletionLabels, MINIMAL_CONTEXT } from "../../testUtils"; + +describe("createJsonLanguageServicePlugins", () => { + let instances: LanguageServicePluginInstance[]; + + beforeEach(() => { + instances = createJsonLanguageServicePlugins().map((plugin) => plugin.create(MINIMAL_CONTEXT)); + }); + + it("createJsonCompletionsPlugin is marked as isAdditionalCompletion", () => { + const completionsInstance = createJsonCompletionsPlugin().create(MINIMAL_CONTEXT); + expect(completionsInstance.isAdditionalCompletion).toBe(true); + }); + + it("schema-driven completions are available on a partial document", async () => { + const { doc, cursorPosition } = treat("{🎯}"); + const labels = await getAllCompletionLabels(instances, doc, cursorPosition); + expect(labels).toContain("document"); + expect(labels).toContain("do"); + }); + + it("OWS completion is available on an empty document alongside schema-driven completions", async () => { + const { doc, cursorPosition } = treat("🎯"); + const labels = await getAllCompletionLabels(instances, doc, cursorPosition); + expect(labels).toContain("Insert Hello World workflow"); + // Schema-driven completions from createJsonSchemaLanguageServicePlugin are also active + // (on an empty document volar-service-json proposes {} as a starting point) + expect(labels).toContain("{}"); + }); +}); diff --git a/packages/language-service/tests/volar/json/schema.test.ts b/packages/language-service/tests/volar/json/createJsonSchemaLanguageServicePlugin.test.ts similarity index 61% rename from packages/language-service/tests/volar/json/schema.test.ts rename to packages/language-service/tests/volar/json/createJsonSchemaLanguageServicePlugin.test.ts index ce82311f..49edb8e8 100644 --- a/packages/language-service/tests/volar/json/schema.test.ts +++ b/packages/language-service/tests/volar/json/createJsonSchemaLanguageServicePlugin.test.ts @@ -16,32 +16,30 @@ import { describe, it, expect, beforeEach } from "vitest"; import { createJsonSchemaLanguageServicePlugin } from "../../../src/index"; -import type { LanguageServiceContext, LanguageServicePluginInstance } from "../../../src/index"; -import { treat, completionLabels } from "../../testUtils"; - -const minimalContext = { env: { workspaceFolders: [] } } as LanguageServiceContext; +import type { LanguageServicePluginInstance } from "../../../src/index"; +import { treat, getAllCompletionLabels, MINIMAL_CONTEXT } from "../../testUtils"; describe("createJsonSchemaLanguageServicePlugin", () => { let instance: LanguageServicePluginInstance; beforeEach(() => { - instance = createJsonSchemaLanguageServicePlugin().create(minimalContext); + instance = createJsonSchemaLanguageServicePlugin().create(MINIMAL_CONTEXT); }); describe("provideCompletionItems", () => { it("suggests top-level OWS properties on an empty object", async () => { const { doc, cursorPosition } = treat("{🎯}"); - const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); - expect(completionLabels(result)).toContain("document"); - expect(completionLabels(result)).toContain("do"); + const labels = await getAllCompletionLabels([instance], doc, cursorPosition); + expect(labels).toContain("document"); + expect(labels).toContain("do"); }); it("suggests document nested properties", async () => { const { doc, cursorPosition } = treat('{ "document": { 🎯 }, "do": [] }'); - const result = await instance.provideCompletionItems!(doc, cursorPosition, {}); - expect(completionLabels(result)).toContain("dsl"); - expect(completionLabels(result)).toContain("namespace"); - expect(completionLabels(result)).toContain("name"); + const labels = await getAllCompletionLabels([instance], doc, cursorPosition); + expect(labels).toContain("dsl"); + expect(labels).toContain("namespace"); + expect(labels).toContain("name"); }); }); }); From f917e425247251339c88d9ee501f765e0873c4c3 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Wed, 16 Sep 2026 11:24:23 +0200 Subject: [PATCH 06/10] Restored previous test filenames Signed-off-by: Fabrizio Antonangeli --- ...eateJsonLanguageServicePlugins.test.ts => all-plugins.test.ts} | 0 .../{createJsonCodeLensesPlugin.test.ts => code-lenses.test.ts} | 0 .../{createJsonCompletionsPlugin.test.ts => completions.test.ts} | 0 ...eateJsonSchemaLanguageServicePlugin.test.ts => schema.test.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/language-service/tests/volar/json/{createJsonLanguageServicePlugins.test.ts => all-plugins.test.ts} (100%) rename packages/language-service/tests/volar/json/{createJsonCodeLensesPlugin.test.ts => code-lenses.test.ts} (100%) rename packages/language-service/tests/volar/json/{createJsonCompletionsPlugin.test.ts => completions.test.ts} (100%) rename packages/language-service/tests/volar/json/{createJsonSchemaLanguageServicePlugin.test.ts => schema.test.ts} (100%) diff --git a/packages/language-service/tests/volar/json/createJsonLanguageServicePlugins.test.ts b/packages/language-service/tests/volar/json/all-plugins.test.ts similarity index 100% rename from packages/language-service/tests/volar/json/createJsonLanguageServicePlugins.test.ts rename to packages/language-service/tests/volar/json/all-plugins.test.ts diff --git a/packages/language-service/tests/volar/json/createJsonCodeLensesPlugin.test.ts b/packages/language-service/tests/volar/json/code-lenses.test.ts similarity index 100% rename from packages/language-service/tests/volar/json/createJsonCodeLensesPlugin.test.ts rename to packages/language-service/tests/volar/json/code-lenses.test.ts diff --git a/packages/language-service/tests/volar/json/createJsonCompletionsPlugin.test.ts b/packages/language-service/tests/volar/json/completions.test.ts similarity index 100% rename from packages/language-service/tests/volar/json/createJsonCompletionsPlugin.test.ts rename to packages/language-service/tests/volar/json/completions.test.ts diff --git a/packages/language-service/tests/volar/json/createJsonSchemaLanguageServicePlugin.test.ts b/packages/language-service/tests/volar/json/schema.test.ts similarity index 100% rename from packages/language-service/tests/volar/json/createJsonSchemaLanguageServicePlugin.test.ts rename to packages/language-service/tests/volar/json/schema.test.ts From e890c46affce89f9812f835e1bd49813fcff443c Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Wed, 16 Sep 2026 11:25:59 +0200 Subject: [PATCH 07/10] Fixes comment: https://github.com/open-workflow-specification/editor/pull/434#discussion_r4024372829 Signed-off-by: Fabrizio Antonangeli --- packages/language-service/src/volar/json/code-lenses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-service/src/volar/json/code-lenses.ts b/packages/language-service/src/volar/json/code-lenses.ts index 667f7142..c48c3b8e 100644 --- a/packages/language-service/src/volar/json/code-lenses.ts +++ b/packages/language-service/src/volar/json/code-lenses.ts @@ -25,7 +25,7 @@ export function createJsonCodeLensesPlugin(): LanguageServicePlugin { create() { return { provideCodeLenses(document) { - if (!isEmptyWorkflow(document.getText())) { + if (document.languageId !== "json" || !isEmptyWorkflow(document.getText())) { return null; } From c54c8869b453cb91a6107d839ddb7493439e22b2 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Wed, 16 Sep 2026 11:26:56 +0200 Subject: [PATCH 08/10] Fixes comment: https://github.com/open-workflow-specification/editor/pull/434#discussion_r4024372896 Signed-off-by: Fabrizio Antonangeli --- packages/language-service/src/volar/json/completions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-service/src/volar/json/completions.ts b/packages/language-service/src/volar/json/completions.ts index 864b219e..c1335790 100644 --- a/packages/language-service/src/volar/json/completions.ts +++ b/packages/language-service/src/volar/json/completions.ts @@ -35,7 +35,7 @@ export function createJsonCompletionsPlugin(): LanguageServicePlugin { return { isAdditionalCompletion: true, provideCompletionItems(document) { - if (!isEmptyWorkflow(document.getText())) { + if (document.languageId !== "json" || !isEmptyWorkflow(document.getText())) { return null; } From 767c68f261326f1f69614f0c7a5aa591b8eed376 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Wed, 16 Sep 2026 12:11:30 +0200 Subject: [PATCH 09/10] Fixes comment: https://github.com/open-workflow-specification/editor/pull/434#discussion_r4024372938 Signed-off-by: Fabrizio Antonangeli --- packages/language-service/tests/testUtils.ts | 44 +++++++++++++------ .../tests/volar/json/schema.test.ts | 37 +++++++++++++++- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/language-service/tests/testUtils.ts b/packages/language-service/tests/testUtils.ts index 3d59c469..d022e7ff 100644 --- a/packages/language-service/tests/testUtils.ts +++ b/packages/language-service/tests/testUtils.ts @@ -16,7 +16,6 @@ import { TextDocument } from "vscode-languageserver-textdocument"; import type { - CompletionList, LanguageServiceContext, LanguageServicePluginInstance, Position, @@ -24,6 +23,14 @@ import type { const CURSOR = "🎯"; +/** + * A minimal LSP CancellationToken that is never cancelled, suitable for unit tests. + */ +const CANCELLATION_TOKEN = { + isCancellationRequested: false, + onCancellationRequested: () => ({ dispose: () => {} }), +}; + /** * A minimal Volar LanguageServiceContext suitable for unit tests. */ @@ -52,17 +59,6 @@ export function treat(content: string, languageId: "json" | "yaml" = "json") { }; } -/** - * Extracts the labels from a completion result for use in test assertions. - * - * @example - * const labels = completionLabels(result); - * expect(labels).toContain("document"); - */ -export function getCompletionLabels(result: CompletionList | null | undefined): string[] { - return result?.items.map((i) => i.label) ?? []; -} - /** * Collects completion labels from one or more plugin instances into a single flat array. * @@ -83,5 +79,27 @@ export async function getAllCompletionLabels( const results = await Promise.all( instances.map((instance) => instance.provideCompletionItems?.(document, position, {})), ); - return results.flatMap(getCompletionLabels); + return results.flatMap((result) => result?.items.map((i) => i.label) ?? []); +} + +/** + * Collects diagnostic messages from one or more plugin instances into a single flat array. + * + * @example + * // single instance + * const messages = await getDiagnosticMessages([instance], doc); + * expect(messages).toContain('Missing property "do".'); + * + * // composed plugins + * const messages = await getDiagnosticMessages(instances, doc); + * expect(messages).toHaveLength(0); + */ +export async function getDiagnosticMessages( + instances: LanguageServicePluginInstance[], + document: TextDocument, +): Promise { + const results = await Promise.all( + instances.map((instance) => instance.provideDiagnostics?.(document, CANCELLATION_TOKEN)), + ); + return results.flatMap((diags) => diags?.map((d) => d.message) ?? []); } diff --git a/packages/language-service/tests/volar/json/schema.test.ts b/packages/language-service/tests/volar/json/schema.test.ts index 49edb8e8..0487dd79 100644 --- a/packages/language-service/tests/volar/json/schema.test.ts +++ b/packages/language-service/tests/volar/json/schema.test.ts @@ -15,9 +15,15 @@ */ import { describe, it, expect, beforeEach } from "vitest"; +import { TextDocument } from "vscode-languageserver-textdocument"; import { createJsonSchemaLanguageServicePlugin } from "../../../src/index"; import type { LanguageServicePluginInstance } from "../../../src/index"; -import { treat, getAllCompletionLabels, MINIMAL_CONTEXT } from "../../testUtils"; +import { + treat, + getAllCompletionLabels, + getDiagnosticMessages, + MINIMAL_CONTEXT, +} from "../../testUtils"; describe("createJsonSchemaLanguageServicePlugin", () => { let instance: LanguageServicePluginInstance; @@ -42,4 +48,33 @@ describe("createJsonSchemaLanguageServicePlugin", () => { expect(labels).toContain("name"); }); }); + + describe("provideDiagnostics", () => { + it("returns no diagnostics for a valid workflow", async () => { + const doc = TextDocument.create( + "file:///test.json", + "json", + 1, + `{ + "document": { "dsl": "1.0.3", "namespace": "examples", "name": "hello-world", "version": "0.1.0" }, + "do": [{ "greet": { "call": "http", "with": { "method": "GET", "endpoint": "https://httpbin.org/get" } } }] + }`, + ); + const messages = await getDiagnosticMessages([instance], doc); + expect(messages).toHaveLength(0); + }); + + it("returns diagnostics for a workflow missing the required 'do' property", async () => { + const doc = TextDocument.create( + "file:///test.json", + "json", + 1, + `{ + "document": { "dsl": "1.0.3", "namespace": "examples", "name": "hello-world", "version": "0.1.0" } + }`, + ); + const messages = await getDiagnosticMessages([instance], doc); + expect(messages).toContain('Missing property "do".'); + }); + }); }); From 94a47b60ce34db41d82661b4b9f3bb24c754bc07 Mon Sep 17 00:00:00 2001 From: Fabrizio Antonangeli Date: Wed, 16 Sep 2026 12:30:13 +0200 Subject: [PATCH 10/10] Add guard for non Json files Signed-off-by: Fabrizio Antonangeli --- .../language-service/tests/volar/json/code-lenses.test.ts | 6 ++++++ .../language-service/tests/volar/json/completions.test.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/language-service/tests/volar/json/code-lenses.test.ts b/packages/language-service/tests/volar/json/code-lenses.test.ts index 81cc2aeb..5b58d0d4 100644 --- a/packages/language-service/tests/volar/json/code-lenses.test.ts +++ b/packages/language-service/tests/volar/json/code-lenses.test.ts @@ -47,5 +47,11 @@ describe("createJsonCodeLensesPlugin", () => { const result = instance.provideCodeLenses!(doc, {}); expect(result).toBeNull(); }); + + it("does not provide a code lens on an empty YAML document", () => { + const { doc } = treat("🎯", "yaml"); + const result = instance.provideCodeLenses!(doc, {}); + expect(result).toBeNull(); + }); }); }); diff --git a/packages/language-service/tests/volar/json/completions.test.ts b/packages/language-service/tests/volar/json/completions.test.ts index 015c736f..4f1accea 100644 --- a/packages/language-service/tests/volar/json/completions.test.ts +++ b/packages/language-service/tests/volar/json/completions.test.ts @@ -46,5 +46,11 @@ describe("createJsonCompletionsPlugin", () => { const labels = await getAllCompletionLabels([instance], doc, cursorPosition); expect(labels).not.toContain("Insert Hello World workflow"); }); + + it("does not propose Hello World completion on an empty YAML document", async () => { + const { doc, cursorPosition } = treat("🎯", "yaml"); + const labels = await getAllCompletionLabels([instance], doc, cursorPosition); + expect(labels).not.toContain("Insert Hello World workflow"); + }); }); });