Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export * from './markdown';
export * from './settings';
export * from './utils';
export * from './clipboard';
export * from './mime';
8 changes: 8 additions & 0 deletions packages/core-common/src/mime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const Mimes = Object.freeze({
text: 'text/plain',
binary: 'application/octet-stream',
unknown: 'application/unknown',
markdown: 'text/markdown',
latex: 'text/latex',
uriList: 'text/uri-list',
});
45 changes: 31 additions & 14 deletions packages/extension/__tests__/browser/main.thread.treeview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const mockMenuRegistry = {
describe('MainThreadTreeView API Test Suite', () => {
let injector: MockInjector;
let mainThreadTreeView: MainThreadTreeView;
const testTreeViewId = 'testView';
const treeViewId = 'testView';
beforeEach(() => {
jest.clearAllMocks();

Expand Down Expand Up @@ -82,43 +82,60 @@ describe('MainThreadTreeView API Test Suite', () => {
);

mainThreadTreeView = injector.get(MainThreadTreeView, [mockProxy as any, 'node']);
mainThreadTreeView.$registerTreeDataProvider(testTreeViewId, {});
mainThreadTreeView.$registerTreeDataProvider(treeViewId, {
hasHandleDrop: false,
hasHandleDrag: false,
});
});

afterEach(() => {
mainThreadTreeView.$unregisterTreeDataProvider(testTreeViewId);
mainThreadTreeView.$unregisterTreeDataProvider(treeViewId);
});

it('should able to $registerTreeDataProvider', async () => {
it('$registerTreeDataProvider api should be worked', async () => {
expect(mockMainLayoutService.replaceViewComponent).toBeCalledTimes(1);
expect(mockMenuRegistry.registerMenuItem).toBeCalledTimes(0);

mainThreadTreeView.$registerTreeDataProvider('testView1', {
showCollapseAll: true,
hasHandleDrag: false,
hasHandleDrop: false,
});
expect(mockMainLayoutService.replaceViewComponent).toBeCalledTimes(2);
expect(mockMenuRegistry.registerMenuItem).toBeCalledTimes(1);
mainThreadTreeView.$unregisterTreeDataProvider('testView1');
});

it('should able to $refresh', async () => {
await mainThreadTreeView.$refresh(testTreeViewId);
it('$resolveDropFileData api should be worked', async () => {});

it('$refresh api should be worked', async () => {
await mainThreadTreeView.$refresh(treeViewId);
});

it('should able to $reveal', async () => {
await mainThreadTreeView.$reveal(testTreeViewId, 'treeItemId', {});
it('$reveal api should be worked', async () => {
await mainThreadTreeView.$reveal(treeViewId, 'treeItemId', {});
expect(mockMainLayoutService.revealView).toBeCalledTimes(1);
});

it('status listener should be work', () => {
it('$unregisterTreeDataProvider api should be worked', () => {
mainThreadTreeView.$unregisterTreeDataProvider(treeViewId);
expect(mockTabbarHandler.disposeView).toBeCalledTimes(1);
});

it('$resolveDropFileData api should be worked', async () => {
const unknownTreeViewId = 'unknown';
await expect(mainThreadTreeView.$resolveDropFileData(unknownTreeViewId, 0, '')).rejects.toThrowError(
'Unknown tree',
);
await expect(mainThreadTreeView.$resolveDropFileData(treeViewId, 0, '')).rejects.toThrowError(
'No data transfer found',
);
});

it('Active event should be effected', () => {
mockActiveEmitter.fire();
expect(mockExtThreadTreeViewProxy.$setVisible).toBeCalledTimes(1);
mockInActiveEmitter.fire();
expect(mockExtThreadTreeViewProxy.$setVisible).toBeCalledTimes(2);
});

it('should able to $unregisterTreeDataProvider', () => {
mainThreadTreeView.$unregisterTreeDataProvider(testTreeViewId);
expect(mockTabbarHandler.disposeView).toBeCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { Emitter, Disposable, CancellationTokenSource } from '@opensumi/ide-core-common';
import {
Emitter,
Disposable,
CancellationTokenSource,
CancellationToken,
uuid,
BinaryBuffer,
} from '@opensumi/ide-core-common';
import { ExtHostTreeViews } from '@opensumi/ide-extension/lib/hosted/api/vscode/ext.host.treeview';

import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
Expand All @@ -9,6 +16,7 @@ import { ExtHostCommands } from '../../../../src/hosted/api/vscode/ext.host.comm
const moackManThreadTreeView = {
$registerTreeDataProvider: jest.fn(),
$unregisterTreeDataProvider: jest.fn(() => Disposable.create(() => {})),
$resolveDropFileData: jest.fn(() => BinaryBuffer.alloc(10)),
};

const mockMainThreadCommandProxy = {
Expand Down Expand Up @@ -64,13 +72,13 @@ describe('extension/__tests__/hosted/api/vscode/ext.host.treeview.test.ts', () =

it('registerTreeDataProvider should be work', () => {
const treeViewId = 'registerTreeDataProvider-TreeViewId';
extHostTreeViews.registerTreeDataProvider(treeViewId, mockTreeDataProvider as any);
extHostTreeViews.registerTreeDataProvider(treeViewId, mockTreeDataProvider);
expect(moackManThreadTreeView.$registerTreeDataProvider).toBeCalledTimes(1);
});

it('resolveTreeItem should be work', async () => {
const treeViewId = 'registerTreeDataProvider-TreeViewId';
extHostTreeViews.registerTreeDataProvider(treeViewId, mockTreeDataProvider as any);
extHostTreeViews.registerTreeDataProvider(treeViewId, mockTreeDataProvider);
await extHostTreeViews.$getChildren(treeViewId);
extHostTreeViews.$resolveTreeItem(treeViewId, mockTreeViewItem.id, new CancellationTokenSource().token);
expect(mockTreeDataProvider.resolveTreeItem).toBeCalledTimes(1);
Expand Down Expand Up @@ -117,4 +125,57 @@ describe('extension/__tests__/hosted/api/vscode/ext.host.treeview.test.ts', () =
extHostTreeViews.$setVisible(treeViewId, true);
});
});

describe('Create TreeView with dragAndDropController', () => {
const treeViewId = 'dragAndDropTreeView-TreeViewId';
let treeView: TreeView<any>;
const mockDragAndDropController = {
dropMimeTypes: [],
dragMimeTypes: [],
handleDrag: jest.fn(),
handleDrop: jest.fn(),
} as any;
beforeAll(async () => {
treeView = extHostTreeViews.createTreeView<any>(treeViewId, {
treeDataProvider: mockTreeDataProvider,
dragAndDropController: mockDragAndDropController,
});
await extHostTreeViews.$getChildren(treeViewId);
});

it('$handleDrag method should be work', async () => {
// Unknown TreeView
await expect(
extHostTreeViews.$handleDrag('unknown', [mockTreeViewItem.id], uuid(), new CancellationTokenSource().token),
).rejects.toThrowError();
await extHostTreeViews.$handleDrag(
treeViewId,
[mockTreeViewItem.id],
uuid(),
new CancellationTokenSource().token,
);
expect(mockDragAndDropController.handleDrag).toBeCalledTimes(1);
});

it('$handleDrop method should be work', async () => {
// Unknown TreeView
await expect(
extHostTreeViews.$handleDrop(
'unknown',
1,
{ items: [] },
mockTreeViewItem.id,
new CancellationTokenSource().token,
),
).rejects.toThrowError();
await extHostTreeViews.$handleDrop(
treeViewId,
1,
{ items: [] },
mockTreeViewItem.id,
new CancellationTokenSource().token,
);
expect(mockDragAndDropController.handleDrop).toBeCalledTimes(1);
});
});
});
131 changes: 92 additions & 39 deletions packages/extension/src/browser/components/extension-tree-view-node.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cls from 'classnames';
import React from 'react';
import React, { CSSProperties, FC, MouseEvent, ReactNode, useCallback, useEffect, useState, DragEvent } from 'react';

import { INodeRendererProps, ClasslistComposite, PromptHandle, TreeNodeType } from '@opensumi/ide-components';
import { Loading } from '@opensumi/ide-components';
Expand All @@ -19,23 +19,21 @@ export interface ITreeViewNodeProps {
defaultLeftPadding?: number;
leftPadding?: number;
decorations?: ClasslistComposite;
onTwistierClick?: (
ev: React.MouseEvent,
item: ExtensionTreeNode | ExtensionCompositeTreeNode,
type: TreeNodeType,
) => void;
onClick: (ev: React.MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode, type: TreeNodeType) => void;
onContextMenu?: (
ev: React.MouseEvent,
item: ExtensionTreeNode | ExtensionCompositeTreeNode,
type: TreeNodeType,
) => void;
onTwistierClick?: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode, type: TreeNodeType) => void;
onClick: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode, type: TreeNodeType) => void;
onContextMenu?: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode, type: TreeNodeType) => void;
onDragStart?: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode) => void;
onDragEnter?: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode) => void;
onDrop?: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode) => void;
onDragOver?: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode) => void;
onDragLeave?: (ev: MouseEvent, item: ExtensionTreeNode | ExtensionCompositeTreeNode) => void;
decorationService: IDecorationsService;
draggable: boolean;
}

export type TreeViewNodeRenderedProps = ITreeViewNodeProps & INodeRendererProps;

export const TreeViewNode: React.FC<TreeViewNodeRenderedProps> = ({
export const TreeViewNode: FC<TreeViewNodeRenderedProps> = ({
item,
onClick,
onContextMenu,
Expand All @@ -46,10 +44,16 @@ export const TreeViewNode: React.FC<TreeViewNodeRenderedProps> = ({
defaultLeftPadding = 8,
treeViewId,
decorationService,
draggable,
onDragStart,
onDragEnter,
onDragLeave,
onDragOver,
onDrop,
}: TreeViewNodeRenderedProps) => {
const [decoration, setDecoration] = React.useState(item.uri && decorationService.getDecoration(item.uri, false));
const [decoration, setDecoration] = useState(item.uri && decorationService.getDecoration(item.uri, false));

React.useEffect(() => {
useEffect(() => {
const disposable = decorationService.onDidChangeDecorations((e) => {
if (item.uri && e.affectsResource(item.uri)) {
setDecoration(decorationService.getDecoration(item.uri, false));
Expand All @@ -62,30 +66,74 @@ export const TreeViewNode: React.FC<TreeViewNodeRenderedProps> = ({

const themeService = useInjectable<IThemeService>(IThemeService);

const handleClick = (ev: React.MouseEvent) => {
if (itemType === TreeNodeType.TreeNode || itemType === TreeNodeType.CompositeTreeNode) {
onClick(ev, item, itemType);
}
};

const handlerTwistierClick = (ev: React.MouseEvent) => {
if (itemType === TreeNodeType.TreeNode || itemType === TreeNodeType.CompositeTreeNode) {
if (onTwistierClick) {
onTwistierClick(ev, item, itemType);
} else {
const handleClick = useCallback(
(ev: MouseEvent) => {
if (itemType === TreeNodeType.TreeNode || itemType === TreeNodeType.CompositeTreeNode) {
onClick(ev, item, itemType);
}
}
};
},
[item, itemType, onClick],
);

const handleContextMenu = (ev: React.MouseEvent) => {
if (ev.nativeEvent.which === 0 || !onContextMenu) {
return;
}
if (itemType === TreeNodeType.TreeNode || itemType === TreeNodeType.CompositeTreeNode) {
onContextMenu(ev, item, itemType);
}
};
const handlerTwistierClick = useCallback(
(ev: MouseEvent) => {
if (itemType === TreeNodeType.TreeNode || itemType === TreeNodeType.CompositeTreeNode) {
if (onTwistierClick) {
onTwistierClick(ev, item, itemType);
} else {
onClick(ev, item, itemType);
}
}
},
[itemType, onTwistierClick, onClick],
);

const handleContextMenu = useCallback(
(ev: MouseEvent) => {
if (ev.nativeEvent.which === 0 || !onContextMenu) {
return;
}
if (itemType === TreeNodeType.TreeNode || itemType === TreeNodeType.CompositeTreeNode) {
onContextMenu(ev, item, itemType);
}
},
[item, itemType, onContextMenu],
);

const handleDragStart = useCallback(
(event: DragEvent) => {
onDragStart && onDragStart(event, item);
},
[item, onDragStart],
);

const handleDragEnter = useCallback(
(event: DragEvent) => {
onDragEnter && onDragEnter(event, item);
},
[item, onDragEnter],
);

const handleDragLeave = useCallback(
(event: DragEvent) => {
onDragLeave && onDragLeave(event, item);
},
[item, onDragLeave],
);

const handleDragOver = useCallback(
(event: DragEvent) => {
onDragOver && onDragOver(event, item);
},
[item, onDragOver],
);

const handleDrop = useCallback(
(event: DragEvent) => {
onDrop && onDrop(event, item);
},
[item, onDrop],
);

const isDirectory = itemType === TreeNodeType.CompositeTreeNode;
const paddingLeft = isDirectory
Expand All @@ -96,7 +144,7 @@ export const TreeViewNode: React.FC<TreeViewNodeRenderedProps> = ({
height: TREE_VIEW_NODE_HEIGHT,
lineHeight: `${TREE_VIEW_NODE_HEIGHT}px`,
paddingLeft,
} as React.CSSProperties;
} as CSSProperties;

const renderFolderToggle = (node: ExtensionCompositeTreeNode | PromptHandle, clickHandler: any) => {
if (decorations && decorations?.classlist.indexOf(styles.mod_loading) > -1) {
Expand All @@ -119,7 +167,7 @@ export const TreeViewNode: React.FC<TreeViewNodeRenderedProps> = ({
const renderDisplayName = (node: ExtensionCompositeTreeNode | ExtensionTreeNode) => {
const displayName = () => {
if (node.highlights) {
let hightlightSnaps: React.ReactNode[] = [];
let hightlightSnaps: ReactNode[] = [];
let endIndex = 0;
const hightlights = node.highlights.sort((a, b) => a[0] - b[0]);
hightlightSnaps = hightlights.map((highlight, index: number) => {
Expand Down Expand Up @@ -213,11 +261,16 @@ export const TreeViewNode: React.FC<TreeViewNodeRenderedProps> = ({
key={item.id}
onClick={handleClick}
onContextMenu={handleContextMenu}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
title={getItemTooltip()}
className={cls(styles.tree_view_node, decorations ? decorations.classlist : null)}
data-id={item.id}
style={fileTreeNodeStyle}
draggable={itemType === TreeNodeType.TreeNode || itemType === TreeNodeType.CompositeTreeNode}
draggable={draggable}
>
<div className={cls(styles.tree_view_node_content)}>
{renderTwice(item)}
Expand Down
Loading