-
Notifications
You must be signed in to change notification settings - Fork 751
feat(lsp): older and delisted versions of lsp are automatically removed #6409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
9cd4544
e7bf0af
90d23b1
2c19cf4
1c366ef
b00ec0d
4198611
32d2ba3
a7a89c1
ee7343b
604d2e1
78aae8f
3b9878f
3b693c6
d5b972e
bf36a48
88db689
bace7ca
5e47cb5
da20782
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /*! | ||
| * 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) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
|
|
||
|
|
@@ -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: { | ||
|
|
@@ -42,4 +44,31 @@ export class WorkspaceLSPResolver implements LspResolver { | |
| }, | ||
| } | ||
| } | ||
|
|
||
| private isDelisted(manifestVersions: LspVersion[], targetVersion: string): boolean { | ||
|
||
| 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)) { | ||
Hweinstock marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await fs.delete(path.join(downloadDirectory, v), { force: true, recursive: true }) | ||
| } | ||
| } | ||
| } | ||
| 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) | ||
| }) | ||
| }) | ||
| }) |
| 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]) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
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/ ?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.