From a01bf3860c2d7528480264b0fa1d97c1225e15b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:35:27 +0000 Subject: [PATCH 1/4] Initial plan From fa96df2a750bac340bfb5e026b7da00bff5cee41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:42:08 +0000 Subject: [PATCH 2/4] Initial plan: gate compose code lenses on workspace folder membership --- packages/compose-language-service/bin/docker-compose-langserver | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 packages/compose-language-service/bin/docker-compose-langserver diff --git a/packages/compose-language-service/bin/docker-compose-langserver b/packages/compose-language-service/bin/docker-compose-langserver old mode 100644 new mode 100755 From 6d4aca82e4a49e68279c108fedee77064c0725a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:45:06 +0000 Subject: [PATCH 3/4] Suppress compose code lenses for documents outside the workspace --- .../ServiceStartupCodeLensProvider.ts | 18 +++++-- .../service/utils/isDocumentInWorkspace.ts | 46 ++++++++++++++++ .../src/test/TestConnection.ts | 11 +++- .../ServiceStartupCodeLensProvider.test.ts | 49 +++++++++++++++++ .../test/utils/isDocumentInWorkspace.test.ts | 54 +++++++++++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 packages/compose-language-service/src/service/utils/isDocumentInWorkspace.ts create mode 100644 packages/compose-language-service/src/test/utils/isDocumentInWorkspace.test.ts diff --git a/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts b/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts index f7ea48f0..7314105c 100644 --- a/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts +++ b/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts @@ -9,16 +9,24 @@ import type { ExtendedParams } from '../ExtendedParams'; import { getCurrentContext } from '../utils/ActionContext'; import { isMap, isPair, isScalar } from 'yaml'; import { yamlRangeToLspRange } from '../utils/yamlRangeToLspRange'; +import { isDocumentInWorkspace } from '../utils/isDocumentInWorkspace'; export class ServiceStartupCodeLensProvider extends ProviderBase { - public on(params: CodeLensParams & ExtendedParams, token: CancellationToken): Promise { + public async on(params: CodeLensParams & ExtendedParams, token: CancellationToken): Promise { const ctx = getCurrentContext(); ctx.telemetry.properties.isActivationEvent = 'true'; // This happens automatically so we'll treat it as isActivationEvent === true const results: CodeLens[] = []; + // The code lens commands (compose up, etc.) require the document to be within an open workspace folder. + // If it is not, running them results in an error, so the code lenses should not be shown. + // See https://github.com/microsoft/vscode-containers/issues/535 + if (!await isDocumentInWorkspace(ctx, params.document.uri)) { + return undefined; + } + if (!params.document.yamlDocument.value.has('services')) { - return Promise.resolve(undefined); + return undefined; } // First add the run-all from the main "services" node @@ -47,7 +55,7 @@ export class ServiceStartupCodeLensProvider extends ProviderBase { + // If the client doesn't support workspace folders, we can't verify, so optimistically show code lenses + if (!ctx.clientCapabilities?.workspace?.workspaceFolders) { + return true; + } + + const folders = await ctx.connection.workspace.getWorkspaceFolders(); + return isDocumentInWorkspaceFolders(documentUri, folders); +} + +/** + * Determines whether a document is located within one of the given workspace folders. + * @param documentUri The URI of the document + * @param folders The workspace folders reported by the client (may be `null`/`undefined` if none are open) + * @returns True if the document is within one of the workspace folders, false otherwise + */ +export function isDocumentInWorkspaceFolders(documentUri: DocumentUri, folders: WorkspaceFolder[] | null | undefined): boolean { + if (!folders?.length) { + return false; + } + + return folders.some(folder => uriIsWithinFolder(documentUri, folder.uri)); +} + +function uriIsWithinFolder(documentUri: string, folderUri: string): boolean { + // Ensure the folder URI ends with a slash so that a folder like `file:///foo` does not match `file:///foobar` + const normalizedFolderUri = folderUri.endsWith('/') ? folderUri : `${folderUri}/`; + return documentUri === folderUri || documentUri.startsWith(normalizedFolderUri); +} diff --git a/packages/compose-language-service/src/test/TestConnection.ts b/packages/compose-language-service/src/test/TestConnection.ts index 20109884..23fb390e 100644 --- a/packages/compose-language-service/src/test/TestConnection.ts +++ b/packages/compose-language-service/src/test/TestConnection.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PassThrough } from 'stream'; -import { type Connection, DidOpenTextDocumentNotification, type DidOpenTextDocumentParams, type Disposable, type InitializeParams, TextDocumentItem } from 'vscode-languageserver'; +import { type Connection, DidOpenTextDocumentNotification, type DidOpenTextDocumentParams, type Disposable, type InitializeParams, TextDocumentItem, type WorkspaceFolder, WorkspaceFoldersRequest } from 'vscode-languageserver'; import type { DocumentUri } from 'vscode-languageserver-textdocument'; import { createConnection } from 'vscode-languageserver/node'; import { Document } from 'yaml'; @@ -23,6 +23,12 @@ export class TestConnection implements Disposable { public readonly server: Connection; public readonly client: Connection; public readonly languageService: ComposeLanguageService; + + /** + * The workspace folders that the (mock) client will report when the server issues a + * `workspace/workspaceFolders` request. Assign to this to simulate open workspace folders. + */ + public workspaceFolders: WorkspaceFolder[] | null = null; private counter = 0; public constructor(public readonly initParams: InitializeParams = DefaultInitializeParams) { @@ -34,6 +40,9 @@ export class TestConnection implements Disposable { this.languageService = new ComposeLanguageService(this.server, initParams); + // Respond to the server's workspace folder requests with the configured folders + this.client.onRequest(WorkspaceFoldersRequest.type, () => this.workspaceFolders); + this.server.listen(); this.client.listen(); } diff --git a/packages/compose-language-service/src/test/providers/ServiceStartupCodeLensProvider.test.ts b/packages/compose-language-service/src/test/providers/ServiceStartupCodeLensProvider.test.ts index 899ae245..65e07453 100644 --- a/packages/compose-language-service/src/test/providers/ServiceStartupCodeLensProvider.test.ts +++ b/packages/compose-language-service/src/test/providers/ServiceStartupCodeLensProvider.test.ts @@ -100,6 +100,55 @@ describe('ServiceStartupCodeLensProvider', () => { }); }); + describe('Workspace folder scenarios', () => { + let workspaceAwareConnection: TestConnection; + + before('Prepare a workspace-folder-aware language server for testing', () => { + workspaceAwareConnection = new TestConnection({ + capabilities: { + workspace: { + workspaceFolders: true, + }, + }, + processId: 1, + rootUri: null, + workspaceFolders: null, + }); + }); + + it('Should provide code lenses when the document is within a workspace folder', async () => { + const uri = workspaceAwareConnection.sendObjectAsYamlDocument({ services: {} }); + workspaceAwareConnection.workspaceFolders = [{ uri: 'file:///', name: 'root' }]; + + await requestServiceStartupCodeLensesAndCompare(workspaceAwareConnection, uri, [ + { + range: Range.create(0, 0, 0, 8), + command: { + command: 'vscode-containers.compose.up' + } + }, + ]); + }); + + it('Should NOT provide code lenses when the document is outside all workspace folders', async () => { + const uri = workspaceAwareConnection.sendObjectAsYamlDocument({ services: {} }); + workspaceAwareConnection.workspaceFolders = [{ uri: 'file:///some/other/folder', name: 'other' }]; + + await requestServiceStartupCodeLensesAndCompare(workspaceAwareConnection, uri, undefined); + }); + + it('Should NOT provide code lenses when there are no open workspace folders', async () => { + const uri = workspaceAwareConnection.sendObjectAsYamlDocument({ services: {} }); + workspaceAwareConnection.workspaceFolders = null; + + await requestServiceStartupCodeLensesAndCompare(workspaceAwareConnection, uri, undefined); + }); + + after('Cleanup', () => { + workspaceAwareConnection.dispose(); + }); + }); + after('Cleanup', () => { testConnection.dispose(); }); diff --git a/packages/compose-language-service/src/test/utils/isDocumentInWorkspace.test.ts b/packages/compose-language-service/src/test/utils/isDocumentInWorkspace.test.ts new file mode 100644 index 00000000..30ce662a --- /dev/null +++ b/packages/compose-language-service/src/test/utils/isDocumentInWorkspace.test.ts @@ -0,0 +1,54 @@ +/*!-------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { WorkspaceFolder } from 'vscode-languageserver'; +import { isDocumentInWorkspaceFolders } from '../../service/utils/isDocumentInWorkspace'; + +function folder(uri: string): WorkspaceFolder { + return { uri, name: uri }; +} + +describe('(Unit) isDocumentInWorkspaceFolders', () => { + describe('Common scenarios', () => { + it('Should return true when the document is directly within a workspace folder', () => { + isDocumentInWorkspaceFolders('file:///workspace/compose.yaml', [folder('file:///workspace')]).should.be.true; + }); + + it('Should return true when the document is nested within a workspace folder', () => { + isDocumentInWorkspaceFolders('file:///workspace/sub/dir/compose.yaml', [folder('file:///workspace')]).should.be.true; + }); + + it('Should return true when the folder URI has a trailing slash', () => { + isDocumentInWorkspaceFolders('file:///workspace/compose.yaml', [folder('file:///workspace/')]).should.be.true; + }); + + it('Should return true when the document is within one of several workspace folders', () => { + isDocumentInWorkspaceFolders('file:///second/compose.yaml', [folder('file:///first'), folder('file:///second')]).should.be.true; + }); + }); + + describe('Negative scenarios', () => { + it('Should return false when the document is outside all workspace folders', () => { + isDocumentInWorkspaceFolders('file:///elsewhere/compose.yaml', [folder('file:///workspace')]).should.be.false; + }); + + it('Should return false for a sibling folder with a matching prefix', () => { + // `file:///workspace` should not match `file:///workspace-other` + isDocumentInWorkspaceFolders('file:///workspace-other/compose.yaml', [folder('file:///workspace')]).should.be.false; + }); + + it('Should return false when there are no workspace folders', () => { + isDocumentInWorkspaceFolders('file:///workspace/compose.yaml', []).should.be.false; + }); + + it('Should return false when the folders are null', () => { + isDocumentInWorkspaceFolders('file:///workspace/compose.yaml', null).should.be.false; + }); + + it('Should return false when the folders are undefined', () => { + isDocumentInWorkspaceFolders('file:///workspace/compose.yaml', undefined).should.be.false; + }); + }); +}); From af77fe07869832ec3ea75192c6c65e4b83ccc350 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:04:06 +0000 Subject: [PATCH 4/4] Reorder ServiceStartupCodeLensProvider early exits --- .../providers/ServiceStartupCodeLensProvider.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts b/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts index 7314105c..b3cba4b3 100644 --- a/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts +++ b/packages/compose-language-service/src/service/providers/ServiceStartupCodeLensProvider.ts @@ -18,6 +18,14 @@ export class ServiceStartupCodeLensProvider extends ProviderBase