Skip to content

Commit 9cd4544

Browse files
committed
implement heuristic
1 parent 2f21092 commit 9cd4544

File tree

5 files changed

+175
-10
lines changed

5 files changed

+175
-10
lines changed

packages/core/src/amazonq/lsp/workspaceInstaller.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,43 @@
44
*/
55

66
import path from 'path'
7-
import { LspResolution, LspResolver } from '../../shared/lsp/types'
7+
import { LspResolution, LspResolver, LspVersion } from '../../shared/lsp/types'
88
import { ManifestResolver } from '../../shared/lsp/manifestResolver'
99
import { LanguageServerResolver } from '../../shared/lsp/lspResolver'
10-
import { Range } from 'semver'
10+
import { Range, sort } from 'semver'
1111
import { getNodeExecutableName } from '../../shared/lsp/utils/platform'
1212
import { fs } from '../../shared/fs/fs'
13+
import { partition } from '../../shared/utilities/tsUtils'
1314

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

1820
export class WorkspaceLSPResolver implements LspResolver {
21+
public constructor(
22+
private readonly options = {
23+
versionRange: new Range(supportedLspServerVersions),
24+
cleanUp: true,
25+
}
26+
) {}
27+
1928
async resolve(): Promise<LspResolution> {
20-
const name = 'AmazonQ-Workspace'
21-
const manifest = await new ManifestResolver(manifestUrl, name).resolve()
29+
const manifest = await new ManifestResolver(manifestUrl, lspWorkspaceName).resolve()
2230
const installationResult = await new LanguageServerResolver(
2331
manifest,
24-
name,
25-
new Range(supportedLspServerVersions)
32+
lspWorkspaceName,
33+
this.options.versionRange
2634
).resolve()
2735

2836
const nodeName =
2937
process.platform === 'win32' ? getNodeExecutableName() : `node-${process.platform}-${process.arch}`
3038
const nodePath = path.join(installationResult.assetDirectory, nodeName)
3139
await fs.chmod(nodePath, 0o755)
3240

33-
// TODO Cleanup old versions of language servers
41+
if (this.options.cleanUp) {
42+
await this.cleanUp(manifest.versions, path.dirname(installationResult.assetDirectory))
43+
}
3444
return {
3545
...installationResult,
3646
resourcePaths: {
@@ -42,4 +52,31 @@ export class WorkspaceLSPResolver implements LspResolver {
4252
},
4353
}
4454
}
55+
56+
private async getDownloadedVersions(downloadDirectory: string): Promise<string[]> {
57+
return (await fs.readdir(downloadDirectory)).map(([f, _], __) => f)
58+
}
59+
60+
private isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean {
61+
return manifestVersions.find((v) => v.serverVersion === targetVersion)?.isDelisted ?? false
62+
}
63+
64+
/**
65+
* Delete all delisted versions and keep the two newest versions that remain
66+
* @param manifest
67+
* @param downloadDirectory
68+
*/
69+
async cleanUp(manifestVersions: LspVersion[], downloadDirectory: string): Promise<void> {
70+
const downloadedVersions = await this.getDownloadedVersions(downloadDirectory)
71+
const [delistedVersions, remainingVersions] = partition(downloadedVersions, (v: string) =>
72+
this.isDelisted(manifestVersions, v)
73+
)
74+
for (const v of delistedVersions) {
75+
await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true })
76+
}
77+
78+
for (const v of sort(remainingVersions).slice(0, -2)) {
79+
await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true })
80+
}
81+
}
4582
}

packages/core/src/shared/lsp/lspResolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { getApplicationSupportFolder } from '../vscode/env'
1414
import { createHash } from '../crypto'
1515
import request from '../request'
1616

