diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b7062220a5ef..87498ca659f03 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,14 @@
- [plugin] added support for `DocumentSymbolProviderMetadata` [#10811](https://github.com/eclipse-theia/theia/pull/10811) - Contributed on behalf of STMicroelectronics
+## v1.24.0 - unreleased
+
+[1.24.0 Milestone](https://github.com/eclipse-theia/theia/milestone/32)
+
+[Breaking Changes:](#breaking_changes_1.24.0)
+
+ - [markers] `ProblemDecorator` reimplemented to reduce redundancy and align more closely with VSCode. `collectMarkers` now returns `Map`, `getOverlayIconColor` renamed to `getColor`, `getOverlayIcon` removed, `appendContainerMarkers` returns `void`.
+
## v1.23.0 - 2/24/2022
[1.23.0 Milestone](https://github.com/eclipse-theia/theia/milestone/31)
diff --git a/packages/core/src/browser/style/tree.css b/packages/core/src/browser/style/tree.css
index 8ecf2c48807d9..4ab5c39bb30d9 100644
--- a/packages/core/src/browser/style/tree.css
+++ b/packages/core/src/browser/style/tree.css
@@ -135,6 +135,10 @@
text-align: center;
}
+.theia-TreeNodeTail:not(:last-of-type)::after {
+ content: ',';
+}
+
.theia-TreeNodeSegment mark {
background-color: var(--theia-list-filterMatchBackground);
color: var(--theia-list-inactiveSelectionForeground);
diff --git a/packages/core/src/browser/tree/tree-widget.tsx b/packages/core/src/browser/tree/tree-widget.tsx
index c0c41f909a49d..acc89c9619e4b 100644
--- a/packages/core/src/browser/tree/tree-widget.tsx
+++ b/packages/core/src/browser/tree/tree-widget.tsx
@@ -799,7 +799,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
* @param props the node properties.
*/
protected renderTailDecorations(node: TreeNode, props: NodeProps): React.ReactNode {
- const tailDecorations = this.getDecorationData(node, 'tailDecorations').filter(notEmpty).reduce((acc, current) => acc.concat(current), []);
+ const tailDecorations = this.getDecorationData(node, 'tailDecorations').reduce((acc, current) => acc.concat(current), []);
if (tailDecorations.length === 0) {
return;
}
@@ -809,7 +809,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
protected renderTailDecorationsForNode(node: TreeNode, props: NodeProps, tailDecorations:
(TreeDecoration.TailDecoration | TreeDecoration.TailDecorationIcon | TreeDecoration.TailDecorationIconClass)[]): React.ReactNode {
return
- {tailDecorations.map((decoration, index) => {
+ {tailDecorations.reverse().map((decoration, index) => {
const { tooltip } = decoration;
const { data, fontData } = decoration as TreeDecoration.TailDecoration;
const color = (decoration as TreeDecoration.TailDecorationIcon).color;
@@ -1037,8 +1037,8 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
*
* @returns the tree decoration data at the given key.
*/
- protected getDecorationData(node: TreeNode, key: K): TreeDecoration.Data[K][] {
- return this.getDecorations(node).filter(data => data[key] !== undefined).map(data => data[key]).filter(notEmpty);
+ protected getDecorationData(node: TreeNode, key: K): Required>[K][] {
+ return this.getDecorations(node).filter(data => data[key] !== undefined).map(data => data[key]);
}
/**
diff --git a/packages/core/src/browser/widget-decoration.ts b/packages/core/src/browser/widget-decoration.ts
index 5c3e82c11dfda..732adbab1ccdf 100644
--- a/packages/core/src/browser/widget-decoration.ts
+++ b/packages/core/src/browser/widget-decoration.ts
@@ -337,7 +337,7 @@ export namespace WidgetDecoration {
}
export namespace Data {
/**
- * Compares the decoration data based on the priority. Lowest priorities come first.
+ * Compares the decoration data based on the priority. Lowest priorities come first (i.e. left.priority - right.priority).
*/
export const comparePriority = (left: Data, right: Data): number => (left.priority || 0) - (right.priority || 0);
}
diff --git a/packages/markers/src/browser/marker-manager.ts b/packages/markers/src/browser/marker-manager.ts
index 603a10d953e09..7ffd75813dacb 100644
--- a/packages/markers/src/browser/marker-manager.ts
+++ b/packages/markers/src/browser/marker-manager.ts
@@ -176,6 +176,10 @@ export abstract class MarkerManager {
return result;
}
+ getMarkersByUri(): IterableIterator<[string, MarkerCollection]> {
+ return this.uri2MarkerCollection.entries();
+ }
+
getUris(): IterableIterator {
return this.uri2MarkerCollection.keys();
}
diff --git a/packages/markers/src/browser/problem/problem-decorator.ts b/packages/markers/src/browser/problem/problem-decorator.ts
index 005ff2574e050..cf05aa022e5e2 100644
--- a/packages/markers/src/browser/problem/problem-decorator.ts
+++ b/packages/markers/src/browser/problem/problem-decorator.ts
@@ -17,9 +17,8 @@
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 { notEmpty } from '@theia/core/lib/common/objects';
import { Event, Emitter } from '@theia/core/lib/common/event';
-import { Tree } from '@theia/core/lib/browser/tree/tree';
+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';
@@ -79,20 +78,18 @@ export class ProblemDecorator implements TreeDecorator {
protected collectDecorators(tree: Tree): Map {
const decorations = new Map();
// If the tree root is undefined or the preference for the decorations is disabled, return an empty result map.
- if (tree.root === undefined || !this.problemPreferences['problems.decorations.enabled']) {
+ if (!tree.root || !this.problemPreferences['problems.decorations.enabled']) {
return decorations;
}
- const markers = this.appendContainerMarkers(tree, this.collectMarkers(tree));
+ const baseDecorations = this.collectMarkers(tree);
for (const node of new DepthFirstTreeIterator(tree.root)) {
- const nodeUri = FileStatNode.getUri(node);
+ const nodeUri = this.getUriFromNode(node);
if (nodeUri) {
- const marker = markers.get(nodeUri);
- let decorator: TreeDecoration.Data | undefined;
- if (marker) {
- decorator = this.toDecorator(marker);
- }
+ let decorator = baseDecorations.get(nodeUri);
if (OpenEditorNode.is(node)) {
decorator = this.appendSuffixDecoration(node.uri, decorator);
+ } else if (decorator) {
+ this.appendContainerMarkers(node, decorator, decorations);
}
if (decorator) {
decorations.set(node.id, decorator);
@@ -134,78 +131,56 @@ export class ProblemDecorator implements TreeDecorator {
return `${workspacePrefixString}${separator}${filePathString}`;
}
- protected appendContainerMarkers(tree: Tree, markers: Marker[]): Map> {
- const result: Map> = new Map();
- // We traverse up and assign the diagnostic to the container directory.
- // Note, instead of stopping at the WS root, we traverse up the driver root.
- // We will filter them later based on the expansion state of the tree.
- for (const [uri, marker] of new Map(markers.map(m => [new URI(m.uri), m] as [URI, Marker])).entries()) {
- const uriString = uri.toString();
- result.set(uriString, marker);
- let parentUri: URI | undefined = uri.parent;
- while (parentUri && !parentUri.path.isRoot) {
- const parentUriString = parentUri.toString();
- const existing = result.get(parentUriString);
- // Make sure the highest diagnostic severity (smaller number) will be propagated to the container directory.
- if (existing === undefined || this.compare(marker, existing) < 0) {
- result.set(parentUriString, {
- data: marker.data,
- uri: parentUriString,
- owner: marker.owner,
- kind: marker.kind
- });
- parentUri = parentUri.parent;
- } else {
- parentUri = undefined;
- }
+ /**
+ * Traverses up the tree from the given node and attaches decorations to any parents.
+ */
+ protected appendContainerMarkers(node: TreeNode, decoration: TreeDecoration.Data, decorations: Map): 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;
}
}
- return result;
}
- protected collectMarkers(tree: Tree): Marker[] {
- return Array.from(this.problemManager.getUris())
- .map(uri => new URI(uri))
- .map(uri => this.problemManager.findMarkers({ uri }))
- .map(markers => markers.sort(this.compare.bind(this)))
- .map(markers => markers.shift())
- .filter(notEmpty)
- .filter(this.filterMarker.bind(this));
+ /**
+ * @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 {
+ 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(marker: Marker): TreeDecoration.Data {
- const position = TreeDecoration.IconOverlayPosition.BOTTOM_RIGHT;
- const icon = this.getOverlayIcon(marker);
- const color = this.getOverlayIconColor(marker);
- const priority = this.getPriority(marker);
+ protected toDecorator(markers: Marker[]): TreeDecoration.Data {
+ const color = this.getColor(markers[0]);
+ const priority = this.getPriority(markers[0]);
return {
priority,
fontData: {
color,
},
- iconOverlay: {
- position,
- icon,
+ tailDecorations: [{
color,
- background: {
- shape: 'circle',
- color: 'transparent'
- }
- },
+ data: markers.length.toString(),
+ }],
};
}
- protected getOverlayIcon(marker: Marker): 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';
- }
- }
-
- protected getOverlayIconColor(marker: Marker): TreeDecoration.Color {
+ protected getColor(marker: Marker): TreeDecoration.Color {
const { severity } = marker.data;
switch (severity) {
case 1: return 'var(--theia-editorError-foreground)';
@@ -241,10 +216,17 @@ export class ProblemDecorator implements TreeDecorator {
|| severity === DiagnosticSeverity.Information;
}
+ protected getUriFromNode(node: TreeNode): string | undefined {
+ return FileStatNode.getUri(node);
+ }
+
protected compare(left: Marker, right: Marker): number {
return ProblemDecorator.severityCompare(left, right);
}
+ protected compareDecorators(left: TreeDecoration.Data, right: TreeDecoration.Data): number {
+ return TreeDecoration.Data.comparePriority(left, right);
+ }
}
export namespace ProblemDecorator {
diff --git a/packages/navigator/src/browser/navigator-widget.tsx b/packages/navigator/src/browser/navigator-widget.tsx
index 42c745d3768f2..1b304ce967db9 100644
--- a/packages/navigator/src/browser/navigator-widget.tsx
+++ b/packages/navigator/src/browser/navigator-widget.tsx
@@ -17,7 +17,7 @@
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import URI from '@theia/core/lib/common/uri';
-import { CommandService, notEmpty } from '@theia/core/lib/common';
+import { CommandService } from '@theia/core/lib/common';
import {
Key, TreeModel, SelectableTreeNode, TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS,
TreeDecoration, NodeProps, OpenerService, ContextMenuRenderer, ExpandableTreeNode, TreeProps, TreeNode
@@ -148,7 +148,7 @@ export class FileNavigatorWidget extends FileTreeWidget {
}
protected override renderTailDecorations(node: TreeNode, props: NodeProps): React.ReactNode {
- const tailDecorations = this.getDecorationData(node, 'tailDecorations').filter(notEmpty).reduce((acc, current) => acc.concat(current), []);
+ const tailDecorations = this.getDecorationData(node, 'tailDecorations').reduce((acc, current) => acc.concat(current), []);
if (tailDecorations.length === 0) {
return;