diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af930a3a34d..302ee885009 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -427,6 +427,19 @@ Example: } ``` +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: + +```json +"aws.dev.amazonqLsp": { + "manifestUrl": "https://custom.url/manifest.json", + "supportedVersions": "4.0.0", + "id": "AmazonQ", + "locationOverride": "/custom/path/to/local/lsp/folder", +} +``` + ### 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/.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/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts new file mode 100644 index 00000000000..44b08fbc396 --- /dev/null +++ b/packages/amazonq/src/lsp/config.ts @@ -0,0 +1,28 @@ +/*! + * 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), + ...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..0447d7f5fcf 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstaller.test.ts @@ -5,8 +5,9 @@ import assert from 'assert' import sinon from 'sinon' -import { AmazonQLSPResolver, manifestURL, supportedLspServerVersions } from '../../../src/lsp/lspInstaller' +import { AmazonQLSPResolver } from '../../../src/lsp/lspInstaller' import { + DevSettings, fs, globals, LanguageServerResolver, @@ -19,6 +20,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 +50,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 () => { @@ -64,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, @@ -76,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() @@ -93,14 +113,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 +155,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 +164,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 +196,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 +222,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 +247,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..c26e4a87239 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -0,0 +1,79 @@ +/*! + * 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() + sandbox.stub(DevSettings, 'instance').get(() => ({ + getServiceConfig: serviceConfigStub, + })) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('uses default config', () => { + 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('overrides 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 } /**