Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/core/src/amazonq/lsp/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These various fetch/download/lsp utils seem potentially applicable to both Toolkit and Q in the future. How much of these util modules are Q-specific? Can most of them live in /core/ ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the logic for downloading/fetching etc is in core. The only thing that should be in the q lsp folder is the codewhisperer language server resolver and the codewhisperer language server activation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh right this is in core but the amazonq part of core... if it's not q-specific then it could live in shared.

* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { fs } from '../../shared/fs/fs'

export async function getDownloadedVersions(installLocation: string) {
return (await fs.readdir(installLocation)).map(([f, _], __) => f)
}
45 changes: 37 additions & 8 deletions packages/core/src/amazonq/lsp/workspaceInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@
*/

import path from 'path'
import { LspResolution, LspResolver } from '../../shared/lsp/types'
import { LspResolution, LspResolver, LspVersion } from '../../shared/lsp/types'
import { ManifestResolver } from '../../shared/lsp/manifestResolver'
import { LanguageServerResolver } from '../../shared/lsp/lspResolver'
import { Range } from 'semver'
import { Range, sort } from 'semver'
import { getNodeExecutableName } from '../../shared/lsp/utils/platform'
import { fs } from '../../shared/fs/fs'
import { partition } from '../../shared/utilities/tsUtils'
import { getDownloadedVersions } from './util'

const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json'
export const lspManifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json'
// this LSP client in Q extension is only going to work with these LSP server versions
const supportedLspServerVersions = '0.1.32'
export const supportedLspServerVersions = '0.1.32'
export const lspWorkspaceName = 'AmazonQ-Workspace'

export class WorkspaceLSPResolver implements LspResolver {
async resolve(): Promise<LspResolution> {
const name = 'AmazonQ-Workspace'
const manifest = await new ManifestResolver(manifestUrl, name).resolve()
const manifest = await new ManifestResolver(lspManifestUrl, lspWorkspaceName).resolve()
const installationResult = await new LanguageServerResolver(
manifest,
name,
lspWorkspaceName,
new Range(supportedLspServerVersions)
).resolve()

Expand All @@ -30,7 +32,7 @@ export class WorkspaceLSPResolver implements LspResolver {
const nodePath = path.join(installationResult.assetDirectory, nodeName)
await fs.chmod(nodePath, 0o755)

// TODO Cleanup old versions of language servers
await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory))
return {
...installationResult,
resourcePaths: {
Expand All @@ -42,4 +44,31 @@ export class WorkspaceLSPResolver implements LspResolver {
},
}
}

private isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean {
Copy link
Contributor

@jpinkney-aws jpinkney-aws Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are still missing from this file as well: https://github.com/aws/aws-toolkit-vscode/blob/feature/amazonqLSP/packages/amazonq/src/lsp/lspInstaller.ts#L21. Since this only does the cleanup for the workspace context language server and not the codewhisperer one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it to a general location in core to avoid duplicating.

return manifestVersions.find((v) => v.serverVersion === targetVersion)?.isDelisted ?? false
}

/**
* Delete all delisted versions and keep the two newest versions that remain
* @param manifest
* @param downloadDirectory
*/
async cleanUp(manifestVersions: LspVersion[], downloadDirectory: string): Promise<void> {
const downloadedVersions = await getDownloadedVersions(downloadDirectory)
const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) =>
this.isDelisted(manifestVersions, v)
)
for (const v of delistedVersions) {
await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true })
}

if (remainingVersions.length <= 2) {
return
}

for (const v of sort(remainingVersions).slice(0, -2)) {
await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true })
}
}
}
8 changes: 6 additions & 2 deletions packages/core/src/shared/lsp/lspResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,13 @@ export class LanguageServerResolver {
return version.targets.find((x) => x.arch === arch && x.platform === platform)
}

// lazy calls to `getApplicationSupportFolder()` to avoid failure on windows.
public static get defaultDir() {
return path.join(getApplicationSupportFolder(), `aws/toolkits/language-servers`)
}

defaultDownloadFolder() {
const applicationSupportFolder = getApplicationSupportFolder()
return path.join(applicationSupportFolder, `aws/toolkits/language-servers/${this.lsName}`)
return path.join(LanguageServerResolver.defaultDir, `${this.lsName}`)
}

