deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/markers/.eslintrc.js
Normal file
10
packages/markers/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
35
packages/markers/README.md
Normal file
35
packages/markers/README.md
Normal 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>
|
||||
51
packages/markers/package.json
Normal file
51
packages/markers/package.json
Normal 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"
|
||||
}
|
||||
18
packages/markers/src/browser/index.ts
Normal file
18
packages/markers/src/browser/index.ts
Normal 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';
|
||||
205
packages/markers/src/browser/marker-manager.ts
Normal file
205
packages/markers/src/browser/marker-manager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
245
packages/markers/src/browser/marker-tree-label-provider.spec.ts
Normal file
245
packages/markers/src/browser/marker-tree-label-provider.spec.ts
Normal 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)
|
||||
};
|
||||
}
|
||||
75
packages/markers/src/browser/marker-tree-label-provider.ts
Normal file
75
packages/markers/src/browser/marker-tree-label-provider.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
47
packages/markers/src/browser/marker-tree-model.ts
Normal file
47
packages/markers/src/browser/marker-tree-model.ts
Normal 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' });
|
||||
}
|
||||
}
|
||||
}
|
||||
154
packages/markers/src/browser/marker-tree.ts
Normal file
154
packages/markers/src/browser/marker-tree.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
;
|
||||
}
|
||||
47
packages/markers/src/browser/problem/problem-container.ts
Normal file
47
packages/markers/src/browser/problem/problem-container.ts
Normal 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);
|
||||
}
|
||||
247
packages/markers/src/browser/problem/problem-contribution.ts
Normal file
247
packages/markers/src/browser/problem/problem-contribution.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
222
packages/markers/src/browser/problem/problem-decorator.ts
Normal file
222
packages/markers/src/browser/problem/problem-decorator.ts
Normal 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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
216
packages/markers/src/browser/problem/problem-manager.spec.ts
Normal file
216
packages/markers/src/browser/problem/problem-manager.spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
51
packages/markers/src/browser/problem/problem-manager.ts
Normal file
51
packages/markers/src/browser/problem/problem-manager.ts
Normal 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 };
|
||||
}
|
||||
|
||||
}
|
||||
45
packages/markers/src/browser/problem/problem-selection.ts
Normal file
45
packages/markers/src/browser/problem/problem-selection.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
152
packages/markers/src/browser/problem/problem-tabbar-decorator.ts
Normal file
152
packages/markers/src/browser/problem/problem-tabbar-decorator.ts
Normal 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)';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
189
packages/markers/src/browser/problem/problem-tree-model.spec.ts
Normal file
189
packages/markers/src/browser/problem/problem-tree-model.spec.ts
Normal 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
|
||||
});
|
||||
}
|
||||
133
packages/markers/src/browser/problem/problem-tree-model.ts
Normal file
133
packages/markers/src/browser/problem/problem-tree-model.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
packages/markers/src/browser/problem/problem-utils.ts
Normal file
90
packages/markers/src/browser/problem/problem-utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
246
packages/markers/src/browser/problem/problem-widget.tsx
Normal file
246
packages/markers/src/browser/problem/problem-widget.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
92
packages/markers/src/browser/style/index.css
Normal file
92
packages/markers/src/browser/style/index.css
Normal 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;
|
||||
}
|
||||
53
packages/markers/src/common/marker.ts
Normal file
53
packages/markers/src/common/marker.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
30
packages/markers/src/common/problem-marker.ts
Normal file
30
packages/markers/src/common/problem-marker.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
63
packages/markers/src/common/problem-preferences.ts
Normal file
63
packages/markers/src/common/problem-preferences.ts
Normal 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);
|
||||
};
|
||||
22
packages/markers/src/node/problem-backend-module.ts
Normal file
22
packages/markers/src/node/problem-backend-module.ts
Normal 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);
|
||||
});
|
||||
22
packages/markers/tsconfig.json
Normal file
22
packages/markers/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user