Revert "feat: optimize tree performance" (#3783)

This commit is contained in:
野声 2024-06-17 14:31:42 +08:00 committed by GitHub
parent ef65ac49e4
commit 6273dbac4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 188 additions and 357 deletions

View File

@ -1,132 +0,0 @@
import { treePath } from '@opensumi/ide-components/lib/recycle-tree/path';
import { Path, findCommonRoot } from '@opensumi/ide-utils/lib/path';
function getCommonPathsLegacy(paths: string[]) {
if (!paths.length) {
return '';
}
// 根据路径层级深度进行排序
paths = paths.sort((a, b) => {
const depthA = Path.pathDepth(a);
const depthB = Path.pathDepth(b);
return depthA - depthB;
});
if (paths.length === 1 || Path.pathDepth(paths[0]) === 1) {
// 说明刷新队列中包含根节点,直接返回根节点进行刷新
return paths[0];
}
const sortedPaths = paths.map((p) => new Path(p));
let rootPath = sortedPaths[0];
for (let i = 1, len = sortedPaths.length; i < len; i++) {
if (rootPath.isEqualOrParent(sortedPaths[i])) {
continue;
} else {
while (!rootPath.isRoot) {
rootPath = rootPath.dir;
if (!rootPath || rootPath.isEqualOrParent(sortedPaths[i])) {
break;
}
}
}
}
if (rootPath) {
return rootPath.toString();
}
return '';
}
function relativeLegacy(parent: string, child: string) {
return new Path(parent).relative(new Path(child))?.toString() as string;
}
function parseLegacy(path: string) {
const _path = new Path(path);
return {
basename: _path.base,
dirname: _path.dir.toString(),
};
}
describe('tree path', () => {
it('get common paths', () => {
const paths = ['/a/b/c', '/a/b/d', '/a/b/e', '/a/b/f'];
expect(getCommonPathsLegacy(paths)).toBe('/a/b');
expect(findCommonRoot(paths)).toBe('/a/b');
const testcases = [
{
paths: ['/a/b/c', '/a/b/d', '/a/b/e', '/a/b/f'],
result: '/a/b',
},
{
paths: ['/a/b/c', '/a/b/d', '/a/b/e', '/a/b/f', '/a/b', '/a/c'],
result: '/a',
},
];
for (const testcase of testcases) {
expect(getCommonPathsLegacy(testcase.paths)).toBe(testcase.result);
expect(findCommonRoot(testcase.paths)).toBe(testcase.result);
}
});
it('get common paths only root', () => {
const paths = ['/a', '/b'];
expect(getCommonPathsLegacy(paths)).toBe('/');
expect(findCommonRoot(paths)).toBe('');
});
it('relative path', () => {
const testcases = [
['/a/b/c', '/a/b/d', undefined],
['/a/b/c', '/a/b', undefined],
['/a/b/c', '/a/b/c', ''],
['/a/b', '/a/b/c', 'c'],
['/a/b/', '/a/b/c', 'c'],
['/a/b', '/a/b/c/d', 'c/d'],
['/a/b/', '/a/b/c/d', 'c/d'],
] as [string, string, string | undefined][];
for (const testcase of testcases) {
const result0 = relativeLegacy(testcase[0], testcase[1]);
const result1 = treePath.relative(testcase[0], testcase[1]);
expect(result0).toBe(testcase[2]);
expect(result1).toBe(testcase[2]);
}
});
it('can parse path', () => {
const testcases = [
{
source: '/a/b/c',
result: {
basename: 'c',
dirname: '/a/b',
},
},
{
source: '/a/b/c/',
result: {
basename: '',
dirname: '/a/b/c',
},
},
{
source: 'asfd/w2er/23r.txt',
result: {
basename: '23r.txt',
dirname: 'asfd/w2er',
},
},
];
for (const testcase of testcases) {
const result = treePath.parse(testcase.source);
const resultLegacy = parseLegacy(testcase.source);
expect(result).toEqual(resultLegacy);
expect(result).toEqual(testcase.result);
}
});
});

View File

@ -1,28 +0,0 @@
import { Path } from '@opensumi/ide-utils/lib/path';
function parse(path: string): { dirname: string; basename: string } {
const parts = path.split(Path.separator);
const basename = parts.pop() || '';
return { dirname: parts.join(Path.separator), basename };
}
function relative(parent: string, child: string): string | undefined {
if (parent === child) {
return '';
}
if (!parent || !child) {
return undefined;
}
const raw = !parent.endsWith(Path.separator) ? parent + Path.separator : parent;
if (!child.startsWith(raw)) {
return undefined;
}
const relativePath = child.substring(raw.length);
return relativePath;
}
export const treePath = {
parse,
relative,
};

View File

@ -9,10 +9,7 @@ import {
isUndefined,
path,
} from '@opensumi/ide-utils';
import { findCommonRoot, sortPathByDepth } from '@opensumi/ide-utils/lib/path';
import { warning } from '../../utils';
import { treePath } from '../path';
import {
IAccessibilityInformation,
ICompositeTreeNode,
@ -69,13 +66,11 @@ export interface IOptionalGlobalTreeState {
loadPathCancelToken?: CancellationTokenSource;
}
const nextIdFactory = () => {
let id = 0;
return () => id++;
};
export class TreeNode implements ITreeNode {
public static nextId = nextIdFactory();
public static nextId = (() => {
let id = 0;
return () => id++;
})();
public static is(node: any): node is ITreeNode {
return !!node && 'depth' in node && 'name' in node && 'path' in node && 'id' in node;
@ -165,14 +160,14 @@ export class TreeNode implements ITreeNode {
tree: ITree,
parent?: ICompositeTreeNode,
watcher?: ITreeWatcher,
metadata?: { [key: string]: any },
optionalMetadata?: { [key: string]: any },
) {
this._uid = TreeNode.nextId();
this._parent = parent;
this._tree = tree;
this._disposed = false;
this._visible = true;
this._metadata = { ...(metadata || {}) };
this._metadata = { ...(optionalMetadata || {}) };
this._depth = parent ? parent.depth + 1 : 0;
if (watcher) {
this._watcher = watcher;
@ -224,51 +219,30 @@ export class TreeNode implements ITreeNode {
* `name`
*/
get name() {
// 根节点保证唯一性
if (!this.parent) {
return `root_${this.id}`;
}
let name = this.getMetadata('name') as string;
this.validateName(name);
if (!name) {
name = String(TreeNode.nextId());
this._metadata['name'] = name;
}
return name;
return this.getMetadata('name') || String(this.id);
}
set name(name: string) {
this.validateName(name);
this.addMetadata('name', name);
// 节点 `name` 变化,更新当前节点的 `path` 属性
this._path = '';
}
protected validateName(name: string) {
if (!name) {
return;
}
warning(!name.includes(Path.separator), `[TreeNode] name should not include path separator: ${name}`);
}
// 节点绝对路径
get path(): string {
if (!this._path) {
if (this.parent) {
this._path = Path.joinPath(this.parent.path, this.name);
if (!this.parent) {
this._path = new Path(`${Path.separator}${this.name}`).toString();
} else {
this._path = `${Path.separator}${this.name}`;
this._path = new Path(this.parent.path).join(this.name).toString();
}
}
return this._path;
}
public joinPath(...paths: string[]) {
return Path.joinPath(this.path, ...paths);
}
get accessibilityInformation(): IAccessibilityInformation {
return {
label: this.name,
@ -277,6 +251,9 @@ export class TreeNode implements ITreeNode {
}
public getMetadata(withKey: string): any {
if (withKey === 'name' && !this._metadata[withKey]) {
this._metadata[withKey] = String(TreeNode.nextId());
}
return this._metadata[withKey];
}
@ -304,7 +281,7 @@ export class TreeNode implements ITreeNode {
type: MetadataChangeType.Removed,
key: withKey,
prevValue,
value: undefined,
value: void 0,
});
}
}
@ -479,9 +456,14 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
return watcher;
}
// parent 为 undefined 即表示该节点为根节点
constructor(tree: ITree, parent?: ICompositeTreeNode, watcher?: ITreeWatcher, metadata?: { [key: string]: any }) {
super(tree, parent, watcher, metadata);
// parent 为undefined即表示该节点为根节点
constructor(
tree: ITree,
parent?: ICompositeTreeNode,
watcher?: ITreeWatcher,
optionalMetadata?: { [key: string]: any },
) {
super(tree, parent, watcher, optionalMetadata);
this.isExpanded = parent ? false : true;
this._branchSize = 0;
if (!parent) {
@ -495,28 +477,28 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
}
}
/**
* setter getter
*/
get name() {
return super.name;
}
/**
* name setter
*/
// 重载 name 的 getter/setter路径改变时需要重新监听文件节点变化
set name(name: string) {
const prevPath = this.path;
if (!CompositeTreeNode.isRoot(this) && typeof this.watchTerminator === 'function') {
this.watchTerminator(prevPath);
this.addMetadata('name', name);
this.watchTerminator = this.watcher.onWatchEvent(this.path, this.handleWatchEvent);
} else {
this.addMetadata('name', name);
}
this.addMetadata('name', name);
// 节点 `name` 变化,更新当前节点的 `path` 属性
this._path = '';
}
get name() {
// 根节点保证路径不重复
if (!this.parent) {
return `root_${this.id}`;
}
return this.getMetadata('name');
}
// 作为根节点唯一的watcher需要在生成新节点的时候传入
get watcher() {
return this._watcher;
@ -1055,7 +1037,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
if (!CompositeTreeNode.isRoot(this)) {
return;
}
collapsedPaths = sortPathByDepth(collapsedPaths);
collapsedPaths = collapsedPaths.sort((a, b) => Path.pathDepth(a) - Path.pathDepth(b));
let path;
while (collapsedPaths.length > 0) {
path = collapsedPaths.pop();
@ -1073,10 +1055,10 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
if (!CompositeTreeNode.isRoot(this)) {
return;
}
expandedPaths = sortPathByDepth(expandedPaths);
let path: string;
expandedPaths = expandedPaths.sort((a, b) => Path.pathDepth(a) - Path.pathDepth(b));
let path;
while (expandedPaths.length > 0) {
path = expandedPaths.pop()!;
path = expandedPaths.pop();
const item = TreeNode.getTreeNodeByPath(path);
if (CompositeTreeNode.is(item)) {
(item as CompositeTreeNode).setCollapsed(true);
@ -1247,24 +1229,24 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
*
*/
private transferItem(oldPath: string, newPath: string) {
const oldP = treePath.parse(oldPath);
const from = oldP.dirname;
const oldP = new Path(oldPath);
const from = oldP.dir.toString();
if (from !== this.path) {
return;
}
const name = oldP.basename;
const name = oldP.base.toString();
const item = this._children?.find((c) => c.name === name);
if (!item) {
return;
}
const newP = treePath.parse(newPath);
const to = newP.dirname;
const newP = new Path(newPath);
const to = newP.dir.toString();
const destDir = to === from ? this : TreeNode.getTreeNodeByPath(to);
if (!CompositeTreeNode.is(destDir)) {
this.unlinkItem(item);
return;
}
item.mv(destDir, newP.basename);
item.mv(destDir, newP.base.toString());
return item;
}
@ -1480,9 +1462,11 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
}
public removeNode(path: string) {
const { basename, dirname } = treePath.parse(path);
if (dirname === this.path && !!this.children) {
const item = this.children.find((c) => c.name === basename);
const pathObject = new Path(path);
const dirName = pathObject.dir.toString();
const name = pathObject.base.toString();
if (dirName === this.path && !!this.children) {
const item = this.children.find((c) => c.name === name);
if (item) {
this.unlinkItem(item);
}
@ -1527,10 +1511,11 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
case WatchEvent.Removed: {
const { path } = event;
const { basename, dirname } = treePath.parse(path);
if (dirname === this.path && !!this.children) {
const item = this.children.find((c) => c.name === basename);
const pathObject = new Path(path);
const dirName = pathObject.dir.toString();
const name = pathObject.base.toString();
if (dirName === this.path && !!this.children) {
const item = this.children.find((c) => c.name === name);
if (item) {
this.unlinkItem(item);
}
@ -1597,20 +1582,39 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
}
private getRefreshNode() {
const paths = sortPathByDepth(Array.from(this.toRefreshPathQueue));
let paths = Array.from(this.toRefreshPathQueue);
this.toRefreshPathQueue.clear();
if (!paths.length) {
return this.root;
}
// 根据路径层级深度进行排序
paths = paths.sort((a, b) => {
const depthA = Path.pathDepth(a);
const depthB = Path.pathDepth(b);
return depthA - depthB;
});
if (paths.length === 1 || Path.pathDepth(paths[0]) === 1) {
// 说明刷新队列中包含根节点,直接返回根节点进行刷新
return TreeNode.getTreeNodeByPath(paths[0]);
}
const rootPath = findCommonRoot(paths);
if (rootPath) {
return TreeNode.getTreeNodeByPath(rootPath);
const sortedPaths = paths.map((p) => new Path(p));
let rootPath = sortedPaths[0];
for (let i = 1, len = sortedPaths.length; i < len; i++) {
if (rootPath.isEqualOrParent(sortedPaths[i])) {
continue;
} else {
while (!rootPath.isRoot) {
rootPath = rootPath.dir;
if (!rootPath || rootPath.isEqualOrParent(sortedPaths[i])) {
break;
}
}
}
}
if (rootPath) {
return TreeNode.getTreeNodeByPath(rootPath.toString());
}
return this.root;
}
@ -1637,13 +1641,15 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
}
private transformToRelativePath(path: string): string[] {
const pathFlag = Path.splitPath(path);
const { splitPath } = Path;
const pathFlag = splitPath(path);
pathFlag.shift();
return pathFlag;
}
/**
*
* @memberof CompositeTreeNode
*/
public async loadTreeNodeByPath(path: string, quiet = false): Promise<ITreeNodeOrCompositeTreeNode | undefined> {
if (!CompositeTreeNode.isRoot(this)) {
@ -1807,6 +1813,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
* ID下标位置
* @param {number} id
* @returns
* @memberof CompositeTreeNode
*/
public getIndexAtTreeNodeId(id: number) {
if (this._flattenedBranch) {
@ -1819,6 +1826,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
*
* @param {ITreeNodeOrCompositeTreeNode} node
* @returns
* @memberof CompositeTreeNode
*/
public getIndexAtTreeNode(node: ITreeNodeOrCompositeTreeNode) {
if (this._flattenedBranch) {
@ -1831,6 +1839,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
*
* @param {number} index
* @returns
* @memberof CompositeTreeNode
*/
public getTreeNodeAtIndex(index: number) {
const id = this._flattenedBranch?.[index];
@ -1844,6 +1853,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
* ID获取节点
* @param {number} id
* @returns
* @memberof CompositeTreeNode
*/
public getTreeNodeById(id: number) {
return TreeNode.getTreeNodeById(id);
@ -1853,6 +1863,7 @@ export class CompositeTreeNode extends TreeNode implements ICompositeTreeNode {
*
* @param {string} path
* @returns
* @memberof CompositeTreeNode
*/
public getTreeNodeByPath(path: string) {
return TreeNode.getTreeNodeByPath(path);

View File

@ -1,11 +1,12 @@
import { Emitter, Event } from '@opensumi/ide-utils';
import { Emitter, Event, path } from '@opensumi/ide-utils';
import { treePath } from '../../../path';
import { ITreeNodeOrCompositeTreeNode, TreeNodeEvent } from '../../../types';
import { CompositeTreeNode, TreeNode } from '../../TreeNode';
import { ISerializableState } from './types';
const { Path } = path;
export enum Operation {
SetExpanded = 1,
SetCollapsed,
@ -118,6 +119,8 @@ export class TreeStateManager {
/**
*
* @private
* @memberof TreeStateManager
*/
private handleExpansionChange = (target: CompositeTreeNode, isExpanded: boolean, isVisibleAtSurface: boolean) => {
if (this.stashing && this.stashKeyframes) {
@ -152,7 +155,7 @@ export class TreeStateManager {
}
let relativePath = this.expandedDirectories.get(target);
if (isExpanded && !relativePath) {
relativePath = treePath.relative(this.root.path, target.path)!;
relativePath = new Path(this.root.path).relative(new Path(target.path))?.toString() as string;
this.expandedDirectories.set(target, relativePath);
this.onDidChangeExpansionStateEmitter.fire({ relativePath, isExpanded, isVisibleAtSurface });
} else if (!isExpanded && relativePath) {
@ -164,7 +167,7 @@ export class TreeStateManager {
private handleDidChangePath = (target: CompositeTreeNode) => {
if (this.expandedDirectories.has(target)) {
const prevPath = this.expandedDirectories.get(target) as string;
const newPath = treePath.relative(this.root.path, target.path)!;
const newPath = new Path(this.root.path).relative(new Path(target.path))?.toString() as string;
this.expandedDirectories.set(target, newPath);
this.onDidChangeRelativePathEmitter.fire({ prevPath, newPath });
}

View File

@ -113,31 +113,32 @@ export interface ITreeWatcher {
on(event: TreeNodeEvent, callback: any): IDisposable;
// 事件分发
notifyWillChangeParent(
target: ITreeNodeOrCompositeTreeNode,
prevParent: ICompositeTreeNode,
newParent: ICompositeTreeNode,
): void;
);
notifyDidChangeParent(
target: ITreeNodeOrCompositeTreeNode,
prevParent: ICompositeTreeNode,
newParent: ICompositeTreeNode,
): void;
notifyDidDispose(target: ITreeNodeOrCompositeTreeNode): void;
);
notifyDidDispose(target: ITreeNodeOrCompositeTreeNode);
notifyWillProcessWatchEvent(target: ICompositeTreeNode, event: IWatcherEvent): void;
notifyDidProcessWatchEvent(target: ICompositeTreeNode, event: IWatcherEvent): void;
notifyWillProcessWatchEvent(target: ICompositeTreeNode, event: IWatcherEvent);
notifyDidProcessWatchEvent(target: ICompositeTreeNode, event: IWatcherEvent);
notifyWillChangeExpansionState(target: ICompositeTreeNode, nowExpanded: boolean): void;
notifyDidChangeExpansionState(target: ICompositeTreeNode, nowExpanded: boolean): void;
notifyWillChangeExpansionState(target: ICompositeTreeNode, nowExpanded: boolean);
notifyDidChangeExpansionState(target: ICompositeTreeNode, nowExpanded: boolean);
notifyWillResolveChildren(target: ICompositeTreeNode, nowExpanded: boolean): void;
notifyDidResolveChildren(target: ICompositeTreeNode, nowExpanded: boolean): void;
notifyWillResolveChildren(target: ICompositeTreeNode, nowExpanded: boolean);
notifyDidResolveChildren(target: ICompositeTreeNode, nowExpanded: boolean);
notifyDidChangePath(target: ITreeNodeOrCompositeTreeNode): void;
notifyDidChangeMetadata(target: ITreeNodeOrCompositeTreeNode, change: IMetadataChange): void;
notifyDidChangePath(target: ITreeNodeOrCompositeTreeNode);
notifyDidChangeMetadata(target: ITreeNodeOrCompositeTreeNode, change: IMetadataChange);
notifyDidUpdateBranch(): void;
notifyDidUpdateBranch();
dispose: IDisposable;
}

View File

@ -6,8 +6,7 @@ import {
TreeNodeEvent,
WatchEvent,
} from '@opensumi/ide-components';
import { Deferred, DisposableCollection, Emitter, Event, pSeries } from '@opensumi/ide-core-browser';
import { sortPathByDepth } from '@opensumi/ide-utils/lib/path';
import { Deferred, DisposableCollection, Emitter, Event, pSeries, path } from '@opensumi/ide-core-browser';
import { DebugVariable, ExpressionContainer, ExpressionNode } from '../tree/debug-tree-node.define';
import styles from '../view/variables/debug-variables.module.less';
@ -15,6 +14,8 @@ import styles from '../view/variables/debug-variables.module.less';
import { DebugHoverModel } from './debug-hover-model';
import { DebugHoverSource, ExpressionVariable } from './debug-hover-source';
const { Path } = path;
export interface IDebugVariablesHandle extends IRecycleTreeHandle {
hasDirectFocus: () => boolean;
}
@ -292,8 +293,11 @@ export class DebugHoverTreeModelService {
if (!this._changeEventDispatchQueue || this._changeEventDispatchQueue.length === 0) {
return;
}
this._changeEventDispatchQueue = sortPathByDepth(this._changeEventDispatchQueue);
this._changeEventDispatchQueue.sort((pathA, pathB) => {
const pathADepth = Path.pathDepth(pathA);
const pathBDepth = Path.pathDepth(pathB);
return pathADepth - pathBDepth;
});
const roots = [this._changeEventDispatchQueue[0]];
for (const path of this._changeEventDispatchQueue) {
if (roots.some((root) => path.indexOf(root) === 0)) {

View File

@ -17,9 +17,9 @@ import {
IClipboardService,
IContextKeyService,
pSeries,
path,
} from '@opensumi/ide-core-browser';
import { AbstractContextMenuService, ICtxMenuRenderer, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
import { sortPathByDepth } from '@opensumi/ide-utils/lib/path';
import { IDebugConsoleModelService, IDebugSessionManager } from '../../../common';
import { LinkDetector } from '../../debug-link-detector';
@ -34,6 +34,7 @@ import { DebugConsoleTreeModel } from './debug-console-model';
import { DebugConsoleSession } from './debug-console-session';
import styles from './debug-console.module.less';
const { Path } = path;
export interface IDebugConsoleHandle extends IRecycleTreeHandle {
hasDirectFocus: () => boolean;
}
@ -499,8 +500,11 @@ export class DebugConsoleModelService implements IDebugConsoleModelService {
if (!this._changeEventDispatchQueue || this._changeEventDispatchQueue.length === 0) {
return;
}
this._changeEventDispatchQueue = sortPathByDepth(this._changeEventDispatchQueue);
this._changeEventDispatchQueue.sort((pathA, pathB) => {
const pathADepth = Path.pathDepth(pathA);
const pathBDepth = Path.pathDepth(pathB);
return pathADepth - pathBDepth;
});
const roots = [this._changeEventDispatchQueue[0]];
for (const path of this._changeEventDispatchQueue) {
if (roots.some((root) => path.indexOf(root) === 0)) {

View File

@ -27,7 +27,6 @@ import {
path,
} from '@opensumi/ide-core-browser';
import { AbstractContextMenuService, ICtxMenuRenderer, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
import { sortPathByDepth } from '@opensumi/ide-utils/lib/path';
import { DebugSessionManager } from '../../debug-session-manager';
import { DebugWatch } from '../../model';
@ -537,8 +536,11 @@ export class DebugWatchModelService {
if (!this._changeEventDispatchQueue || this._changeEventDispatchQueue.length === 0) {
return;
}
this._changeEventDispatchQueue = sortPathByDepth(this._changeEventDispatchQueue);
this._changeEventDispatchQueue.sort((pathA, pathB) => {
const pathADepth = Path.pathDepth(pathA);
const pathBDepth = Path.pathDepth(pathB);
return pathADepth - pathBDepth;
});
const roots = [this._changeEventDispatchQueue[0]];
for (const path of this._changeEventDispatchQueue) {
if (roots.some((root) => path.indexOf(root) === 0)) {

View File

@ -89,11 +89,11 @@ export class MarkerModelService {
}
async initTreeModel() {
const children = await this.markerService.resolveChildren();
if (!children) {
const childs = await this.markerService.resolveChildren();
if (!childs) {
return;
}
const root = children[0];
const root = childs[0];
if (!root) {
return;
}

View File

@ -1,11 +1,10 @@
import cls from 'classnames';
import { TreeNodeType } from '@opensumi/ide-components';
import { EDITOR_COMMANDS, Emitter, IEventBus, URI, sleep } from '@opensumi/ide-core-browser';
import { EDITOR_COMMANDS } from '@opensumi/ide-core-browser';
import { Emitter, IEventBus, URI } from '@opensumi/ide-core-browser';
import { IDecorationsService } from '@opensumi/ide-decoration';
import { FileDecorationsService } from '@opensumi/ide-decoration/lib/browser/decorationsService';
import { createBrowserInjector } from '@opensumi/ide-dev-tool/src/injector-helper';
import { MockInjector } from '@opensumi/ide-dev-tool/src/mock-injector';
import { IResource, ResourceDecorationChangeEvent, WorkbenchEditorService } from '@opensumi/ide-editor';
import { IEditorDocumentModelService, ResourceService } from '@opensumi/ide-editor/lib/browser';
import { MockWorkbenchEditorService } from '@opensumi/ide-editor/lib/common/mocks/workbench-editor.service';
@ -15,12 +14,16 @@ import { MockThemeService } from '@opensumi/ide-theme/lib/common/mocks/theme.ser
import { IWorkspaceService } from '@opensumi/ide-workspace';
import { MockWorkspaceService } from '@opensumi/ide-workspace/lib/common/mocks';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { MockInjector } from '../../../../tools/dev-tool/src/mock-injector';
import { OpenedEditorModule } from '../../src/browser';
import { EditorFile, OpenedEditorData } from '../../src/browser/opened-editor-node.define';
import { OpenedEditorModelService } from '../../src/browser/services/opened-editor-model.service';
import { OpenedEditorService } from '../../src/browser/services/opened-editor-tree.service';
import styles from '../src/browser/opened-editor-node.module.less';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe('OpenedEditorModelService should be work', () => {
let openedEditorModelService: OpenedEditorModelService;
let openedEditorService: OpenedEditorService;

View File

@ -20,14 +20,8 @@ export class EditorFileRoot extends CompositeTreeNode {
}
}
/**
* EditorFileGroup ,
*/
// EditorFileGroup 节点不包含父节点, 同时默认为展开状态
export class EditorFileGroup extends CompositeTreeNode {
static makeName(groupIndex: number) {
return formatLocalize('opened.editors.group.title', groupIndex + 1);
}
static isEditorGroup(data: OpenedEditorData): data is IEditorGroup {
return typeof (data as any).resources !== 'undefined';
}
@ -48,7 +42,7 @@ export class EditorFileGroup extends CompositeTreeNode {
}
get name() {
return EditorFileGroup.makeName(this.groupIndex);
return formatLocalize('opened.editors.group.title', this.groupIndex + 1);
}
get displayName() {
@ -69,10 +63,6 @@ export class EditorFile extends TreeNode {
return TreeNode.is(node) && 'uri' in node;
}
static makeName(uri: URI) {
return uri.toString().replace(/\//g, '_');
}
constructor(
tree: OpenedEditorService,
public readonly resource: IResource,
@ -80,7 +70,7 @@ export class EditorFile extends TreeNode {
public dirty: boolean = false,
parent: EditorFileGroup | undefined,
) {
super(tree as ITree, parent, undefined, { name: EditorFile.makeName(resource.uri) });
super(tree as ITree, parent, undefined, { name: `${resource.uri.toString()}` });
}
get displayName() {

View File

@ -12,6 +12,7 @@ import {
URI,
formatLocalize,
pSeries,
path,
} from '@opensumi/ide-core-browser';
import { AbstractContextMenuService, ICtxMenuRenderer, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
import { LabelService } from '@opensumi/ide-core-browser/lib/services';
@ -19,7 +20,6 @@ import { IEditorGroup, IResource, WorkbenchEditorService } from '@opensumi/ide-e
import { WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service';
import { EXPLORER_CONTAINER_ID } from '@opensumi/ide-explorer/lib/browser/explorer-contribution';
import { IMainLayoutService } from '@opensumi/ide-main-layout';
import { sortPathByDepth } from '@opensumi/ide-utils/lib/path';
import { ExplorerOpenedEditorViewId } from '../../common/index';
import { EditorFile, EditorFileGroup } from '../opened-editor-node.define';
@ -30,6 +30,8 @@ import { OpenedEditorEventService } from './opened-editor-event.service';
import { OpenedEditorModel } from './opened-editor-model';
import { OpenedEditorService } from './opened-editor-tree.service';
const { Path } = path;
export interface IEditorTreeHandle extends IRecycleTreeHandle {
hasDirectFocus: () => boolean;
}
@ -385,9 +387,11 @@ export class OpenedEditorModelService {
if (!this._changeEventDispatchQueue || this._changeEventDispatchQueue.length === 0) {
return;
}
this._changeEventDispatchQueue = sortPathByDepth(this._changeEventDispatchQueue);
this._changeEventDispatchQueue.sort((pathA, pathB) => {
const pathADepth = Path.pathDepth(pathA);
const pathBDepth = Path.pathDepth(pathB);
return pathADepth - pathBDepth;
});
const roots = [this._changeEventDispatchQueue[0]];
for (const path of this._changeEventDispatchQueue) {
if (roots.some((root) => path.indexOf(root) === 0)) {

View File

@ -1,6 +1,6 @@
import { Autowired, Injectable } from '@opensumi/di';
import { ITreeNodeOrCompositeTreeNode, Tree, TreeNodeType } from '@opensumi/ide-components';
import { Schemes, URI, path } from '@opensumi/ide-core-browser';
import { Schemes, URI, formatLocalize, path } from '@opensumi/ide-core-browser';
import { LabelService } from '@opensumi/ide-core-browser/lib/services';
import { IEditorGroup, IResource, ResourceService, WorkbenchEditorService } from '@opensumi/ide-editor';
import { IWorkspaceService } from '@opensumi/ide-workspace';
@ -23,7 +23,7 @@ export class OpenedEditorService extends Tree {
@Autowired(ResourceService)
private readonly resourceService: ResourceService;
private _dirtyUri = new Set<string>();
private _dirtyUri: string[] = [];
// 是否为分组的节点树
private _isGroupTree = false;
@ -33,13 +33,16 @@ export class OpenedEditorService extends Tree {
}
addDirtyUri(uri: string) {
if (!this._dirtyUri.has(uri)) {
this._dirtyUri.add(uri);
if (!this._dirtyUri.includes(uri)) {
this._dirtyUri.push(uri);
}
}
removeDirtyUri(uri: string) {
this._dirtyUri.delete(uri);
if (this._dirtyUri.includes(uri)) {
const index = this._dirtyUri.findIndex((value) => value === uri);
this._dirtyUri.splice(index, 1);
}
}
async resolveChildren(
@ -66,7 +69,7 @@ export class OpenedEditorService extends Tree {
this,
item as IResource,
tooltip,
this._dirtyUri.has((item as IResource).uri.toString()),
this._dirtyUri.includes((item as IResource).uri.toString()),
parent as EditorFileGroup,
),
);
@ -79,7 +82,9 @@ export class OpenedEditorService extends Tree {
} else {
for (const resource of (parent as EditorFileGroup).group.resources) {
const tooltip = await this.getReadableTooltip(resource.uri);
children.push(new EditorFile(this, resource, tooltip, this._dirtyUri.has(resource.uri.toString()), parent));
children.push(
new EditorFile(this, resource, tooltip, this._dirtyUri.includes(resource.uri.toString()), parent),
);
}
}
return children;
@ -105,38 +110,44 @@ export class OpenedEditorService extends Tree {
}
}
// numeric 参数确保数字为第一排序优先级
return a.name.localeCompare(b.name, 'kn', { numeric: true });
return a.name.localeCompare(b.name, 'kn', { numeric: true }) as any;
}
return a.type === TreeNodeType.CompositeTreeNode ? -1 : b.type === TreeNodeType.CompositeTreeNode ? 1 : 0;
};
public getEditorNodeByUri(resource?: IResource | URI, group?: IEditorGroup) {
let path = this.root!.path;
if (resource) {
let path: string;
if (this._isGroupTree) {
if (!group) {
return;
}
path = this.root!.joinPath(
EditorFileGroup.makeName(group.index),
EditorFile.makeName(
resource && (resource as IResource).uri ? (resource as IResource).uri : (resource as URI),
),
);
const groupName = formatLocalize('opened.editors.group.title', group.index + 1);
path = new Path(path)
.join(groupName)
.join(
resource && (resource as IResource).uri
? (resource as IResource).uri.toString()
: (resource as URI).toString(),
)
.toString();
} else {
path = this.root!.joinPath(
EditorFile.makeName(
resource && (resource as IResource).uri ? (resource as IResource).uri : (resource as URI),
),
);
path = new Path(path)
.join(
resource && (resource as IResource).uri
? (resource as IResource).uri.toString()
: (resource as URI).toString(),
)
.toString();
}
return this.root!.getTreeNodeByPath(path);
return this.root?.getTreeNodeByPath(path);
} else {
if (!group) {
return;
}
const path = this.root!.joinPath(EditorFileGroup.makeName(group.index));
return this.root!.getTreeNodeByPath(path);
const groupName = formatLocalize('opened.editors.group.title', group.index + 1);
path = new Path(path).join(groupName).toString();
return this.root?.getTreeNodeByPath(path);
}
}

View File

@ -16,6 +16,7 @@ import {
ThrottledDelayer,
URI,
pSeries,
path,
} from '@opensumi/ide-core-browser';
import { WorkbenchEditorService } from '@opensumi/ide-editor/lib/browser';
import {
@ -25,7 +26,6 @@ import {
import { EXPLORER_CONTAINER_ID } from '@opensumi/ide-explorer/lib/browser/explorer-contribution';
import { IMainLayoutService } from '@opensumi/ide-main-layout';
import * as monaco from '@opensumi/ide-monaco';
import { sortPathByDepth } from '@opensumi/ide-utils/lib/path';
import { IOutlineDecorationService, OUTLINE_VIEW_ID } from '../../common';
import { OutlineCompositeTreeNode, OutlineRoot, OutlineTreeNode } from '../outline-node.define';
@ -35,6 +35,8 @@ import { OutlineEventService } from './outline-event.service';
import { OutlineTreeModel } from './outline-model';
import { OutlineTreeService } from './outline-tree.service';
const { Path } = path;
export interface IEditorTreeHandle extends IRecycleTreeHandle {
hasDirectFocus: () => boolean;
}
@ -548,8 +550,11 @@ export class OutlineModelService {
if (!this._changeEventDispatchQueue || this._changeEventDispatchQueue.length === 0) {
return;
}
this._changeEventDispatchQueue = sortPathByDepth(this._changeEventDispatchQueue);
this._changeEventDispatchQueue.sort((pathA, pathB) => {
const pathADepth = Path.pathDepth(pathA);
const pathBDepth = Path.pathDepth(pathB);
return pathADepth - pathBDepth;
});
const roots = [this._changeEventDispatchQueue[0]];
for (const path of this._changeEventDispatchQueue) {
if (roots.some((root) => path.indexOf(root) === 0)) {

View File

@ -31,7 +31,6 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import { isWindows, path } from '../src';
import { sortPathByDepth } from '../src/path';
const { Path } = path;
@ -752,10 +751,4 @@ describe('Path.join', () => {
expect(path.join('./././', '././').toString()).toBe('/a/b/');
expect(path.join('../', './b/', './', '../', '../', './a').toString()).toBe('/a');
});
test('sort path by depth', () => {
const paths = ['/a/b/c', '/a', '/a/b', '/a/b/c/d', '/a/b/c/d/e', '/a/b/c/d/e/f'];
const result = sortPathByDepth(paths);
expect(result).toEqual(['/a', '/a/b', '/a/b/c', '/a/b/c/d', '/a/b/c/d/e', '/a/b/c/d/e/f']);
});
});

View File

@ -32,10 +32,6 @@ export class Path {
return path.split(Path.separator).filter((path) => !!path);
}
static joinPath(...parts: string[]): string {
return parts.join(Path.separator);
}
static isRelative(path: string): boolean {
return !path.startsWith(Path.separator);
}
@ -1887,39 +1883,3 @@ export function isValidBasename(name: string | null | undefined, isWindowsOS: bo
return true;
}
export function sortPathByDepth(paths: string[], sep = Path.separator) {
const depths = {} as Record<string, number>;
for (const path of paths) {
const parts = path.split(sep);
depths[path] = parts.length;
}
return paths.sort((a, b) => depths[a] - depths[b]);
}
export function findCommonRoot(paths: string[], sep = Path.separator) {
const [first = '', ...remaining] = paths;
if (first === '' || remaining.length === 0) {
return '';
}
const parts = first.split(sep);
let endOfPrefix = parts.length;
for (const path of remaining) {
const compare = path.split(sep);
for (let i = 0; i < endOfPrefix; i++) {
if (compare[i] !== parts[i]) {
endOfPrefix = i;
}
}
if (endOfPrefix === 0) {
return '';
}
}
return parts.slice(0, endOfPrefix).join(sep);
}