提交 5082e729 编写于 作者: R Rob Lourens

Avoid cell execution icon flickering due to rerendering spinner

上级 201063aa
...@@ -66,6 +66,24 @@ export function groupBy<T>(data: T[], groupFn: (element: T) => string): IStringD ...@@ -66,6 +66,24 @@ export function groupBy<T>(data: T[], groupFn: (element: T) => string): IStringD
return result; return result;
} }
/**
* Groups the collection into a dictionary based on the provided
* group function.
*/
export function groupByNumber<T>(data: T[], groupFn: (element: T) => number): Map<number, T[]> {
const result = new Map<number, T[]>();
for (const element of data) {
const key = groupFn(element);
let target = result.get(key);
if (!target) {
target = [];
result.set(key, target);
}
target.push(element);
}
return result;
}
export function fromMap<T>(original: Map<string, T>): IStringDictionary<T> { export function fromMap<T>(original: Map<string, T>): IStringDictionary<T> {
const result: IStringDictionary<T> = Object.create(null); const result: IStringDictionary<T> = Object.create(null);
if (original) { if (original) {
......
...@@ -53,4 +53,26 @@ suite('Collections', () => { ...@@ -53,4 +53,26 @@ suite('Collections', () => {
assert.strictEqual(grouped[group2].length, 1); assert.strictEqual(grouped[group2].length, 1);
assert.strictEqual(grouped[group2][0].value, value3); assert.strictEqual(grouped[group2][0].value, value3);
}); });
test('groupByNumber', () => {
const group1 = 1, group2 = 2;
const value1 = 'a', value2 = 'b', value3 = 'c';
let source = [
{ key: group1, value: value1 },
{ key: group1, value: value2 },
{ key: group2, value: value3 },
];
let grouped = collections.groupByNumber(source, x => x.key);
// Group 1
assert.strictEqual(grouped.get(group1)!.length, 2);
assert.strictEqual(grouped.get(group1)![0].value, value1);
assert.strictEqual(grouped.get(group1)![1].value, value2);
// Group 2
assert.strictEqual(grouped.get(group2)!.length, 1);
assert.strictEqual(grouped.get(group2)![0].value, value3);
});
}); });
...@@ -11,7 +11,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; ...@@ -11,7 +11,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { stripIcons } from 'vs/base/common/iconLabels'; import { stripIcons } from 'vs/base/common/iconLabels';
import { KeyCode } from 'vs/base/common/keyCodes'; import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
import { IDimension, isThemeColor } from 'vs/editor/common/editorCommon'; import { IDimension, isThemeColor } from 'vs/editor/common/editorCommon';
import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICommandService } from 'vs/platform/commands/common/commands';
...@@ -39,11 +39,12 @@ export const enum ClickTargetType { ...@@ -39,11 +39,12 @@ export const enum ClickTargetType {
export class CellEditorStatusBar extends Disposable { export class CellEditorStatusBar extends Disposable {
readonly statusBarContainer: HTMLElement; readonly statusBarContainer: HTMLElement;
private readonly leftContributedItemsContainer: HTMLElement; private readonly leftItemsContainer: HTMLElement;
private readonly rightContributedItemsContainer: HTMLElement; private readonly rightItemsContainer: HTMLElement;
private readonly itemsDisposable: DisposableStore; private readonly itemsDisposable: DisposableStore;
private items: CellStatusBarItem[] = []; private leftItems: CellStatusBarItem[] = [];
private rightItems: CellStatusBarItem[] = [];
private width: number = 0; private width: number = 0;
private currentContext: INotebookCellActionContext | undefined; private currentContext: INotebookCellActionContext | undefined;
...@@ -60,8 +61,8 @@ export class CellEditorStatusBar extends Disposable { ...@@ -60,8 +61,8 @@ export class CellEditorStatusBar extends Disposable {
this.statusBarContainer.tabIndex = -1; this.statusBarContainer.tabIndex = -1;
const leftItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-left')); const leftItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-left'));
const rightItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-right')); const rightItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-right'));
this.leftContributedItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items.cell-contributed-items-left')); this.leftItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items.cell-contributed-items-left'));
this.rightContributedItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items.cell-contributed-items-right')); this.rightItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items.cell-contributed-items-right'));
this.itemsDisposable = this._register(new DisposableStore()); this.itemsDisposable = this._register(new DisposableStore());
...@@ -107,7 +108,8 @@ export class CellEditorStatusBar extends Disposable { ...@@ -107,7 +108,8 @@ export class CellEditorStatusBar extends Disposable {
this.statusBarContainer.style.width = `${width}px`; this.statusBarContainer.style.width = `${width}px`;
const maxItemWidth = this.getMaxItemWidth(); const maxItemWidth = this.getMaxItemWidth();
this.items.forEach(item => item.maxWidth = maxItemWidth); this.leftItems.forEach(item => item.maxWidth = maxItemWidth);
this.rightItems.forEach(item => item.maxWidth = maxItemWidth);
} }
private getMaxItemWidth() { private getMaxItemWidth() {
...@@ -136,22 +138,44 @@ export class CellEditorStatusBar extends Disposable { ...@@ -136,22 +138,44 @@ export class CellEditorStatusBar extends Disposable {
} }
private updateRenderedItems(): void { private updateRenderedItems(): void {
DOM.clearNode(this.leftContributedItemsContainer);
DOM.clearNode(this.rightContributedItemsContainer);
const items = this.currentContext!.cell.getCellStatusBarItems(); const items = this.currentContext!.cell.getCellStatusBarItems();
items.sort((itemA, itemB) => { items.sort((itemA, itemB) => {
return (itemB.priority ?? 0) - (itemA.priority ?? 0); return (itemB.priority ?? 0) - (itemA.priority ?? 0);
}); });
const maxItemWidth = this.getMaxItemWidth(); const maxItemWidth = this.getMaxItemWidth();
const leftItems = items.filter(item => item.alignment === CellStatusbarAlignment.Left) const newLeftItems = items.filter(item => item.alignment === CellStatusbarAlignment.Left);
.map(item => this.itemsDisposable.add(this._instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item, maxItemWidth))); const newRightItems = items.filter(item => item.alignment === CellStatusbarAlignment.Right).reverse();
const rightItems = items.filter(item => item.alignment === CellStatusbarAlignment.Right).reverse()
.map(item => this.itemsDisposable.add(this._instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item, maxItemWidth))); const updateItems = (renderedItems: CellStatusBarItem[], newItems: INotebookCellStatusBarItem[], container: HTMLElement) => {
leftItems.forEach(itemView => this.leftContributedItemsContainer.appendChild(itemView.container)); if (renderedItems.length > newItems.length) {
rightItems.forEach(itemView => this.rightContributedItemsContainer.appendChild(itemView.container)); const deleted = renderedItems.splice(newItems.length, renderedItems.length - newItems.length);
this.items = [...leftItems, ...rightItems]; for (let deletedItem of deleted) {
container.removeChild(deletedItem.container);
deletedItem.dispose();
}
}
newItems.forEach((newLeftItem, i) => {
const existingItem = renderedItems[i];
if (existingItem) {
existingItem.updateItem(newLeftItem, maxItemWidth);
} else {
const item = this._instantiationService.createInstance(CellStatusBarItem, this.currentContext!, newLeftItem, maxItemWidth);
renderedItems.push(item);
container.appendChild(item.container);
}
});
};
updateItems(this.leftItems, newLeftItems, this.leftItemsContainer);
updateItems(this.rightItems, newRightItems, this.rightItemsContainer);
}
override dispose() {
super.dispose();
dispose(this.leftItems);
dispose(this.rightItems);
} }
} }
...@@ -163,9 +187,12 @@ class CellStatusBarItem extends Disposable { ...@@ -163,9 +187,12 @@ class CellStatusBarItem extends Disposable {
this.container.style.maxWidth = v + 'px'; this.container.style.maxWidth = v + 'px';
} }
private _currentItem!: INotebookCellStatusBarItem;
private _itemDisposables = this._register(new DisposableStore());
constructor( constructor(
private readonly _context: INotebookCellActionContext, private readonly _context: INotebookCellActionContext,
private readonly _itemModel: INotebookCellStatusBarItem, itemModel: INotebookCellStatusBarItem,
maxWidth: number | undefined, maxWidth: number | undefined,
@ITelemetryService private readonly _telemetryService: ITelemetryService, @ITelemetryService private readonly _telemetryService: ITelemetryService,
@ICommandService private readonly _commandService: ICommandService, @ICommandService private readonly _commandService: ICommandService,
...@@ -174,32 +201,27 @@ class CellStatusBarItem extends Disposable { ...@@ -174,32 +201,27 @@ class CellStatusBarItem extends Disposable {
) { ) {
super(); super();
const resolveColor = (color: ThemeColor | string) => { this.updateItem(itemModel, maxWidth);
return isThemeColor(color) ? }
this._themeService.getColorTheme().getColor(color.id)?.toString() :
color;
};
if (this._itemModel.text) { updateItem(item: INotebookCellStatusBarItem, maxWidth: number | undefined) {
new SimpleIconLabel(this.container).text = this._itemModel.text.replace(/\n/g, ' '); this._itemDisposables.clear();
}
if (this._itemModel.color) { if (!this._currentItem || this._currentItem.text !== item.text) {
this.container.style.color = resolveColor(this._itemModel.color) || ''; new SimpleIconLabel(this.container).text = item.text.replace(/\n/g, ' ');
} }
if (this._itemModel.backgroundColor) { const resolveColor = (color: ThemeColor | string) => {
const colorResult = resolveColor(this._itemModel.backgroundColor); return isThemeColor(color) ?
this.container.style.backgroundColor = colorResult || ''; (this._themeService.getColorTheme().getColor(color.id)?.toString() || '') :
} color;
};
if (this._itemModel.opacity) { this.container.style.color = item.color ? resolveColor(item.color) : '';
this.container.style.opacity = this._itemModel.opacity; this.container.style.backgroundColor = item.backgroundColor ? resolveColor(item.backgroundColor) : '';
} this.container.style.opacity = item.opacity ? item.opacity : '';
if (this._itemModel.onlyShowWhenActive) { this.container.classList.toggle('cell-status-item-show-when-active', !!item.onlyShowWhenActive);
this.container.classList.add('cell-status-item-show-when-active');
}
if (typeof maxWidth === 'number') { if (typeof maxWidth === 'number') {
this.maxWidth = maxWidth; this.maxWidth = maxWidth;
...@@ -207,41 +229,39 @@ class CellStatusBarItem extends Disposable { ...@@ -207,41 +229,39 @@ class CellStatusBarItem extends Disposable {
let ariaLabel: string; let ariaLabel: string;
let role: string | undefined; let role: string | undefined;
if (this._itemModel.accessibilityInformation) { if (item.accessibilityInformation) {
ariaLabel = this._itemModel.accessibilityInformation.label; ariaLabel = item.accessibilityInformation.label;
role = this._itemModel.accessibilityInformation.role; role = item.accessibilityInformation.role;
} else { } else {
ariaLabel = this._itemModel.text ? stripIcons(this._itemModel.text).trim() : ''; ariaLabel = item.text ? stripIcons(item.text).trim() : '';
}
if (ariaLabel) {
this.container.setAttribute('aria-label', ariaLabel);
} }
if (role) { this.container.setAttribute('aria-label', ariaLabel);
this.container.setAttribute('role', role); this.container.setAttribute('role', role || '');
} this.container.title = item.tooltip ?? '';
this.container.title = this._itemModel.tooltip ?? ''; this.container.classList.toggle('cell-status-item-has-command', !!item.command);
if (item.command) {
if (this._itemModel.command) {
this.container.classList.add('cell-status-item-has-command');
this.container.tabIndex = 0; this.container.tabIndex = 0;
this._register(DOM.addDisposableListener(this.container, DOM.EventType.CLICK, _e => { this._itemDisposables.add(DOM.addDisposableListener(this.container, DOM.EventType.CLICK, _e => {
this.executeCommand(); this.executeCommand();
})); }));
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, e => { this._itemDisposables.add(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e); const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
this.executeCommand(); this.executeCommand();
} }
})); }));
} else {
this.container.tabIndex = -1;
} }
this._currentItem = item;
} }
private async executeCommand(): Promise<void> { private async executeCommand(): Promise<void> {
const command = this._itemModel.command; const command = this._currentItem.command;
if (!command) { if (!command) {
return; return;
} }
......
...@@ -35,6 +35,7 @@ import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/edit ...@@ -35,6 +35,7 @@ import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/edit
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection';
import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { groupByNumber } from 'vs/base/common/collections';
export interface INotebookEditorViewState { export interface INotebookEditorViewState {
editingCells: { [key: number]: boolean }; editingCells: { [key: number]: boolean };
...@@ -732,20 +733,14 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD ...@@ -732,20 +733,14 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
} }
deltaCellStatusBarItems(oldItems: string[], newItems: INotebookDeltaCellStatusBarItems[]): string[] { deltaCellStatusBarItems(oldItems: string[], newItems: INotebookDeltaCellStatusBarItems[]): string[] {
oldItems.forEach(id => { const deletesByHandle = groupByNumber(oldItems, id => this._statusBarItemIdToCellMap.get(id) ?? -1);
const handle = this._statusBarItemIdToCellMap.get(id);
if (handle !== undefined) {
const cell = this.getCellByHandle(handle);
cell?.deltaCellStatusBarItems([id], []);
}
});
const result: string[] = []; const result: string[] = [];
newItems.forEach(itemDelta => { newItems.forEach(itemDelta => {
const cell = this.getCellByHandle(itemDelta.handle); const cell = this.getCellByHandle(itemDelta.handle);
const ret = cell?.deltaCellStatusBarItems([], itemDelta.items) || []; const deleted = deletesByHandle.get(itemDelta.handle) ?? [];
deletesByHandle.delete(itemDelta.handle);
const ret = cell?.deltaCellStatusBarItems(deleted, itemDelta.items) || [];
ret.forEach(id => { ret.forEach(id => {
this._statusBarItemIdToCellMap.set(id, itemDelta.handle); this._statusBarItemIdToCellMap.set(id, itemDelta.handle);
}); });
...@@ -753,6 +748,11 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD ...@@ -753,6 +748,11 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
result.push(...ret); result.push(...ret);
}); });
deletesByHandle.forEach((ids, handle) => {
const cell = this.getCellByHandle(handle);
cell?.deltaCellStatusBarItems(ids, []);
});
return result; return result;
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册