17+
export const langugeServerDefaultDir = path.join(getApplicationSupportFolder(), `aws/toolkits/language-servers`)
1718
export class LanguageServerResolver {
1819
constructor(
1920
private readonly manifest: Manifest,
@@ -340,8 +341,7 @@ export class LanguageServerResolver {
340341
}
341342

342343
defaultDownloadFolder() {
343-
const applicationSupportFolder = getApplicationSupportFolder()
344-
return path.join(applicationSupportFolder, `aws/toolkits/language-servers/${this.lsName}`)
344+
return path.join(langugeServerDefaultDir, `${this.lsName}`)
345345
}
346346

347347
private getDownloadDirectory(version: string) {

packages/core/src/shared/utilities/tsUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ export function createFactoryFunction<T extends new (...args: any[]) => any>(cto
9494
return (...args) => new ctor(...args)
9595
}
9696

97+
export function partition<T>(lst: T[], pred: (arg: T) => boolean): [T[], T[]] {
98+
return lst.reduce(
99+
([leftAcc, rightAcc], item) => {
100+
return pred(item) ? [[...leftAcc, item], rightAcc] : [leftAcc, [...rightAcc, item]]
101+
},
102+
[[], []] as [T[], T[]]
103+
)
104+
}
105+
97106
type NoSymbols<T> = { [Property in keyof T]: Property extends symbol ? never : Property }[keyof T]
98107
export type InterfaceNoSymbol<T> = Pick<T, NoSymbols<T>>
99108
/**
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Uri } from 'vscode'
7+
import { fs } from '../../../shared'
8+
import { createTestWorkspaceFolder } from '../../testUtil'
9+
import path from 'path'
10+
import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller'
11+
import assert from 'assert'
12+
13+
async function fakeInstallVersion(version: string, installationDir: string): Promise<void> {
14+
const versionDir = path.join(installationDir, version)
15+
await fs.mkdir(versionDir)
16+
await fs.writeFile(path.join(versionDir, 'file.txt'), 'content')
17+
}
18+
19+
describe('workspaceInstaller', function () {
20+
describe('cleanUp', function () {
21+
let installationDir: Uri
22+
let versions: string[]
23+
24+
before(async function () {
25+
installationDir = (await createTestWorkspaceFolder()).uri
26+
versions = ['1.0.0', '1.0.1', '1.1.1', '2.1.1']
27+
})
28+
29+
beforeEach(async function () {
30+
for (const v of versions) {
31+
await fakeInstallVersion(v, installationDir.fsPath)
32+
}
33+
})
34+
35+
after(async function () {
36+
await fs.delete(installationDir, { force: true, recursive: true })
37+
})
38+
it('keeps two newest versions', async function () {
39+
const wsr = new WorkspaceLSPResolver()
40+
await wsr.cleanUp([], installationDir.fsPath)
41+
42+
const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename)
43+
assert.strictEqual(result.length, 2)
44+
assert.ok(result.includes('2.1.1'))
45+
assert.ok(result.includes('1.1.1'))
46+
})
47+
48+
it('deletes delisted versions', async function () {
49+
const wsr = new WorkspaceLSPResolver()
50+
await wsr.cleanUp([{ serverVersion: '1.1.1', isDelisted: true, targets: [] }], installationDir.fsPath)
51+
52+
const result = (await fs.readdir(installationDir.fsPath)).map(([filename, _filetype], _index) => filename)
53+
assert.strictEqual(result.length, 2)
54+
assert.ok(result.includes('2.1.1'))
55+
assert.ok(result.includes('1.0.1'))
56+
})
57+
})
58+
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Range } from 'semver'
7+
import assert from 'assert'
8+
import { WorkspaceLSPResolver } from '../../../amazonq/lsp/workspaceInstaller'
9+
import { fs } from '../../../shared/fs/fs'
10+
import path from 'path'
11+
import * as sinon from 'sinon'
12+
import { langugeServerDefaultDir } from '../../../shared/lsp/lspResolver'
13+
14+
async function installVersion(version: string, cleanUp: boolean = false) {
15+
const resolver = new WorkspaceLSPResolver({ versionRange: new Range(version), cleanUp: cleanUp })
16+
return await resolver.resolve()
17+
}
18+
19+
/**
20+
* Installs all versions, only running 'cleanUp' on the last install.
21+
* @param versions
22+
* @returns
23+
*/
24+
async function testInstallVersions(versions: string[]) {
25+
await Promise.all(versions.slice(0, -1).map(async (version) => await installVersion(version)))
26+
const finalVersionResult = await installVersion(versions[versions.length - 1], true)
27+
const allVersions = path.dirname(finalVersionResult.assetDirectory)
28+
const versionsDownloaded = (await fs.readdir(allVersions)).map(([f, _], __) => f)
29+
return versionsDownloaded
30+
}
31+
32+
describe('workspaceInstaller', function () {
33+
before(async function () {
34+
await fs.delete(langugeServerDefaultDir, { force: true, recursive: true })
35+
})
36+
37+
it('removes all but the latest two versions', async function () {
38+
const versionsToInstall = ['0.1.25', '0.1.26', '0.1.27', '0.1.28']
39+
const versionsDownloaded = await testInstallVersions(versionsToInstall)
40+
41+
assert.strictEqual(versionsDownloaded.length, 2)
42+
assert.ok(versionsDownloaded.includes('0.1.28'))
43+
assert.ok(versionsDownloaded.includes('0.1.29'))
44+
})
45+
46+
it('removes delisted versions then keeps 2 remaining most recent', async function () {
47+
const isDelisted = sinon.stub(WorkspaceLSPResolver.prototype, 'isDelisted' as any)
48+
isDelisted.callsFake((_manifestVersions, version) => {
49+
return version === '0.1.27' || version === '0.1.29'
50+
})
51+
52+
const versionsToInstall = ['0.1.25', '0.1.26', '0.1.27', '0.1.28']
53+
const versionsDownloaded = await testInstallVersions(versionsToInstall)
54+
55+
console.log(versionsDownloaded)
56+
assert.strictEqual(versionsDownloaded.length, 2)
57+
assert.ok(versionsDownloaded.includes('0.1.28'))
58+
assert.ok(versionsDownloaded.includes('0.1.26'))
59+
isDelisted.restore()
60+
})
61+
})

0 commit comments

Comments
 (0)