diff --git a/src/common/id.ts b/src/common/id.ts index 3ca58090c..e520f4aef 100644 --- a/src/common/id.ts +++ b/src/common/id.ts @@ -5,3 +5,4 @@ export const ID_SIDE_BAR = 'sidebar'; export const ID_EXPLORER = 'explorer'; export const ID_STATUS_BAR = 'statusBar'; export const ID_FOLDER_TREE = 'folderTree'; +export const ID_EDITOR_TREE = 'editorTree'; diff --git a/src/controller/explorer/editorTree.tsx b/src/controller/explorer/editorTree.tsx new file mode 100644 index 000000000..66af0c048 --- /dev/null +++ b/src/controller/explorer/editorTree.tsx @@ -0,0 +1,132 @@ +import 'reflect-metadata'; +import React from 'react'; +import { Controller } from 'mo/react/controller'; +import { container, singleton } from 'tsyringe'; +import { + builtInEditorTreeContextMenu, + builtInEditorTreeHeaderContextMenu, + EditorTreeEvent, +} from 'mo/model/workbench/explorer/editorTree'; +import { EditorService, ExplorerService, FolderTreeService } from 'mo/services'; +import { + builtInExplorerEditorPanel, + EDITOR_MENU_CLOSE, + EDITOR_MENU_CLOSE_ALL, + EDITOR_MENU_CLOSE_OTHERS, + EDITOR_MENU_CLOSE_SAVED, +} from 'mo/model'; +import { + EditorTree, + IOpenEditProps, +} from 'mo/workbench/sidebar/explore/editorTree'; +import { connect } from 'mo/react'; +import { IMenuItemProps, ITabProps } from 'mo/components'; + +export interface IEditorTreeController { + readonly onClose: (tabId: string, groupId: number) => void; + readonly onSelect: (tabId: string, groupId: number) => void; + readonly onCloseGroup: (groupId: number) => void; + readonly onSaveGroup: (groupId: number) => void; + /** + * Trigger by context menu click event + * When click the context menu from group header, it doesn't have file info + */ + readonly onContextMenu: ( + menu: IMenuItemProps, + groupId: number, + file?: ITabProps + ) => void; +} + +@singleton() +export class EditorTreeController + extends Controller + implements IEditorTreeController { + private readonly explorerService: ExplorerService; + private readonly folderTreeService: FolderTreeService; + private readonly editService: EditorService; + + constructor() { + super(); + this.editService = container.resolve(EditorService); + this.explorerService = container.resolve(ExplorerService); + this.folderTreeService = container.resolve(FolderTreeService); + this.initView(); + } + + public initView() { + const EditorTreeView = connect( + this.editService, + EditorTree + ); + const { groupToolbar, ...restEditor } = builtInExplorerEditorPanel(); + const contextMenu = builtInEditorTreeContextMenu(); + const headerContextMenu = builtInEditorTreeHeaderContextMenu(); + + this.explorerService.addPanel({ + ...restEditor, + renderPanel: () => ( + + ), + }); + } + + public onContextMenu = ( + menu: IMenuItemProps, + groupId: number, + file?: ITabProps + ) => { + switch (menu.id) { + case EDITOR_MENU_CLOSE: + this.onClose(file?.id!, groupId); + break; + + case EDITOR_MENU_CLOSE_OTHERS: + this.emit(EditorTreeEvent.onCloseOthers, file, groupId); + break; + + case EDITOR_MENU_CLOSE_SAVED: + this.emit(EditorTreeEvent.onCloseSaved, groupId); + break; + + case EDITOR_MENU_CLOSE_ALL: + this.emit(EditorTreeEvent.onCloseAll, groupId); + break; + + default: + this.emit(EditorTreeEvent.onContextMenu, menu, file, groupId); + break; + } + }; + + public onClose = (tabId: string, groupId: number) => { + this.emit(EditorTreeEvent.onClose, tabId, groupId); + }; + + public onSelect = (tabId: string, groupId: number) => { + this.emit(EditorTreeEvent.onSelect, tabId, groupId); + }; + + public onCloseGroup = (groupId: number) => { + this.emit(EditorTreeEvent.onCloseAll, groupId); + }; + + public onSaveGroup = (groupId: number) => { + this.emit(EditorTreeEvent.onSaveAll, groupId); + }; +} + +// Register singleton +container.resolve(EditorTreeController); diff --git a/src/controller/explorer/explorer.tsx b/src/controller/explorer/explorer.tsx index 712da318f..0ad423a7f 100644 --- a/src/controller/explorer/explorer.tsx +++ b/src/controller/explorer/explorer.tsx @@ -10,8 +10,10 @@ import { IActivityBarItem } from 'mo/model/workbench/activityBar'; import { builtInExplorerActivityItem, builtInExplorerFolderPanel, - builtInExplorerEditorPanel, ExplorerEvent, + EXPLORER_TOGGLE_CLOSE_ALL_EDITORS, + EXPLORER_TOGGLE_SAVE_ALL, + EXPLORER_TOGGLE_VERTICAL, IExplorerPanelItem, } from 'mo/model/workbench/explorer/explorer'; import { @@ -21,6 +23,7 @@ import { REMOVE_COMMAND_ID, FileTypes, FolderTreeEvent, + EditorTreeEvent, } from 'mo/model'; import { IActionBarItemProps } from 'mo/components/actionBar'; import { @@ -36,6 +39,7 @@ import { IMenuBarService, } from 'mo/services'; import { FolderTreeController, IFolderTreeController } from './folderTree'; + export interface IExplorerController { onActionsContextMenuClick?: ( e: React.MouseEvent, @@ -122,11 +126,6 @@ export class ExplorerController ...builtInExplorerFolderPanel(), renderPanel: this.renderFolderTree, }); - - // add editor panel - this.explorerService.addPanel({ - ...builtInExplorerEditorPanel(), - }); } private createFileOrFolder = (type: keyof typeof FileTypes) => { @@ -177,6 +176,17 @@ export class ExplorerController this.emit(ExplorerEvent.onDeletePanel, parentPanel); break; } + case EXPLORER_TOGGLE_CLOSE_ALL_EDITORS: { + this.emit(EditorTreeEvent.onCloseAll); + break; + } + case EXPLORER_TOGGLE_SAVE_ALL: { + this.emit(EditorTreeEvent.onSaveAll); + break; + } + case EXPLORER_TOGGLE_VERTICAL: { + this.emit(EditorTreeEvent.onSplitEditorLayout); + } default: console.log('onCollapseToolbar'); } diff --git a/src/controller/index.ts b/src/controller/index.ts index 9a93442a0..ef653d7e8 100644 --- a/src/controller/index.ts +++ b/src/controller/index.ts @@ -10,5 +10,6 @@ export * from './statusBar'; export * from './workbench'; export * from './explorer/explorer'; export * from './explorer/folderTree'; +export * from './explorer/editorTree'; export * from './explorer/outline'; export * from './search/search'; diff --git a/src/extensions/editorTree/index.ts b/src/extensions/editorTree/index.ts new file mode 100644 index 000000000..8e2e4d4c7 --- /dev/null +++ b/src/extensions/editorTree/index.ts @@ -0,0 +1,41 @@ +import molecule from 'mo'; +import { IExtension } from 'mo/model/extension'; + +export const ExtendsEditorTree: IExtension = { + activate() { + molecule.editorTree.onSelect((tabId, groupId) => { + molecule.editor.setActive(groupId, tabId); + }); + + molecule.editorTree.onClose((tabId, groupId) => { + molecule.editor.closeTab(tabId, groupId); + }); + + molecule.editorTree.onCloseOthers((tabItem, groupId) => { + molecule.editor.closeOthers(tabItem, groupId); + }); + + molecule.editorTree.onCloseSaved((groupId) => { + // TODO: editor close saved + }); + + molecule.editorTree.onCloseAll((groupId) => { + if (groupId) { + molecule.editor.closeAll(groupId); + } else { + const { groups } = molecule.editor.getState(); + groups?.forEach((group) => { + molecule.editor.closeAll(group.id!); + }); + } + }); + + molecule.editorTree.onSaveAll((groupId) => { + // TODO: editor save + }); + + molecule.editorTree.onLayout(() => { + // TODO: layoutService + }); + }, +}; diff --git a/src/extensions/index.ts b/src/extensions/index.ts index 835e37da9..c149ed1e2 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -6,6 +6,7 @@ import { ExtendsMenuBar } from './menuBar'; import { ExtendsActivityBar } from './activityBar'; import { ExtendsPanel } from './panel'; import { ExtendsExplorer } from './explorer'; +import { ExtendsEditorTree } from './editorTree'; import { defaultColorThemeExtension } from './theme-defaults'; import { monokaiColorThemeExtension } from './theme-monokai'; @@ -22,6 +23,7 @@ export const defaultExtensions = [ ExtendsStatusBar, ExtendsProblems, ExtendsExplorer, + ExtendsEditorTree, defaultColorThemeExtension, monokaiColorThemeExtension, paleNightColorThemeExtension, diff --git a/src/extensions/theme-defaults/themes/dark_defaults.json b/src/extensions/theme-defaults/themes/dark_defaults.json index d3913e27a..4388265c0 100644 --- a/src/extensions/theme-defaults/themes/dark_defaults.json +++ b/src/extensions/theme-defaults/themes/dark_defaults.json @@ -12,6 +12,7 @@ "editorGroupHeader.tabsBackground": "rgb(37, 37, 38)", "list.dropBackground": "#383B3D", "list.activeSelectionBackground": "#094771", + "list.inactiveSelectionBackground": "#37373D", "list.focusOutline": "#007FD4", "activityBarBadge.background": "#007ACC", "sidebarTitle.foreground": "#BBBBBB", diff --git a/src/extensions/theme-defaults/themes/light_defaults.json b/src/extensions/theme-defaults/themes/light_defaults.json index cece0df49..b38b17302 100644 --- a/src/extensions/theme-defaults/themes/light_defaults.json +++ b/src/extensions/theme-defaults/themes/light_defaults.json @@ -15,6 +15,7 @@ "sideBarSectionHeader.border": "#61616130", "list.hoverBackground": "#E8E8E8", "list.activeSelectionBackground": "#0060C0", + "list.inactiveSelectionBackground": "#E4E6F1", "list.focusOutline": "#0090F1", "input.placeholderForeground": "#767676", "inputOption.activeBackground": "#007fd466", diff --git a/src/i18n/source/en.ts b/src/i18n/source/en.ts index 52808b5e1..45056b79f 100644 --- a/src/i18n/source/en.ts +++ b/src/i18n/source/en.ts @@ -27,6 +27,7 @@ export default { 'sidebar.explore.title': 'Explorer', 'sidebar.explore.folders': 'Folders', 'sidebar.explore.openEditor': 'Open Editors', + 'sidebar.explore.openEditor.group': 'Group ${i}', 'sidebar.explore.outline': 'Outline', 'sidebar.search.title': 'Search', 'sidebar.replace.placement': 'Replace', @@ -53,6 +54,7 @@ export default { 'editor.closeToRight': 'Close To Right', 'editor.closeToLeft': 'Close To Left', 'editor.closeAll': 'Close All', + 'editor.closeSaved': 'Close Saved', 'editor.closeOthers': 'Close Others', 'editor.close': 'Close', 'editor.showOpenEditors': 'Show Opened Editors', diff --git a/src/i18n/source/zh-CN.json b/src/i18n/source/zh-CN.json index 81cb25027..71cf7c44e 100644 --- a/src/i18n/source/zh-CN.json +++ b/src/i18n/source/zh-CN.json @@ -32,6 +32,7 @@ "menu.about": "关于", "sidebar.explore.title": "浏览", "sidebar.explore.openEditor": "打开的编辑器", + "sidebar.explore.openEditor.group": "第 ${i} 组", "sidebar.explore.outline": "轮廓", "sidebar.explore.outlineMore": "更多操作...", "sidebar.explore.refresh": "刷新浏览", @@ -52,6 +53,7 @@ "editor.closeToLeft": "关闭左边", "editor.closeAll": "关闭所有", "editor.closeOthers": "关闭其他", + "editor.closeSaved": "关闭已保存", "editor.close": "关闭", "editor.showOpenEditors": "展示已打开的编辑器" } diff --git a/src/model/workbench/editor.ts b/src/model/workbench/editor.ts index f2bead2cb..5e2a9b625 100644 --- a/src/model/workbench/editor.ts +++ b/src/model/workbench/editor.ts @@ -52,6 +52,7 @@ export const EDITOR_MENU_CLOSE_TO_RIGHT = 'editor.closeToRight'; export const EDITOR_MENU_CLOSE_TO_LEFT = 'editor.closeToLeft'; export const EDITOR_MENU_CLOSE_ALL = 'editor.closeAll'; export const EDITOR_MENU_CLOSE_OTHERS = 'editor.closeOthers'; +export const EDITOR_MENU_CLOSE_SAVED = 'editor.closeSaved'; export const EDITOR_MENU_CLOSE = 'editor.close'; export const EDITOR_MENU_SHOW_OPENEDITORS = 'editor.showOpenEditors'; diff --git a/src/model/workbench/explorer/editorTree.ts b/src/model/workbench/explorer/editorTree.ts new file mode 100644 index 000000000..05fea23e2 --- /dev/null +++ b/src/model/workbench/explorer/editorTree.ts @@ -0,0 +1,56 @@ +import { localize } from 'mo/i18n/localize'; +import { + EDITOR_MENU_CLOSE, + EDITOR_MENU_CLOSE_ALL, + EDITOR_MENU_CLOSE_OTHERS, + EDITOR_MENU_CLOSE_SAVED, +} from 'mo/model'; + +export enum EditorTreeEvent { + onClose = 'editorTree.close', + onSelect = 'editorTree.select', + onCloseOthers = 'editorTree.closeOthers', + onCloseSaved = 'editorTree.closeSaved', + onCloseAll = 'editorTree.closeAll', + onSaveAll = 'editorTree.saveAll', + onSplitEditorLayout = 'editorTree.splitEditorLayout', + onContextMenu = 'editorTree.contextMenuClick', +} + +export function builtInEditorTreeHeaderContextMenu() { + return [ + { + id: EDITOR_MENU_CLOSE_SAVED, + name: localize(EDITOR_MENU_CLOSE_SAVED, 'Close Saved'), + }, + { + id: EDITOR_MENU_CLOSE_ALL, + name: localize(EDITOR_MENU_CLOSE_ALL, 'Close All'), + }, + ]; +} + +export function builtInEditorTreeContextMenu() { + return [ + { + id: EDITOR_MENU_CLOSE, + name: localize(EDITOR_MENU_CLOSE, 'Close'), + }, + { + id: EDITOR_MENU_CLOSE_OTHERS, + name: localize(EDITOR_MENU_CLOSE_OTHERS, 'Close Others'), + }, + { + id: EDITOR_MENU_CLOSE_SAVED, + name: localize(EDITOR_MENU_CLOSE_SAVED, 'Close Saved'), + }, + { + id: EDITOR_MENU_CLOSE_ALL, + name: localize(EDITOR_MENU_CLOSE_ALL, 'Close All'), + }, + ]; +} + +export class EditorTree { + constructor() {} +} diff --git a/src/model/workbench/explorer/explorer.tsx b/src/model/workbench/explorer/explorer.tsx index 798a2a5cc..3ef5db5ee 100644 --- a/src/model/workbench/explorer/explorer.tsx +++ b/src/model/workbench/explorer/explorer.tsx @@ -50,6 +50,9 @@ export const EXPLORER_TOGGLE_VERTICAL = 'sidebar.explore.toggleVertical'; export const EXPLORER_TOGGLE_SAVE_ALL = 'sidebar.explore.saveALL'; export const EXPLORER_TOGGLE_CLOSE_ALL_EDITORS = 'sidebar.explore.closeAllEditors'; +export const EXPLORER_TOGGLE_SAVE_GROUP = 'sidebar.explore.saveGroup'; +export const EXPLORER_TOGGLE_CLOSE_GROUP_EDITORS = + 'sidebar.explore.closeGroupEditors'; export function builtInExplorerActivityItem() { return { @@ -77,13 +80,11 @@ export function builtInExplorerEditorPanel() { { id: EXPLORER_TOGGLE_VERTICAL, title: localize(EXPLORER_TOGGLE_VERTICAL, 'Toggle Vertical'), - disabled: true, iconName: 'codicon-editor-layout', }, { id: EXPLORER_TOGGLE_SAVE_ALL, title: localize(EXPLORER_TOGGLE_SAVE_ALL, 'Save All'), - disabled: true, iconName: 'codicon-save-all', }, { @@ -95,6 +96,21 @@ export function builtInExplorerEditorPanel() { iconName: 'codicon-close-all', }, ], + groupToolbar: [ + { + id: EXPLORER_TOGGLE_SAVE_GROUP, + title: localize(EXPLORER_TOGGLE_SAVE_GROUP, 'Save Group'), + iconName: 'codicon-save-all', + }, + { + id: EXPLORER_TOGGLE_CLOSE_GROUP_EDITORS, + title: localize( + EXPLORER_TOGGLE_CLOSE_GROUP_EDITORS, + 'Close Group Editors' + ), + iconName: 'codicon-close-all', + }, + ], }; } diff --git a/src/model/workbench/index.ts b/src/model/workbench/index.ts index 8fa359191..016394b5a 100644 --- a/src/model/workbench/index.ts +++ b/src/model/workbench/index.ts @@ -7,6 +7,7 @@ export * from './statusBar'; export * from './menuBar'; export * from './explorer/explorer'; export * from './explorer/folderTree'; +export * from './explorer/editorTree'; export * from './search'; export * from './panel'; export interface IWorkbench { diff --git a/src/molecule.api.ts b/src/molecule.api.ts index 41150d643..2f15eef52 100644 --- a/src/molecule.api.ts +++ b/src/molecule.api.ts @@ -46,6 +46,8 @@ import { SettingsService, IProblemsService, ProblemsService, + IEditorTreeService, + EditorTreeService, } from 'mo/services'; import { ILocaleService, LocaleService } from './i18n/localeService'; @@ -70,6 +72,10 @@ export const folderTree: IFolderTreeService = container.resolve( + EditorTreeService +); + export const search = container.resolve(SearchService); export const sidebar = container.resolve(SidebarService); export const menuBar = container.resolve(MenuBarService); diff --git a/src/services/workbench/explorer/editorTreeService.ts b/src/services/workbench/explorer/editorTreeService.ts new file mode 100644 index 000000000..6c4da227a --- /dev/null +++ b/src/services/workbench/explorer/editorTreeService.ts @@ -0,0 +1,101 @@ +import { IMenuItemProps, ITabProps } from 'mo/components'; +import { IEditor, IEditorTab } from 'mo/model'; +import { EditorTreeEvent } from 'mo/model/workbench/explorer/editorTree'; +import { Component } from 'mo/react'; +import { EditorService } from 'mo/services'; +import { container, singleton } from 'tsyringe'; + +export interface IEditorTreeService { + /** + * Callabck for close a certain tab + */ + onClose(callback: (tabId: string, groupId: number) => void): void; + /** + * Callback for close others tabs except this tabItem + */ + onCloseOthers( + callback: (tabItem: IEditorTab, groupId: number) => void + ): void; + /** + * Callback for close saved tabs in this group + */ + onCloseSaved(callback: (groupId: number) => void): void; + onSelect(callback: (tabId: string, groupId: number) => void): void; + /** + * Callback for close all tabs + * When specify groupId, it'll close that group + */ + onCloseAll(callback: (groupId?: number) => void): void; + /** + * Callback for save all tabs + * When specify groupId, it'll save that group + */ + onSaveAll(callback: (groupId?: number) => void): void; + /** + * Callback for adjust editor layout + */ + onLayout(callback: () => void): void; + /** + * Callback for context menu click event which isn't in buit-in menus + */ + onContextMenu( + callback: ( + menu: IMenuItemProps, + file: ITabProps, + groupId: number + ) => void + ): void; +} + +@singleton() +export class EditorTreeService + extends Component + implements IEditorTreeService { + protected state: IEditor; + private readonly editorService: EditorService; + + constructor() { + super(); + this.editorService = container.resolve(EditorService); + this.state = this.editorService.getState(); + } + public onClose(callback: (tabId: string, groupId: number) => void) { + this.subscribe(EditorTreeEvent.onClose, callback); + } + + public onCloseOthers( + callback: (tabItem: IEditorTab, groupId: number) => void + ) { + this.subscribe(EditorTreeEvent.onCloseOthers, callback); + } + + public onCloseSaved(callback: (groupId: number) => void) { + this.subscribe(EditorTreeEvent.onCloseSaved, callback); + } + + public onSelect(callback: (tabId: string, groupId: number) => void) { + this.subscribe(EditorTreeEvent.onSelect, callback); + } + + public onCloseAll(callback: (groupId?: number) => void) { + this.subscribe(EditorTreeEvent.onCloseAll, callback); + } + + public onSaveAll(callback: (groupId?: number) => void) { + this.subscribe(EditorTreeEvent.onSaveAll, callback); + } + + public onLayout(callback: () => void) { + this.subscribe(EditorTreeEvent.onSplitEditorLayout, callback); + } + + public onContextMenu( + callback: ( + menu: IMenuItemProps, + file: ITabProps, + groupId: number + ) => void + ) { + this.subscribe(EditorTreeEvent.onContextMenu, callback); + } +} diff --git a/src/services/workbench/index.ts b/src/services/workbench/index.ts index dbf4ee74f..52d714963 100644 --- a/src/services/workbench/index.ts +++ b/src/services/workbench/index.ts @@ -5,5 +5,6 @@ export * from './editorService'; export * from './statusBarService'; export * from './explorer/explorerService'; export * from './explorer/folderTreeService'; +export * from './explorer/editorTreeService'; export * from './searchService'; export * from './panelService'; diff --git a/src/style/common.scss b/src/style/common.scss index 6ab7daede..f5f5053e9 100644 --- a/src/style/common.scss +++ b/src/style/common.scss @@ -43,6 +43,7 @@ $activityBar: prefix('activityBar'); $notification: prefix('notification'); $problems: prefix('problems'); $folderTree: prefix('folderTree'); +$editorTree: prefix('editorTree'); // The Naming of BEM Element @function bem-ele($block, $element) { diff --git a/src/workbench/sidebar/explore/base.ts b/src/workbench/sidebar/explore/base.ts index bd5aa130b..c0fc6ce46 100644 --- a/src/workbench/sidebar/explore/base.ts +++ b/src/workbench/sidebar/explore/base.ts @@ -8,6 +8,7 @@ import { ID_SIDE_BAR, ID_EXPLORER, ID_FOLDER_TREE, + ID_EDITOR_TREE, } from 'mo/common/id'; const defaultClassName = prefixClaName(ID_SIDE_BAR); @@ -21,10 +22,39 @@ const folderTreeClassName = prefixClaName(ID_FOLDER_TREE); const folderTreeInputClassName = getBEMModifier(folderTreeClassName, 'input'); const folderTreeEditClassName = getBEMModifier(folderTreeClassName, 'editable'); +const editorTreeClassName = prefixClaName(ID_EDITOR_TREE); +const editorTreeItemClassName = getBEMElement(editorTreeClassName, 'item'); +const editorTreeGroupClassName = getBEMElement(editorTreeClassName, 'group'); +const editorTreeFileNameClassName = getBEMElement( + editorTreeItemClassName, + 'fileName' +); +const editorTreeFilePathClassName = getBEMElement( + editorTreeItemClassName, + 'filePath' +); +const editorTreeActiveItemClassName = getBEMModifier( + editorTreeItemClassName, + 'active' +); +const editorTreeCloseIconClassName = getBEMElement( + editorTreeClassName, + 'close' +); +const editorTreeFileIconClassName = getBEMElement(editorTreeClassName, 'file'); + export { defaultExplorerClassName, activityBarItemFloatClassName, folderTreeClassName, folderTreeInputClassName, folderTreeEditClassName, + editorTreeClassName, + editorTreeItemClassName, + editorTreeGroupClassName, + editorTreeFileNameClassName, + editorTreeFilePathClassName, + editorTreeActiveItemClassName, + editorTreeCloseIconClassName, + editorTreeFileIconClassName, }; diff --git a/src/workbench/sidebar/explore/editorTree.tsx b/src/workbench/sidebar/explore/editorTree.tsx new file mode 100644 index 000000000..125b19a7b --- /dev/null +++ b/src/workbench/sidebar/explore/editorTree.tsx @@ -0,0 +1,234 @@ +import React from 'react'; +import { IEditorTreeController } from 'mo/controller'; +import { + EXPLORER_TOGGLE_CLOSE_GROUP_EDITORS, + EXPLORER_TOGGLE_SAVE_GROUP, + FileTypes, + IEditor, + IEditorGroup, +} from 'mo/model'; +import { + IActionBarItemProps, + Icon, + IMenuItemProps, + ITabProps, + Menu, + Toolbar, + useContextView, +} from 'mo/components'; +import { IFolderTreeService } from 'mo/services'; +import { + editorTreeActiveItemClassName, + editorTreeClassName, + editorTreeCloseIconClassName, + editorTreeFileIconClassName, + editorTreeFileNameClassName, + editorTreeFilePathClassName, + editorTreeGroupClassName, + editorTreeItemClassName, +} from './base'; +import { classNames } from 'mo/common/className'; +import { getEventPosition } from 'mo/common/dom'; +import { localize } from 'mo/i18n/localize'; + +// override onContextMenu +type UnionEditor = Omit; +export interface IOpenEditProps extends UnionEditor { + getFileIconByExtensionName: IFolderTreeService['getFileIconByExtensionName']; + /** + * Group Header toolbar + */ + groupToolbar?: IActionBarItemProps[]; + /** + * Item context menus + */ + contextMenu?: IMenuItemProps[]; + /** + * Group Header context menus + * It'll use the value of contextMenu if specify contextMenu but not specify headerContextMenu + */ + headerContextMenu?: IMenuItemProps[]; + onContextMenu?: ( + menu: IMenuItemProps, + groupId: number, + file?: ITabProps + ) => void; +} + +const EditorTree = (props: IOpenEditProps) => { + const { + current, + groups, + groupToolbar, + contextMenu = [], + headerContextMenu, + getFileIconByExtensionName, + onSelect, + onSaveGroup, + onContextMenu, + onCloseGroup, + onClose, + } = props; + if (!groups || !groups.length) return null; + + const contextView = useContextView(); + + const handleCloseClick = (group: IEditorGroup, file: ITabProps) => { + onClose?.(file.id!, group.id!); + }; + + const handleItemClick = (group: IEditorGroup, file: ITabProps) => { + if (group.id !== current?.id || file.id !== current?.tab?.id) { + onSelect?.(file.id!, group.id!); + } + }; + + const handleOnMenuClick = ( + menu: IMenuItemProps, + group: IEditorGroup, + file?: ITabProps + ) => { + contextView.hide(); + onContextMenu?.(menu, group.id!, file); + }; + + const handleRightClick = ( + e: React.MouseEvent, + group: IEditorGroup, + file: ITabProps + ) => { + e.preventDefault(); + contextView.show(getEventPosition(e), () => ( + handleOnMenuClick(item!, group, file)} + data={contextMenu} + /> + )); + }; + + const handleHeaderRightClick = ( + e: React.MouseEvent, + group: IEditorGroup + ) => { + e.preventDefault(); + const groupHeaderContext = headerContextMenu || contextMenu; + contextView.show(getEventPosition(e), () => ( + handleOnMenuClick(item!, group)} + data={groupHeaderContext} + /> + )); + }; + + // click group title will open the first file in this group + const handleGroupClick = (e, group: IEditorGroup) => { + const { target } = e; + const firstFile = group.data?.[0]; + if (target.nextElementSibling && firstFile) { + onSelect?.(firstFile.id!, group.id!); + target.nextElementSibling.focus(); + } + }; + + const handleToolBarClick = ( + e: React.MouseEvent, + item: IActionBarItemProps, + group: IEditorGroup + ) => { + e.stopPropagation(); + switch (item.id) { + case EXPLORER_TOGGLE_CLOSE_GROUP_EDITORS: + onCloseGroup?.(group.id!); + break; + case EXPLORER_TOGGLE_SAVE_GROUP: + onSaveGroup?.(group.id!); + break; + default: + // default behavior + break; + } + }; + + return ( +
+ {groups.map((group, index) => { + return ( + + {groups.length !== 1 && ( +
handleGroupClick(e, group)} + onContextMenu={(e) => + handleHeaderRightClick(e, group) + } + key={index} + > + {localize( + 'sidebar.explore.openEditor.group', + 'Group', + (index + 1).toString() + )} + {groupToolbar && ( + + handleToolBarClick(e, item, group) + } + /> + )} +
+ )} + {group.data?.map((file) => { + const isActive = + group.id === current?.id && + file.id === current?.tab?.id; + return ( +
handleItemClick(group, file)} + onContextMenu={(e) => + handleRightClick(e, group, file) + } + > + + handleCloseClick(group, file) + } + type="close" + /> + + + {file.name} + + + {file.data.path} + +
+ ); + })} +
+ ); + })} +
+ ); +}; + +export { EditorTree }; diff --git a/src/workbench/sidebar/explore/index.tsx b/src/workbench/sidebar/explore/index.tsx index 3c94ffebe..e241ffe25 100644 --- a/src/workbench/sidebar/explore/index.tsx +++ b/src/workbench/sidebar/explore/index.tsx @@ -5,6 +5,7 @@ import { FolderTreeService } from 'mo/services'; import { Explorer } from './explore'; import FolderTree from './folderTree'; import { FolderTreeController } from 'mo/controller/explorer/folderTree'; +import { EditorTree } from './editorTree'; const folderTreeService = container.resolve(FolderTreeService); const folderTreeController = container.resolve(FolderTreeController); @@ -14,4 +15,5 @@ const FolderTreeView = connect( FolderTree, folderTreeController ); -export { Explorer, FolderTreeView, FolderTree }; + +export { Explorer, FolderTreeView, FolderTree, EditorTree }; diff --git a/src/workbench/sidebar/explore/style.scss b/src/workbench/sidebar/explore/style.scss index 4d8392bae..f97300c0c 100644 --- a/src/workbench/sidebar/explore/style.scss +++ b/src/workbench/sidebar/explore/style.scss @@ -46,3 +46,116 @@ } } } + +#{$editorTree} { + font-size: 13px; + height: 100%; + + &__item { + // keep same with tree node + align-items: center; + border: 1px solid transparent; + cursor: pointer; + display: flex; + height: 22px; + line-height: 22px; + list-style: none; + margin: 0; + outline: 0; + padding-left: 16px; + user-select: none; + white-space: nowrap; + + &--active { + // 这里的 inactive 是相对于 focus 的 active 来说的,是相对的 inactive + background-color: var(--list-inactiveSelectionBackground); + + #{$editorTree}__close { + opacity: 1; + } + } + + &:hover { + &:not(#{$editorTree}__item--active) { + background-color: var(--list-hoverBackground); + } + + #{$editorTree}__close { + opacity: 1; + } + } + + &:focus { + background: var(--list-activeSelectionBackground); + border-color: var(--list-focusOutline); + color: #fff; + } + + &__fileName { + flex-basis: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre; + } + + &__filePath { + flex: 1; + font-size: 0.9em; + margin-left: 5px; + opacity: 0.8; + overflow: hidden; + padding-top: 2px; + text-overflow: ellipsis; + white-space: pre; + } + } + + &__group { + align-items: center; + display: flex; + font-size: 11px; + height: 22px; + line-height: 22px; + padding-left: 16px; + user-select: none; + + &:hover { + background-color: var(--list-hoverBackground); + + #{$toolBar} { + opacity: 1; + } + } + + #{$toolBar} { + margin-left: auto; + opacity: 0; + + .codicon { + color: var(--activityBar-inactiveForeground); + + &:hover { + color: var(--activityBar-activeBorder); + } + } + } + } + + &__close { + color: var(--activityBar-inactiveForeground); + opacity: 0; + + &:hover { + color: var(--activityBar-activeBorder); + } + } + + &__file[class*='codicon-'] { + flex-shrink: 0; + font-size: inherit; + height: 22px; + line-height: 23px; + margin: 0 2px; + width: 16px; + } +}