private getDownloadDirectory(version: string) {
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/shared/utilities/tsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ export function createFactoryFunction<T extends new (...args: any[]) => any>(cto
return (...args) => new ctor(...args)
}

/**
* Split a list into two sublists based on the result of a predicate.
* @param lst list to split
* @param pred predicate to apply to each element
* @returns two nested lists, where for all items x in the left sublist, pred(x) returns true. The remaining elements are in the right sublist.
*/
export function partition<T>(lst: T[], pred: (arg: T) => boolean): [T[], T[]] {
return lst.reduce(
([leftAcc, rightAcc], item) => {
return pred(item) ? [[...leftAcc, item], rightAcc] : [leftAcc, [...rightAcc, item]]
},
[[], []] as [T[], T[]]
)
}

type NoSymbols<T> = { [Property in keyof T]: Property extends symbol ? never : Property }[keyof T]
export type InterfaceNoSymbol<T> = Pick<T, NoSymbols<T>>
/**
Expand Down
101 changes: 101 additions & 0 deletions packages/core/src/test/amazonq/lsp/workplaceInstaller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { Uri } from 'vscode'
import { fs } from '../../../shared'
import { createTestWorkspaceFolder } from '../../testUtil'
import path from 'path'
import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller'
import assert from 'assert'

async function fakeInstallVersion(version: string, installationDir: string): Promise<void> {
const versionDir = path.join(installationDir, version)
await fs.mkdir(versionDir)
await fs.writeFile(path.join(versionDir, 'file.txt'), 'content')
}

async function fakeInstallVersions(versions: string[], installationDir: string): Promise<void> {
for (const v of versions) {
await fakeInstallVersion(v, installationDir)
}
}

describe('workspaceInstaller', function () {
describe('cleanUp', function () {
let installationDir: Uri

before(async function () {
installationDir = (await createTestWorkspaceFolder()).uri
})

afterEach(async function () {
const files = await fs.readdir(installationDir.fsPath)
for (const [name, _type] of files) {
await fs.delete(path.join(installationDir.fsPath, name), { force: true, recursive: true })
}
})

after(async function () {
await fs.delete(installationDir, { force: true, recursive: true })
})

it('keeps two newest versions', async function () {
await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath)
const wsr = new WorkspaceLSPResolver()
await wsr.cleanUp([], installationDir.fsPath)

const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename)
assert.strictEqual(result.length, 2)
assert.ok(result.includes('2.1.1'))
assert.ok(result.includes('1.1.1'))
})

it('deletes delisted versions', async function () {
await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath)
const wsr = new WorkspaceLSPResolver()
await wsr.cleanUp([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath)

const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename)
assert.strictEqual(result.length, 2)
assert.ok(result.includes('2.1.1'))
assert.ok(result.includes('1.0.1'))
})

it('handles case where less than 2 versions are not delisted', async function () {
await fakeInstallVersions(['1.0.0', '1.0.1', '1.1.1', '2.1.1'], installationDir.fsPath)
const wsr = new WorkspaceLSPResolver()
await wsr.cleanUp(
[
{ serverVersion: '1.1.1', isDelisted: true, targets: [] },
{ serverVersion: '2.1.1', isDelisted: true, targets: [] },
{ serverVersion: '1.0.0', isDelisted: true, targets: [] },
],
installationDir.fsPath
)

const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename)
assert.strictEqual(result.length, 1)
assert.ok(result.includes('1.0.1'))
})

it('handles case where less than 2 versions exist', async function () {
await fakeInstallVersions(['1.0.0'], installationDir.fsPath)
const wsr = new WorkspaceLSPResolver()
await wsr.cleanUp([], installationDir.fsPath)

const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename)
assert.strictEqual(result.length, 1)
})

it('does not install delisted version when no other option exists', async function () {
await fakeInstallVersions(['1.0.0'], installationDir.fsPath)
const wsr = new WorkspaceLSPResolver()
await wsr.cleanUp([{ serverVersion: '1.0.0', isDelisted: true, targets: [] }], installationDir.fsPath)

const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename)
assert.strictEqual(result.length, 0)
})
})
})
16 changes: 16 additions & 0 deletions packages/core/src/test/shared/utilities/tsUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { partition } from '../../../shared/utilities/tsUtils'
import assert from 'assert'

describe('partition', function () {
it('should split the list according to predicate', function () {
const items = [1, 2, 3, 4, 5, 6, 7, 8]
const [even, odd] = partition(items, (i) => i % 2 === 0)
assert.deepStrictEqual(even, [2, 4, 6, 8])
assert.deepStrictEqual(odd, [1, 3, 5, 7])
})
})
Loading