// ***************************************************************************** // Copyright (C) 2023 STMicroelectronics and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { BaseWidget, LabelProvider, Message, OpenerService, codicon } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { TestOutputUIModel } from './test-output-ui-model'; import { Disposable, DisposableCollection, nls } from '@theia/core'; import { TestFailure, TestMessage, TestMessageStackFrame } from '../test-service'; import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; import { URI } from '@theia/core/lib/common/uri'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/navigation-location-service'; import { NavigationLocation, Position } from '@theia/editor/lib/browser/navigation/navigation-location'; @injectable() export class TestResultWidget extends BaseWidget { static readonly ID = 'test-result-widget'; @inject(TestOutputUIModel) uiModel: TestOutputUIModel; @inject(MarkdownRenderer) markdownRenderer: MarkdownRenderer; @inject(OpenerService) openerService: OpenerService; @inject(FileService) fileService: FileService; @inject(NavigationLocationService) navigationService: NavigationLocationService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; protected toDisposeOnRender = new DisposableCollection(); protected input: TestMessage[] = []; protected content: HTMLDivElement; constructor() { super(); this.addClass('theia-test-result-view'); this.id = TestResultWidget.ID; this.title.label = nls.localizeByDefault('Test Results'); this.title.caption = nls.localizeByDefault('Test Results'); this.title.iconClass = codicon('checklist'); this.title.closable = true; this.scrollOptions = { minScrollbarLength: 35, }; } @postConstruct() init(): void { this.toDispose.pushAll([Disposable.create(() => this.toDisposeOnRender.dispose()), this.uiModel.onDidChangeSelectedTestState(e => { if (TestFailure.is(e)) { this.setInput(e.messages); } })]); const currentState = this.uiModel.selectedTestState; if (TestFailure.is(currentState)) { this.setInput(currentState.messages); } } protected override onAfterAttach(msg: Message): void { super.onAfterAttach(msg); this.content = this.node.ownerDocument.createElement('div'); this.node.append(this.content); } setInput(messages: TestMessage[]): void { this.input = messages; this.update(); } protected override onUpdateRequest(msg: Message): void { this.render(); super.onUpdateRequest(msg); } render(): void { this.toDisposeOnRender.dispose(); this.toDisposeOnRender = new DisposableCollection(); this.content.innerHTML = ''; this.input.forEach(message => { if (MarkdownString.is(message.message)) { const line = this.markdownRenderer.render(message.message); this.content.append(line.element); this.toDisposeOnRender.push(line); } else { this.content.append(this.node.ownerDocument.createTextNode(message.message)); } if (message.stackTrace) { const stackTraceElement = this.node.ownerDocument.createElement('div'); message.stackTrace.map(frame => this.renderFrame(frame, stackTraceElement)); this.content.append(stackTraceElement); } }); } renderFrame(stackFrame: TestMessageStackFrame, stackTraceElement: HTMLElement): void { const frameElement = stackTraceElement.ownerDocument.createElement('div'); frameElement.classList.add('debug-frame'); frameElement.append(` ${nls.localize('theia/test/stackFrameAt', 'at')} ${stackFrame.label}`); // Add URI information as clickable links if (stackFrame.uri) { frameElement.append(' ('); const uri = new URI(stackFrame.uri); const link = this.node.ownerDocument.createElement('a'); let content = `${this.labelProvider.getName(uri)}`; if (stackFrame.position) { // Display Position as a 1-based position, similar to Monaco ones. const monacoPosition = { lineNumber: stackFrame.position.line + 1, column: stackFrame.position.character + 1 }; content += `:${monacoPosition.lineNumber}:${monacoPosition.column}`; } link.textContent = content; link.href = `${uri}`; link.onclick = () => this.openUriInWorkspace(uri, stackFrame.position); frameElement.append(link); frameElement.append(')'); } stackTraceElement.append(frameElement); } async openUriInWorkspace(uri: URI, position?: Position): Promise { this.fileService.resolve(uri).then(stat => { if (stat.isFile) { this.navigationService.reveal(NavigationLocation.create(uri, position ?? { line: 0, character: 0 })); } }); } }