deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,35 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - MARKERS EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/markers` adds support for file markers (diagnostic markers (`errors`, `warnings`, `infos`, `hint`)) for a given file.
The extension contributes, the following:
- `problems view`: a dedicated view to viewing diagnostic markers contributed by language-servers, linters, task problem matchers for the workspace
- `marker decoration`: ability to decorate different components of the application based on markers (ex: file explorer)
## Additional Information
- [API documentation for `@theia/markers`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_markers.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,51 @@
{
"name": "@theia/markers",
"version": "1.68.0",
"description": "Theia - Markers Extension",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/workspace": "1.68.0",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/problem/problem-frontend-module",
"backend": "lib/node/problem-backend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,18 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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
// *****************************************************************************
export * from './marker-manager';
export * from './problem/problem-manager';

View File

@@ -0,0 +1,205 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common';
import URI from '@theia/core/lib/common/uri';
import { Marker } from '../common/marker';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files';
/*
* argument to the `findMarkers` method.
*/
export interface SearchFilter<D> {
uri?: URI,
owner?: string,
dataFilter?: (data: D) => boolean
}
export class MarkerCollection<T> {
protected readonly owner2Markers = new Map<string, Readonly<Marker<T>>[]>();
constructor(
public readonly uri: URI,
public readonly kind: string
) { }
get empty(): boolean {
return !this.owner2Markers.size;
}
getOwners(): string[] {
return Array.from(this.owner2Markers.keys());
}
getMarkers(owner: string): Readonly<Marker<T>>[] {
return this.owner2Markers.get(owner) || [];
}
setMarkers(owner: string, markerData: T[]): Marker<T>[] {
const before = this.owner2Markers.get(owner);
if (markerData.length > 0) {
this.owner2Markers.set(owner, markerData.map(data => this.createMarker(owner, data)));
} else {
this.owner2Markers.delete(owner);
}
return before || [];
}
protected createMarker(owner: string, data: T): Readonly<Marker<T>> {
return Object.freeze({
uri: this.uri.toString(),
kind: this.kind,
owner: owner,
data
});
}
findMarkers(filter: SearchFilter<T>): Marker<T>[] {
if (filter.owner) {
if (this.owner2Markers.has(filter.owner)) {
return this.filterMarkers(filter, this.owner2Markers.get(filter.owner));
}
return [];
} else {
const result: Marker<T>[] = [];
for (const markers of this.owner2Markers.values()) {
result.push(...this.filterMarkers(filter, markers));
}
return result;
}
}
protected filterMarkers(filter: SearchFilter<T>, toFilter?: Marker<T>[]): Marker<T>[] {
if (!toFilter) {
return [];
}
if (filter.dataFilter) {
return toFilter.filter(d => filter.dataFilter!(d.data));
} else {
return toFilter;
}
}
}
export interface Uri2MarkerEntry {
uri: string
markers: Owner2MarkerEntry[]
}
export interface Owner2MarkerEntry {
owner: string
markerData: object[];
}
@injectable()
export abstract class MarkerManager<D extends object> {
public abstract getKind(): string;
protected readonly uri2MarkerCollection = new Map<string, MarkerCollection<D>>();
protected readonly onDidChangeMarkersEmitter = new Emitter<URI>();
@inject(FileService)
protected readonly fileService: FileService;
@postConstruct()
protected init(): void {
this.fileService.onDidFilesChange(event => {
if (event.gotDeleted()) {
this.cleanMarkers(event);
}
});
}
protected cleanMarkers(event: FileChangesEvent): void {
for (const uriString of this.uri2MarkerCollection.keys()) {
const uri = new URI(uriString);
if (event.contains(uri, FileChangeType.DELETED)) {
this.cleanAllMarkers(uri);
}
}
}
get onDidChangeMarkers(): Event<URI> {
return this.onDidChangeMarkersEmitter.event;
}
protected fireOnDidChangeMarkers(uri: URI): void {
this.onDidChangeMarkersEmitter.fire(uri);
}
/*
* replaces the current markers for the given uri and owner with the given data.
*/
setMarkers(uri: URI, owner: string, data: D[]): Marker<D>[] {
const uriString = uri.toString();
const collection = this.uri2MarkerCollection.get(uriString) || new MarkerCollection<D>(uri, this.getKind());
const oldMarkers = collection.setMarkers(owner, data);
if (collection.empty) {
this.uri2MarkerCollection.delete(uri.toString());
} else {
this.uri2MarkerCollection.set(uriString, collection);
}
this.fireOnDidChangeMarkers(uri);
return oldMarkers;
}
/*
* returns all markers that satisfy the given filter.
*/
findMarkers(filter: SearchFilter<D> = {}): Marker<D>[] {
if (filter.uri) {
const collection = this.uri2MarkerCollection.get(filter.uri.toString());
return collection ? collection.findMarkers(filter) : [];
}
const result: Marker<D>[] = [];
for (const uri of this.getUris()) {
result.push(...this.uri2MarkerCollection.get(uri)!.findMarkers(filter));
}
return result;
}
getMarkersByUri(): IterableIterator<[string, MarkerCollection<D>]> {
return this.uri2MarkerCollection.entries();
}
getUris(): IterableIterator<string> {
return this.uri2MarkerCollection.keys();
}
cleanAllMarkers(uri?: URI): void {
if (uri) {
this.doCleanAllMarkers(uri);
} else {
for (const uriString of this.getUris()) {
this.doCleanAllMarkers(new URI(uriString));
}
}
}
protected doCleanAllMarkers(uri: URI): void {
const uriString = uri.toString();
const collection = this.uri2MarkerCollection.get(uriString);
if (collection !== undefined) {
this.uri2MarkerCollection.delete(uriString);
this.fireOnDidChangeMarkers(uri);
}
}
}

View File

@@ -0,0 +1,245 @@
// *****************************************************************************
// Copyright (C) 2020 Ericsson 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
let disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import URI from '@theia/core/lib/common/uri';
import { expect } from 'chai';
import { Container } from '@theia/core/shared/inversify';
import { ContributionProvider, Event } from '@theia/core/lib/common';
import { LabelProvider, LabelProviderContribution, DefaultUriLabelProviderContribution, ApplicationShell, WidgetManager } from '@theia/core/lib/browser';
import { MarkerInfoNode } from './marker-tree';
import { MarkerTreeLabelProvider } from './marker-tree-label-provider';
import { TreeLabelProvider } from '@theia/core/lib/browser/tree/tree-label-provider';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { WorkspaceUriLabelProviderContribution } from '@theia/workspace/lib/browser/workspace-uri-contribution';
import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { MockEnvVariablesServerImpl } from '@theia/core/lib/browser/test/mock-env-variables-server';
import { FileUri } from '@theia/core/lib/node';
import { OS } from '@theia/core/lib/common/os';
import * as temp from 'temp';
disableJSDOM();
let markerTreeLabelProvider: MarkerTreeLabelProvider;
let workspaceService: WorkspaceService;
before(() => {
disableJSDOM = enableJSDOM();
const testContainer = new Container();
workspaceService = new WorkspaceService();
testContainer.bind(WorkspaceService).toConstantValue(workspaceService);
testContainer.bind(WorkspaceVariableContribution).toSelf().inSingletonScope();
testContainer.bind(ApplicationShell).toConstantValue({
onDidChangeCurrentWidget: () => undefined,
widgets: []
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
testContainer.bind(WidgetManager).toConstantValue({
onDidCreateWidget: Event.None
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
testContainer.bind(FileService).toConstantValue(<FileService>{});
testContainer.bind(DefaultUriLabelProviderContribution).toSelf().inSingletonScope();
testContainer.bind(WorkspaceUriLabelProviderContribution).toSelf().inSingletonScope();
testContainer.bind(LabelProvider).toSelf().inSingletonScope();
testContainer.bind(MarkerTreeLabelProvider).toSelf().inSingletonScope();
testContainer.bind(TreeLabelProvider).toSelf().inSingletonScope();
testContainer.bind(EnvVariablesServer).toConstantValue(new MockEnvVariablesServerImpl(FileUri.create(temp.track().mkdirSync())));
testContainer.bind<ContributionProvider<LabelProviderContribution>>(ContributionProvider).toDynamicValue(ctx => ({
getContributions(): LabelProviderContribution[] {
return [
ctx.container.get<MarkerTreeLabelProvider>(MarkerTreeLabelProvider),
ctx.container.get<TreeLabelProvider>(TreeLabelProvider),
ctx.container.get<WorkspaceUriLabelProviderContribution>(WorkspaceUriLabelProviderContribution),
ctx.container.get<DefaultUriLabelProviderContribution>(DefaultUriLabelProviderContribution)
];
}
})).inSingletonScope();
markerTreeLabelProvider = testContainer.get<MarkerTreeLabelProvider>(MarkerTreeLabelProvider);
workspaceService = testContainer.get<WorkspaceService>(WorkspaceService);
});
after(() => {
disableJSDOM();
});
describe('Marker Tree Label Provider', () => {
describe('#getName', () => {
it('should return the correct filename and extension', () => {
const label = markerTreeLabelProvider.getName(
createMarkerInfoNode('a/b/c/foo.ts')
);
expect(label).equals('foo.ts');
});
});
describe('getLongName', () => {
describe('single-root workspace', () => {
beforeEach(() => {
const root = FileStat.dir('file:///home/a');
workspaceService['_workspace'] = root;
workspaceService['_roots'] = [root];
});
it('should return the proper label for a directory', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///home/a/b/c/foo.ts')
);
expect(label).equals('b/c');
});
it('should return the proper label for a directory starting with \'.\'', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///home/a/b/.c/foo.ts')
);
expect(label).equals('b/.c');
});
it('should return the proper label when the resource is located at the workspace root', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///home/a/foo.ts')
);
expect(label).equals('');
});
it('should return the full path when the resource does not exist in the workspace root', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///home/b/foo.ts')
);
if (OS.backend.isWindows) {
expect(label).eq('\\home\\b');
} else {
expect(label).eq('/home/b');
}
});
});
describe('multi-root workspace', () => {
beforeEach(() => {
const uri: string = 'file:///file';
const file = FileStat.file(uri);
const root1 = FileStat.dir('file:///root1');
const root2 = FileStat.dir('file:///root2');
workspaceService['_workspace'] = file;
workspaceService['_roots'] = [root1, root2];
});
it('should return the proper root \'root1\' and directory', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///root1/foo/foo.ts')
);
expect(label).equals('root1 ● foo');
});
it('should return the proper root \'root2\' and directory', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///root2/foo/foo.ts')
);
expect(label).equals('root2 ● foo');
});
it('should only return the root when the resource is located at the workspace root', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///root1/foo.ts')
);
expect(label).equals('root1');
});
it('should return the full path when the resource does not exist in any workspace root', () => {
const label = markerTreeLabelProvider.getLongName(
createMarkerInfoNode('file:///home/a/b/foo.ts')
);
if (OS.backend.isWindows) {
expect(label).eq('\\home\\a\\b');
} else {
expect(label).eq('/home/a/b');
}
});
});
});
describe('#getIcon', () => {
it('should return a typescript icon for a typescript file', () => {
const icon = markerTreeLabelProvider.getIcon(
createMarkerInfoNode('a/b/c/foo.ts')
);
expect(icon).contain('ts-icon');
});
it('should return a json icon for a json file', () => {
const icon = markerTreeLabelProvider.getIcon(
createMarkerInfoNode('a/b/c/foo.json')
);
expect(icon).contain('database-icon');
});
it('should return a generic icon for a file with no extension', () => {
const icon = markerTreeLabelProvider.getIcon(
createMarkerInfoNode('a/b/c/foo.md')
);
expect(icon).contain('markdown-icon');
});
});
describe('#getDescription', () => {
beforeEach(() => {
const root = FileStat.dir('file:///home/a');
workspaceService['_workspace'] = root;
workspaceService['_roots'] = [root];
});
it('should return the parent\' long name', () => {
const label = markerTreeLabelProvider.getDescription(
createMarkerInfoNode('file:///home/a/b/c/foo.ts')
);
expect(label).equals('b/c');
});
});
describe('#canHandle', () => {
it('should successfully handle \'MarkerInfoNodes\'', () => {
const node = createMarkerInfoNode('a/b/c/foo.ts');
expect(markerTreeLabelProvider.canHandle(node)).greaterThan(0);
});
});
});
/**
* Create a marker info node for test purposes.
* @param uri the marker uri.
*
* @returns a mock marker info node.
*/
function createMarkerInfoNode(uri: string): MarkerInfoNode {
return {
id: 'id',
parent: {
id: 'parent-id',
kind: '',
parent: undefined,
children: []
},
numberOfMarkers: 1,
children: [],
expanded: true,
selected: true,
uri: new URI(uri)
};
}

View File

@@ -0,0 +1,75 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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 { injectable, inject } from '@theia/core/shared/inversify';
import { LabelProvider, LabelProviderContribution, DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider';
import { MarkerInfoNode } from './marker-tree';
import { TreeLabelProvider } from '@theia/core/lib/browser/tree/tree-label-provider';
import { WorkspaceService } from '@theia/workspace/lib/browser';
@injectable()
export class MarkerTreeLabelProvider implements LabelProviderContribution {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(TreeLabelProvider)
protected readonly treeLabelProvider: TreeLabelProvider;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
canHandle(element: object): number {
return MarkerInfoNode.is(element) ?
this.treeLabelProvider.canHandle(element) + 1 :
0;
}
getIcon(node: MarkerInfoNode): string {
return this.labelProvider.getIcon(node.uri);
}
getName(node: MarkerInfoNode): string {
return this.labelProvider.getName(node.uri);
}
getLongName(node: MarkerInfoNode): string {
const description: string[] = [];
const rootUri = this.workspaceService.getWorkspaceRootUri(node.uri);
// In a multiple-root workspace include the root name to the label before the parent directory.
if (this.workspaceService.isMultiRootWorkspaceOpened && rootUri) {
description.push(this.labelProvider.getName(rootUri));
}
// If the given resource is not at the workspace root, include the parent directory to the label.
if (rootUri && rootUri.toString() !== node.uri.parent.toString()) {
description.push(this.labelProvider.getLongName(node.uri.parent));
}
// Get the full path of a resource which does not exist in the given workspace.
if (!rootUri) {
description.push(this.labelProvider.getLongName(node.uri.parent.withScheme('markers')));
}
return description.join(' ● ');
}
getDescription(node: MarkerInfoNode): string {
return this.labelProvider.getLongName(node.uri.parent);
}
affects(node: MarkerInfoNode, event: DidChangeLabelEvent): boolean {
return event.affects(node.uri) || event.affects(node.uri.parent);
}
}

View File

@@ -0,0 +1,47 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { injectable, inject } from '@theia/core/shared/inversify';
import { MarkerNode } from './marker-tree';
import { TreeModelImpl, OpenerService, open, TreeNode, OpenerOptions } from '@theia/core/lib/browser';
@injectable()
export class MarkerTreeModel extends TreeModelImpl {
@inject(OpenerService) protected readonly openerService: OpenerService;
protected override doOpenNode(node: TreeNode): void {
if (MarkerNode.is(node)) {
open(this.openerService, node.uri, this.getOpenerOptionsByMarker(node));
} else {
super.doOpenNode(node);
}
}
protected getOpenerOptionsByMarker(node: MarkerNode): OpenerOptions | undefined {
return undefined;
}
/**
* Reveal the corresponding node at the marker.
* @param node {TreeNode} the tree node.
*/
revealNode(node: TreeNode): void {
if (MarkerNode.is(node)) {
open(this.openerService, node.uri, { ...this.getOpenerOptionsByMarker(node), mode: 'reveal' });
}
}
}

View File

@@ -0,0 +1,154 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { injectable, unmanaged } from '@theia/core/shared/inversify';
import { TreeImpl, CompositeTreeNode, TreeNode, SelectableTreeNode, ExpandableTreeNode } from '@theia/core/lib/browser';
import { MarkerManager } from './marker-manager';
import { Marker } from '../common/marker';
import { UriSelection } from '@theia/core/lib/common/selection';
import URI from '@theia/core/lib/common/uri';
import { ProblemSelection } from './problem/problem-selection';
import { DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
export const MarkerOptions = Symbol('MarkerOptions');
export interface MarkerOptions {
readonly kind: string;
}
@injectable()
export abstract class MarkerTree<T extends object> extends TreeImpl {
constructor(
@unmanaged() protected readonly markerManager: MarkerManager<T>,
@unmanaged() protected readonly markerOptions: MarkerOptions
) {
super();
this.toDispose.push(markerManager.onDidChangeMarkers(uri => this.refreshMarkerInfo(uri)));
this.root = <MarkerRootNode>{
visible: false,
id: 'theia-' + markerOptions.kind + '-marker-widget',
name: 'MarkerTree',
kind: markerOptions.kind,
children: [],
parent: undefined
};
}
protected async refreshMarkerInfo(uri: URI): Promise<void> {
const id = uri.toString();
const existing = this.getNode(id);
const markers = this.markerManager.findMarkers({ uri });
if (markers.length <= 0) {
if (MarkerInfoNode.is(existing)) {
CompositeTreeNode.removeChild(existing.parent, existing);
this.removeNode(existing);
this.fireChanged();
}
return;
}
const node = MarkerInfoNode.is(existing) ? existing : this.createMarkerInfo(id, uri);
this.insertNodeWithMarkers(node, markers);
}
protected insertNodeWithMarkers(node: MarkerInfoNode, markers: Marker<T>[]): void {
CompositeTreeNode.addChild(node.parent, node);
const children = this.getMarkerNodes(node, markers);
node.numberOfMarkers = markers.length;
this.setChildren(node, children);
}
protected override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
if (MarkerRootNode.is(parent)) {
const nodes: MarkerInfoNode[] = [];
for (const id of this.markerManager.getUris()) {
const uri = new URI(id);
const existing = this.getNode(id);
const markers = this.markerManager.findMarkers({ uri });
const node = MarkerInfoNode.is(existing) ? existing : this.createMarkerInfo(id, uri);
node.children = this.getMarkerNodes(node, markers);
node.numberOfMarkers = node.children.length;
nodes.push(node);
}
return nodes;
}
return super.resolveChildren(parent);
}
protected createMarkerInfo(id: string, uri: URI): MarkerInfoNode {
return {
children: [],
expanded: true,
uri,
id,
parent: this.root as MarkerRootNode,
selected: false,
numberOfMarkers: 0
};
}
protected getMarkerNodes(parent: MarkerInfoNode, markers: Marker<T>[]): MarkerNode[] {
return markers.map((marker, index) =>
this.createMarkerNode(marker, index, parent)
);
}
protected createMarkerNode(marker: Marker<T>, index: number, parent: MarkerInfoNode): MarkerNode {
const id = parent.id + '_' + index;
const existing = this.getNode(id);
if (MarkerNode.is(existing)) {
existing.marker = marker;
return existing;
}
return {
id,
name: 'marker',
parent,
selected: false,
uri: parent.uri,
marker
};
}
}
export interface MarkerNode extends UriSelection, SelectableTreeNode, ProblemSelection {
marker: Marker<object>;
}
export namespace MarkerNode {
export function is(node: TreeNode | undefined): node is MarkerNode {
return UriSelection.is(node) && SelectableTreeNode.is(node) && ProblemSelection.is(node);
}
}
export interface MarkerInfoNode extends UriSelection, SelectableTreeNode, ExpandableTreeNode {
parent: MarkerRootNode;
numberOfMarkers: number;
severity?: DiagnosticSeverity;
}
export namespace MarkerInfoNode {
export function is(node: unknown): node is MarkerInfoNode {
return ExpandableTreeNode.is(node) && UriSelection.is(node) && 'numberOfMarkers' in node;
}
}
export interface MarkerRootNode extends CompositeTreeNode {
kind: string;
}
export namespace MarkerRootNode {
export function is(node: TreeNode | undefined): node is MarkerRootNode {
return CompositeTreeNode.is(node) && 'kind' in node;
}
}

View File

@@ -0,0 +1,277 @@
// *****************************************************************************
// Copyright (C) 2021 EclipseSource 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
let disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import URI from '@theia/core/lib/common/uri';
import { expect } from 'chai';
import { Container } from '@theia/core/shared/inversify';
import { Diagnostic, Range, DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import { Event } from '@theia/core/lib/common/event';
import { MarkerManager } from '../marker-manager';
import { MarkerInfoNode, MarkerNode, MarkerOptions, MarkerRootNode } from '../marker-tree';
import { PROBLEM_OPTIONS } from './problem-container';
import { ProblemManager } from './problem-manager';
import { ProblemTree } from './problem-tree-model';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ProblemCompositeTreeNode } from './problem-composite-tree-node';
import { Marker } from '../../common/marker';
disableJSDOM();
let rootNode: MarkerRootNode;
before(() => {
disableJSDOM = enableJSDOM();
const testContainer = new Container();
testContainer.bind(MarkerManager).toSelf().inSingletonScope();
testContainer.bind(ProblemManager).toSelf();
testContainer.bind(MarkerOptions).toConstantValue(PROBLEM_OPTIONS);
testContainer.bind(FileService).toConstantValue(<FileService>{
onDidFilesChange: Event.None
});
testContainer.bind(ProblemTree).toSelf().inSingletonScope();
});
beforeEach(() => {
rootNode = getRootNode('theia-problem-marker-widget');
});
after(() => {
disableJSDOM();
});
describe('problem-composite-tree-node', () => {
describe('#sortMarkersInfo', () => {
describe('should sort markersInfo based on the highest severity', () => {
function testSeveritySorting(high: DiagnosticSeverity, low: DiagnosticSeverity): void {
const highMarker = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, high);
const lowMarker = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, low);
const highNode = createMockMarkerNode(highMarker);
const lowNode = createMockMarkerNode(lowMarker);
const highMarkerNode = createMarkerInfo('1', new URI('a'), [highNode]);
const lowMarkerNode = createMarkerInfo('2', new URI('b'), [lowNode]);
const highFirstRoot = getRootNode('highFirstRoot');
ProblemCompositeTreeNode.addChildren(highFirstRoot, [
{ node: highMarkerNode, markers: [highMarker] },
{ node: lowMarkerNode, markers: [lowMarker] },
]);
expectCorrectOrdering(highFirstRoot);
const lowFirstRoot = getRootNode('lowFirstRoot');
ProblemCompositeTreeNode.addChildren(lowFirstRoot, [
{ node: lowMarkerNode, markers: [lowMarker] },
{ node: highMarkerNode, markers: [highMarker] },
]);
expectCorrectOrdering(lowFirstRoot);
function expectCorrectOrdering(root: MarkerRootNode): void {
expect(root.children.length).to.equal(2);
expect(root.children[0]).to.equal(highMarkerNode);
expect(highMarkerNode.nextSibling).to.equal(lowMarkerNode);
expect(root.children[1]).to.equal(lowMarkerNode);
expect(lowMarkerNode.previousSibling).to.equal(highMarkerNode);
}
}
it('should sort error higher than warnings', () => {
testSeveritySorting(DiagnosticSeverity.Error, DiagnosticSeverity.Warning);
});
it('should sort errors higher than infos', () => {
testSeveritySorting(DiagnosticSeverity.Error, DiagnosticSeverity.Information);
});
it('should sort errors higher than hints', () => {
testSeveritySorting(DiagnosticSeverity.Error, DiagnosticSeverity.Hint);
});
it('should sort warnings higher than infos', () => {
testSeveritySorting(DiagnosticSeverity.Warning, DiagnosticSeverity.Information);
});
it('should sort warnings higher than hints', () => {
testSeveritySorting(DiagnosticSeverity.Warning, DiagnosticSeverity.Hint);
});
it('should sort infos higher than hints', () => {
testSeveritySorting(DiagnosticSeverity.Information, DiagnosticSeverity.Hint);
});
});
it('should sort markersInfo based on URI if severities are equal', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
const markerInfoNodeA = createMarkerInfo('1', new URI('a'), [nodeA]);
const markerInfoNodeB = createMarkerInfo('2', new URI('b'), [nodeB]);
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeA, markers: [markerA] },
{ node: markerInfoNodeB, markers: [markerB] },
]);
expect(rootNode.children.length).to.equal(2);
expect(rootNode.children[0]).to.equal(markerInfoNodeA);
expect(markerInfoNodeA.nextSibling).to.equal(markerInfoNodeB);
expect(rootNode.children[1]).to.equal(markerInfoNodeB);
expect(markerInfoNodeB.previousSibling).to.equal(markerInfoNodeA);
});
it('changing marker content should lead to update in ProblemCompositeTree', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const nodeA = createMockMarkerNode(markerA);
const markerInfoNodeA = createMarkerInfo('1', new URI('a'), [nodeA]);
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeA, markers: [markerA] }
]);
markerA.data.severity = DiagnosticSeverity.Hint;
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeA, markers: [markerA] }
]);
expect(rootNode.children.length).to.equal(1);
expect(rootNode.children[0]).to.equal(markerInfoNodeA);
});
it('changing marker content from error to hint should lead to lower rank', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const nodeA = createMockMarkerNode(markerA);
const markerInfoNodeA = createMarkerInfo('1', new URI('a'), [nodeA]);
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeA, markers: [markerA] }
]);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const nodeB = createMockMarkerNode(markerB);
const markerInfoNodeB = createMarkerInfo('2', new URI('b'), [nodeB]);
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeB, markers: [markerB] }
]);
markerA.data.severity = DiagnosticSeverity.Hint;
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeA, markers: [markerA] }
]);
expect(rootNode.children.length).to.equal(2);
expect(rootNode.children[0]).to.equal(markerInfoNodeB);
expect(markerInfoNodeB.nextSibling).to.equal(markerInfoNodeA);
expect(rootNode.children[1]).to.equal(markerInfoNodeA);
expect(markerInfoNodeA.previousSibling).to.equal(markerInfoNodeB);
});
it('changing marker content from error to hint should lead to higher rank', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Hint);
const nodeA = createMockMarkerNode(markerA);
const markerInfoNodeA = createMarkerInfo('1', new URI('a'), [nodeA]);
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeA, markers: [markerA] }
]);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const nodeB = createMockMarkerNode(markerB);
const markerInfoNodeB = createMarkerInfo('2', new URI('b'), [nodeB]);
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeB, markers: [markerB] }
]);
markerA.data.severity = DiagnosticSeverity.Error;
ProblemCompositeTreeNode.addChildren(rootNode, [
{ node: markerInfoNodeA, markers: [markerA] }
]);
expect(rootNode.children.length).to.equal(2);
expect(rootNode.children[0]).to.equal(markerInfoNodeA);
expect(markerInfoNodeA.nextSibling).to.equal(markerInfoNodeB);
expect(rootNode.children[1]).to.equal(markerInfoNodeB);
expect(markerInfoNodeB.previousSibling).to.equal(markerInfoNodeA);
});
});
});
function createMarkerInfo(id: string, uri: URI, marker: MarkerNode[]): MarkerInfoNode {
return {
children: marker ? marker : [],
expanded: true,
uri,
id,
parent: rootNode,
selected: false,
numberOfMarkers: marker ? marker.length : 0
};
}
/**
* Create a mock marker node with the given diagnostic marker.
* @param marker the diagnostic marker.
*
* @returns a mock marker node.
*/
function createMockMarkerNode(marker: Marker<Diagnostic>): MarkerNode {
return {
id: 'id',
name: 'marker',
parent: undefined,
selected: false,
uri: new URI(''),
marker
};
}
/**
* Create a mock diagnostic marker.
* @param range the diagnostic range.
* @param severity the diagnostic severity.
* @param owner the optional owner of the diagnostic
*
* @returns a mock diagnostic marker.
*/
function createMockMarker(range: Range, severity: DiagnosticSeverity, owner?: string): Readonly<Marker<Diagnostic>> {
const data: Diagnostic = {
range: range,
severity: severity,
message: 'message'
};
return Object.freeze({
uri: 'uri',
kind: 'marker',
owner: owner ?? 'owner',
data
});
}
function getRootNode(id: string): MarkerRootNode {
return {
visible: false,
id: id,
name: 'MarkerTree',
kind: 'problem',
children: [],
parent: undefined
};
}

View File

@@ -0,0 +1,86 @@
// *****************************************************************************
// Copyright (C) 2021 EclipseSource 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 { CompositeTreeNode } from '@theia/core/lib/browser/tree/tree';
import { MarkerInfoNode } from '../marker-tree';
import URI from '@theia/core/lib/common/uri';
import { Marker } from '../../common/marker';
import { Diagnostic, DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import { ProblemUtils } from './problem-utils';
export namespace ProblemCompositeTreeNode {
export interface Child {
node: MarkerInfoNode;
markers: Marker<Diagnostic>[];
}
export function setSeverity(parent: MarkerInfoNode, markers: Marker<Diagnostic>[]): void {
let maxSeverity: DiagnosticSeverity | undefined;
markers.forEach(marker => {
if (ProblemUtils.severityCompare(marker.data.severity, maxSeverity) < 0) {
maxSeverity = marker.data.severity;
}
});
parent.severity = maxSeverity;
};
export function addChildren(parent: CompositeTreeNode, insertChildren: ProblemCompositeTreeNode.Child[]): void {
for (const { node, markers } of insertChildren) {
ProblemCompositeTreeNode.setSeverity(node, markers);
}
const sortedInsertChildren = insertChildren.sort(
(a, b) => (ProblemUtils.severityCompare(a.node.severity, b.node.severity) || compareURI(a.node.uri, b.node.uri))
);
let startIndex = 0;
const children = parent.children as MarkerInfoNode[];
for (const { node } of sortedInsertChildren) {
const index = children.findIndex(value => value.id === node.id);
if (index !== -1) {
CompositeTreeNode.removeChild(parent, node);
}
if (children.length === 0) {
children.push(node);
startIndex = 1;
CompositeTreeNode.setParent(node, 0, parent);
} else {
let inserted = false;
for (let i = startIndex; i < children.length; i++) {
// sort by severity, equal severity => sort by URI
if (ProblemUtils.severityCompare(node.severity, children[i].severity) < 0
|| (ProblemUtils.severityCompare(node.severity, children[i].severity) === 0 && compareURI(node.uri, children[i].uri) < 0)) {
children.splice(i, 0, node);
inserted = true;
startIndex = i + 1;
CompositeTreeNode.setParent(node, i, parent);
break;
};
}
if (inserted === false) {
children.push(node);
startIndex = children.length;
CompositeTreeNode.setParent(node, children.length - 1, parent);
}
}
}
}
const compareURI = (uri1: URI, uri2: URI): number =>
uri1.toString().localeCompare(uri2.toString(), undefined, { sensitivity: 'base' });
;
}

View File

@@ -0,0 +1,47 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { interfaces, Container } from '@theia/core/shared/inversify';
import { MarkerOptions } from '../marker-tree';
import { ProblemWidget } from './problem-widget';
import { ProblemTreeModel, ProblemTree } from './problem-tree-model';
import { TreeProps, defaultTreeProps, createTreeContainer } from '@theia/core/lib/browser';
import { PROBLEM_KIND } from '../../common/problem-marker';
export const PROBLEM_TREE_PROPS = <TreeProps>{
...defaultTreeProps,
contextMenuPath: [PROBLEM_KIND],
globalSelection: true
};
export const PROBLEM_OPTIONS = <MarkerOptions>{
kind: 'problem'
};
export function createProblemTreeContainer(parent: interfaces.Container): Container {
const child = createTreeContainer(parent, {
tree: ProblemTree,
widget: ProblemWidget,
model: ProblemTreeModel,
props: PROBLEM_TREE_PROPS,
});
child.bind(MarkerOptions).toConstantValue(PROBLEM_OPTIONS);
return child;
}
export function createProblemWidget(parent: interfaces.Container): ProblemWidget {
return createProblemTreeContainer(parent).get(ProblemWidget);
}

View File

@@ -0,0 +1,247 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 debounce = require('@theia/core/shared/lodash.debounce');
import { injectable, inject } from '@theia/core/shared/inversify';
import { FrontendApplication, FrontendApplicationContribution, CompositeTreeNode, SelectableTreeNode, Widget, codicon } from '@theia/core/lib/browser';
import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { PROBLEM_KIND, ProblemMarker } from '../../common/problem-marker';
import { ProblemManager, ProblemStat } from './problem-manager';
import { ProblemWidget, PROBLEMS_WIDGET_ID } from './problem-widget';
import { MenuPath, MenuModelRegistry } from '@theia/core/lib/common/menu';
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { ProblemSelection } from './problem-selection';
import { nls } from '@theia/core/lib/common/nls';
export const PROBLEMS_CONTEXT_MENU: MenuPath = [PROBLEM_KIND];
export namespace ProblemsMenu {
export const CLIPBOARD = [...PROBLEMS_CONTEXT_MENU, '1_clipboard'];
export const PROBLEMS = [...PROBLEMS_CONTEXT_MENU, '2_problems'];
}
export namespace ProblemsCommands {
export const COLLAPSE_ALL: Command = {
id: 'problems.collapse.all'
};
export const COLLAPSE_ALL_TOOLBAR: Command = {
id: 'problems.collapse.all.toolbar',
iconClass: codicon('collapse-all')
};
export const COPY: Command = {
id: 'problems.copy'
};
export const COPY_MESSAGE: Command = {
id: 'problems.copy.message',
};
export const CLEAR_ALL = Command.toLocalizedCommand({
id: 'problems.clear.all',
category: 'Problems',
label: 'Clear All',
iconClass: codicon('clear-all')
}, 'theia/markers/clearAll', nls.getDefaultKey('Problems'));
}
@injectable()
export class ProblemContribution extends AbstractViewContribution<ProblemWidget> implements FrontendApplicationContribution, TabBarToolbarContribution {
@inject(ProblemManager) protected readonly problemManager: ProblemManager;
@inject(StatusBar) protected readonly statusBar: StatusBar;
@inject(SelectionService) protected readonly selectionService: SelectionService;
constructor() {
super({
widgetId: PROBLEMS_WIDGET_ID,
widgetName: nls.localizeByDefault('Problems'),
defaultWidgetOptions: {
area: 'bottom'
},
toggleCommandId: 'problemsView:toggle',
toggleKeybinding: 'ctrlcmd+shift+m'
});
}
onStart(app: FrontendApplication): void {
this.updateStatusBarElement();
this.problemManager.onDidChangeMarkers(this.updateStatusBarElement);
}
async initializeLayout(app: FrontendApplication): Promise<void> {
await this.openView();
}
protected updateStatusBarElement = debounce(() => this.setStatusBarElement(this.problemManager.getProblemStat()), 10);
protected setStatusBarElement(problemStat: ProblemStat): void {
this.statusBar.setElement('problem-marker-status', {
text: problemStat.infos <= 0
? `$(codicon-error) ${problemStat.errors} $(codicon-warning) ${problemStat.warnings}`
: `$(codicon-error) ${problemStat.errors} $(codicon-warning) ${problemStat.warnings} $(codicon-info) ${problemStat.infos}`,
alignment: StatusBarAlignment.LEFT,
priority: 10,
command: this.toggleCommand ? this.toggleCommand.id : undefined,
tooltip: this.getStatusBarTooltip(problemStat)
});
}
/**
* Get the tooltip to be displayed when hovering over the problem statusbar item.
* - Displays `No Problems` when no problems are present.
* - Displays a human-readable label which describes for each type of problem stat properties,
* their overall count and type when any one of these properties has a positive count.
* @param stat the problem stat describing the number of `errors`, `warnings` and `infos`.
*
* @return the tooltip to be displayed in the statusbar.
*/
protected getStatusBarTooltip(stat: ProblemStat): string {
if (stat.errors <= 0 && stat.warnings <= 0 && stat.infos <= 0) {
return nls.localizeByDefault('No Problems');
}
const tooltip: string[] = [];
if (stat.errors > 0) {
tooltip.push(nls.localizeByDefault('{0} Errors', stat.errors));
}
if (stat.warnings > 0) {
tooltip.push(nls.localizeByDefault('{0} Warnings', stat.warnings));
}
if (stat.infos > 0) {
tooltip.push(nls.localizeByDefault('{0} Infos', stat.infos));
}
return tooltip.join(', ');
}
override registerCommands(commands: CommandRegistry): void {
super.registerCommands(commands);
commands.registerCommand(ProblemsCommands.COLLAPSE_ALL, {
execute: () => this.collapseAllProblems()
});
commands.registerCommand(ProblemsCommands.COLLAPSE_ALL_TOOLBAR, {
isEnabled: widget => this.withWidget(widget, () => true),
isVisible: widget => this.withWidget(widget, () => true),
execute: widget => this.withWidget(widget, () => this.collapseAllProblems())
});
commands.registerCommand(ProblemsCommands.COPY,
new ProblemSelection.CommandHandler(this.selectionService, {
multi: false,
isEnabled: () => true,
isVisible: () => true,
execute: selection => this.copy(selection)
})
);
commands.registerCommand(ProblemsCommands.COPY_MESSAGE,
new ProblemSelection.CommandHandler(this.selectionService, {
multi: false,
isEnabled: () => true,
isVisible: () => true,
execute: selection => this.copyMessage(selection)
})
);
commands.registerCommand(ProblemsCommands.CLEAR_ALL, {
isEnabled: widget => this.withWidget(widget, () => true),
isVisible: widget => this.withWidget(widget, () => true),
execute: widget => this.withWidget(widget, () => this.problemManager.cleanAllMarkers())
});
}
override registerMenus(menus: MenuModelRegistry): void {
super.registerMenus(menus);
menus.registerMenuAction(ProblemsMenu.CLIPBOARD, {
commandId: ProblemsCommands.COPY.id,
label: nls.localizeByDefault('Copy'),
order: '0'
});
menus.registerMenuAction(ProblemsMenu.CLIPBOARD, {
commandId: ProblemsCommands.COPY_MESSAGE.id,
label: nls.localizeByDefault('Copy Message'),
order: '1'
});
menus.registerMenuAction(ProblemsMenu.PROBLEMS, {
commandId: ProblemsCommands.COLLAPSE_ALL.id,
label: nls.localizeByDefault('Collapse All'),
order: '2'
});
}
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
toolbarRegistry.registerItem({
id: ProblemsCommands.COLLAPSE_ALL_TOOLBAR.id,
command: ProblemsCommands.COLLAPSE_ALL_TOOLBAR.id,
tooltip: nls.localizeByDefault('Collapse All'),
priority: 0,
});
toolbarRegistry.registerItem({
id: ProblemsCommands.CLEAR_ALL.id,
command: ProblemsCommands.CLEAR_ALL.id,
tooltip: ProblemsCommands.CLEAR_ALL.label,
priority: 1,
});
}
protected async collapseAllProblems(): Promise<void> {
const { model } = await this.widget;
const root = model.root as CompositeTreeNode;
const firstChild = root.children[0];
root.children.forEach(child => CompositeTreeNode.is(child) && model.collapseAll(child));
if (SelectableTreeNode.is(firstChild)) {
model.selectNode(firstChild);
}
}
protected addToClipboard(content: string): void {
const handleCopy = (e: ClipboardEvent) => {
document.removeEventListener('copy', handleCopy);
if (e.clipboardData) {
e.clipboardData.setData('text/plain', content);
e.preventDefault();
}
};
document.addEventListener('copy', handleCopy);
document.execCommand('copy');
}
protected copy(selection: ProblemSelection): void {
const marker = selection.marker as ProblemMarker;
const serializedProblem = JSON.stringify({
resource: marker.uri,
owner: marker.owner,
code: marker.data.code,
severity: marker.data.severity,
message: marker.data.message,
source: marker.data.source,
startLineNumber: marker.data.range.start.line,
startColumn: marker.data.range.start.character,
endLineNumber: marker.data.range.end.line,
endColumn: marker.data.range.end.character
}, undefined, '\t');
this.addToClipboard(serializedProblem);
}
protected copyMessage(selection: ProblemSelection): void {
const marker = selection.marker as ProblemMarker;
this.addToClipboard(marker.data.message);
}
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), cb: (problems: ProblemWidget) => T): T | false {
if (widget instanceof ProblemWidget && widget.id === PROBLEMS_WIDGET_ID) {
return cb(widget);
}
return false;
}
}

View File

@@ -0,0 +1,72 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Decoration, DecorationsProvider, DecorationsService } from '@theia/core/lib/browser/decorations-service';
import { ProblemManager } from './problem-manager';
import { ProblemUtils } from './problem-utils';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { CancellationToken, Emitter, Event, nls } from '@theia/core';
import debounce = require('@theia/core/shared/lodash.debounce');
@injectable()
export class ProblemDecorationsProvider implements DecorationsProvider {
@inject(ProblemManager) protected readonly problemManager: ProblemManager;
protected currentUris: URI[] = [];
protected readonly onDidChangeEmitter = new Emitter<URI[]>();
get onDidChange(): Event<URI[]> {
return this.onDidChangeEmitter.event;
}
@postConstruct()
protected init(): void {
this.problemManager.onDidChangeMarkers(() => this.fireDidDecorationsChanged());
}
protected fireDidDecorationsChanged = debounce(() => this.doFireDidDecorationsChanged(), 50);
protected doFireDidDecorationsChanged(): void {
const newUris = Array.from(this.problemManager.getUris(), stringified => new URI(stringified));
this.onDidChangeEmitter.fire(newUris.concat(this.currentUris));
this.currentUris = newUris;
}
provideDecorations(uri: URI, token: CancellationToken): Decoration | Promise<Decoration | undefined> | undefined {
const markers = this.problemManager.findMarkers({ uri }).filter(ProblemUtils.filterMarker).sort(ProblemUtils.severityCompareMarker);
if (markers.length) {
return {
bubble: true,
letter: markers.length.toString(),
weight: ProblemUtils.getPriority(markers[0]),
colorId: ProblemUtils.getColor(markers[0]),
tooltip: markers.length === 1 ? nls.localizeByDefault('1 problem in this file') : nls.localizeByDefault('{0} problems in this file', markers.length),
};
}
}
}
@injectable()
export class ProblemDecorationContribution implements FrontendApplicationContribution {
@inject(DecorationsService) protected readonly decorationsService: DecorationsService;
@inject(ProblemDecorationsProvider) protected readonly problemDecorationProvider: ProblemDecorationsProvider;
initialize(): void {
this.decorationsService.registerDecorationsProvider(this.problemDecorationProvider);
}
}

View File

@@ -0,0 +1,222 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { Diagnostic, DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import URI from '@theia/core/lib/common/uri';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { Tree, TreeNode } from '@theia/core/lib/browser/tree/tree';
import { DepthFirstTreeIterator } from '@theia/core/lib/browser/tree/tree-iterator';
import { TreeDecorator, TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator';
import { FileStatNode } from '@theia/filesystem/lib/browser';
import { Marker } from '../../common/marker';
import { ProblemManager } from './problem-manager';
import { ProblemPreferences } from '../../common/problem-preferences';
import { ProblemUtils } from './problem-utils';
import { LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
/**
* @deprecated since 1.25.0
* URI-based decorators should implement `DecorationsProvider` and contribute decorations via the `DecorationsService`.
*/
@injectable()
export class ProblemDecorator implements TreeDecorator {
@inject(ProblemPreferences)
protected problemPreferences: ProblemPreferences;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
readonly id = 'theia-problem-decorator';
protected readonly emitter: Emitter<(tree: Tree) => Map<string, TreeDecoration.Data>>;
constructor(@inject(ProblemManager) protected readonly problemManager: ProblemManager) {
this.emitter = new Emitter();
this.problemManager.onDidChangeMarkers(() => this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)));
}
@postConstruct()
protected init(): void {
this.problemPreferences.onPreferenceChanged(event => {
if (event.preferenceName === 'problems.decorations.enabled') {
this.fireDidChangeDecorations(tree => this.collectDecorators(tree));
}
});
this.workspaceService.onWorkspaceChanged(() => {
this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree));
});
this.workspaceService.onWorkspaceLocationChanged(() => {
this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree));
});
}
async decorations(tree: Tree): Promise<Map<string, TreeDecoration.Data>> {
return this.collectDecorators(tree);
}
get onDidChangeDecorations(): Event<(tree: Tree) => Map<string, TreeDecoration.Data>> {
return this.emitter.event;
}
protected fireDidChangeDecorations(event: (tree: Tree) => Map<string, TreeDecoration.Data>): void {
this.emitter.fire(event);
}
protected collectDecorators(tree: Tree): Map<string, TreeDecoration.Data> {
const decorations = new Map<string, TreeDecoration.Data>();
// If the tree root is undefined or the preference for the decorations is disabled, return an empty result map.
if (!tree.root || !this.problemPreferences['problems.decorations.enabled']) {
return decorations;
}
const baseDecorations = this.collectMarkers(tree);
for (const node of new DepthFirstTreeIterator(tree.root)) {
const nodeUri = this.getUriFromNode(node);
if (nodeUri) {
const decorator = baseDecorations.get(nodeUri);
if (decorator) {
this.appendContainerMarkers(node, decorator, decorations);
}
if (decorator) {
decorations.set(node.id, decorator);
}
}
}
return decorations;
}
protected generateCaptionSuffix(nodeURI: URI): string {
const workspaceRoots = this.workspaceService.tryGetRoots();
const parentWorkspace = this.workspaceService.getWorkspaceRootUri(nodeURI);
let workspacePrefixString = '';
let separator = '';
let filePathString = '';
const nodeURIDir = nodeURI.parent;
if (parentWorkspace) {
const relativeDirFromWorkspace = parentWorkspace.relative(nodeURIDir);
workspacePrefixString = workspaceRoots.length > 1 ? this.labelProvider.getName(parentWorkspace) : '';
filePathString = relativeDirFromWorkspace?.fsPath() ?? '';
separator = filePathString && workspacePrefixString ? ' \u2022 ' : ''; // add a bullet point between workspace and path
} else {
workspacePrefixString = nodeURIDir.path.fsPath();
}
return `${workspacePrefixString}${separator}${filePathString}`;
}
/**
* Traverses up the tree from the given node and attaches decorations to any parents.
*/
protected appendContainerMarkers(node: TreeNode, decoration: TreeDecoration.Data, decorations: Map<string, TreeDecoration.Data>): void {
let parent = node?.parent;
while (parent) {
const existing = decorations.get(parent.id);
// Make sure the highest diagnostic severity (smaller number) will be propagated to the container directory.
if (existing === undefined || this.compareDecorators(existing, decoration) < 0) {
decorations.set(parent.id, decoration);
parent = parent.parent;
} else {
break;
}
}
}
/**
* @returns a map matching stringified URI's to a decoration whose features reflect the highest-severity problem found
* and the number of problems found (based on {@link ProblemDecorator.toDecorator })
*/
protected collectMarkers(tree: Tree): Map<string, TreeDecoration.Data> {
const decorationsForUri = new Map();
const compare = this.compare.bind(this);
const filter = this.filterMarker.bind(this);
for (const [, markers] of this.problemManager.getMarkersByUri()) {
const relevant = markers.findMarkers({}).filter(filter).sort(compare);
if (relevant.length) {
decorationsForUri.set(relevant[0].uri, this.toDecorator(relevant));
}
}
return decorationsForUri;
}
protected toDecorator(markers: Marker<Diagnostic>[]): TreeDecoration.Data {
const color = this.getColor(markers[0]);
const priority = this.getPriority(markers[0]);
return {
priority,
fontData: {
color,
},
tailDecorations: [{
color,
data: markers.length.toString(),
}],
};
}
protected getColor(marker: Marker<Diagnostic>): TreeDecoration.Color {
const { severity } = marker.data;
switch (severity) {
case 1: return 'var(--theia-list-errorForeground)';
case 2: return 'var(--theia-list-warningForeground)';
default: return 'var(--theia-successBackground)';
}
}
/**
* Get the decoration for a given marker diagnostic.
* Markers with higher severity have a higher priority and should be displayed.
* @param marker the diagnostic marker.
*/
protected getPriority(marker: Marker<Diagnostic>): number {
const { severity } = marker.data;
switch (severity) {
case 1: return 30; // Errors.
case 2: return 20; // Warnings.
case 3: return 10; // Infos.
default: return 0;
}
}
/**
* Returns `true` if the diagnostic (`data`) of the marker argument has `Error`, `Warning`, or `Information` severity.
* Otherwise, returns `false`.
*/
protected filterMarker(marker: Marker<Diagnostic>): boolean {
const { severity } = marker.data;
return severity === DiagnosticSeverity.Error
|| severity === DiagnosticSeverity.Warning
|| severity === DiagnosticSeverity.Information;
}
protected getUriFromNode(node: TreeNode): string | undefined {
return FileStatNode.getUri(node);
}
protected compare(left: Marker<Diagnostic>, right: Marker<Diagnostic>): number {
return ProblemDecorator.severityCompare(left, right);
}
protected compareDecorators(left: TreeDecoration.Data, right: TreeDecoration.Data): number {
return TreeDecoration.Data.comparePriority(left, right);
}
}
export namespace ProblemDecorator {
// Highest severities (errors) come first, then the others. Undefined severities treated as the last ones.
export const severityCompare = ProblemUtils.severityCompareMarker;
}

View File

@@ -0,0 +1,64 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 '../../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { ProblemWidget, PROBLEMS_WIDGET_ID } from './problem-widget';
import { ProblemContribution } from './problem-contribution';
import { createProblemWidget } from './problem-container';
import { FrontendApplicationContribution, bindViewContribution, ApplicationShellLayoutMigration, LabelProviderContribution } from '@theia/core/lib/browser';
import { ProblemManager } from './problem-manager';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { ProblemTabBarDecorator } from './problem-tabbar-decorator';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ProblemLayoutVersion3Migration } from './problem-layout-migrations';
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { MarkerTreeLabelProvider } from '../marker-tree-label-provider';
import { ProblemWidgetTabBarDecorator } from './problem-widget-tab-bar-decorator';
import { ProblemDecorationContribution, ProblemDecorationsProvider } from './problem-decorations-provider';
import { bindProblemPreferences } from '../../common/problem-preferences';
export default new ContainerModule(bind => {
bindProblemPreferences(bind);
bind(ProblemManager).toSelf().inSingletonScope();
bind(ProblemWidget).toDynamicValue(ctx =>
createProblemWidget(ctx.container)
);
bind(WidgetFactory).toDynamicValue(context => ({
id: PROBLEMS_WIDGET_ID,
createWidget: () => context.container.get<ProblemWidget>(ProblemWidget)
}));
bind(ApplicationShellLayoutMigration).to(ProblemLayoutVersion3Migration).inSingletonScope();
bindViewContribution(bind, ProblemContribution);
bind(FrontendApplicationContribution).toService(ProblemContribution);
bind(TabBarToolbarContribution).toService(ProblemContribution);
bind(ProblemDecorationsProvider).toSelf().inSingletonScope();
bind(ProblemTabBarDecorator).toSelf().inSingletonScope();
bind(TabBarDecorator).toService(ProblemTabBarDecorator);
bind(ProblemDecorationContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ProblemDecorationContribution);
bind(MarkerTreeLabelProvider).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(MarkerTreeLabelProvider);
bind(ProblemWidgetTabBarDecorator).toSelf().inSingletonScope();
bind(TabBarDecorator).toService(ProblemWidgetTabBarDecorator);
});

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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 { injectable } from '@theia/core/shared/inversify';
import { ApplicationShellLayoutMigration, WidgetDescription } from '@theia/core/lib/browser/shell/shell-layout-restorer';
import { PROBLEM_KIND } from '../../common/problem-marker';
import { PROBLEMS_WIDGET_ID } from './problem-widget';
@injectable()
export class ProblemLayoutVersion3Migration implements ApplicationShellLayoutMigration {
readonly layoutVersion = 3.0;
onWillInflateWidget(desc: WidgetDescription): WidgetDescription | undefined {
if (desc.constructionOptions.factoryId === PROBLEM_KIND) {
desc.constructionOptions.factoryId = PROBLEMS_WIDGET_ID;
return desc;
}
return undefined;
}
}

View File

@@ -0,0 +1,216 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();
import * as chai from 'chai';
import URI from '@theia/core/lib/common/uri';
import { Container } from '@theia/core/shared/inversify';
import { ProblemManager } from './problem-manager';
import { Event } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { DiagnosticSeverity, Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { LocalStorageService, StorageService } from '@theia/core/lib/browser/storage-service';
disableJSDOM();
const expect = chai.expect;
let manager: ProblemManager;
let container: Container;
/**
* The default range for test purposes.
*/
const range: Range = { start: { line: 0, character: 10 }, end: { line: 0, character: 10 } };
describe('problem-manager', () => {
beforeEach(() => {
container = new Container();
container.bind(ILogger).to(MockLogger);
container.bind(StorageService).to(LocalStorageService).inSingletonScope();
container.bind(LocalStorageService).toSelf().inSingletonScope();
container.bind(FileService).toConstantValue(<FileService>{
onDidFilesChange: Event.None
});
container.bind(ProblemManager).toSelf();
manager = container.get(ProblemManager);
});
describe('#setMarkers', () => {
it('should successfully set new markers', () => {
expect(Array.from(manager.getUris()).length).to.equal(0);
manager.setMarkers(new URI('a'), 'a', [{ message: 'a', range }]);
expect(Array.from(manager.getUris()).length).to.equal(1);
});
it('should replace markers', () => {
const uri = new URI('a');
let events = 0;
manager.onDidChangeMarkers(() => events++);
expect(events).equal(0);
const initial = manager.setMarkers(uri, 'a', [{ message: 'a', range }]);
expect(initial.length).equal(0);
expect(events).equal(1);
const updated = manager.setMarkers(uri, 'a', [{ message: 'a', range }]);
expect(updated.length).equal(1);
expect(events).equal(2);
expect(manager.findMarkers({ uri }).length).equal(1);
});
});
describe('#cleanAllMarkers', () => {
it('should successfully clean all markers', () => {
// Create mock markers.
manager.setMarkers(new URI('a'), 'a', [{ message: 'a', range }]);
manager.setMarkers(new URI('b'), 'b', [{ message: 'a', range }]);
manager.setMarkers(new URI('c'), 'c', [{ message: 'a', range }]);
expect(Array.from(manager.getUris()).length).to.equal(3);
// Clean the markers.
manager.cleanAllMarkers();
expect(Array.from(manager.getUris()).length).to.equal(0);
});
});
describe('#findMarkers', () => {
it('should find markers by `owner`', () => {
const owner: string = 'foo';
manager.setMarkers(new URI('a'), owner, [{ message: 'a', range }]);
manager.setMarkers(new URI('b'), owner, [{ message: 'a', range }]);
expect(manager.findMarkers({ owner }).length).equal(2);
expect(manager.findMarkers({ owner: 'unknown' }).length).equal(0);
});
it('should find markers by `owner` and `uri`', () => {
const owner: string = 'foo';
const uri = new URI('bar');
// Create a marker to match the filter.
manager.setMarkers(uri, owner, [{ message: 'a', range }]);
// Create 2 markers that do not match the filter.
manager.setMarkers(new URI('invalid'), 'invalid-owner', [{ message: 'a', range }]);
manager.setMarkers(new URI('invalid'), 'invalid-owner', [{ message: 'a', range }]);
// Expect to find the markers which satisfy the filter only.
expect(manager.findMarkers({ owner, uri }).length).equal(1);
});
describe('dataFilter', () => {
it('should find markers that satisfy filter for `severity`', () => {
manager.setMarkers(new URI('a'), 'a', [{ message: 'a', range, severity: DiagnosticSeverity.Error }]);
expect(manager.findMarkers({ dataFilter: d => d.severity === DiagnosticSeverity.Error }).length).equal(1);
expect(manager.findMarkers({ dataFilter: d => d.severity !== DiagnosticSeverity.Error }).length).equal(0);
});
it('should find markers that satisfy filter for `code`', () => {
const code = 100;
manager.setMarkers(new URI('a'), 'a', [{ message: 'a', range, code }]);
expect(manager.findMarkers({ dataFilter: d => d.code === code }).length).equal(1);
expect(manager.findMarkers({ dataFilter: d => d.code !== code }).length).equal(0);
});
it('should find markers that satisfy filter for `message`', () => {
const message = 'foo';
manager.setMarkers(new URI('a'), 'a', [{ message, range }]);
expect(manager.findMarkers({ dataFilter: d => d.message === message }).length).equal(1);
expect(manager.findMarkers({ dataFilter: d => d.message !== message }).length).equal(0);
});
it('should find markers that satisfy filter for `source`', () => {
const source = 'typescript';
manager.setMarkers(new URI('a'), 'a', [{ message: 'a', range, source }]);
expect(manager.findMarkers({ dataFilter: d => d.source === source }).length).equal(1);
expect(manager.findMarkers({ dataFilter: d => d.source !== source }).length).equal(0);
});
it('should find markers that satisfy filter for `range`', () => {
manager.setMarkers(new URI('a'), 'a', [{ message: 'a', range }]);
// The default `range` has a start line number of 0.
expect(manager.findMarkers({ dataFilter: d => d.range.start.line === 0 }).length).equal(1);
expect(manager.findMarkers({ dataFilter: d => d.range.start.line > 0 }).length).equal(0);
});
});
});
describe('#getUris', () => {
it('should return 0 uris when no markers are present', () => {
expect(Array.from(manager.getUris()).length).to.equal(0);
});
it('should return the list of uris', () => {
manager.setMarkers(new URI('a'), 'a', [{ message: 'a', range, severity: DiagnosticSeverity.Error }]);
manager.setMarkers(new URI('b'), 'b', [{ message: 'a', range, severity: DiagnosticSeverity.Error }]);
expect(Array.from(manager.getUris()).length).to.equal(2);
});
});
describe('#getProblemStat', () => {
it('should return 0 stats when no markers are present', () => {
const { errors, warnings, infos } = manager.getProblemStat();
expect(errors).to.equal(0);
expect(warnings).to.equal(0);
expect(infos).to.equal(0);
});
it('should return the proper problem stats', () => {
// Create 3 error markers.
manager.setMarkers(new URI('error-1'), 'error-1', [{ message: 'a', range, severity: DiagnosticSeverity.Error }]);
manager.setMarkers(new URI('error-2'), 'error-2', [{ message: 'a', range, severity: DiagnosticSeverity.Error }]);
manager.setMarkers(new URI('error-3'), 'error-3', [{ message: 'a', range, severity: DiagnosticSeverity.Error }]);
// Create 2 warning markers.
manager.setMarkers(new URI('warning-1'), 'warning-1', [{ message: 'a', range, severity: DiagnosticSeverity.Warning }]);
manager.setMarkers(new URI('warning-2'), 'warning-2', [{ message: 'a', range, severity: DiagnosticSeverity.Warning }]);
// Create 1 info marker.
manager.setMarkers(new URI('info-1'), 'info-1', [{ message: 'a', range, severity: DiagnosticSeverity.Information }]);
// Collect the total problem stats for the application.
const { errors, warnings, infos } = manager.getProblemStat();
expect(errors).to.equal(3);
expect(warnings).to.equal(2);
expect(infos).to.equal(1);
});
});
});

View File

@@ -0,0 +1,51 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { injectable } from '@theia/core/shared/inversify';
import { MarkerManager } from '../marker-manager';
import { PROBLEM_KIND } from '../../common/problem-marker';
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-protocol';
export interface ProblemStat {
errors: number;
warnings: number;
infos: number;
}
@injectable()
export class ProblemManager extends MarkerManager<Diagnostic> {
public getKind(): string {
return PROBLEM_KIND;
}
getProblemStat(): ProblemStat {
let errors = 0;
let warnings = 0;
let infos = 0;
for (const marker of this.findMarkers()) {
if (marker.data.severity === 1) {
errors++;
} else if (marker.data.severity === 2) {
warnings++;
} else if (marker.data.severity === 3) {
infos++;
}
}
return { errors, warnings, infos };
}
}

View File

@@ -0,0 +1,45 @@
// *****************************************************************************
// Copyright (C) 2019 Ericsson 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 { SelectionService } from '@theia/core/lib/common/selection-service';
import { SelectionCommandHandler } from '@theia/core/lib/common/selection-command-handler';
import { isObject } from '@theia/core/lib/common';
import { Marker } from '../../common/marker';
import { ProblemMarker } from '../../common/problem-marker';
export interface ProblemSelection {
marker: Marker<object>;
}
export namespace ProblemSelection {
export function is(arg: unknown): arg is ProblemSelection {
return isObject<ProblemSelection>(arg) && ProblemMarker.is(arg.marker);
}
export class CommandHandler extends SelectionCommandHandler<ProblemSelection> {
constructor(
selectionService: SelectionService,
options: SelectionCommandHandler.Options<ProblemSelection>
) {
super(
selectionService,
arg => ProblemSelection.is(arg) ? arg : undefined,
options
);
}
}
}

View File

@@ -0,0 +1,152 @@
// *****************************************************************************
// Copyright (C) 2019 Ericsson 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { Diagnostic, DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { Title, Widget } from '@theia/core/shared/@lumino/widgets';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { Marker } from '../../common/marker';
import { ProblemManager } from './problem-manager';
import { ProblemPreferences, ProblemConfiguration } from '../../common/problem-preferences';
import { Navigatable } from '@theia/core/lib/browser';
import { PreferenceChangeEvent } from '@theia/core';
@injectable()
export class ProblemTabBarDecorator implements TabBarDecorator {
readonly id = 'theia-problem-tabbar-decorator';
protected readonly emitter = new Emitter<void>();
@inject(ProblemPreferences)
protected readonly preferences: ProblemPreferences;
@inject(ProblemManager)
protected readonly problemManager: ProblemManager;
@postConstruct()
protected init(): void {
this.problemManager.onDidChangeMarkers(() => this.fireDidChangeDecorations());
this.preferences.onPreferenceChanged(event => this.handlePreferenceChange(event));
}
decorate(title: Title<Widget>): WidgetDecoration.Data[] {
if (!this.preferences['problems.decorations.tabbar.enabled']) {
return [];
}
const widget = title.owner;
if (Navigatable.is(widget)) {
const resourceUri = widget.getResourceUri();
if (resourceUri) {
// Get the list of problem markers for the given resource URI.
const markers: Marker<Diagnostic>[] = this.problemManager.findMarkers({ uri: resourceUri });
// If no markers are available, return early.
if (markers.length === 0) {
return [];
}
// Store the marker with the highest severity.
let maxSeverity: Marker<Diagnostic> | undefined;
// Iterate over available markers to determine that which has the highest severity.
// Only display a decoration if an error or warning marker is available.
for (const marker of markers) {
// Break early if an error marker is present, since it represents the highest severity.
if (marker.data.severity === DiagnosticSeverity.Error) {
maxSeverity = marker;
break;
} else if (marker.data.severity === DiagnosticSeverity.Warning) {
maxSeverity = marker;
}
}
// Decorate the tabbar with the highest marker severity if available.
return maxSeverity ? [this.toDecorator(maxSeverity)] : [];
}
}
return [];
}
get onDidChangeDecorations(): Event<void> {
return this.emitter.event;
}
protected fireDidChangeDecorations(): void {
this.emitter.fire(undefined);
}
/**
* Handle changes in preference.
* @param {PreferenceChangeEvent<ProblemConfiguration>} event The event of the changes in preference.
*/
protected async handlePreferenceChange(event: PreferenceChangeEvent<ProblemConfiguration>): Promise<void> {
const { preferenceName } = event;
if (preferenceName === 'problems.decorations.tabbar.enabled') {
this.fireDidChangeDecorations();
}
}
/**
* Convert a diagnostic marker to a decorator.
* @param {Marker<Diagnostic>} marker A diagnostic marker.
* @returns {WidgetDecoration.Data} The decoration data.
*/
protected toDecorator(marker: Marker<Diagnostic>): WidgetDecoration.Data {
const position = WidgetDecoration.IconOverlayPosition.BOTTOM_RIGHT;
const icon = this.getOverlayIcon(marker);
const color = this.getOverlayIconColor(marker);
return {
iconOverlay: {
position,
icon,
color,
background: {
shape: 'circle',
color: 'transparent'
}
}
};
}
/**
* Get the appropriate overlay icon for decoration.
* @param {Marker<Diagnostic>} marker A diagnostic marker.
* @returns {string} A string representing the overlay icon class.
*/
protected getOverlayIcon(marker: Marker<Diagnostic>): string {
const { severity } = marker.data;
switch (severity) {
case 1: return 'times-circle';
case 2: return 'exclamation-circle';
case 3: return 'info-circle';
default: return 'hand-o-up';
}
}
/**
* Get the appropriate overlay icon color for decoration.
* @param {Marker<Diagnostic>} marker A diagnostic marker.
* @returns {WidgetDecoration.Color} The decoration color.
*/
protected getOverlayIconColor(marker: Marker<Diagnostic>): WidgetDecoration.Color {
const { severity } = marker.data;
switch (severity) {
case 1: return 'var(--theia-list-errorForeground)';
case 2: return 'var(--theia-list-warningForeground)';
default: return 'var(--theia-successBackground)';
}
}
}

View File

@@ -0,0 +1,189 @@
// *****************************************************************************
// Copyright (C) 2020 Ericsson 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
let disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import URI from '@theia/core/lib/common/uri';
import { expect } from 'chai';
import { Container } from '@theia/core/shared/inversify';
import { Diagnostic, Range, DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import { Event } from '@theia/core/lib/common/event';
import { Marker } from '../../common/marker';
import { MarkerManager } from '../marker-manager';
import { MarkerNode, MarkerOptions } from '../marker-tree';
import { PROBLEM_OPTIONS } from './problem-container';
import { ProblemManager } from './problem-manager';
import { ProblemTree } from './problem-tree-model';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
disableJSDOM();
let problemTree: ProblemTree;
before(() => {
disableJSDOM = enableJSDOM();
const testContainer = new Container();
testContainer.bind(MarkerManager).toSelf().inSingletonScope();
testContainer.bind(ProblemManager).toSelf();
testContainer.bind(MarkerOptions).toConstantValue(PROBLEM_OPTIONS);
testContainer.bind(FileService).toConstantValue(<FileService>{
onDidFilesChange: Event.None
});
testContainer.bind(ProblemTree).toSelf().inSingletonScope();
problemTree = testContainer.get<ProblemTree>(ProblemTree);
});
after(() => {
disableJSDOM();
});
describe('Problem Tree', () => {
describe('#sortMarkers', () => {
describe('should sort markers based on the highest severity', () => {
it('should sort errors higher than warnings', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Warning);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-1);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(1);
});
it('should sort errors higher than infos', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Information);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-2);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(2);
});
it('should sort errors higher than hints', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Hint);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-3);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(3);
});
it('should sort warnings higher than infos', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Warning);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Information);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-1);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(1);
});
it('should sort warnings higher than hints', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Warning);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Hint);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-2);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(2);
});
it('should sort infos higher than hints', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Information);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Hint);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-1);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(1);
});
});
it('should sort markers based on lowest line number if their severities are equal', () => {
const markerA = createMockMarker({ start: { line: 1, character: 10 }, end: { line: 1, character: 20 } }, DiagnosticSeverity.Error);
const markerB = createMockMarker({ start: { line: 5, character: 10 }, end: { line: 5, character: 20 } }, DiagnosticSeverity.Error);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-4);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(4);
});
it('should sort markers based on lowest column number if their severities and line numbers are equal', () => {
const markerA = createMockMarker({ start: { line: 1, character: 10 }, end: { line: 1, character: 10 } }, DiagnosticSeverity.Error);
const markerB = createMockMarker({ start: { line: 1, character: 20 }, end: { line: 1, character: 20 } }, DiagnosticSeverity.Error);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-10);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(10);
});
it('should sort markers based on owner if their severities, line numbers and columns are equal', () => {
const markerA = createMockMarker({ start: { line: 1, character: 10 }, end: { line: 1, character: 10 } }, DiagnosticSeverity.Error, 'A');
const markerB = createMockMarker({ start: { line: 1, character: 10 }, end: { line: 1, character: 10 } }, DiagnosticSeverity.Error, 'B');
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(-1);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(1);
});
it('should not sort if markers are equal', () => {
const markerA = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const markerB = createMockMarker({ start: { line: 0, character: 10 }, end: { line: 0, character: 10 } }, DiagnosticSeverity.Error);
const nodeA = createMockMarkerNode(markerA);
const nodeB = createMockMarkerNode(markerB);
expect(problemTree['sortMarkers'](nodeA, nodeB)).equals(0);
expect(problemTree['sortMarkers'](nodeB, nodeA)).equals(0);
});
});
});
/**
* Create a mock marker node with the given diagnostic marker.
* @param marker the diagnostic marker.
*
* @returns a mock marker node.
*/
function createMockMarkerNode(marker: Marker<Diagnostic>): MarkerNode {
return {
id: 'id',
name: 'marker',
parent: undefined,
selected: false,
uri: new URI(''),
marker
};
}
/**
* Create a mock diagnostic marker.
* @param range the diagnostic range.
* @param severity the diagnostic severity.
* @param owner the optional owner of the diagnostic
*
* @returns a mock diagnostic marker.
*/
function createMockMarker(range: Range, severity: DiagnosticSeverity, owner?: string): Readonly<Marker<Diagnostic>> {
const data: Diagnostic = {
range: range,
severity: severity,
message: 'message'
};
return Object.freeze({
uri: 'uri',
kind: 'marker',
owner: owner ?? 'owner',
data
});
}

View File

@@ -0,0 +1,133 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { ProblemMarker } from '../../common/problem-marker';
import { ProblemManager } from './problem-manager';
import { ProblemCompositeTreeNode } from './problem-composite-tree-node';
import { MarkerNode, MarkerTree, MarkerOptions, MarkerInfoNode, MarkerRootNode } from '../marker-tree';
import { MarkerTreeModel } from '../marker-tree-model';
import { injectable, inject } from '@theia/core/shared/inversify';
import { OpenerOptions, TreeNode } from '@theia/core/lib/browser';
import { Marker } from '../../common/marker';
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-protocol';
import { ProblemUtils } from './problem-utils';
import debounce = require('@theia/core/shared/lodash.debounce');
@injectable()
export class ProblemTree extends MarkerTree<Diagnostic> {
protected queuedMarkers = new Map<string, ProblemCompositeTreeNode.Child>();
constructor(
@inject(ProblemManager) markerManager: ProblemManager,
@inject(MarkerOptions) markerOptions: MarkerOptions
) {
super(markerManager, markerOptions);
}
protected override getMarkerNodes(parent: MarkerInfoNode, markers: Marker<Diagnostic>[]): MarkerNode[] {
const nodes = super.getMarkerNodes(parent, markers);
return nodes.sort((a, b) => this.sortMarkers(a, b));
}
/**
* Sort markers based on the following rules:
* - Markers are fist sorted by `severity`.
* - Markers are sorted by `line number` if applicable.
* - Markers are sorted by `column number` if applicable.
* - Markers are then finally sorted by `owner` if applicable.
* @param a the first marker for comparison.
* @param b the second marker for comparison.
*/
protected sortMarkers(a: MarkerNode, b: MarkerNode): number {
const markerA = a.marker as Marker<Diagnostic>;
const markerB = b.marker as Marker<Diagnostic>;
// Determine the marker with the highest severity.
const severity = ProblemUtils.severityCompareMarker(markerA, markerB);
if (severity !== 0) {
return severity;
}
// Determine the marker with the lower line number.
const lineNumber = ProblemUtils.lineNumberCompare(markerA, markerB);
if (lineNumber !== 0) {
return lineNumber;
}
// Determine the marker with the lower column number.
const columnNumber = ProblemUtils.columnNumberCompare(markerA, markerB);
if (columnNumber !== 0) {
return columnNumber;
}
// Sort by owner in alphabetical order.
const owner = ProblemUtils.ownerCompare(markerA, markerB);
if (owner !== 0) {
return owner;
}
return 0;
}
protected override insertNodeWithMarkers(node: MarkerInfoNode, markers: Marker<Diagnostic>[]): void {
// Add the element to the queue.
// In case a diagnostics collection for the same file already exists, it will be replaced.
this.queuedMarkers.set(node.id, { node, markers });
this.doInsertNodesWithMarkers();
}
protected doInsertNodesWithMarkers = debounce(() => {
const root = this.root;
// Sanity check; This should always be of type `MarkerRootNode`
if (!MarkerRootNode.is(root)) {
return;
}
const queuedItems = Array.from(this.queuedMarkers.values());
ProblemCompositeTreeNode.addChildren(root, queuedItems);
for (const { node, markers } of queuedItems) {
const children = this.getMarkerNodes(node, markers);
node.numberOfMarkers = markers.length;
this.setChildren(node, children);
}
this.queuedMarkers.clear();
}, 50);
}
@injectable()
export class ProblemTreeModel extends MarkerTreeModel {
@inject(ProblemManager) protected readonly problemManager: ProblemManager;
protected override getOpenerOptionsByMarker(node: MarkerNode): OpenerOptions | undefined {
if (ProblemMarker.is(node.marker)) {
return {
selection: node.marker.data.range
};
}
return undefined;
}
removeNode(node: TreeNode): void {
if (MarkerInfoNode.is(node)) {
this.problemManager.cleanAllMarkers(node.uri);
}
if (MarkerNode.is(node)) {
const { uri } = node;
const { owner } = node.marker;
const diagnostics = this.problemManager.findMarkers({ uri, owner, dataFilter: data => node.marker.data !== data }).map(({ data }) => data);
this.problemManager.setMarkers(uri, owner, diagnostics);
}
}
}

View File

@@ -0,0 +1,90 @@
// *****************************************************************************
// Copyright (C) 2020 Ericsson 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 { Marker } from '../../common/marker';
import { Diagnostic, DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
export namespace ProblemUtils {
/**
* Comparator for severity.
* - The highest severity (`error`) come first followed by others.
* - `undefined` severities are treated as the last ones.
* @param a the first marker for comparison.
* @param b the second marker for comparison.
*/
export const severityCompareMarker = (a: Marker<Diagnostic>, b: Marker<Diagnostic>): number =>
(a.data.severity || Number.MAX_SAFE_INTEGER) - (b.data.severity || Number.MAX_SAFE_INTEGER);
/**
* Comparator for severity.
* - The highest severity (`error`) come first followed by others.
* - `undefined` severities are treated as the last ones.
* @param a the first severity for comparison.
* @param b the second severity for comparison.
*/
export const severityCompare = (a: DiagnosticSeverity | undefined, b: DiagnosticSeverity | undefined): number =>
(a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER);
/**
* Comparator for line numbers.
* - The lowest line number comes first.
* @param a the first marker for comparison.
* @param b the second marker for comparison.
*/
export const lineNumberCompare = (a: Marker<Diagnostic>, b: Marker<Diagnostic>): number => a.data.range.start.line - b.data.range.start.line;
/**
* Comparator for column numbers.
* - The lowest column number comes first.
* @param a the first marker for comparison.
* @param b the second marker for comparison.
*/
export const columnNumberCompare = (a: Marker<Diagnostic>, b: Marker<Diagnostic>): number => a.data.range.start.character - b.data.range.start.character;
/**
* Comparator for marker owner (source).
* - The order is alphabetical.
* @param a the first marker for comparison.
* @param b the second marker for comparison.
*/
export const ownerCompare = (a: Marker<Diagnostic>, b: Marker<Diagnostic>): number => a.owner.localeCompare(b.owner);
export function getPriority(marker: Marker<Diagnostic>): number {
const { severity } = marker.data;
switch (severity) {
case DiagnosticSeverity.Error: return 30;
case DiagnosticSeverity.Warning: return 20;
case DiagnosticSeverity.Information: return 10;
default: return 0;
}
}
export function getColor(marker: Marker<Diagnostic>): string {
const { severity } = marker.data;
switch (severity) {
case DiagnosticSeverity.Error: return 'list.errorForeground';
case DiagnosticSeverity.Warning: return 'list.warningForeground';
default: return ''; // other severities should not be decorated.
}
}
export function filterMarker(marker: Marker<Diagnostic>): boolean {
const { severity } = marker.data;
return severity === DiagnosticSeverity.Error
|| severity === DiagnosticSeverity.Warning;
}
}

View File

@@ -0,0 +1,55 @@
// *****************************************************************************
// Copyright (C) 2020 Ericsson 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { ProblemManager } from './problem-manager';
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { Title, Widget } from '@theia/core/lib/browser';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
@injectable()
export class ProblemWidgetTabBarDecorator implements TabBarDecorator {
readonly id = 'theia-problems-widget-tabbar-decorator';
protected readonly emitter = new Emitter<void>();
@inject(ProblemManager)
protected readonly problemManager: ProblemManager;
@postConstruct()
protected init(): void {
this.problemManager.onDidChangeMarkers(() => this.fireDidChangeDecorations());
}
decorate(title: Title<Widget>): WidgetDecoration.Data[] {
if (title.owner.id === 'problems') {
const { infos, warnings, errors } = this.problemManager.getProblemStat();
const markerCount = infos + warnings + errors;
return markerCount > 0 ? [{ badge: markerCount }] : [];
} else {
return [];
}
}
get onDidChangeDecorations(): Event<void> {
return this.emitter.event;
}
protected fireDidChangeDecorations(): void {
this.emitter.fire(undefined);
}
}

View File

@@ -0,0 +1,246 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ProblemManager } from './problem-manager';
import { ProblemMarker } from '../../common/problem-marker';
import { ProblemTreeModel } from './problem-tree-model';
import { MarkerInfoNode, MarkerNode, MarkerRootNode } from '../marker-tree';
import {
TreeWidget, TreeProps, ContextMenuRenderer, TreeNode, NodeProps, TreeModel,
ApplicationShell, Navigatable, ExpandableTreeNode, SelectableTreeNode, TREE_NODE_INFO_CLASS, codicon, Message
} from '@theia/core/lib/browser';
import { DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
import * as React from '@theia/core/shared/react';
import { ProblemPreferences } from '../../common/problem-preferences';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
export const PROBLEMS_WIDGET_ID = 'problems';
@injectable()
export class ProblemWidget extends TreeWidget {
protected readonly toDisposeOnCurrentWidgetChanged = new DisposableCollection();
@inject(ProblemPreferences)
protected readonly preferences: ProblemPreferences;
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(ProblemManager)
protected readonly problemManager: ProblemManager;
constructor(
@inject(TreeProps) treeProps: TreeProps,
@inject(ProblemTreeModel) override readonly model: ProblemTreeModel,
@inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer
) {
super(treeProps, model, contextMenuRenderer);
this.id = PROBLEMS_WIDGET_ID;
this.title.label = nls.localizeByDefault('Problems');
this.title.caption = this.title.label;
this.title.iconClass = codicon('warning');
this.title.closable = true;
this.addClass('theia-marker-container');
this.addClipboardListener(this.node, 'copy', e => this.handleCopy(e));
}
@postConstruct()
protected override init(): void {
super.init();
this.updateFollowActiveEditor();
this.toDispose.push(this.preferences.onPreferenceChanged(e => {
if (e.preferenceName === 'problems.autoReveal') {
this.updateFollowActiveEditor();
}
}));
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.update();
}
protected updateFollowActiveEditor(): void {
this.toDisposeOnCurrentWidgetChanged.dispose();
this.toDispose.push(this.toDisposeOnCurrentWidgetChanged);
if (this.preferences.get('problems.autoReveal')) {
this.followActiveEditor();
}
}
protected followActiveEditor(): void {
this.autoRevealFromActiveEditor();
this.toDisposeOnCurrentWidgetChanged.push(this.shell.onDidChangeCurrentWidget(() => this.autoRevealFromActiveEditor()));
}
protected autoRevealFromActiveEditor(): void {
const widget = this.shell.currentWidget;
if (widget && Navigatable.is(widget)) {
const uri = widget.getResourceUri();
const node = uri && this.model.getNode(uri.toString());
if (ExpandableTreeNode.is(node) && SelectableTreeNode.is(node)) {
this.model.expandNode(node);
this.model.selectNode(node);
}
}
}
override storeState(): object {
// no-op
return {};
}
protected superStoreState(): object {
return super.storeState();
}
override restoreState(state: object): void {
// no-op
}
protected superRestoreState(state: object): void {
super.restoreState(state);
return;
}
protected override tapNode(node?: TreeNode): void {
super.tapNode(node);
if (MarkerNode.is(node)) {
this.model.revealNode(node);
}
}
protected handleCopy(event: ClipboardEvent): void {
const uris = this.model.selectedNodes.filter(MarkerNode.is).map(node => node.uri.toString());
if (uris.length > 0 && event.clipboardData) {
event.clipboardData.setData('text/plain', uris.join('\n'));
event.preventDefault();
}
}
protected override handleDown(event: KeyboardEvent): void {
const node = this.model.getNextSelectableNode();
super.handleDown(event);
if (MarkerNode.is(node)) {
this.model.revealNode(node);
}
}
protected override handleUp(event: KeyboardEvent): void {
const node = this.model.getPrevSelectableNode();
super.handleUp(event);
if (MarkerNode.is(node)) {
this.model.revealNode(node);
}
}
protected override renderTree(model: TreeModel): React.ReactNode {
if (MarkerRootNode.is(model.root) && model.root.children.length > 0) {
return super.renderTree(model);
}
return <div className='theia-widget-noInfo noMarkers'>{nls.localize('theia/markers/noProblems', 'No problems have been detected in the workspace so far.')}</div>;
}
protected override renderCaption(node: TreeNode, props: NodeProps): React.ReactNode {
if (MarkerInfoNode.is(node)) {
return this.decorateMarkerFileNode(node);
} else if (MarkerNode.is(node)) {
return this.decorateMarkerNode(node);
}
return 'caption';
}
protected override renderTailDecorations(node: TreeNode, props: NodeProps): JSX.Element {
return <div className='row-button-container'>
{this.renderRemoveButton(node)}
</div>;
}
protected renderRemoveButton(node: TreeNode): React.ReactNode {
return <ProblemMarkerRemoveButton model={this.model} node={node} />;
}
protected decorateMarkerNode(node: MarkerNode): React.ReactNode {
if (ProblemMarker.is(node.marker)) {
let severityClass: string = '';
const problemMarker = node.marker;
if (problemMarker.data.severity) {
severityClass = this.getSeverityClass(problemMarker.data.severity);
}
const location = nls.localizeByDefault('Ln {0}, Col {1}', problemMarker.data.range.start.line + 1, problemMarker.data.range.start.character + 1);
return <div
className='markerNode'
title={`${problemMarker.data.message} (${problemMarker.data.range.start.line + 1}, ${problemMarker.data.range.start.character + 1})`}>
<div>
<i className={`${severityClass} ${TREE_NODE_INFO_CLASS}`}></i>
</div>
<div className='message'>{problemMarker.data.message}
{(!!problemMarker.data.source || !!problemMarker.data.code) &&
<span className={'owner ' + TREE_NODE_INFO_CLASS}>
{problemMarker.data.source || ''}
{problemMarker.data.code ? `(${problemMarker.data.code})` : ''}
</span>
}
<span className={'position ' + TREE_NODE_INFO_CLASS}>
{`[${location}]`}
</span>
</div>
</div>;
}
return '';
}
protected getSeverityClass(severity: DiagnosticSeverity): string {
switch (severity) {
case 1: return `${codicon('error')} error`;
case 2: return `${codicon('warning')} warning`;
case 3: return `${codicon('info')} information`;
default: return `${codicon('thumbsup')} hint`;
}
}
protected decorateMarkerFileNode(node: MarkerInfoNode): React.ReactNode {
const icon = this.toNodeIcon(node);
const name = this.toNodeName(node);
const description = this.toNodeDescription(node);
// Use a custom scheme so that we fallback to the `DefaultUriLabelProviderContribution`.
const path = this.labelProvider.getLongName(node.uri.withScheme('marker'));
return <div title={path} className='markerFileNode'>
{icon && <div className={icon + ' file-icon'}></div>}
<div className='name'>{name}</div>
<div className={'path ' + TREE_NODE_INFO_CLASS}>{description}</div>
<div className='notification-count-container'>
<span className='notification-count'>{node.numberOfMarkers.toString()}</span>
</div>
</div>;
}
}
export class ProblemMarkerRemoveButton extends React.Component<{ model: ProblemTreeModel, node: TreeNode }> {
override render(): React.ReactNode {
return <span className={codicon('close')} onClick={this.remove}></span>;
}
protected readonly remove = (e: React.MouseEvent<HTMLElement>) => this.doRemove(e);
protected doRemove(e: React.MouseEvent<HTMLElement>): void {
this.props.model.removeNode(this.props.node);
e.stopPropagation();
}
}

View File

@@ -0,0 +1,92 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox 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
********************************************************************************/
.theia-marker-container {
font-size: var(--theia-ui-font-size1);
}
.theia-side-panel .theia-marker-container .noMarkers {
padding-left: 19px;
}
.theia-marker-container .markerNode,
.theia-marker-container .markerFileNode {
display: flex;
align-items: center;
}
.theia-marker-container .markerNode div,
.theia-marker-container .markerFileNode div:not(.file-icon) {
display: flex;
margin-right: 5px;
}
.theia-marker-container .markerFileNode .name,
.theia-marker-container .markerFileNode .path {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.theia-marker-container .markerFileNode .path {
font-size: var(--theia-ui-font-size0);
align-self: flex-end;
white-space: nowrap;
}
.theia-marker-container .error {
color: var(--theia-editorError-foreground);
opacity: 1;
}
.theia-marker-container .warning {
color: var(--theia-editorWarning-foreground);
opacity: 1;
}
.theia-marker-container .information {
color: var(--theia-editorInfo-foreground);
opacity: 1;
}
.theia-marker-container .hint {
color: var(--theia-successBackground);
opacity: 1;
}
.theia-marker-container .markerNode .position,
.theia-marker-container .markerNode .owner {
white-space: nowrap;
margin-left: 5px;
}
.theia-marker-container .markerNode .message {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theia-marker-container .row-button-container {
display: none;
}
.theia-marker-container .theia-TreeNodeContent:hover .row-button-container {
display: flex;
justify-content: flex-end;
flex: 1;
align-items: center;
align-self: center;
}

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson 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 { isObject, isString } from '@theia/core/lib/common/types';
/*
* A marker represents meta information for a given uri
*/
export interface Marker<T> {
/**
* the uri this marker is associated with.
*/
uri: string;
/*
* the owner of this marker. Any string provided by the registrar.
*/
owner: string;
/**
* the kind, e.g. 'problem'
*/
kind?: string;
/*
* marker kind specific data
*/
data: T;
}
export namespace Marker {
export function is(value: unknown): value is Marker<object>;
export function is<T>(value: unknown, subTypeCheck: (value: unknown) => value is T): value is Marker<T>;
export function is(value: unknown, subTypeCheck?: (value: unknown) => boolean): boolean {
subTypeCheck ??= isObject;
return isObject<Marker<object>>(value)
&& !Array.isArray(value)
&& subTypeCheck(value.data)
&& isString(value.uri)
&& isString(value.owner);
}
}

View File

@@ -0,0 +1,30 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson 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 { Marker } from './marker';
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-protocol';
export const PROBLEM_KIND = 'problem';
export interface ProblemMarker extends Marker<Diagnostic> {
kind: 'problem';
}
export namespace ProblemMarker {
export function is(node: unknown): node is ProblemMarker {
return Marker.is(node) && node.kind === PROBLEM_KIND;
}
}

View File

@@ -0,0 +1,63 @@
// *****************************************************************************
// Copyright (C) 2019 Ericsson 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 { interfaces } from '@theia/core/shared/inversify';
import { createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService } from '@theia/core/lib/common';
import { nls } from '@theia/core/lib/common/nls';
export const ProblemConfigSchema: PreferenceSchema = {
'properties': {
'problems.decorations.enabled': {
'type': 'boolean',
'description': nls.localizeByDefault('Show Errors & Warnings on files and folder. Overwritten by {0} when it is off.', '`#problems.visibility#`'),
'default': true,
},
'problems.decorations.tabbar.enabled': {
'type': 'boolean',
'description': nls.localize('theia/markers/tabbarDecorationsEnabled', 'Show problem decorators (diagnostic markers) in the tab bars.'),
'default': true
},
'problems.autoReveal': {
'type': 'boolean',
'description': nls.localizeByDefault('Controls whether Problems view should automatically reveal files when opening them.'),
'default': true
}
}
};
export interface ProblemConfiguration {
'problems.decorations.enabled': boolean,
'problems.decorations.tabbar.enabled': boolean,
'problems.autoReveal': boolean
}
export const ProblemPreferenceContribution = Symbol('ProblemPreferenceContribution');
export const ProblemPreferences = Symbol('ProblemPreferences');
export type ProblemPreferences = PreferenceProxy<ProblemConfiguration>;
export function createProblemPreferences(preferences: PreferenceService, schema: PreferenceSchema = ProblemConfigSchema): ProblemPreferences {
return createPreferenceProxy(preferences, schema);
}
export const bindProblemPreferences = (bind: interfaces.Bind): void => {
bind(ProblemPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(ProblemPreferenceContribution);
return createProblemPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(ProblemPreferenceContribution).toConstantValue({ schema: ProblemConfigSchema });
bind(PreferenceContribution).toService(ProblemPreferenceContribution);
};

View File

@@ -0,0 +1,22 @@
// *****************************************************************************
// Copyright (C) 2025 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 { ContainerModule } from '@theia/core/shared/inversify';
import { bindProblemPreferences } from '../common/problem-preferences';
export default new ContainerModule(bind => {
bindProblemPreferences(bind);
});

View File

@@ -0,0 +1,22 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../filesystem"
},
{
"path": "../workspace"
}
]
}