testing: initial implementation of test decorations
This commit is contained in:
parent
3e55989cca
commit
add5b32d95
24 changed files with 810 additions and 202 deletions
|
@ -32,7 +32,7 @@
|
||||||
"testing.enableProblemDiagnostics": {
|
"testing.enableProblemDiagnostics": {
|
||||||
"description": "%config.enableProblemDiagnostics%",
|
"description": "%config.enableProblemDiagnostics%",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ class TestingConfig implements IDisposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get diagnostics() {
|
public get diagnostics() {
|
||||||
return this.section.get(Constants.EnableDiagnosticsConfig, true);
|
return this.section.get(Constants.EnableDiagnosticsConfig, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isEnabled() {
|
public get isEnabled() {
|
||||||
|
|
|
@ -106,6 +106,7 @@ export abstract class PeekViewWidget extends ZoneWidget {
|
||||||
|
|
||||||
private readonly _onDidClose = new Emitter<PeekViewWidget>();
|
private readonly _onDidClose = new Emitter<PeekViewWidget>();
|
||||||
readonly onDidClose = this._onDidClose.event;
|
readonly onDidClose = this._onDidClose.event;
|
||||||
|
private disposed?: true;
|
||||||
|
|
||||||
protected _headElement?: HTMLDivElement;
|
protected _headElement?: HTMLDivElement;
|
||||||
protected _primaryHeading?: HTMLElement;
|
protected _primaryHeading?: HTMLElement;
|
||||||
|
@ -124,8 +125,11 @@ export abstract class PeekViewWidget extends ZoneWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
super.dispose();
|
if (!this.disposed) {
|
||||||
this._onDidClose.fire(this);
|
this.disposed = true; // prevent consumers who dispose on onDidClose from looping
|
||||||
|
super.dispose();
|
||||||
|
this._onDidClose.fire(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
style(styles: IPeekViewStyles): void {
|
style(styles: IPeekViewStyles): void {
|
||||||
|
|
|
@ -172,6 +172,24 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi
|
||||||
this.setDecorationsScheduler.schedule();
|
this.setDecorationsScheduler.schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns context menu actions at the line number if breakpoints can be
|
||||||
|
* set. This is used by the {@link TestingDecorations} to allow breakpoint
|
||||||
|
* setting on lines where breakpoint "run" actions are present.
|
||||||
|
*/
|
||||||
|
public getContextMenuActionsAtPosition(lineNumber: number, model: ITextModel) {
|
||||||
|
if (!this.debugService.getAdapterManager().hasDebuggers()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.debugService.canSetBreakpointsIn(model)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri: model.uri });
|
||||||
|
return this.getContextMenuActions(breakpoints, model.uri, lineNumber);
|
||||||
|
}
|
||||||
|
|
||||||
private registerListeners(): void {
|
private registerListeners(): void {
|
||||||
this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => {
|
this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => {
|
||||||
if (!this.debugService.getAdapterManager().hasDebuggers()) {
|
if (!this.debugService.getAdapterManager().hasDebuggers()) {
|
||||||
|
@ -376,7 +394,8 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi
|
||||||
const decorations = this.editor.getLineDecorations(line);
|
const decorations = this.editor.getLineDecorations(line);
|
||||||
if (decorations) {
|
if (decorations) {
|
||||||
for (const { options } of decorations) {
|
for (const { options } of decorations) {
|
||||||
if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('codicon-') === -1) {
|
const clz = options.glyphMarginClassName;
|
||||||
|
if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-'))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur
|
||||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||||
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
|
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
|
||||||
import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot';
|
import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot';
|
||||||
|
import { IAction } from 'vs/base/common/actions';
|
||||||
|
|
||||||
export const VIEWLET_ID = 'workbench.view.debug';
|
export const VIEWLET_ID = 'workbench.view.debug';
|
||||||
|
|
||||||
|
@ -951,6 +952,7 @@ export interface IDebugEditorContribution extends editorCommon.IEditorContributi
|
||||||
export interface IBreakpointEditorContribution extends editorCommon.IEditorContribution {
|
export interface IBreakpointEditorContribution extends editorCommon.IEditorContribution {
|
||||||
showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void;
|
showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void;
|
||||||
closeBreakpointWidget(): void;
|
closeBreakpointWidget(): void;
|
||||||
|
getContextMenuActionsAtPosition(lineNumber: number, model: EditorIModel): IAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// temporary debug helper service
|
// temporary debug helper service
|
||||||
|
|
|
@ -46,14 +46,8 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
|
||||||
this._register(listener.onFolderChange(this.applyFolderChange, this));
|
this._register(listener.onFolderChange(this.applyFolderChange, this));
|
||||||
|
|
||||||
for (const [folder, collection] of listener.workspaceFolderCollections) {
|
for (const [folder, collection] of listener.workspaceFolderCollections) {
|
||||||
const queue = [collection.rootIds];
|
for (const node of collection.all) {
|
||||||
while (queue.length) {
|
this.storeItem(this.createItem(node, folder.folder));
|
||||||
for (const id of queue.pop()!) {
|
|
||||||
const node = collection.getNodeById(id)!;
|
|
||||||
const item = this.createItem(node, folder.folder);
|
|
||||||
this.storeItem(item);
|
|
||||||
queue.push(node.children);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { localize } from 'vs/nls';
|
||||||
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
|
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
|
||||||
import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||||
import { testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme';
|
import { testingColorRunAction, testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme';
|
||||||
|
|
||||||
export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
|
export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
|
||||||
export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.'));
|
export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.'));
|
||||||
export const testingRunAllIcon = registerIcon('testing-run-icon', Codicon.runAll, localize('testingRunAllIcon', 'Icon of the "run all tests" action.'));
|
export const testingRunAllIcon = registerIcon('testing-run-all-icon', Codicon.runAll, localize('testingRunAllIcon', 'Icon of the "run all tests" action.'));
|
||||||
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAlt, localize('testingDebugIcon', 'Icon of the "debug test" action.'));
|
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAlt, localize('testingDebugIcon', 'Icon of the "debug test" action.'));
|
||||||
export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.close, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.'));
|
export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.close, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.'));
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export const testingStatesToIcons = new Map<TestRunState, ThemeIcon>([
|
||||||
[TestRunState.Failed, registerIcon('testing-failed-icon', Codicon.close, localize('testingFailedIcon', 'Icon shown for tests that failed.'))],
|
[TestRunState.Failed, registerIcon('testing-failed-icon', Codicon.close, localize('testingFailedIcon', 'Icon shown for tests that failed.'))],
|
||||||
[TestRunState.Passed, registerIcon('testing-passed-icon', Codicon.pass, localize('testingPassedIcon', 'Icon shown for tests that passed.'))],
|
[TestRunState.Passed, registerIcon('testing-passed-icon', Codicon.pass, localize('testingPassedIcon', 'Icon shown for tests that passed.'))],
|
||||||
[TestRunState.Queued, registerIcon('testing-queued-icon', Codicon.watch, localize('testingQueuedIcon', 'Icon shown for tests that are queued.'))],
|
[TestRunState.Queued, registerIcon('testing-queued-icon', Codicon.watch, localize('testingQueuedIcon', 'Icon shown for tests that are queued.'))],
|
||||||
[TestRunState.Running, Codicon.loading],
|
[TestRunState.Running, ThemeIcon.modify(Codicon.loading, 'spin')],
|
||||||
[TestRunState.Skipped, registerIcon('testing-skipped-icon', Codicon.debugStepOver, localize('testingSkippedIcon', 'Icon shown for tests that are skipped.'))],
|
[TestRunState.Skipped, registerIcon('testing-skipped-icon', Codicon.debugStepOver, localize('testingSkippedIcon', 'Icon shown for tests that are skipped.'))],
|
||||||
[TestRunState.Unset, registerIcon('testing-unset-icon', Codicon.circleOutline, localize('testingUnsetIcon', 'Icon shown for tests that are in an unset state.'))],
|
[TestRunState.Unset, registerIcon('testing-unset-icon', Codicon.circleOutline, localize('testingUnsetIcon', 'Icon shown for tests that are in an unset state.'))],
|
||||||
]);
|
]);
|
||||||
|
@ -39,4 +39,11 @@ registerThemingParticipant((theme, collector) => {
|
||||||
color: ${theme.getColor(color)} !important;
|
color: ${theme.getColor(color)} !important;
|
||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collector.addRule(`
|
||||||
|
.monaco-editor ${ThemeIcon.asCSSSelector(testingRunIcon)},
|
||||||
|
.monaco-editor ${ThemeIcon.asCSSSelector(testingRunAllIcon)} {
|
||||||
|
color: ${theme.getColor(testingColorRunAction)};
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,7 +71,6 @@
|
||||||
max-height: 29px;
|
max-height: 29px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** -- filter */
|
/** -- filter */
|
||||||
.testing-filter-action-bar {
|
.testing-filter-action-bar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -92,3 +91,13 @@
|
||||||
.testing-filter-action-item .testing-filter-wrapper {
|
.testing-filter-action-item .testing-filter-wrapper {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** -- decorations */
|
||||||
|
|
||||||
|
.monaco-editor .testing-run-glyph {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor .testing-inline-message {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { localize } from 'vs/nls';
|
||||||
import { Action2, MenuId } from 'vs/platform/actions/common/actions';
|
import { Action2, MenuId } from 'vs/platform/actions/common/actions';
|
||||||
import { ContextKeyAndExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
|
import { ContextKeyAndExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||||
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
||||||
|
@ -20,10 +21,9 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons';
|
||||||
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
||||||
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||||
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||||
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
|
||||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||||
import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
|
import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||||
|
|
||||||
const category = localize('testing.category', 'Test');
|
const category = localize('testing.category', 'Test');
|
||||||
|
|
||||||
|
@ -197,18 +197,18 @@ abstract class RunOrDebugAllAllAction extends Action2 {
|
||||||
|
|
||||||
const tests: TestIdWithProvider[] = [];
|
const tests: TestIdWithProvider[] = [];
|
||||||
await Promise.all(workspace.getWorkspace().folders.map(async (folder) => {
|
await Promise.all(workspace.getWorkspace().folders.map(async (folder) => {
|
||||||
const handle = testService.subscribeToDiffs(ExtHostTestingResource.Workspace, folder.uri);
|
const ref = testService.subscribeToDiffs(ExtHostTestingResource.Workspace, folder.uri);
|
||||||
try {
|
try {
|
||||||
await waitForAllRoots(handle.collection);
|
await waitForAllRoots(ref.object);
|
||||||
|
|
||||||
for (const root of handle.collection.rootIds) {
|
for (const root of ref.object.rootIds) {
|
||||||
const node = handle.collection.getNodeById(root);
|
const node = ref.object.getNodeById(root);
|
||||||
if (node && (this.debug ? node.item.debuggable : node.item.runnable)) {
|
if (node && (this.debug ? node.item.debuggable : node.item.runnable)) {
|
||||||
tests.push({ testId: node.id, providerId: node.providerId });
|
tests.push({ testId: node.id, providerId: node.providerId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
handle.dispose();
|
ref.dispose();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
|
|
||||||
import { Codicon } from 'vs/base/common/codicons';
|
import { Codicon } from 'vs/base/common/codicons';
|
||||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||||
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
|
||||||
import { EditorAction2, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
import { EditorAction2, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { registerAction2 } from 'vs/platform/actions/common/actions';
|
import { registerAction2 } from 'vs/platform/actions/common/actions';
|
||||||
|
@ -20,6 +19,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
||||||
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
|
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
|
||||||
import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, ViewContainerLocation } from 'vs/workbench/common/views';
|
import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, ViewContainerLocation } from 'vs/workbench/common/views';
|
||||||
import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons';
|
import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons';
|
||||||
|
import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations';
|
||||||
import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
|
import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
|
||||||
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
||||||
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
||||||
|
@ -32,7 +32,6 @@ import { ITestResultService, TestResultService } from 'vs/workbench/contrib/test
|
||||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl';
|
import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl';
|
||||||
import { IWorkspaceTestCollectionService, WorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
import { IWorkspaceTestCollectionService, WorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
|
||||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||||
import * as Action from './testExplorerActions';
|
import * as Action from './testExplorerActions';
|
||||||
|
|
||||||
|
@ -99,6 +98,7 @@ registerAction2(Action.DebugAllAction);
|
||||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
|
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
|
||||||
|
|
||||||
registerEditorContribution(Testing.OutputPeekContributionId, TestingOutputPeekController);
|
registerEditorContribution(Testing.OutputPeekContributionId, TestingOutputPeekController);
|
||||||
|
registerEditorContribution(Testing.DecorationsContributionId, TestingDecorations);
|
||||||
|
|
||||||
CommandsRegistry.registerCommand({
|
CommandsRegistry.registerCommand({
|
||||||
id: 'vscode.runTests',
|
id: 'vscode.runTests',
|
||||||
|
@ -117,28 +117,10 @@ CommandsRegistry.registerCommand({
|
||||||
});
|
});
|
||||||
|
|
||||||
CommandsRegistry.registerCommand({
|
CommandsRegistry.registerCommand({
|
||||||
id: 'vscode.revealTestMessage',
|
id: 'vscode.revealTestInExplorer',
|
||||||
handler: async (accessor: ServicesAccessor, testRef: TestIdWithProvider, messageIndex: number) => {
|
handler: async (accessor: ServicesAccessor, path: string[]) => {
|
||||||
const editorService = accessor.get(IEditorService);
|
accessor.get(ITestExplorerFilterState).reveal = path;
|
||||||
const testService = accessor.get(ITestService);
|
await new Action.ShowTestView().run(accessor);
|
||||||
|
|
||||||
const test = await testService.lookupTest(testRef);
|
|
||||||
const message = test?.item.state.messages[messageIndex];
|
|
||||||
if (!test || !message?.location) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pane = await editorService.openEditor({
|
|
||||||
resource: URI.revive(message.location.uri),
|
|
||||||
options: { selection: message.location.range }
|
|
||||||
});
|
|
||||||
|
|
||||||
const control = pane?.getControl();
|
|
||||||
if (!isCodeEditor(control)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TestingOutputPeekController.get(control).show(test, messageIndex);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
328
src/vs/workbench/contrib/testing/browser/testingDecorations.ts
Normal file
328
src/vs/workbench/contrib/testing/browser/testingDecorations.ts
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { Action, IAction, Separator } from 'vs/base/common/actions';
|
||||||
|
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||||
|
import { Disposable, dispose, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
import { URI } from 'vs/base/common/uri';
|
||||||
|
import { generateUuid } from 'vs/base/common/uuid';
|
||||||
|
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||||
|
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||||
|
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||||
|
import { IRange } from 'vs/editor/common/core/range';
|
||||||
|
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||||
|
import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||||
|
import { Location as ModeLocation } from 'vs/editor/common/modes';
|
||||||
|
import { overviewRulerError, overviewRulerInfo, overviewRulerWarning } from 'vs/editor/common/view/editorColorRegistry';
|
||||||
|
import { localize } from 'vs/nls';
|
||||||
|
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||||
|
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||||
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
|
import { IThemeService, themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||||
|
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
||||||
|
import { TestMessageSeverity, TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||||
|
import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug';
|
||||||
|
import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
|
||||||
|
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
||||||
|
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
|
||||||
|
import { IncrementalTestCollectionItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||||
|
import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||||
|
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
|
import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
|
|
||||||
|
export class TestingDecorations extends Disposable implements IEditorContribution {
|
||||||
|
private collection = this._register(new MutableDisposable<IReference<IMainThreadTestCollection>>());
|
||||||
|
private lastDecorations: ITestDecoration[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly editor: ICodeEditor,
|
||||||
|
@ITestService private readonly testService: ITestService,
|
||||||
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.attachModel(editor.getModel()?.uri);
|
||||||
|
this._register(this.editor.onDidChangeModel(e => this.attachModel(e.newModelUrl || undefined)));
|
||||||
|
this._register(this.editor.onMouseDown(e => {
|
||||||
|
for (const decoration of this.lastDecorations) {
|
||||||
|
if (decoration.click(e)) {
|
||||||
|
e.event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachModel(uri?: URI) {
|
||||||
|
if (!uri) {
|
||||||
|
this.collection.value = undefined;
|
||||||
|
this.clearDecorations();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collection.value = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri, () => this.setDecorations(uri));
|
||||||
|
this.setDecorations(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDecorations(uri: URI): void {
|
||||||
|
const ref = this.collection.value;
|
||||||
|
if (!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.changeDecorations(accessor => {
|
||||||
|
const newDecorations: ITestDecoration[] = [];
|
||||||
|
for (const test of ref.object.all) {
|
||||||
|
if (hasValidLocation(uri, test.item)) {
|
||||||
|
newDecorations.push(this.instantiationService.createInstance(
|
||||||
|
RunTestDecoration, test, ref.object, test.item.location, this.editor));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < test.item.state.messages.length; i++) {
|
||||||
|
const m = test.item.state.messages[i];
|
||||||
|
if (hasValidLocation(uri, m)) {
|
||||||
|
const uri = buildTestUri({
|
||||||
|
type: TestUriType.LiveMessage,
|
||||||
|
messageIndex: i,
|
||||||
|
providerId: test.providerId,
|
||||||
|
testId: test.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accessor
|
||||||
|
.deltaDecorations(this.lastDecorations.map(d => d.id), newDecorations.map(d => d.editorDecoration))
|
||||||
|
.forEach((id, i) => newDecorations[i].id = id);
|
||||||
|
|
||||||
|
this.lastDecorations = newDecorations;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearDecorations(): void {
|
||||||
|
this.editor.changeDecorations(accessor => {
|
||||||
|
for (const decoration of this.lastDecorations) {
|
||||||
|
accessor.removeDecoration(decoration.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastDecorations = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestDecoration extends IDisposable {
|
||||||
|
/**
|
||||||
|
* ID of the decoration after being added to the editor, set after the
|
||||||
|
* decoration is applied.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
readonly editorDecoration: IModelDeltaDecoration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a click event, returns true if it was handled.
|
||||||
|
*/
|
||||||
|
click(e: IEditorMouseEvent): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValidLocation = <T extends { location?: ModeLocation }>(editorUri: URI, t: T): t is T & { location: ModeLocation } =>
|
||||||
|
t.location?.uri.toString() === editorUri.toString();
|
||||||
|
|
||||||
|
const firstLineRange = (editor: ICodeEditor, originalRange: IRange) => {
|
||||||
|
const model = editor.getModel();
|
||||||
|
const endColumn = model?.getLineMaxColumn(originalRange.startLineNumber);
|
||||||
|
return {
|
||||||
|
startLineNumber: originalRange.startLineNumber,
|
||||||
|
endLineNumber: originalRange.startLineNumber,
|
||||||
|
startColumn: 0,
|
||||||
|
endColumn: endColumn || 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class RunTestDecoration implements ITestDecoration {
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
id = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public readonly editorDecoration: IModelDeltaDecoration;
|
||||||
|
|
||||||
|
private line: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly test: IncrementalTestCollectionItem,
|
||||||
|
private readonly collection: IMainThreadTestCollection,
|
||||||
|
private readonly location: ModeLocation,
|
||||||
|
private readonly editor: ICodeEditor,
|
||||||
|
@ITestService private readonly testService: ITestService,
|
||||||
|
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||||
|
@ICommandService private readonly commandService: ICommandService,
|
||||||
|
) {
|
||||||
|
this.line = location.range.startLineNumber;
|
||||||
|
|
||||||
|
const queue = [test.children];
|
||||||
|
let state = this.test.item.state.runState;
|
||||||
|
while (queue.length) {
|
||||||
|
for (const child of queue.pop()!) {
|
||||||
|
const node = collection.getNodeById(child);
|
||||||
|
if (node) {
|
||||||
|
state = maxPriority(node.item.state.runState, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = state !== TestRunState.Unset
|
||||||
|
? testingStatesToIcons.get(state)!
|
||||||
|
: test.children.size > 0 ? testingRunAllIcon : testingRunIcon;
|
||||||
|
|
||||||
|
this.editorDecoration = {
|
||||||
|
range: firstLineRange(this.editor, this.location.range),
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
glyphMarginClassName: ThemeIcon.asClassName(icon) + ' testing-run-glyph',
|
||||||
|
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||||
|
glyphMarginHoverMessage: new MarkdownString().appendText(localize('testing.clickToRun', 'Click to run tests, right click for more options')),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public click(e: IEditorMouseEvent): boolean {
|
||||||
|
if (e.target.position?.lineNumber !== this.line || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.event.rightButton) {
|
||||||
|
const actions = this.getContextMenu();
|
||||||
|
this.contextMenuService.showContextMenu({
|
||||||
|
getAnchor: () => ({ x: e.event.posx, y: e.event.posy }),
|
||||||
|
getActions: () => actions,
|
||||||
|
onHide: () => dispose(actions),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// todo: customize click behavior
|
||||||
|
this.testService.runTests({ tests: [{ testId: this.test.id, providerId: this.test.providerId }], debug: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContextMenu() {
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (!model) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const testActions: IAction[] = [];
|
||||||
|
if (this.test.item.runnable) {
|
||||||
|
testActions.push(new Action('testing.run', localize('run test', 'Run Test'), undefined, undefined, () => this.testService.runTests({
|
||||||
|
debug: false,
|
||||||
|
tests: [{ providerId: this.test.providerId, testId: this.test.id }],
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.test.item.debuggable) {
|
||||||
|
testActions.push(new Action('testing.debug', localize('debug test', 'Debug Test'), undefined, undefined, () => this.testService.runTests({
|
||||||
|
debug: true,
|
||||||
|
tests: [{ providerId: this.test.providerId, testId: this.test.id }],
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
testActions.push(new Action('testing.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, async () => {
|
||||||
|
const path = [];
|
||||||
|
for (let id: string | null = this.test.id; id;) {
|
||||||
|
const node = this.collection.getNodeById(id);
|
||||||
|
if (!node) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
path.unshift(node.item.label);
|
||||||
|
id = node.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.commandService.executeCommand('vscode.revealTestInExplorer', this.test);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const breakpointActions = this.editor
|
||||||
|
.getContribution<IBreakpointEditorContribution>(BREAKPOINT_EDITOR_CONTRIBUTION_ID)
|
||||||
|
.getContextMenuActionsAtPosition(this.line, model);
|
||||||
|
|
||||||
|
return breakpointActions.length ? [...testActions, new Separator(), ...breakpointActions] : testActions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestMessageDecoration implements ITestDecoration {
|
||||||
|
public id = '';
|
||||||
|
|
||||||
|
public readonly editorDecoration: IModelDeltaDecoration;
|
||||||
|
private readonly decorationId = `testmessage-${generateUuid()}`;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ message, severity }: ITestMessage,
|
||||||
|
private readonly messageUri: URI,
|
||||||
|
location: ModeLocation,
|
||||||
|
private readonly editor: ICodeEditor,
|
||||||
|
@ICodeEditorService private readonly editorService: ICodeEditorService,
|
||||||
|
@IThemeService themeService: IThemeService,
|
||||||
|
) {
|
||||||
|
severity = severity || TestMessageSeverity.Error;
|
||||||
|
|
||||||
|
const colorTheme = themeService.getColorTheme();
|
||||||
|
editorService.registerDecorationType(this.decorationId, {
|
||||||
|
after: {
|
||||||
|
contentText: message.toString(),
|
||||||
|
color: `${colorTheme.getColor(testMessageSeverityColors[severity].decorationForeground)}`,
|
||||||
|
fontSize: `${editor.getOption(EditorOption.fontSize)}px`,
|
||||||
|
fontFamily: editor.getOption(EditorOption.fontFamily),
|
||||||
|
padding: `0px 12px`,
|
||||||
|
},
|
||||||
|
}, undefined, editor);
|
||||||
|
|
||||||
|
const options = editorService.resolveDecorationOptions(this.decorationId, true);
|
||||||
|
options.hoverMessage = typeof message === 'string' ? new MarkdownString().appendText(message) : message;
|
||||||
|
options.afterContentClassName = `${options.afterContentClassName} testing-inline-message`;
|
||||||
|
options.zIndex = 10; // todo: in spite of the z-index, this appears behind gitlens
|
||||||
|
|
||||||
|
const rulerColor = severity === TestMessageSeverity.Error
|
||||||
|
? overviewRulerError
|
||||||
|
: severity === TestMessageSeverity.Warning
|
||||||
|
? overviewRulerWarning
|
||||||
|
: severity === TestMessageSeverity.Information
|
||||||
|
? overviewRulerInfo
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (rulerColor) {
|
||||||
|
options.overviewRuler = { color: themeColorFromId(rulerColor), position: OverviewRulerLane.Right };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editorDecoration = { range: firstLineRange(editor, location.range), options };
|
||||||
|
}
|
||||||
|
|
||||||
|
click(e: IEditorMouseEvent): boolean {
|
||||||
|
if (e.event.rightButton) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.target.element?.className.includes(this.decorationId)) {
|
||||||
|
TestingOutputPeekController.get(this.editor).show(this.messageUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.editorService.removeDecorationType(this.decorationId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,19 +27,34 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC
|
||||||
export interface ITestExplorerFilterState {
|
export interface ITestExplorerFilterState {
|
||||||
_serviceBrand: undefined;
|
_serviceBrand: undefined;
|
||||||
readonly onDidChange: Event<string>;
|
readonly onDidChange: Event<string>;
|
||||||
|
readonly onDidRequestReveal: Event<string[]>;
|
||||||
value: string;
|
value: string;
|
||||||
|
reveal: string[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ITestExplorerFilterState = createDecorator<ITestExplorerFilterState>('testingFilterState');
|
export const ITestExplorerFilterState = createDecorator<ITestExplorerFilterState>('testingFilterState');
|
||||||
|
|
||||||
export class TestExplorerFilterState implements ITestExplorerFilterState {
|
export class TestExplorerFilterState implements ITestExplorerFilterState {
|
||||||
declare _serviceBrand: undefined;
|
declare _serviceBrand: undefined;
|
||||||
|
private readonly revealRequest = new Emitter<string[]>();
|
||||||
private readonly changeEmitter = new Emitter<string>();
|
private readonly changeEmitter = new Emitter<string>();
|
||||||
private _value = '';
|
private _value = '';
|
||||||
|
private _reveal?: string[];
|
||||||
|
|
||||||
|
public readonly onDidRequestReveal = this.revealRequest.event;
|
||||||
public readonly onDidChange = this.changeEmitter.event;
|
public readonly onDidChange = this.changeEmitter.event;
|
||||||
|
|
||||||
|
public get reveal() {
|
||||||
|
return this._reveal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set reveal(v: string[] | undefined) {
|
||||||
|
this._reveal = v;
|
||||||
|
if (v !== undefined) {
|
||||||
|
this.revealRequest.fire(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public get value() {
|
public get value() {
|
||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse
|
||||||
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||||
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
|
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||||
|
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
import { ITestResultService, sumCounts, TestStateCount } from 'vs/workbench/contrib/testing/common/testResultService';
|
import { ITestResultService, sumCounts, TestStateCount } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||||
|
@ -388,7 +389,13 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TestingOutputPeekController.get(control).show(item.test, index);
|
TestingOutputPeekController.get(control).show(buildTestUri({
|
||||||
|
type: TestUriType.LiveMessage,
|
||||||
|
messageIndex: index,
|
||||||
|
providerId: item.test.providerId,
|
||||||
|
testId: item.test.id,
|
||||||
|
}));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -648,10 +655,6 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
|
||||||
const state = getComputedState(element);
|
const state = getComputedState(element);
|
||||||
const icon = testingStatesToIcons.get(state);
|
const icon = testingStatesToIcons.get(state);
|
||||||
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
|
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
|
||||||
if (state === TestRunState.Running) {
|
|
||||||
data.icon.className += ' codicon-modifier-spin';
|
|
||||||
}
|
|
||||||
|
|
||||||
const test = element.test;
|
const test = element.test;
|
||||||
if (test) {
|
if (test) {
|
||||||
if (test.item.location) {
|
if (test.item.location) {
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
import * as dom from 'vs/base/browser/dom';
|
import * as dom from 'vs/base/browser/dom';
|
||||||
import { Color } from 'vs/base/common/color';
|
import { Color } from 'vs/base/common/color';
|
||||||
import { DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
import { IReference, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { clamp } from 'vs/base/common/numbers';
|
import { clamp } from 'vs/base/common/numbers';
|
||||||
import { count } from 'vs/base/common/strings';
|
import { count } from 'vs/base/common/strings';
|
||||||
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||||
import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||||
import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions';
|
import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||||
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
|
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||||
import { IPeekViewService, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView';
|
import { IPeekViewService, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/peekView';
|
||||||
|
@ -22,7 +23,17 @@ import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
|
||||||
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
|
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||||
import { InternalTestItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
import { InternalTestItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
|
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||||
|
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
|
|
||||||
|
interface ITestDto {
|
||||||
|
messageIndex: number;
|
||||||
|
test: InternalTestItem;
|
||||||
|
expectedUri: URI;
|
||||||
|
actualUri: URI;
|
||||||
|
messageUri: URI;
|
||||||
|
}
|
||||||
|
|
||||||
export class TestingOutputPeekController implements IEditorContribution {
|
export class TestingOutputPeekController implements IEditorContribution {
|
||||||
/**
|
/**
|
||||||
|
@ -35,7 +46,7 @@ export class TestingOutputPeekController implements IEditorContribution {
|
||||||
/**
|
/**
|
||||||
* Currently-shown peek view.
|
* Currently-shown peek view.
|
||||||
*/
|
*/
|
||||||
private peek?: TestingOutputPeek;
|
private readonly peek = new MutableDisposable<TestingOutputPeek>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context key updated when the peek is visible/hidden.
|
* Context key updated when the peek is visible/hidden.
|
||||||
|
@ -45,6 +56,8 @@ export class TestingOutputPeekController implements IEditorContribution {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly editor: ICodeEditor,
|
private readonly editor: ICodeEditor,
|
||||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||||
|
@ITestResultService private readonly testResults: ITestResultService,
|
||||||
|
@ITestService private readonly testService: ITestService,
|
||||||
@IContextKeyService contextKeyService: IContextKeyService,
|
@IContextKeyService contextKeyService: IContextKeyService,
|
||||||
) {
|
) {
|
||||||
this.visible = TestingContextKeys.peekVisible.bindTo(contextKeyService);
|
this.visible = TestingContextKeys.peekVisible.bindTo(contextKeyService);
|
||||||
|
@ -60,54 +73,92 @@ export class TestingOutputPeekController implements IEditorContribution {
|
||||||
/**
|
/**
|
||||||
* Shows a peek for the message in th editor.
|
* Shows a peek for the message in th editor.
|
||||||
*/
|
*/
|
||||||
public async show(test: InternalTestItem, messageIndex: number) {
|
public async show(uri: URI) {
|
||||||
const message = test?.item.state.messages[messageIndex];
|
const dto = await this.retrieveTest(uri);
|
||||||
if (!test || !message?.location) {
|
if (!dto) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.peek) {
|
const message = dto.test.item.state.messages[dto.messageIndex];
|
||||||
this.peek = this.instantiationService.createInstance(TestingOutputPeek, this.editor);
|
if (!message?.location) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.visible.set(true);
|
const ctor = message.actualOutput !== undefined && message.expectedOutput !== undefined
|
||||||
this.peek.setModel(test, messageIndex);
|
? TestingDiffOutputPeek : TestingMessageOutputPeek;
|
||||||
this.peek.onDidClose(() => {
|
const isNew = !(this.peek.value instanceof ctor);
|
||||||
this.visible.set(false);
|
if (isNew) {
|
||||||
this.peek = undefined;
|
this.peek.value = this.instantiationService.createInstance(ctor, this.editor);
|
||||||
});
|
this.peek.value.onDidClose(() => {
|
||||||
|
this.visible.set(false);
|
||||||
|
this.peek.value = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
this.visible.set(true);
|
||||||
|
this.peek.value!.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.peek.value!.setModel(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes the peek view, if any.
|
* Disposes the peek view, if any.
|
||||||
*/
|
*/
|
||||||
public removePeek() {
|
public removePeek() {
|
||||||
if (this.peek) {
|
this.peek.value = undefined;
|
||||||
this.peek.dispose();
|
}
|
||||||
this.peek = undefined;
|
|
||||||
|
private async retrieveTest(uri: URI): Promise<ITestDto | undefined> {
|
||||||
|
const parts = parseTestUri(uri);
|
||||||
|
if (!parts) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('resultId' in parts) {
|
||||||
|
const test = this.testResults.lookup(parts.resultId)?.tests.find(t => t.id === parts.testId);
|
||||||
|
return test && {
|
||||||
|
test,
|
||||||
|
messageIndex: parts.messageIndex,
|
||||||
|
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
|
||||||
|
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
|
||||||
|
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = await this.testService.lookupTest({ providerId: parts.providerId, testId: parts.testId });
|
||||||
|
if (!test) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
test,
|
||||||
|
messageIndex: parts.messageIndex,
|
||||||
|
expectedUri: buildTestUri({ ...parts, type: TestUriType.LiveActualOutput }),
|
||||||
|
actualUri: buildTestUri({ ...parts, type: TestUriType.LiveExpectedOutput }),
|
||||||
|
messageUri: buildTestUri({ ...parts, type: TestUriType.LiveMessage }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TestingOutputPeek extends PeekViewWidget {
|
abstract class TestingOutputPeek extends PeekViewWidget {
|
||||||
private readonly disposable = new DisposableStore();
|
protected model = new MutableDisposable();
|
||||||
private diff?: EmbeddedDiffEditorWidget;
|
protected dimension?: dom.Dimension;
|
||||||
private model?: IDisposable;
|
|
||||||
private dimension?: dom.Dimension;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
editor: ICodeEditor,
|
editor: ICodeEditor,
|
||||||
@IThemeService themeService: IThemeService,
|
@IThemeService themeService: IThemeService,
|
||||||
@IPeekViewService peekViewService: IPeekViewService,
|
@IPeekViewService peekViewService: IPeekViewService,
|
||||||
@IInstantiationService protected readonly instantiationService: IInstantiationService,
|
@IInstantiationService protected readonly instantiationService: IInstantiationService,
|
||||||
@ITextModelService private readonly modelService: ITextModelService,
|
@ITextModelService protected readonly modelService: ITextModelService,
|
||||||
) {
|
) {
|
||||||
super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService);
|
super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService);
|
||||||
|
|
||||||
this._disposables.add(themeService.onDidColorThemeChange(this.applyTheme, this));
|
this._disposables.add(themeService.onDidColorThemeChange(this.applyTheme, this));
|
||||||
|
this._disposables.add(this.model);
|
||||||
this.applyTheme(themeService.getColorTheme());
|
this.applyTheme(themeService.getColorTheme());
|
||||||
peekViewService.addExclusiveWidget(editor, this);
|
peekViewService.addExclusiveWidget(editor, this);
|
||||||
this.create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyTheme(theme: IColorTheme) {
|
private applyTheme(theme: IColorTheme) {
|
||||||
|
@ -121,67 +172,83 @@ export class TestingOutputPeek extends PeekViewWidget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the test to be shown.
|
||||||
|
*/
|
||||||
|
public abstract setModel(dto: ITestDto): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public dispose() {
|
protected _doLayoutBody(height: number, width: number) {
|
||||||
super.dispose();
|
super._doLayoutBody(height, width);
|
||||||
this.model?.dispose();
|
this.dimension = new dom.Dimension(width, height);
|
||||||
this.disposable.dispose();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonEditorOptions: IEditorOptions = {
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
scrollbar: {
|
||||||
|
verticalScrollbarSize: 14,
|
||||||
|
horizontal: 'auto',
|
||||||
|
useShadows: true,
|
||||||
|
verticalHasArrows: false,
|
||||||
|
horizontalHasArrows: false,
|
||||||
|
alwaysConsumeMouseWheel: false
|
||||||
|
},
|
||||||
|
fixedOverflowWidgets: true,
|
||||||
|
readOnly: true,
|
||||||
|
minimap: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const diffEditorOptions: IDiffEditorOptions = {
|
||||||
|
...commonEditorOptions,
|
||||||
|
enableSplitViewResizing: true,
|
||||||
|
isInEmbeddedEditor: true,
|
||||||
|
renderOverviewRuler: false,
|
||||||
|
ignoreTrimWhitespace: false,
|
||||||
|
renderSideBySide: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestingDiffOutputPeek extends TestingOutputPeek {
|
||||||
|
private readonly diff = this._disposables.add(new MutableDisposable<EmbeddedDiffEditorWidget>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
protected _fillBody(containerElement: HTMLElement): void {
|
protected _fillBody(containerElement: HTMLElement): void {
|
||||||
const diffContainer = dom.append(containerElement, dom.$('div.preview.inline'));
|
const diffContainer = dom.append(containerElement, dom.$('div.preview.inline'));
|
||||||
let options: IDiffEditorOptions = {
|
const preview = this.diff.value = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, diffContainer, diffEditorOptions, this.editor);
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
scrollbar: {
|
|
||||||
verticalScrollbarSize: 14,
|
|
||||||
horizontal: 'auto',
|
|
||||||
useShadows: true,
|
|
||||||
verticalHasArrows: false,
|
|
||||||
horizontalHasArrows: false,
|
|
||||||
alwaysConsumeMouseWheel: false
|
|
||||||
},
|
|
||||||
fixedOverflowWidgets: true,
|
|
||||||
readOnly: true,
|
|
||||||
minimap: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
enableSplitViewResizing: true,
|
|
||||||
isInEmbeddedEditor: true,
|
|
||||||
renderOverviewRuler: false,
|
|
||||||
ignoreTrimWhitespace: false,
|
|
||||||
renderSideBySide: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const preview = this.diff = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, diffContainer, options, this.editor);
|
|
||||||
this.disposable.add(preview);
|
|
||||||
|
|
||||||
if (this.dimension) {
|
if (this.dimension) {
|
||||||
preview.layout(this.dimension);
|
preview.layout(this.dimension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setModel(test: InternalTestItem, messageIndex: number) {
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public async setModel({ test, messageIndex, expectedUri, actualUri }: ITestDto) {
|
||||||
const message = test.item.state.messages[messageIndex];
|
const message = test.item.state.messages[messageIndex];
|
||||||
if (!message?.location) {
|
if (!message?.location) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.show(message.location.range, hintPeekHeight(message));
|
this.show(message.location.range, hintDiffPeekHeight(message));
|
||||||
|
|
||||||
if (this.model) {
|
|
||||||
this.model.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setTitle(message.message.toString(), test.item.label);
|
this.setTitle(message.message.toString(), test.item.label);
|
||||||
if (message.actualOutput !== undefined && message.expectedOutput !== undefined) {
|
|
||||||
await this.showDiffInEditor(test, messageIndex);
|
const [original, modified] = await Promise.all([
|
||||||
|
this.modelService.createModelReference(expectedUri),
|
||||||
|
this.modelService.createModelReference(actualUri),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const model = this.model.value = new SimpleDiffEditorModel(original, modified);
|
||||||
|
if (!this.diff.value) {
|
||||||
|
this.model.value = undefined;
|
||||||
} else {
|
} else {
|
||||||
await this.showMessageInEditor(test, messageIndex);
|
this.diff.value.setModel(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,37 +257,58 @@ export class TestingOutputPeek extends PeekViewWidget {
|
||||||
*/
|
*/
|
||||||
protected _doLayoutBody(height: number, width: number) {
|
protected _doLayoutBody(height: number, width: number) {
|
||||||
super._doLayoutBody(height, width);
|
super._doLayoutBody(height, width);
|
||||||
this.dimension = new dom.Dimension(width, height);
|
this.diff.value?.layout(this.dimension);
|
||||||
this.diff?.layout(this.dimension);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async showMessageInEditor(test: InternalTestItem, messageIndex: number) {
|
|
||||||
// todo? not sure if this is a useful experience
|
|
||||||
this.model?.dispose();
|
|
||||||
this.diff?.setModel(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async showDiffInEditor(test: InternalTestItem, messageIndex: number) {
|
|
||||||
const uriParts = { messageIndex, testId: test.id, providerId: test.providerId };
|
|
||||||
const [original, modified] = await Promise.all([
|
|
||||||
this.modelService.createModelReference(buildTestUri({ ...uriParts, type: TestUriType.ExpectedOutput })),
|
|
||||||
this.modelService.createModelReference(buildTestUri({ ...uriParts, type: TestUriType.ActualOutput })),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.model?.dispose();
|
|
||||||
const model = this.model = new SimpleDiffEditorModel(original, modified);
|
|
||||||
if (!this.diff) {
|
|
||||||
model.dispose();
|
|
||||||
} else {
|
|
||||||
this.diff.setModel(model);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hintPeekHeight = (message: ITestMessage) => {
|
class TestingMessageOutputPeek extends TestingOutputPeek {
|
||||||
const lines = Math.max(count(message.actualOutput || '', '\n'), count(message.expectedOutput || '', '\n'));
|
private readonly preview = this._disposables.add(new MutableDisposable<EmbeddedCodeEditorWidget>());
|
||||||
return clamp(lines, 5, 20);
|
|
||||||
};
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
protected _fillBody(containerElement: HTMLElement): void {
|
||||||
|
const diffContainer = dom.append(containerElement, dom.$('div.preview.inline'));
|
||||||
|
const preview = this.preview.value = this.instantiationService.createInstance(EmbeddedCodeEditorWidget, diffContainer, commonEditorOptions, this.editor);
|
||||||
|
|
||||||
|
if (this.dimension) {
|
||||||
|
preview.layout(this.dimension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
public async setModel({ test, messageIndex, messageUri }: ITestDto) {
|
||||||
|
const message = test.item.state.messages[messageIndex];
|
||||||
|
if (!message?.location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.show(message.location.range, hintPeekStrHeight(message.message.toString()));
|
||||||
|
this.setTitle(message.message.toString(), test.item.label);
|
||||||
|
|
||||||
|
const modelRef = this.model.value = await this.modelService.createModelReference(messageUri);
|
||||||
|
if (this.preview.value) {
|
||||||
|
this.preview.value.setModel(modelRef.object.textEditorModel);
|
||||||
|
} else {
|
||||||
|
this.model.value = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
protected _doLayoutBody(height: number, width: number) {
|
||||||
|
super._doLayoutBody(height, width);
|
||||||
|
this.preview.value?.layout(this.dimension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hintDiffPeekHeight = (message: ITestMessage) =>
|
||||||
|
Math.max(hintPeekStrHeight(message.actualOutput), hintPeekStrHeight(message.expectedOutput));
|
||||||
|
|
||||||
|
const hintPeekStrHeight = (str: string | undefined) => clamp(count(str || '', '\n'), 5, 20);
|
||||||
|
|
||||||
class SimpleDiffEditorModel extends EditorModel {
|
class SimpleDiffEditorModel extends EditorModel {
|
||||||
public readonly original = this._original.object.textEditorModel;
|
public readonly original = this._original.object.textEditorModel;
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { editorErrorForeground, registerColor } from 'vs/platform/theme/common/colorRegistry';
|
import { editorErrorForeground, editorForeground, editorHintForeground, editorInfoForeground, editorWarningForeground, registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
import { TestMessageSeverity, TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||||
|
|
||||||
export const testingColorIconFailed = registerColor('testing.iconFailed', {
|
export const testingColorIconFailed = registerColor('testing.iconFailed', {
|
||||||
dark: '#f14c4c',
|
dark: '#f14c4c',
|
||||||
|
@ -19,13 +19,18 @@ export const testingColorIconErrored = registerColor('testing.iconErrored', {
|
||||||
hc: '#000000'
|
hc: '#000000'
|
||||||
}, localize('testing.iconErrored', "Color for the 'Errored' icon in the test explorer."));
|
}, localize('testing.iconErrored', "Color for the 'Errored' icon in the test explorer."));
|
||||||
|
|
||||||
|
|
||||||
export const testingColorIconPassed = registerColor('testing.iconPassed', {
|
export const testingColorIconPassed = registerColor('testing.iconPassed', {
|
||||||
dark: '#73c991',
|
dark: '#73c991',
|
||||||
light: '#73c991',
|
light: '#73c991',
|
||||||
hc: '#000000'
|
hc: '#000000'
|
||||||
}, localize('testing.iconPassed', "Color for the 'passed' icon in the test explorer."));
|
}, localize('testing.iconPassed', "Color for the 'passed' icon in the test explorer."));
|
||||||
|
|
||||||
|
export const testingColorRunAction = registerColor('testing.runAction', {
|
||||||
|
dark: testingColorIconPassed,
|
||||||
|
light: testingColorIconPassed,
|
||||||
|
hc: testingColorIconPassed
|
||||||
|
}, localize('testing.runAction', "Color for 'run' icons in the editor."));
|
||||||
|
|
||||||
export const testingColorIconQueued = registerColor('testing.iconQueued', {
|
export const testingColorIconQueued = registerColor('testing.iconQueued', {
|
||||||
dark: '#cca700',
|
dark: '#cca700',
|
||||||
light: '#cca700',
|
light: '#cca700',
|
||||||
|
@ -50,6 +55,41 @@ export const testingPeekBorder = registerColor('testing.peekBorder', {
|
||||||
hc: editorErrorForeground,
|
hc: editorErrorForeground,
|
||||||
}, localize('testing.peekBorder', 'Color of the peek view borders and arrow.'));
|
}, localize('testing.peekBorder', 'Color of the peek view borders and arrow.'));
|
||||||
|
|
||||||
|
export const testMessageSeverityColors: {
|
||||||
|
[K in TestMessageSeverity]: {
|
||||||
|
decorationForeground: string,
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
[TestMessageSeverity.Error]: {
|
||||||
|
decorationForeground: registerColor(
|
||||||
|
'testing.message.error.decorationForeground',
|
||||||
|
{ dark: editorErrorForeground, light: editorErrorForeground, hc: editorForeground },
|
||||||
|
localize('testing.message.error.decorationForeground', 'Text color of test error messages shown inline in the editor.')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[TestMessageSeverity.Warning]: {
|
||||||
|
decorationForeground: registerColor(
|
||||||
|
'testing.message.warning.decorationForeground',
|
||||||
|
{ dark: editorWarningForeground, light: editorWarningForeground, hc: editorForeground },
|
||||||
|
localize('testing.message.warning.decorationForeground', 'Text color of test warning messages shown inline in the editor.')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[TestMessageSeverity.Information]: {
|
||||||
|
decorationForeground: registerColor(
|
||||||
|
'testing.message.info.decorationForeground',
|
||||||
|
{ dark: editorInfoForeground, light: editorInfoForeground, hc: editorForeground },
|
||||||
|
localize('testing.message.info.decorationForeground', 'Text color of test info messages shown inline in the editor.')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[TestMessageSeverity.Hint]: {
|
||||||
|
decorationForeground: registerColor(
|
||||||
|
'testing.message.hint.decorationForeground',
|
||||||
|
{ dark: editorHintForeground, light: editorHintForeground, hc: editorForeground },
|
||||||
|
localize('testing.message.hint.decorationForeground', 'Text color of test hint messages shown inline in the editor.')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const testStatesToIconColors: { [K in TestRunState]?: string } = {
|
export const testStatesToIconColors: { [K in TestRunState]?: string } = {
|
||||||
[TestRunState.Errored]: testingColorIconErrored,
|
[TestRunState.Errored]: testingColorIconErrored,
|
||||||
[TestRunState.Failed]: testingColorIconFailed,
|
[TestRunState.Failed]: testingColorIconFailed,
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const enum Testing {
|
||||||
ViewletId = 'workbench.view.extension.test',
|
ViewletId = 'workbench.view.extension.test',
|
||||||
ExplorerViewId = 'workbench.view.testing',
|
ExplorerViewId = 'workbench.view.testing',
|
||||||
OutputPeekContributionId = 'editor.contrib.testingOutputPeek',
|
OutputPeekContributionId = 'editor.contrib.testingOutputPeek',
|
||||||
|
DecorationsContributionId = 'editor.contrib.testingDecorations',
|
||||||
FilterActionId = 'workbench.actions.treeView.testExplorer.filter',
|
FilterActionId = 'workbench.actions.treeView.testExplorer.filter',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { Emitter, Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
import { generateUuid } from 'vs/base/common/uuid';
|
||||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||||
|
@ -88,6 +89,11 @@ export class TestResult {
|
||||||
public onChange = this.changeEmitter.event;
|
public onChange = this.changeEmitter.event;
|
||||||
public onComplete = this.completeEmitter.event;
|
public onComplete = this.completeEmitter.event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique ID for referring to this set of test results.
|
||||||
|
*/
|
||||||
|
public readonly id = generateUuid();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets whether the test run has finished.
|
* Gets whether the test run has finished.
|
||||||
*/
|
*/
|
||||||
|
@ -175,11 +181,16 @@ export interface ITestResultService {
|
||||||
* Adds a new test result to the collection.
|
* Adds a new test result to the collection.
|
||||||
*/
|
*/
|
||||||
push(result: TestResult): TestResult;
|
push(result: TestResult): TestResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a set of test results by ID.
|
||||||
|
*/
|
||||||
|
lookup(resultId: string): TestResult | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ITestResultService = createDecorator<ITestResultService>('testResultService');
|
export const ITestResultService = createDecorator<ITestResultService>('testResultService');
|
||||||
|
|
||||||
const RETAIN_LAST_RESULTS = 10;
|
const RETAIN_LAST_RESULTS = 16;
|
||||||
|
|
||||||
export class TestResultService implements ITestResultService {
|
export class TestResultService implements ITestResultService {
|
||||||
declare _serviceBrand: undefined;
|
declare _serviceBrand: undefined;
|
||||||
|
@ -216,6 +227,13 @@ export class TestResultService implements ITestResultService {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public lookup(id: string) {
|
||||||
|
return this.results.find(r => r.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
private onComplete() {
|
private onComplete() {
|
||||||
// move the complete test run down behind any still-running ones
|
// move the complete test run down behind any still-running ones
|
||||||
for (let i = 0; i < this.results.length - 2; i++) {
|
for (let i = 0; i < this.results.length - 2; i++) {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||||
import { Event } from 'vs/base/common/event';
|
import { Event } from 'vs/base/common/event';
|
||||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
||||||
|
@ -39,6 +39,11 @@ export interface IMainThreadTestCollection extends AbstractIncrementalTestCollec
|
||||||
*/
|
*/
|
||||||
rootIds: ReadonlySet<string>;
|
rootIds: ReadonlySet<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over every test in the collection.
|
||||||
|
*/
|
||||||
|
all: Iterable<IncrementalTestCollectionItem>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a node in the collection by ID.
|
* Gets a node in the collection by ID.
|
||||||
*/
|
*/
|
||||||
|
@ -82,10 +87,7 @@ export interface ITestService {
|
||||||
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<RunTestsResult>;
|
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<RunTestsResult>;
|
||||||
cancelTestRun(req: RunTestsRequest): void;
|
cancelTestRun(req: RunTestsRequest): void;
|
||||||
publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void;
|
publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void;
|
||||||
subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): {
|
subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference<IMainThreadTestCollection>;
|
||||||
collection: IMainThreadTestCollection;
|
|
||||||
dispose(): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the number of sources who provide test roots when subscription
|
* Updates the number of sources who provide test roots when subscription
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { groupBy } from 'vs/base/common/arrays';
|
||||||
import { disposableTimeout } from 'vs/base/common/async';
|
import { disposableTimeout } from 'vs/base/common/async';
|
||||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||||
import { Emitter } from 'vs/base/common/event';
|
import { Emitter } from 'vs/base/common/event';
|
||||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
import { Disposable, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||||
import { isDefined } from 'vs/base/common/types';
|
import { isDefined } from 'vs/base/common/types';
|
||||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
|
@ -131,7 +131,7 @@ export class TestService extends Disposable implements ITestService {
|
||||||
const subscriptions = [...this.testSubscriptions.values()]
|
const subscriptions = [...this.testSubscriptions.values()]
|
||||||
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
|
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
|
||||||
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri, () => result?.notifyChanged()));
|
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri, () => result?.notifyChanged()));
|
||||||
result = this.testResults.push(TestResult.from(subscriptions.map(s => s.collection), req.tests));
|
result = this.testResults.push(TestResult.from(subscriptions.map(s => s.object), req.tests));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1);
|
const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1);
|
||||||
|
@ -175,7 +175,7 @@ export class TestService extends Disposable implements ITestService {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener) {
|
public subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference<IMainThreadTestCollection> {
|
||||||
const subscriptionKey = getTestSubscriptionKey(resource, uri);
|
const subscriptionKey = getTestSubscriptionKey(resource, uri);
|
||||||
let subscription = this.testSubscriptions.get(subscriptionKey);
|
let subscription = this.testSubscriptions.get(subscriptionKey);
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
|
@ -200,7 +200,7 @@ export class TestService extends Disposable implements ITestService {
|
||||||
|
|
||||||
const listener = acceptDiff && subscription.onDiff.event(acceptDiff);
|
const listener = acceptDiff && subscription.onDiff.event(acceptDiff);
|
||||||
return {
|
return {
|
||||||
collection: subscription.collection,
|
object: subscription.collection,
|
||||||
dispose: () => {
|
dispose: () => {
|
||||||
listener?.dispose();
|
listener?.dispose();
|
||||||
|
|
||||||
|
@ -277,6 +277,14 @@ class MainThreadTestCollection extends AbstractIncrementalTestCollection<Increme
|
||||||
return this.roots;
|
return this.roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public get all() {
|
||||||
|
return this.getIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public readonly onPendingRootProvidersChange = this.pendingRootChangeEmitter.event;
|
public readonly onPendingRootProvidersChange = this.pendingRootChangeEmitter.event;
|
||||||
public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event;
|
public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event;
|
||||||
|
|
||||||
|
@ -350,4 +358,15 @@ class MainThreadTestCollection extends AbstractIncrementalTestCollection<Increme
|
||||||
protected createItem(internal: InternalTestItem): IncrementalTestCollectionItem {
|
protected createItem(internal: InternalTestItem): IncrementalTestCollectionItem {
|
||||||
return { ...internal, children: new Set() };
|
return { ...internal, children: new Set() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private *getIterator() {
|
||||||
|
const queue = [this.rootIds];
|
||||||
|
while (queue.length) {
|
||||||
|
for (const id of queue.pop()!) {
|
||||||
|
const node = this.getNodeById(id)!;
|
||||||
|
yield node;
|
||||||
|
queue.push(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
|
||||||
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
|
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||||
import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri';
|
import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
|
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,6 +21,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
|
||||||
@ITextModelService textModelResolverService: ITextModelService,
|
@ITextModelService textModelResolverService: ITextModelService,
|
||||||
@IModelService private readonly modelService: IModelService,
|
@IModelService private readonly modelService: IModelService,
|
||||||
@ITestService private readonly testService: ITestService,
|
@ITestService private readonly testService: ITestService,
|
||||||
|
@ITestService private readonly resultService: ITestResultService,
|
||||||
) {
|
) {
|
||||||
textModelResolverService.registerTextModelContentProvider(TEST_DATA_SCHEME, this);
|
textModelResolverService.registerTextModelContentProvider(TEST_DATA_SCHEME, this);
|
||||||
}
|
}
|
||||||
|
@ -38,20 +40,26 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const test = await this.testService.lookupTest({ providerId: parsed.providerId, testId: parsed.testId });
|
const test = 'providerId' in parsed
|
||||||
|
? await this.testService.lookupTest({ providerId: parsed.providerId, testId: parsed.testId })
|
||||||
|
: this.resultService.lookup(parsed.resultId)?.tests.find(t => t.id === parsed.testId);
|
||||||
|
|
||||||
if (!test) {
|
if (!test) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let text: string | undefined;
|
let text: string | undefined;
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case TestUriType.ActualOutput:
|
case TestUriType.ResultActualOutput:
|
||||||
|
case TestUriType.LiveActualOutput:
|
||||||
text = test.item.state.messages[parsed.messageIndex]?.actualOutput;
|
text = test.item.state.messages[parsed.messageIndex]?.actualOutput;
|
||||||
break;
|
break;
|
||||||
case TestUriType.ExpectedOutput:
|
case TestUriType.ResultExpectedOutput:
|
||||||
|
case TestUriType.LiveExpectedOutput:
|
||||||
text = test.item.state.messages[parsed.messageIndex]?.expectedOutput;
|
text = test.item.state.messages[parsed.messageIndex]?.expectedOutput;
|
||||||
break;
|
break;
|
||||||
case TestUriType.Message:
|
case TestUriType.ResultMessage:
|
||||||
|
case TestUriType.LiveMessage:
|
||||||
text = test.item.state.messages[parsed.messageIndex]?.message.toString();
|
text = test.item.state.messages[parsed.messageIndex]?.message.toString();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ export const statePriority: { [K in TestRunState]: number } = {
|
||||||
[TestRunState.Unset]: 0,
|
[TestRunState.Unset]: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const isFailedState = (s: TestRunState) => s === TestRunState.Errored || s === TestRunState.Failed;
|
export const isFailedState = (s: TestRunState) => s === TestRunState.Errored || s === TestRunState.Failed;
|
||||||
|
|
||||||
export const stateNodes = Object.entries(statePriority).reduce(
|
export const stateNodes = Object.entries(statePriority).reduce(
|
||||||
|
|
|
@ -8,29 +8,54 @@ import { URI } from 'vs/base/common/uri';
|
||||||
export const TEST_DATA_SCHEME = 'vscode-test-data';
|
export const TEST_DATA_SCHEME = 'vscode-test-data';
|
||||||
|
|
||||||
export const enum TestUriType {
|
export const enum TestUriType {
|
||||||
Message,
|
LiveMessage,
|
||||||
ActualOutput,
|
LiveActualOutput,
|
||||||
ExpectedOutput,
|
LiveExpectedOutput,
|
||||||
|
ResultMessage,
|
||||||
|
ResultActualOutput,
|
||||||
|
ResultExpectedOutput,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGenericTestUri {
|
interface ILiveTestUri {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
testId: string;
|
testId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITestMessageReference extends IGenericTestUri {
|
interface ILiveTestMessageReference extends ILiveTestUri {
|
||||||
type: TestUriType.Message;
|
type: TestUriType.LiveMessage;
|
||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITestOutputReference extends IGenericTestUri {
|
interface ILiveTestOutputReference extends ILiveTestUri {
|
||||||
type: TestUriType.ActualOutput | TestUriType.ExpectedOutput;
|
type: TestUriType.LiveActualOutput | TestUriType.LiveExpectedOutput;
|
||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParsedTestUri = ITestMessageReference | ITestOutputReference;
|
interface IResultTestUri {
|
||||||
|
resultId: string;
|
||||||
|
testId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IResultTestMessageReference extends IResultTestUri {
|
||||||
|
type: TestUriType.ResultMessage;
|
||||||
|
messageIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IResultTestOutputReference extends IResultTestUri {
|
||||||
|
type: TestUriType.ResultActualOutput | TestUriType.ResultExpectedOutput;
|
||||||
|
messageIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParsedTestUri =
|
||||||
|
| IResultTestMessageReference
|
||||||
|
| IResultTestOutputReference
|
||||||
|
| ILiveTestMessageReference
|
||||||
|
| ILiveTestOutputReference;
|
||||||
|
|
||||||
const enum TestUriParts {
|
const enum TestUriParts {
|
||||||
|
Results = 'results',
|
||||||
|
Live = 'live',
|
||||||
|
|
||||||
Messages = 'message',
|
Messages = 'message',
|
||||||
Text = 'text',
|
Text = 'text',
|
||||||
ActualOutput = 'actualOutput',
|
ActualOutput = 'actualOutput',
|
||||||
|
@ -38,21 +63,30 @@ const enum TestUriParts {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
|
export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
|
||||||
const providerId = uri.authority;
|
const type = uri.authority;
|
||||||
const [testId, ...request] = uri.path.slice(1).split('/');
|
const [locationId, testId, ...request] = uri.path.slice(1).split('/');
|
||||||
|
|
||||||
if (request[0] === TestUriParts.Messages) {
|
if (request[0] === TestUriParts.Messages) {
|
||||||
const index = Number(request[1]);
|
const index = Number(request[1]);
|
||||||
const part = request[2];
|
const part = request[2];
|
||||||
switch (part) {
|
if (type === TestUriParts.Results) {
|
||||||
case TestUriParts.Text:
|
switch (part) {
|
||||||
return { providerId, testId, messageIndex: index, type: TestUriType.Message };
|
case TestUriParts.Text:
|
||||||
case TestUriParts.ActualOutput:
|
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultMessage };
|
||||||
return { providerId, testId, messageIndex: index, type: TestUriType.ActualOutput };
|
case TestUriParts.ActualOutput:
|
||||||
case TestUriParts.ExpectedOutput:
|
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultActualOutput };
|
||||||
return { providerId, testId, messageIndex: index, type: TestUriType.ExpectedOutput };
|
case TestUriParts.ExpectedOutput:
|
||||||
default:
|
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultExpectedOutput };
|
||||||
return undefined;
|
}
|
||||||
|
} else if (type === TestUriParts.Live) {
|
||||||
|
switch (part) {
|
||||||
|
case TestUriParts.Text:
|
||||||
|
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveMessage };
|
||||||
|
case TestUriParts.ActualOutput:
|
||||||
|
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveActualOutput };
|
||||||
|
case TestUriParts.ExpectedOutput:
|
||||||
|
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveExpectedOutput };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,17 +94,29 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildTestUri = (parsed: ParsedTestUri): URI => {
|
export const buildTestUri = (parsed: ParsedTestUri): URI => {
|
||||||
const uriParts = { scheme: TEST_DATA_SCHEME, authority: parsed.testId };
|
const uriParts = {
|
||||||
const msgRef = (index: number, ...remaining: string[]) =>
|
scheme: TEST_DATA_SCHEME,
|
||||||
URI.from({ ...uriParts, path: ['', parsed.testId, TestUriParts.Messages, index, ...remaining].join('/') });
|
authority: 'resultId' in parsed ? TestUriParts.Results : TestUriParts.Live
|
||||||
|
};
|
||||||
|
const msgRef = (locationId: string, index: number, ...remaining: string[]) =>
|
||||||
|
URI.from({
|
||||||
|
...uriParts,
|
||||||
|
path: ['', locationId, parsed.testId, TestUriParts.Messages, index, ...remaining].join('/'),
|
||||||
|
});
|
||||||
|
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case TestUriType.ActualOutput:
|
case TestUriType.ResultActualOutput:
|
||||||
return msgRef(parsed.messageIndex, TestUriParts.ActualOutput);
|
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ActualOutput);
|
||||||
case TestUriType.ExpectedOutput:
|
case TestUriType.ResultExpectedOutput:
|
||||||
return msgRef(parsed.messageIndex, TestUriParts.ExpectedOutput);
|
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput);
|
||||||
case TestUriType.Message:
|
case TestUriType.ResultMessage:
|
||||||
return msgRef(parsed.messageIndex, TestUriParts.Text);
|
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text);
|
||||||
|
case TestUriType.LiveActualOutput:
|
||||||
|
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ActualOutput);
|
||||||
|
case TestUriType.LiveExpectedOutput:
|
||||||
|
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ExpectedOutput);
|
||||||
|
case TestUriType.LiveMessage:
|
||||||
|
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.Text);
|
||||||
default:
|
default:
|
||||||
throw new Error('Invalid test uri');
|
throw new Error('Invalid test uri');
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,8 +195,8 @@ class TestSubscription extends Disposable {
|
||||||
const folderNode: ITestSubscriptionFolder = {
|
const folderNode: ITestSubscriptionFolder = {
|
||||||
folder,
|
folder,
|
||||||
getChildren: function* () {
|
getChildren: function* () {
|
||||||
for (const rootId of listener.collection.rootIds) {
|
for (const rootId of ref.object.rootIds) {
|
||||||
const node = listener.collection.getNodeById(rootId);
|
const node = ref.object.getNodeById(rootId);
|
||||||
if (node) {
|
if (node) {
|
||||||
yield node;
|
yield node;
|
||||||
}
|
}
|
||||||
|
@ -204,7 +204,7 @@ class TestSubscription extends Disposable {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const listener = this.testService.subscribeToDiffs(
|
const ref = this.testService.subscribeToDiffs(
|
||||||
ExtHostTestingResource.Workspace,
|
ExtHostTestingResource.Workspace,
|
||||||
folder.uri,
|
folder.uri,
|
||||||
diff => {
|
diff => {
|
||||||
|
@ -215,16 +215,15 @@ class TestSubscription extends Disposable {
|
||||||
);
|
);
|
||||||
|
|
||||||
const disposable = new DisposableStore();
|
const disposable = new DisposableStore();
|
||||||
disposable.add(listener);
|
disposable.add(ref);
|
||||||
disposable.add(listener.collection.onBusyProvidersChange(
|
disposable.add(ref.object.onBusyProvidersChange(
|
||||||
() => this.pendingRootChangeEmitter.fire(this.pendingRootProviders)));
|
() => this.pendingRootChangeEmitter.fire(this.pendingRootProviders)));
|
||||||
disposable.add(listener.collection.onBusyProvidersChange(
|
disposable.add(ref.object.onBusyProvidersChange(
|
||||||
() => this.busyProvidersChangeEmitter.fire(this.busyProviders)));
|
() => this.busyProvidersChangeEmitter.fire(this.busyProviders)));
|
||||||
|
|
||||||
|
|
||||||
this.collectionsForWorkspaces.set(folder.uri.toString(), {
|
this.collectionsForWorkspaces.set(folder.uri.toString(), {
|
||||||
listener: disposable,
|
listener: disposable,
|
||||||
collection: listener.collection,
|
collection: ref.object,
|
||||||
folder: folderNode,
|
folder: folderNode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
|
|
||||||
|
suite('Workbench - Testing URIs', () => {
|
||||||
|
test('round trip', () => {
|
||||||
|
const uris: ParsedTestUri[] = [
|
||||||
|
{ type: TestUriType.LiveActualOutput, messageIndex: 42, providerId: 'p', testId: 't' },
|
||||||
|
{ type: TestUriType.LiveExpectedOutput, messageIndex: 42, providerId: 'p', testId: 't' },
|
||||||
|
{ type: TestUriType.LiveMessage, messageIndex: 42, providerId: 'p', testId: 't' },
|
||||||
|
{ type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||||
|
{ type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||||
|
{ type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const uri of uris) {
|
||||||
|
const serialized = buildTestUri(uri);
|
||||||
|
assert.deepStrictEqual(uri, parseTestUri(serialized));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue