-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a CodeLens for starting services individually or all at once (#158)
- Loading branch information
1 parent
87dabbf
commit 2be9db5
Showing
5 changed files
with
232 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/*-------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See LICENSE in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import { CancellationToken, CodeLens, CodeLensParams } from 'vscode-languageserver'; | ||
import { ProviderBase } from './ProviderBase'; | ||
import { ExtendedParams } from '../ExtendedParams'; | ||
import { getCurrentContext } from '../utils/ActionContext'; | ||
import { isMap, isPair, isScalar } from 'yaml'; | ||
import { yamlRangeToLspRange } from '../utils/yamlRangeToLspRange'; | ||
|
||
export class ServiceStartupCodeLensProvider extends ProviderBase<CodeLensParams & ExtendedParams, CodeLens[] | undefined, never, never> { | ||
public on(params: CodeLensParams & ExtendedParams, token: CancellationToken): CodeLens[] | undefined { | ||
const ctx = getCurrentContext(); | ||
ctx.telemetry.properties.isActivationEvent = 'true'; // This happens automatically so we'll treat it as isActivationEvent === true | ||
|
||
const results: CodeLens[] = []; | ||
|
||
if (!params.document.yamlDocument.value.has('services')) { | ||
return undefined; | ||
} | ||
|
||
// First add the run-all from the main "services" node | ||
const documentMap = params.document.yamlDocument.value.contents; | ||
if (isMap(documentMap)) { | ||
const servicesNode = documentMap.items.find(item => { | ||
return isScalar(item.key) && item.key.value === 'services'; | ||
}); | ||
|
||
if (isPair(servicesNode)) { | ||
const servicesKey = servicesNode.key; | ||
|
||
if (isScalar(servicesKey) && servicesKey.range && isMap(servicesNode.value)) { | ||
const lens = CodeLens.create(yamlRangeToLspRange(params.document.textDocument, servicesKey.range)); | ||
lens.command = { | ||
title: '$(run-all) Run All Services', | ||
command: 'vscode-docker.compose.up', | ||
arguments: [ | ||
/* dockerComposeFileUri: */ params.document.uri | ||
], | ||
}; | ||
results.push(lens); | ||
} | ||
} | ||
} | ||
|
||
// Check for cancellation | ||
if (token.isCancellationRequested) { | ||
return undefined; | ||
} | ||
|
||
// Then add the run-single for each service | ||
const serviceMap = params.document.yamlDocument.value.getIn(['services']); | ||
if (isMap(serviceMap)) { | ||
for (const service of serviceMap.items) { | ||
// Within each loop we'll check for cancellation (though this is expected to be very fast) | ||
if (token.isCancellationRequested) { | ||
return undefined; | ||
} | ||
|
||
if (isScalar(service.key) && typeof service.key.value === 'string' && service.key.range) { | ||
const lens = CodeLens.create(yamlRangeToLspRange(params.document.textDocument, service.key.range)); | ||
lens.command = { | ||
title: '$(play) Run Service', | ||
command: 'vscode-docker.compose.up.subset', | ||
arguments: [ // Arguments are from here: https://github.com/microsoft/vscode-docker/blob/a45a3dfc8e582f563292a707bbe56f616f7fedeb/src/commands/compose/compose.ts#L79 | ||
/* dockerComposeFileUri: */ params.document.uri, | ||
/* selectedComposeFileUris: */ undefined, | ||
/* preselectedServices: */[service.key.value], | ||
], | ||
}; | ||
results.push(lens); | ||
} | ||
} | ||
} | ||
|
||
return results; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
src/test/providers/ServiceStartupCodeLensProvider.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/*!-------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See LICENSE in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import { expect } from 'chai'; | ||
import { CodeLensRequest, CodeLens, DocumentUri, Range, ResponseError } from 'vscode-languageserver'; | ||
import { TestConnection } from '../TestConnection'; | ||
|
||
interface ExpectedServiceStartupCodeLens { | ||
range: Range; | ||
command: { | ||
command: string; | ||
} | ||
} | ||
|
||
describe('ServiceStartupCodeLensProvider', () => { | ||
let testConnection: TestConnection; | ||
before('Prepare a language server for testing', async () => { | ||
testConnection = new TestConnection(); | ||
}); | ||
|
||
describe('Common scenarios', () => { | ||
it('Should provide a code lens to start all services at the root services node', async () => { | ||
const testObject = { | ||
services: {} | ||
}; | ||
|
||
const expected = [ | ||
{ | ||
range: Range.create(0, 0, 0, 8), | ||
command: { | ||
command: 'vscode-docker.compose.up' | ||
} | ||
}, | ||
]; | ||
|
||
const uri = testConnection.sendObjectAsYamlDocument(testObject); | ||
await requestServiceStartupCodeLensesAndCompare(testConnection, uri, expected); | ||
}); | ||
|
||
it('Should provide a code lens for starting each service', async () => { | ||
const testObject = { | ||
version: '123', | ||
services: { | ||
abc: { | ||
image: 'alpine' | ||
}, | ||
def: { | ||
image: 'mysql:latest' | ||
}, | ||
} | ||
}; | ||
|
||
const expected = [ | ||
{ | ||
range: Range.create(1, 0, 1, 8), | ||
command: { | ||
command: 'vscode-docker.compose.up' | ||
} | ||
}, | ||
{ | ||
range: Range.create(2, 2, 2, 5), | ||
command: { | ||
command: 'vscode-docker.compose.up.subset', | ||
} | ||
}, | ||
{ | ||
range: Range.create(4, 2, 4, 5), | ||
command: { | ||
command: 'vscode-docker.compose.up.subset', | ||
} | ||
}, | ||
]; | ||
|
||
const uri = testConnection.sendObjectAsYamlDocument(testObject); | ||
await requestServiceStartupCodeLensesAndCompare(testConnection, uri, expected); | ||
}); | ||
}); | ||
|
||
describe('Error scenarios', () => { | ||
it('Should return an error for nonexistent files', () => { | ||
return testConnection | ||
.client.sendRequest(CodeLensRequest.type, { textDocument: { uri: 'file:///bogus' } }) | ||
.should.eventually.be.rejectedWith(ResponseError); | ||
}); | ||
|
||
it('Should NOT provide service startup code lenses if `services` isn\'t present', async () => { | ||
const uri = testConnection.sendObjectAsYamlDocument({}); | ||
await requestServiceStartupCodeLensesAndCompare(testConnection, uri, undefined); | ||
}); | ||
|
||
it('Should NOT provide service startup code lenses if `services` isn\'t a map', async () => { | ||
const testObject = { | ||
services: 'a' | ||
}; | ||
|
||
const uri = testConnection.sendObjectAsYamlDocument(testObject); | ||
await requestServiceStartupCodeLensesAndCompare(testConnection, uri, []); | ||
}); | ||
}); | ||
|
||
after('Cleanup', () => { | ||
testConnection.dispose(); | ||
}); | ||
}); | ||
|
||
async function requestServiceStartupCodeLensesAndCompare(testConnection: TestConnection, uri: DocumentUri, expected: ExpectedServiceStartupCodeLens[] | undefined): Promise<void> { | ||
const result = await testConnection.client.sendRequest(CodeLensRequest.type, { textDocument: { uri } }) as CodeLens[]; | ||
|
||
if (expected === undefined) { | ||
expect(result).to.be.null; | ||
return; | ||
} | ||
|
||
expect(result).to.be.ok; // Should always be OK result even if 0 code lenses | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
result.length.should.equal(expected!.length); | ||
|
||
if (expected!.length) { | ||
// Each diagnostic should have a matching range and content canary in the results | ||
for (const expectedCodeLens of expected!) { | ||
result.some(actualCodeLens => lensesMatch(actualCodeLens, expectedCodeLens)).should.be.true; | ||
} | ||
} | ||
/* eslint-enable @typescript-eslint/no-non-null-assertion */ | ||
} | ||
|
||
function lensesMatch(actual: CodeLens, expected: ExpectedServiceStartupCodeLens): boolean { | ||
return ( | ||
actual.command?.command === expected.command.command && | ||
actual.range.start.line === expected.range.start.line && | ||
actual.range.start.character === expected.range.start.character && | ||
actual.range.end.line === expected.range.end.line && | ||
actual.range.end.character === expected.range.end.character | ||
); | ||
} |