From 70afbcf724ea552d3f7c05e1b114f585cec7d2a2 Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 13 Feb 2025 09:42:52 -0500 Subject: [PATCH 1/4] config(amazonq): simplify language server configuration options --- CONTRIBUTING.md | 17 ++++ packages/amazonq/src/lsp/config.ts | 29 +++++++ packages/amazonq/src/lsp/lspInstaller.ts | 25 +++--- .../amazonq/test/e2e/lsp/lspInstaller.test.ts | 33 ++++---- .../test/unit/amazonq/lsp/config.test.ts | 81 +++++++++++++++++++ packages/core/src/shared/index.ts | 2 +- packages/core/src/shared/settings.ts | 2 + 7 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 packages/amazonq/src/lsp/config.ts create mode 100644 packages/amazonq/test/unit/amazonq/lsp/config.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af930a3a34d..de564036aac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -427,6 +427,19 @@ Example: } ``` +Overrides specifically for the language server can be set using the `aws.dev.amazonqLsp` setting. This is a JSON object consisting of keys/values required to override language server: `manifestUrl`, 'supportedVersions', 'id', and 'locationOverride'. + +Example: + +```json +"aws.dev.amazonqLsp": { + "manifestUrl": "https://custom.url/manifest.json", + "supportedVersions": "4.0.0", + "id": "AmazonQSetting", + "locationOverride": "/custom/path", +} +``` + ### Environment variables Environment variables can be used to modify the behaviour of VSCode. The following are environment variables that can be used to configure the extension: @@ -472,6 +485,10 @@ Unlike the user setting overrides, not all of these environment variables have t - `__CODEWHISPERER_REGION`: for aws.dev.codewhispererService.region - `__CODEWHISPERER_ENDPOINT`: for aws.dev.codewhispererService.endpoint +- `__AMAZONQLSP_MANIFEST_URL`: for aws.dev.amazonqLsp.manifestUrl +- `__AMAZONQLSP_SUPPORTED_VERSIONS`: for aws.dev.amazonqLsp.supportedVersions +- `__AMAZONQLSP_ID`: for aws.dev.amazonqLsp.id +- `__AMAZONQLSP_LOCATION_OVERRIDE`: for aws.dev.amazonqLsp.locationOverride #### Lambda diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts new file mode 100644 index 00000000000..a0480a4e2e5 --- /dev/null +++ b/packages/amazonq/src/lsp/config.ts @@ -0,0 +1,29 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DevSettings, getServiceEnvVarConfig } from 'aws-core-vscode/shared' + +export interface AmazonQLspConfig { + manifestUrl: string + supportedVersions: string + id: string + locationOverride?: string +} + +export const defaultAmazonQLspConfig: AmazonQLspConfig = { + manifestUrl: 'https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json', + supportedVersions: '^3.1.1', + id: 'AmazonQ', // used for identification in global storage/local disk location. Do not change. + locationOverride: undefined, +} + +export function getAmazonQLspConfig(): AmazonQLspConfig { + return { + ...defaultAmazonQLspConfig, + ...(DevSettings.instance.getServiceConfig('amazonqLsp', {}) as AmazonQLspConfig), + // Environment variable overrides + ...getServiceEnvVarConfig('amazonqLsp', Object.keys(defaultAmazonQLspConfig)), + } +} diff --git a/packages/amazonq/src/lsp/lspInstaller.ts b/packages/amazonq/src/lsp/lspInstaller.ts index b872422f048..e3be27a9580 100644 --- a/packages/amazonq/src/lsp/lspInstaller.ts +++ b/packages/amazonq/src/lsp/lspInstaller.ts @@ -16,37 +16,34 @@ import { } from 'aws-core-vscode/shared' import path from 'path' import { Range } from 'semver' +import { getAmazonQLspConfig } from './config' -export const manifestURL = 'https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json' -export const supportedLspServerVersions = new Range('^3.1.1', { - includePrerelease: true, -}) const logger = getLogger('amazonqLsp') export class AmazonQLSPResolver implements LspResolver { async resolve(): Promise { - const overrideLocation = process.env.AWS_LANGUAGE_SERVER_OVERRIDE - if (overrideLocation) { - logger.info(`Using language server override location: ${overrideLocation}`) - void vscode.window.showInformationMessage(`Using language server override location: ${overrideLocation}`) + const { id, manifestUrl, supportedVersions, locationOverride } = getAmazonQLspConfig() + if (locationOverride) { + void vscode.window.showInformationMessage(`Using language server override location: ${locationOverride}`) return { - assetDirectory: overrideLocation, + assetDirectory: locationOverride, location: 'override', version: '0.0.0', resourcePaths: { - lsp: overrideLocation, + lsp: locationOverride, node: getNodeExecutableName(), }, } } // "AmazonQ" is shared across toolkits to provide a common access point, don't change it - const name = 'AmazonQ' - const manifest = await new ManifestResolver(manifestURL, name).resolve() + const manifest = await new ManifestResolver(manifestUrl, id).resolve() const installationResult = await new LanguageServerResolver( manifest, - name, - supportedLspServerVersions + id, + new Range(supportedVersions, { + includePrerelease: true, + }) ).resolve() const nodePath = path.join(installationResult.assetDirectory, `servers/${getNodeExecutableName()}`) diff --git a/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts b/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts index 04638c02c39..75d1e328294 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts @@ -5,7 +5,7 @@ import assert from 'assert' import sinon from 'sinon' -import { AmazonQLSPResolver, manifestURL, supportedLspServerVersions } from '../../../src/lsp/lspInstaller' +import { AmazonQLSPResolver } from '../../../src/lsp/lspInstaller' import { fs, globals, @@ -19,6 +19,7 @@ import * as semver from 'semver' import { assertTelemetry } from 'aws-core-vscode/test' import { LspController } from 'aws-core-vscode/amazonq' import { LanguageServerSetup } from 'aws-core-vscode/telemetry' +import { AmazonQLspConfig, getAmazonQLspConfig } from '../../../src/lsp/config' function createVersion(version: string) { return { @@ -48,9 +49,11 @@ describe('AmazonQLSPInstaller', () => { // If globalState contains an ETag that is up to date with remote, we won't fetch it resulting in inconsistent behavior. // Therefore, we clear it temporarily for these tests to ensure consistent behavior. let manifestStorage: { [key: string]: any } + let lspConfig: AmazonQLspConfig before(async () => { manifestStorage = globals.globalState.get(manifestStorageKey) || {} + lspConfig = getAmazonQLspConfig() }) beforeEach(async () => { @@ -93,14 +96,14 @@ describe('AmazonQLSPInstaller', () => { assert.ok(download.assetDirectory.startsWith(tempDir)) assert.deepStrictEqual(download.location, 'remote') - assert.ok(semver.satisfies(download.version, supportedLspServerVersions)) + assert.ok(semver.satisfies(download.version, lspConfig.supportedVersions)) // Second try - Should see the contents in the cache const cache = await resolver.resolve() assert.ok(cache.assetDirectory.startsWith(tempDir)) assert.deepStrictEqual(cache.location, 'cache') - assert.ok(semver.satisfies(cache.version, supportedLspServerVersions)) + assert.ok(semver.satisfies(cache.version, lspConfig.supportedVersions)) /** * Always make sure the latest version is one patch higher. This stops a problem @@ -135,7 +138,7 @@ describe('AmazonQLSPInstaller', () => { assert.ok(fallback.assetDirectory.startsWith(tempDir)) assert.deepStrictEqual(fallback.location, 'fallback') - assert.ok(semver.satisfies(fallback.version, supportedLspServerVersions)) + assert.ok(semver.satisfies(fallback.version, lspConfig.supportedVersions)) /* First Try Telemetry getManifest: remote succeeds @@ -144,25 +147,25 @@ describe('AmazonQLSPInstaller', () => { */ const firstTryTelemetry: Partial[] = [ { - id: 'AmazonQ', + id: lspConfig.id, manifestLocation: 'remote', languageServerSetupStage: 'getManifest', result: 'Succeeded', }, { - id: 'AmazonQ', + id: lspConfig.id, languageServerLocation: 'cache', languageServerSetupStage: 'getServer', result: 'Failed', }, { - id: 'AmazonQ', + id: lspConfig.id, languageServerLocation: 'remote', languageServerSetupStage: 'validate', result: 'Succeeded', }, { - id: 'AmazonQ', + id: lspConfig.id, languageServerLocation: 'remote', languageServerSetupStage: 'getServer', result: 'Succeeded', @@ -176,19 +179,19 @@ describe('AmazonQLSPInstaller', () => { */ const secondTryTelemetry: Partial[] = [ { - id: 'AmazonQ', + id: lspConfig.id, manifestLocation: 'remote', languageServerSetupStage: 'getManifest', result: 'Failed', }, { - id: 'AmazonQ', + id: lspConfig.id, manifestLocation: 'cache', languageServerSetupStage: 'getManifest', result: 'Succeeded', }, { - id: 'AmazonQ', + id: lspConfig.id, languageServerLocation: 'cache', languageServerSetupStage: 'getServer', result: 'Succeeded', @@ -202,19 +205,19 @@ describe('AmazonQLSPInstaller', () => { */ const thirdTryTelemetry: Partial[] = [ { - id: 'AmazonQ', + id: lspConfig.id, languageServerLocation: 'cache', languageServerSetupStage: 'getServer', result: 'Failed', }, { - id: 'AmazonQ', + id: lspConfig.id, languageServerLocation: 'remote', languageServerSetupStage: 'getServer', result: 'Failed', }, { - id: 'AmazonQ', + id: lspConfig.id, languageServerLocation: 'fallback', languageServerSetupStage: 'getServer', result: 'Succeeded', @@ -227,7 +230,7 @@ describe('AmazonQLSPInstaller', () => { }) it('resolves release candidiates', async () => { - const original = new ManifestResolver(manifestURL, 'AmazonQ').resolve() + const original = new ManifestResolver(lspConfig.manifestUrl, lspConfig.id).resolve() sandbox.stub(ManifestResolver.prototype, 'resolve').callsFake(async () => { const originalManifest = await original diff --git a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts new file mode 100644 index 00000000000..b98cabc4ded --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -0,0 +1,81 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { DevSettings } from 'aws-core-vscode/shared' +import sinon from 'sinon' +import { defaultAmazonQLspConfig, getAmazonQLspConfig } from '../../../../src/lsp/config' + +describe('getAmazonQLspConfig', () => { + let sandbox: sinon.SinonSandbox + let serviceConfigStub: sinon.SinonStub + const settingConfig = { + manifestUrl: 'https://custom.url/manifest.json', + supportedVersions: '4.0.0', + id: 'AmazonQSetting', + locationOverride: '/custom/path', + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + + serviceConfigStub = sandbox.stub() + + // Create the DevSettings mock with the properly typed stub + sandbox.stub(DevSettings, 'instance').get(() => ({ + getServiceConfig: serviceConfigStub, + })) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('uses default config when no overrides are present', () => { + serviceConfigStub.returns({}) + const config = getAmazonQLspConfig() + assert.deepStrictEqual(config, defaultAmazonQLspConfig) + }) + + it('overrides location', () => { + const locationOverride = '/custom/path/to/lsp' + serviceConfigStub.returns({ locationOverride }) + + const config = getAmazonQLspConfig() + assert.deepStrictEqual(config, { + ...defaultAmazonQLspConfig, + locationOverride, + }) + }) + + it('override default settings', () => { + serviceConfigStub.returns(settingConfig) + + const config = getAmazonQLspConfig() + assert.deepStrictEqual(config, settingConfig) + }) + + it('environment variable takes precedence over settings', () => { + const envConfig = { + manifestUrl: 'https://another-custom.url/manifest.json', + supportedVersions: '5.1.1', + id: 'AmazonQEnv', + locationOverride: '/some/new/custom/path', + } + + process.env.__AMAZONQLSP_MANIFEST_URL = envConfig.manifestUrl + process.env.__AMAZONQLSP_SUPPORTED_VERSIONS = envConfig.supportedVersions + process.env.__AMAZONQLSP_ID = envConfig.id + process.env.__AMAZONQLSP_LOCATION_OVERRIDE = envConfig.locationOverride + + serviceConfigStub.returns(settingConfig) + + const config = getAmazonQLspConfig() + assert.deepStrictEqual(config, { + ...defaultAmazonQLspConfig, + ...envConfig, + }) + }) +}) diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index b45948308bf..6193a68445c 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -18,7 +18,7 @@ export * from './extensionUtilities' export * from './extensionStartup' export { RegionProvider } from './regions/regionProvider' export { Commands } from './vscode/commands2' -export { getMachineId } from './vscode/env' +export { getMachineId, getServiceEnvVarConfig } from './vscode/env' export { getLogger } from './logger/logger' export { activateExtension, openUrl } from './utilities/vsCodeUtils' export { waitUntil, sleep, Timeout } from './utilities/timeoutUtils' diff --git a/packages/core/src/shared/settings.ts b/packages/core/src/shared/settings.ts index a486784fe14..09b7396f0d1 100644 --- a/packages/core/src/shared/settings.ts +++ b/packages/core/src/shared/settings.ts @@ -757,6 +757,7 @@ const devSettings = { endpoints: Record(String, String), codecatalystService: Record(String, String), codewhispererService: Record(String, String), + amazonqLsp: Record(String, String), ssoCacheDirectory: String, autofillStartUrl: String, webAuth: Boolean, @@ -769,6 +770,7 @@ type ServiceClients = keyof ServiceTypeMap interface ServiceTypeMap { codecatalystService: codecatalyst.CodeCatalystConfig codewhispererService: codewhisperer.CodeWhispererConfig + amazonqLsp: object // type is provided inside of amazon q } /** From 6e0c84cf7fb170566944bee7e71b7e9dfc948fd5 Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 13 Feb 2025 09:51:51 -0500 Subject: [PATCH 2/4] fixup --- CONTRIBUTING.md | 6 +++--- packages/amazonq/.vscode/launch.json | 2 +- packages/amazonq/test/unit/amazonq/lsp/config.test.ts | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de564036aac..302ee885009 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -427,7 +427,7 @@ Example: } ``` -Overrides specifically for the language server can be set using the `aws.dev.amazonqLsp` setting. This is a JSON object consisting of keys/values required to override language server: `manifestUrl`, 'supportedVersions', 'id', and 'locationOverride'. +Overrides specifically for the Amazon Q language server can be set using the `aws.dev.amazonqLsp` setting. This is a JSON object consisting of keys/values required to override language server: `manifestUrl`, `supportedVersions`, `id`, and `locationOverride`. Example: @@ -435,8 +435,8 @@ Example: "aws.dev.amazonqLsp": { "manifestUrl": "https://custom.url/manifest.json", "supportedVersions": "4.0.0", - "id": "AmazonQSetting", - "locationOverride": "/custom/path", + "id": "AmazonQ", + "locationOverride": "/custom/path/to/local/lsp/folder", } ``` diff --git a/packages/amazonq/.vscode/launch.json b/packages/amazonq/.vscode/launch.json index 9ff3c01849c..d2182273882 100644 --- a/packages/amazonq/.vscode/launch.json +++ b/packages/amazonq/.vscode/launch.json @@ -14,7 +14,7 @@ "env": { "SSMDOCUMENT_LANGUAGESERVER_PORT": "6010", "WEBPACK_DEVELOPER_SERVER": "http://localhost:8080" - // "AWS_LANGUAGE_SERVER_OVERRIDE": "${workspaceFolder}/../../../language-servers/app/aws-lsp-codewhisperer-runtimes/out/token-standalone.js", + // "__AMAZONQLSP_LOCATION_OVERRIDE": "${workspaceFolder}/../../../language-servers/app/aws-lsp-codewhisperer-runtimes/out/token-standalone.js", }, "envFile": "${workspaceFolder}/.local.env", "outFiles": ["${workspaceFolder}/dist/**/*.js", "${workspaceFolder}/../core/dist/**/*.js"], diff --git a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts index b98cabc4ded..c26e4a87239 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -22,8 +22,6 @@ describe('getAmazonQLspConfig', () => { sandbox = sinon.createSandbox() serviceConfigStub = sandbox.stub() - - // Create the DevSettings mock with the properly typed stub sandbox.stub(DevSettings, 'instance').get(() => ({ getServiceConfig: serviceConfigStub, })) @@ -33,7 +31,7 @@ describe('getAmazonQLspConfig', () => { sandbox.restore() }) - it('uses default config when no overrides are present', () => { + it('uses default config', () => { serviceConfigStub.returns({}) const config = getAmazonQLspConfig() assert.deepStrictEqual(config, defaultAmazonQLspConfig) @@ -50,7 +48,7 @@ describe('getAmazonQLspConfig', () => { }) }) - it('override default settings', () => { + it('overrides default settings', () => { serviceConfigStub.returns(settingConfig) const config = getAmazonQLspConfig() From 766904447dead74c3bcc17876b1f2cb7b9973ebd Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 13 Feb 2025 09:52:43 -0500 Subject: [PATCH 3/4] fixup --- packages/amazonq/src/lsp/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts index a0480a4e2e5..44b08fbc396 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -23,7 +23,6 @@ export function getAmazonQLspConfig(): AmazonQLspConfig { return { ...defaultAmazonQLspConfig, ...(DevSettings.instance.getServiceConfig('amazonqLsp', {}) as AmazonQLspConfig), - // Environment variable overrides ...getServiceEnvVarConfig('amazonqLsp', Object.keys(defaultAmazonQLspConfig)), } } From c1a8c593efd52a547e55e653e15d266b1fb0fb6e Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 13 Feb 2025 13:25:58 -0500 Subject: [PATCH 4/4] update --- .../amazonq/test/e2e/lsp/lspInstaller.test.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts b/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts index 75d1e328294..0447d7f5fcf 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts @@ -7,6 +7,7 @@ import assert from 'assert' import sinon from 'sinon' import { AmazonQLSPResolver } from '../../../src/lsp/lspInstaller' import { + DevSettings, fs, globals, LanguageServerResolver, @@ -67,7 +68,7 @@ describe('AmazonQLSPInstaller', () => { }) afterEach(async () => { - delete process.env.AWS_LANGUAGE_SERVER_OVERRIDE + delete process.env.__AMAZONQLSP_LOCATION_OVERRIDE sandbox.restore() await fs.delete(tempDir, { recursive: true, @@ -79,9 +80,25 @@ describe('AmazonQLSPInstaller', () => { }) describe('resolve()', () => { - it('uses AWS_LANGUAGE_SERVER_OVERRIDE', async () => { + it('uses dev setting override', async () => { + const locationOverride = '/custom/path/to/lsp' + const serviceConfigStub = sandbox.stub().returns({ + locationOverride, + }) + sandbox.stub(DevSettings, 'instance').get(() => ({ + getServiceConfig: serviceConfigStub, + })) + + const result = await resolver.resolve() + + assert.strictEqual(result.assetDirectory, locationOverride) + assert.strictEqual(result.location, 'override') + assert.strictEqual(result.version, '0.0.0') + }) + + it('uses environment variable override', async () => { const overridePath = '/custom/path/to/lsp' - process.env.AWS_LANGUAGE_SERVER_OVERRIDE = overridePath + process.env.__AMAZONQLSP_LOCATION_OVERRIDE = overridePath const result = await resolver.resolve()