From 5440b20e5010a42a101ec8e50223faea582b932f Mon Sep 17 00:00:00 2001 From: glennsl Date: Tue, 17 Dec 2019 13:57:03 +0100 Subject: [PATCH 01/13] focus -> active --- src/Model/FileExplorer.re | 4 ++-- src/Model/FileExplorer.rei | 2 +- src/Store/FileExplorerStore.re | 14 +++++++------- src/UI/FileExplorerView.re | 4 ++-- src/UI/FileTreeView.re | 11 ++++++----- src/UI/TreeView.re | 18 +++++++++--------- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/Model/FileExplorer.re b/src/Model/FileExplorer.re index a7660ac957..757dbc8085 100644 --- a/src/Model/FileExplorer.re +++ b/src/Model/FileExplorer.re @@ -6,7 +6,7 @@ type t = { tree: option(FsTreeNode.t), isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], - focus: option(string) // path + active: option(string) // path }; [@deriving show({with_path: false})] @@ -136,5 +136,5 @@ let initial = { tree: None, isOpen: true, scrollOffset: `Start(0.), - focus: None, + active: None, }; diff --git a/src/Model/FileExplorer.rei b/src/Model/FileExplorer.rei index 7ccdf2f35c..8d2bb5cb85 100644 --- a/src/Model/FileExplorer.rei +++ b/src/Model/FileExplorer.rei @@ -4,7 +4,7 @@ type t = { tree: option(FsTreeNode.t), isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], - focus: option(string) // path + active: option(string) // path }; [@deriving show] diff --git a/src/Store/FileExplorerStore.re b/src/Store/FileExplorerStore.re index c277426fbe..28b67d2d7f 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -67,8 +67,8 @@ let setTree = (tree, state) => updateFileExplorer(s => {...s, tree: Some(tree)}, state); let setOpen = (isOpen, state) => updateFileExplorer(s => {...s, isOpen}, state); -let setFocus = (focus, state) => - updateFileExplorer(s => {...s, focus}, state); +let setActive = (maybePath, state) => + updateFileExplorer(s => {...s, active: maybePath}, state); let setScrollOffset = (scrollOffset, state) => updateFileExplorer(s => {...s, scrollOffset}, state); @@ -144,14 +144,14 @@ let start = () => { | NodeLoaded(id, node) => (replaceNode(id, node), Isolinear.Effect.none) | FocusNodeLoaded(id, node) => - switch (state.fileExplorer.focus) { + switch (state.fileExplorer.active) { | Some(path) => revealPath(replaceNode(id, node), path) | None => (state, Isolinear.Effect.none) } | NodeClicked(node) => - // Set focus here to avoid scrolling in BufferEnter - let state = setFocus(Some(node.path), state); + // Set active here to avoid scrolling in BufferEnter + let state = setActive(Some(node.path), state); switch (node) { | {kind: File, path, _} => (state, openFileByPathEffect(path)) @@ -212,8 +212,8 @@ let start = () => { | BufferEnter({filePath, _}, _) => switch (state.fileExplorer) { - | {focus, _} when focus != filePath => - let state = setFocus(filePath, state); + | {active, _} when active != filePath => + let state = setActive(filePath, state); switch (filePath) { | Some(path) => revealPath(state, path) | None => (state, Isolinear.Effect.none) diff --git a/src/UI/FileExplorerView.re b/src/UI/FileExplorerView.re index 0abe2bffc9..bcb63e3891 100644 --- a/src/UI/FileExplorerView.re +++ b/src/UI/FileExplorerView.re @@ -11,7 +11,7 @@ let make = (~state: State.t, ()) => { switch (state.fileExplorer) { | {tree: None, _} => React.empty - | {tree: Some(tree), focus, _} => - + | {tree: Some(tree), active, _} => + }; }; diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index 07985d45aa..867ff2a043 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -1,3 +1,4 @@ +open Oni_Core; open Oni_Model; open Revery; @@ -24,7 +25,7 @@ module Styles = { let item = [flexDirection(`Row), alignItems(`Center)]; - let text = (~fg, ~font: Core.UiFont.t) => [ + let text = (~fg, ~font: UiFont.t) => [ fontSize(font.fontSize), fontFamily(font.fontFile), color(fg), @@ -49,7 +50,7 @@ let setiIcon = (~icon, ~fontSize as size, ~fg, ()) => { />; }; -let nodeView = (~font: Core.UiFont.t, ~fg, ~node: FsTreeNode.t, ()) => { +let nodeView = (~font: UiFont.t, ~fg, ~node: FsTreeNode.t, ()) => { let icon = () => switch (node.icon) { | Some(icon) => @@ -95,8 +96,8 @@ let make = let fg = state.theme.sideBarForeground; - let focus = - switch (focus, state.workspace) { + let active = + switch (active, state.workspace) { | (Some(path), Some(workspace)) => toNodePath(workspace, tree, path) | _ => None }; @@ -112,7 +113,7 @@ let make = scrollOffset onScrollOffsetChange tree - focus + active theme itemHeight=22 onClick=onNodeClick> diff --git a/src/UI/TreeView.re b/src/UI/TreeView.re index 208e75fe5e..e8a4a9208d 100644 --- a/src/UI/TreeView.re +++ b/src/UI/TreeView.re @@ -53,13 +53,13 @@ module Styles = { bottom(0), ]; - let item = (~itemHeight, ~isFocused, ~theme: Theme.t) => [ + let item = (~itemHeight, ~isActive, ~theme: Theme.t) => [ height(itemHeight), cursor(Revery.MouseCursors.pointer), flexDirection(`Row), overflow(`Hidden), backgroundColor( - isFocused ? theme.menuSelectionBackground : Colors.transparentWhite, + isActive ? theme.menuSelectionBackground : Colors.transparentWhite, ), ]; @@ -91,7 +91,7 @@ module Make = (Model: TreeModel) => { let rec nodeView = ( ~renderContent, - ~focus, + ~active, ~itemHeight, ~clipRange as (clipStart, clipEnd), ~onClick, @@ -101,8 +101,8 @@ module Make = (Model: TreeModel) => { ) => { let subtreeSize = Model.expandedSubtreeSize(node); - let (isFocused, childFocus) = - switch (focus) { + let (isActive, descendantActive) = + switch (active) { | Some([last]) when last == node => (true, None) | Some([head, ...tail]) when head == node => (false, Some(tail)) | Some(_) => (false, None) @@ -115,7 +115,7 @@ module Make = (Model: TreeModel) => { let item = (~arrow, ()) => onClick(node)} - style={Styles.item(~itemHeight, ~isFocused, ~theme)}> + style={Styles.item(~itemHeight, ~isActive, ~theme)}> {renderContent(node)} ; @@ -127,7 +127,7 @@ module Make = (Model: TreeModel) => { let element = { let%component make = ( ~children as renderContent, - ~focus: option(list(Model.t)), + ~active: option(list(Model.t)), ~itemHeight, ~initialRowsToRender=10, ~onClick, @@ -266,7 +266,7 @@ module Make = (Model: TreeModel) => { Date: Tue, 17 Dec 2019 16:41:23 +0100 Subject: [PATCH 02/13] add separate focus property to state, remove active from TreeView --- src/Model/FileExplorer.re | 3 +- src/Model/FileExplorer.rei | 3 +- src/Model/FsTreeNode.re | 31 ++++++++++++++++++ src/Model/FsTreeNode.rei | 1 + src/Store/FileExplorerStore.re | 7 +++- src/UI/FileExplorerView.re | 4 +-- src/UI/FileTreeView.re | 59 ++++++++++++++++++++++------------ src/UI/TreeView.re | 19 ++--------- 8 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/Model/FileExplorer.re b/src/Model/FileExplorer.re index 757dbc8085..05dccbf7d0 100644 --- a/src/Model/FileExplorer.re +++ b/src/Model/FileExplorer.re @@ -6,7 +6,8 @@ type t = { tree: option(FsTreeNode.t), isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], - active: option(string) // path + active: option(string), // path + focus: option(int), // node id }; [@deriving show({with_path: false})] diff --git a/src/Model/FileExplorer.rei b/src/Model/FileExplorer.rei index 8d2bb5cb85..d2138c79e9 100644 --- a/src/Model/FileExplorer.rei +++ b/src/Model/FileExplorer.rei @@ -4,7 +4,8 @@ type t = { tree: option(FsTreeNode.t), isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], - active: option(string) // path + active: option(string), // path + focus: option(int), // node id }; [@deriving show] diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index 3810f41b4e..814025e524 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -90,6 +90,37 @@ let findNodesByLocalPath = (path, tree) => { }; }; +let findByLocalPath = (path, tree) => { + let pathSegments = path |> String.split_on_char(Filename.dir_sep.[0]); + + let rec loop = (node, children, pathSegments) => + switch (pathSegments) { + | [] => Some(node) + | [pathSegment, ...rest] => + switch (children) { + | [] => + None + + | [node, ...children] => + if (node.displayName == pathSegment) { + let children = + switch (node.kind) { + | Directory({children, _}) => children + | File => [] + }; + loop(node, children, rest); + } else { + loop(node, children, pathSegments); + } + } + }; + + switch (tree.kind) { + | Directory({children, _}) => loop(tree, children, pathSegments) + | File => None + }; +}; + let update = (~tree, ~updater, nodeId) => { let rec loop = fun diff --git a/src/Model/FsTreeNode.rei b/src/Model/FsTreeNode.rei index 24fb589b52..af6dfdc881 100644 --- a/src/Model/FsTreeNode.rei +++ b/src/Model/FsTreeNode.rei @@ -30,6 +30,7 @@ let directory: let findNodesByLocalPath: (string, t) => [ | `Success(list(t)) | `Partial(t) | `Failed]; +let findByLocalPath: (string, t) => option(t); let update: (~tree: t, ~updater: t => t, int) => t; let updateNodesInPath: (~tree: t, ~updater: t => t, list(t)) => t; diff --git a/src/Store/FileExplorerStore.re b/src/Store/FileExplorerStore.re index 28b67d2d7f..8223677113 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -69,6 +69,8 @@ let setOpen = (isOpen, state) => updateFileExplorer(s => {...s, isOpen}, state); let setActive = (maybePath, state) => updateFileExplorer(s => {...s, active: maybePath}, state); +let setFocus = (maybeId, state) => + updateFileExplorer(s => {...s, focus: maybeId}, state); let setScrollOffset = (scrollOffset, state) => updateFileExplorer(s => {...s, scrollOffset}, state); @@ -150,8 +152,11 @@ let start = () => { } | NodeClicked(node) => + let state = + state // Set active here to avoid scrolling in BufferEnter - let state = setActive(Some(node.path), state); + |> setActive(Some(node.path)) + |> setFocus(Some(node.id)); switch (node) { | {kind: File, path, _} => (state, openFileByPathEffect(path)) diff --git a/src/UI/FileExplorerView.re b/src/UI/FileExplorerView.re index bcb63e3891..e569f80260 100644 --- a/src/UI/FileExplorerView.re +++ b/src/UI/FileExplorerView.re @@ -11,7 +11,7 @@ let make = (~state: State.t, ()) => { switch (state.fileExplorer) { | {tree: None, _} => React.empty - | {tree: Some(tree), active, _} => - + | {tree: Some(tree), active, focus, _} => + }; }; diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index 867ff2a043..608090e006 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -4,31 +4,45 @@ open Oni_Model; open Revery; open Revery.UI; -module Core = Oni_Core; +module Option = Utility.Option; -let toNodePath = (workspace: Workspace.workspace, tree, path) => - Core.Log.perf("FileTreeview.toNodePath", () => { - let localPath = - Workspace.toRelativePath(workspace.workingDirectory, path); +let findNodeByPath = (workspace: Workspace.workspace, tree, path) => { + let localPath = Workspace.toRelativePath(workspace.workingDirectory, path); - switch (FsTreeNode.findNodesByLocalPath(localPath, tree)) { - | `Success(nodes) => Some(nodes) - | `Partial(_) - | `Failed => None - }; - }); + FsTreeNode.findByLocalPath(localPath, tree); +}; module Styles = { open Style; let container = [flexGrow(1)]; - let item = [flexDirection(`Row), alignItems(`Center)]; - - let text = (~fg, ~font: UiFont.t) => [ + let title = (~fg, ~bg, ~font: UiFont.t) => [ fontSize(font.fontSize), fontFamily(font.fontFile), + backgroundColor(bg), color(fg), + ]; + + let heading = (theme: Theme.t) => [ + flexDirection(`Row), + justifyContent(`Center), + alignItems(`Center), + backgroundColor(theme.sideBarBackground), + height(Constants.default.tabHeight), + ]; + + let item = (~isFocus, ~theme: Theme.t) => [ + flexDirection(`Row), + flexGrow(1), + alignItems(`Center), + backgroundColor(isFocus ? theme.menuSelectionBackground : theme.sideBarBackground) + ]; + + let text = (~isActive, ~theme: Theme.t, ~font: UiFont.t) => [ + fontSize(font.fontSize), + fontFamily(font.fontFile), + color(isActive ? theme.oniNormalModeBackground : theme.sideBarForeground), marginLeft(10), marginVertical(2), textWrap(TextWrapping.NoWrap), @@ -50,7 +64,7 @@ let setiIcon = (~icon, ~fontSize as size, ~fg, ()) => { />; }; -let nodeView = (~font: UiFont.t, ~fg, ~node: FsTreeNode.t, ()) => { +let nodeView = (~isFocus, ~isActive, ~font: UiFont.t, ~theme: Theme.t, ~node: FsTreeNode.t, ()) => { let icon = () => switch (node.icon) { | Some(icon) => @@ -68,16 +82,16 @@ let nodeView = (~font: UiFont.t, ~fg, ~node: FsTreeNode.t, ()) => { switch (node.kind) { | Directory({isOpen, _}) => | _ => }; - + - + ; }; @@ -86,7 +100,8 @@ module TreeView = TreeView.Make(FsTreeNode.Model); let make = ( ~tree: FsTreeNode.t, - ~focus: option(string), + ~active: option(string), + ~focus: option(int), ~onNodeClick, ~state: State.t, (), @@ -98,7 +113,9 @@ let make = let active = switch (active, state.workspace) { - | (Some(path), Some(workspace)) => toNodePath(workspace, tree, path) + | (Some(path), Some(workspace)) => + findNodeByPath(workspace, tree, path) + |> Option.map((node: FsTreeNode.t) => node.id) | _ => None }; @@ -117,7 +134,7 @@ let make = theme itemHeight=22 onClick=onNodeClick> - ...{node => } + ...{node => } ; }; diff --git a/src/UI/TreeView.re b/src/UI/TreeView.re index e8a4a9208d..cf482fa246 100644 --- a/src/UI/TreeView.re +++ b/src/UI/TreeView.re @@ -53,14 +53,11 @@ module Styles = { bottom(0), ]; - let item = (~itemHeight, ~isActive, ~theme: Theme.t) => [ + let item = (~itemHeight, ~theme: Theme.t) => [ height(itemHeight), cursor(Revery.MouseCursors.pointer), flexDirection(`Row), overflow(`Hidden), - backgroundColor( - isActive ? theme.menuSelectionBackground : Colors.transparentWhite, - ), ]; let placeholder = (~height) => [Style.height(height)]; @@ -91,7 +88,6 @@ module Make = (Model: TreeModel) => { let rec nodeView = ( ~renderContent, - ~active, ~itemHeight, ~clipRange as (clipStart, clipEnd), ~onClick, @@ -101,21 +97,13 @@ module Make = (Model: TreeModel) => { ) => { let subtreeSize = Model.expandedSubtreeSize(node); - let (isActive, descendantActive) = - switch (active) { - | Some([last]) when last == node => (true, None) - | Some([head, ...tail]) when head == node => (false, Some(tail)) - | Some(_) => (false, None) - | _ => (false, None) - }; - let placeholder = (~size, ()) => ; let item = (~arrow, ()) => onClick(node)} - style={Styles.item(~itemHeight, ~isActive, ~theme)}> + style={Styles.item(~itemHeight, ~theme)}> {renderContent(node)} ; @@ -127,7 +115,6 @@ module Make = (Model: TreeModel) => { let element = { let%component make = ( ~children as renderContent, - ~active: option(list(Model.t)), ~itemHeight, ~initialRowsToRender=10, ~onClick, @@ -266,7 +252,6 @@ module Make = (Model: TreeModel) => { Date: Wed, 18 Dec 2019 16:35:54 +0100 Subject: [PATCH 03/13] add Utility.ArrayEx.findIndex --- src/Core/Utility.re | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Core/Utility.re b/src/Core/Utility.re index 358b7db620..28f892a4b1 100644 --- a/src/Core/Utility.re +++ b/src/Core/Utility.re @@ -254,6 +254,24 @@ module RangeUtil = { }; }; +module ArrayEx = { + exception Found(int); + + let findIndex = (predicate, array) => + try( + { + for (i in 0 to Array.length(array) - 1) { + if (predicate(array[i])) { + raise(Found(i)); + }; + }; + None; + } + ) { + | Found(i) => Some(i) + }; +}; + // TODO: Remove after 4.08 upgrade module List = { include List; From a07fd564350c43c0c956a8a33d846a7cf8275a98 Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 17:02:36 +0100 Subject: [PATCH 04/13] add prev/nextExpandedNode to FsTreeNode --- src/Model/FsTreeNode.re | 89 ++++++++++++++++++++++++++++++++++++++-- src/Model/FsTreeNode.rei | 7 +++- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index 814025e524..dd45cfeb93 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -1,3 +1,7 @@ +open Oni_Core; + +module ArrayEx = Utility.ArrayEx; + type t = { id: int, path: string, @@ -55,7 +59,8 @@ let directory = (~isOpen=false, path, ~id, ~icon, ~children) => { }; }; -let findNodesByLocalPath = (path, tree) => { +let findNodesByPath = (path, tree) => { + let path = Workspace.toRelativePath(tree.path, path); let pathHashes = path |> String.split_on_char(Filename.dir_sep.[0]) @@ -90,7 +95,8 @@ let findNodesByLocalPath = (path, tree) => { }; }; -let findByLocalPath = (path, tree) => { +let findByPath = (path, tree) => { + let path = Workspace.toRelativePath(tree.path, path); let pathSegments = path |> String.split_on_char(Filename.dir_sep.[0]); let rec loop = (node, children, pathSegments) => @@ -98,8 +104,7 @@ let findByLocalPath = (path, tree) => { | [] => Some(node) | [pathSegment, ...rest] => switch (children) { - | [] => - None + | [] => None | [node, ...children] => if (node.displayName == pathSegment) { @@ -121,6 +126,82 @@ let findByLocalPath = (path, tree) => { }; }; +let prevExpandedNode = (path, tree) => + switch (findNodesByPath(path, tree)) { + | `Success(nodePath) => + switch (List.rev(nodePath)) { + // Has a parent, and therefore also siblings + | [focus, {kind: Directory({children, _}), _} as parent, ..._] => + let children = children |> Array.of_list; + let index = children |> ArrayEx.findIndex(child => child.id == focus.id); + + switch (index) { + // is first child + | Some(index) when index == 0 => Some(parent) + + | Some(index) => + switch (children[index - 1]) { + // is open directory with at least one child + | { + kind: Directory({isOpen: true, children: [_, ..._] as children}), + _, + } => + let children = children |> Array.of_list; + let lastChild = children[Array.length(children) - 1]; + Some(lastChild); + + | prev => Some(prev) + } + + | None => None // is not a child of its parent (?!) + }; + | _ => None // has neither parent or siblings + } + | `Partial(_) + | `Failed => None // path does not exist in this ree + }; + +let nextExpandedNode = (path, tree) => + switch (findNodesByPath(path, tree)) { + | `Success(nodePath) => + let rec loop = revNodePath => + switch (revNodePath) { + | [ + focus, + ...[{kind: Directory({children, _}), _}, ..._] as ancestors, + ] => + let children = children |> Array.of_list; + let index = + children |> ArrayEx.findIndex(child => child.id == focus.id); + + switch (index) { + // is last child + | Some(index) when index == Array.length(children) - 1 => + loop(ancestors) + + | Some(index) => + let next = children[index + 1]; + Some(next); + + | None => None // is not a child of its parent (?!) + }; + | _ => None // has neither parent or siblings + }; + + switch (List.rev(nodePath)) { + // Focus is open directory with at least one child + | [ + {kind: Directory({isOpen: true, children: [firstChild, ..._]}), _}, + ..._, + ] => + Some(firstChild) + | revNodePath => loop(revNodePath) + }; + + | `Partial(_) + | `Failed => None // path does not exist in this tree + }; + let update = (~tree, ~updater, nodeId) => { let rec loop = fun diff --git a/src/Model/FsTreeNode.rei b/src/Model/FsTreeNode.rei index af6dfdc881..e3ab353d69 100644 --- a/src/Model/FsTreeNode.rei +++ b/src/Model/FsTreeNode.rei @@ -28,9 +28,12 @@ let directory: ) => t; -let findNodesByLocalPath: +let findNodesByPath: (string, t) => [ | `Success(list(t)) | `Partial(t) | `Failed]; -let findByLocalPath: (string, t) => option(t); +let findByPath: (string, t) => option(t); + +let prevExpandedNode: (string, t) => option(t); +let nextExpandedNode: (string, t) => option(t); let update: (~tree: t, ~updater: t => t, int) => t; let updateNodesInPath: (~tree: t, ~updater: t => t, list(t)) => t; From dd17482eff5b07a8b8c963e3dc606c21a59c5945 Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 17:20:03 +0100 Subject: [PATCH 05/13] focus previous and next --- src/Model/FileExplorer.re | 7 ++- src/Model/FileExplorer.rei | 6 ++- src/Store/FileExplorerStore.re | 79 ++++++++++++++++++++--------- src/UI/FileTreeView.re | 92 +++++++++++++++++++++++----------- 4 files changed, 126 insertions(+), 58 deletions(-) diff --git a/src/Model/FileExplorer.re b/src/Model/FileExplorer.re index 05dccbf7d0..7be65a2147 100644 --- a/src/Model/FileExplorer.re +++ b/src/Model/FileExplorer.re @@ -7,7 +7,7 @@ type t = { isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], active: option(string), // path - focus: option(int), // node id + focus: option(string) // node id }; [@deriving show({with_path: false})] @@ -16,7 +16,9 @@ type action = | NodeLoaded(int, [@opaque] FsTreeNode.t) | FocusNodeLoaded(int, [@opaque] FsTreeNode.t) | NodeClicked([@opaque] FsTreeNode.t) - | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]); + | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) + | FocusPrev + | FocusNext; module ExplorerId = UniqueId.Make({}); @@ -138,4 +140,5 @@ let initial = { isOpen: true, scrollOffset: `Start(0.), active: None, + focus: None, }; diff --git a/src/Model/FileExplorer.rei b/src/Model/FileExplorer.rei index d2138c79e9..b5a2a92ff8 100644 --- a/src/Model/FileExplorer.rei +++ b/src/Model/FileExplorer.rei @@ -5,7 +5,7 @@ type t = { isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], active: option(string), // path - focus: option(int), // node id + focus: option(string) // node id }; [@deriving show] @@ -14,7 +14,9 @@ type action = | NodeLoaded(int, [@opaque] FsTreeNode.t) | FocusNodeLoaded(int, [@opaque] FsTreeNode.t) | NodeClicked([@opaque] FsTreeNode.t) - | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]); + | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) + | FocusPrev + | FocusNext; let initial: t; diff --git a/src/Store/FileExplorerStore.re b/src/Store/FileExplorerStore.re index 8223677113..153ca898c7 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -69,17 +69,15 @@ let setOpen = (isOpen, state) => updateFileExplorer(s => {...s, isOpen}, state); let setActive = (maybePath, state) => updateFileExplorer(s => {...s, active: maybePath}, state); -let setFocus = (maybeId, state) => - updateFileExplorer(s => {...s, focus: maybeId}, state); +let setFocus = (maybePath, state) => + updateFileExplorer(s => {...s, focus: maybePath}, state); let setScrollOffset = (scrollOffset, state) => updateFileExplorer(s => {...s, scrollOffset}, state); let revealPath = (state: State.t, path) => { - switch (state.fileExplorer.tree, state.workspace) { - | (Some(tree), Some({workingDirectory, _})) => - let localPath = Workspace.toRelativePath(workingDirectory, path); - - switch (FsTreeNode.findNodesByLocalPath(localPath, tree)) { + switch (state.fileExplorer.tree) { + | Some(tree) => + switch (FsTreeNode.findNodesByPath(path, tree)) { // Nothing to do | `Success([]) | `Failed => (state, Isolinear.Effect.none) @@ -115,12 +113,19 @@ let revealPath = (state: State.t, path) => { state |> setTree(tree) |> setScrollOffset(offset), Isolinear.Effect.none, ); - }; + } - | _ => (state, Isolinear.Effect.none) + | None => (state, Isolinear.Effect.none) }; }; +let replaceNode = (nodeId, node, state: State.t) => + switch (state.fileExplorer.tree) { + | Some(tree) => + setTree(FsTreeNode.update(nodeId, ~tree, ~updater=_ => node), state) + | None => state + }; + let start = () => { let (stream, _) = Isolinear.Stream.create(); @@ -133,36 +138,32 @@ let start = () => { }; let updater = (state: State.t, action: FileExplorer.action) => { - let replaceNode = (nodeId, node) => - switch (state.fileExplorer.tree) { - | Some(tree) => - setTree(FsTreeNode.update(nodeId, ~tree, ~updater=_ => node), state) - | None => state - }; - switch (action) { | TreeLoaded(tree) => (setTree(tree, state), Isolinear.Effect.none) - | NodeLoaded(id, node) => (replaceNode(id, node), Isolinear.Effect.none) + | NodeLoaded(id, node) => ( + replaceNode(id, node, state), + Isolinear.Effect.none, + ) | FocusNodeLoaded(id, node) => switch (state.fileExplorer.active) { - | Some(path) => revealPath(replaceNode(id, node), path) + | Some(path) => + let state = replaceNode(id, node, state); + revealPath(state, path); | None => (state, Isolinear.Effect.none) } | NodeClicked(node) => - let state = - state - // Set active here to avoid scrolling in BufferEnter - |> setActive(Some(node.path)) - |> setFocus(Some(node.id)); + let state = state |> setFocus(Some(node.path)); switch (node) { - | {kind: File, path, _} => (state, openFileByPathEffect(path)) + | {kind: File, path, _} => + // Set active here to avoid scrolling in BufferEnter + (state |> setActive(Some(node.path)), openFileByPathEffect(path)) | {kind: Directory({isOpen, _}), _} => ( - replaceNode(node.id, FsTreeNode.toggleOpen(node)), + replaceNode(node.id, FsTreeNode.toggleOpen(node), state), isOpen ? Isolinear.Effect.none : Effects.load( @@ -180,6 +181,34 @@ let start = () => { setScrollOffset(offset, state), Isolinear.Effect.none, ) + + | FocusPrev => + switch (state.fileExplorer.tree, state.fileExplorer.focus) { + | (Some(tree), Some(path)) => + switch (FsTreeNode.prevExpandedNode(path, tree)) { + | Some(node) => ( + setFocus(Some(node.path), state), + Isolinear.Effect.none, + ) + | None => (state, Isolinear.Effect.none) + } + + | _ => (state, Isolinear.Effect.none) + } + + | FocusNext => + switch (state.fileExplorer.tree, state.fileExplorer.focus) { + | (Some(tree), Some(path)) => + switch (FsTreeNode.nextExpandedNode(path, tree)) { + | Some(node) => ( + setFocus(Some(node.path), state), + Isolinear.Effect.none, + ) + | None => (state, Isolinear.Effect.none) + } + + | _ => (state, Isolinear.Effect.none) + } }; }; diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index 608090e006..523207a0b1 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -6,12 +6,6 @@ open Revery.UI; module Option = Utility.Option; -let findNodeByPath = (workspace: Workspace.workspace, tree, path) => { - let localPath = Workspace.toRelativePath(workspace.workingDirectory, path); - - FsTreeNode.findByLocalPath(localPath, tree); -}; - module Styles = { open Style; @@ -36,7 +30,9 @@ module Styles = { flexDirection(`Row), flexGrow(1), alignItems(`Center), - backgroundColor(isFocus ? theme.menuSelectionBackground : theme.sideBarBackground) + backgroundColor( + isFocus ? theme.menuSelectionBackground : theme.sideBarBackground, + ), ]; let text = (~isActive, ~theme: Theme.t, ~font: UiFont.t) => [ @@ -64,7 +60,15 @@ let setiIcon = (~icon, ~fontSize as size, ~fg, ()) => { />; }; -let nodeView = (~isFocus, ~isActive, ~font: UiFont.t, ~theme: Theme.t, ~node: FsTreeNode.t, ()) => { +let nodeView = + ( + ~isFocus, + ~isActive, + ~font: UiFont.t, + ~theme: Theme.t, + ~node: FsTreeNode.t, + (), + ) => { let icon = () => switch (node.icon) { | Some(icon) => @@ -82,42 +86,49 @@ let nodeView = (~isFocus, ~isActive, ~font: UiFont.t, ~theme: Theme.t, ~node: Fs switch (node.kind) { | Directory({isOpen, _}) => | _ => }; - + - + ; }; module TreeView = TreeView.Make(FsTreeNode.Model); -let make = - ( - ~tree: FsTreeNode.t, - ~active: option(string), - ~focus: option(int), - ~onNodeClick, - ~state: State.t, - (), - ) => { +let%component make = + ( + ~tree: FsTreeNode.t, + ~active: option(string), + ~focus: option(string), + ~onNodeClick, + ~state: State.t, + (), + ) => { + let%hook (containerRef, setContainerRef) = Hooks.ref(None); + [@warning "-27"] let State.{theme, uiFont as font, _} = state; let fg = state.theme.sideBarForeground; let active = - switch (active, state.workspace) { - | (Some(path), Some(workspace)) => - findNodeByPath(workspace, tree, path) - |> Option.map((node: FsTreeNode.t) => node.id) - | _ => None - }; + active + |> Option.bind(path => FsTreeNode.findByPath(path, tree)) + |> Option.map((node: FsTreeNode.t) => node.id); + + let focus = + focus + |> Option.bind(path => FsTreeNode.findByPath(path, tree)) + |> Option.map((node: FsTreeNode.t) => node.id); let FileExplorer.{scrollOffset, _} = state.fileExplorer; let onScrollOffsetChange = offset => @@ -125,16 +136,39 @@ let make = FileExplorer(ScrollOffsetChanged(offset)), ); - + let onNodeClick = node => { + Option.iter(Revery.UI.Focus.focus, containerRef); + onNodeClick(node); + }; + + let onKeyDown = (event: NodeEvents.keyEventParams) => { + switch (event.keycode) { + | v when v == 1073741906 => + GlobalContext.current().dispatch(Actions.FileExplorer(FocusPrev)) + | v when v == 1073741905 => + GlobalContext.current().dispatch(Actions.FileExplorer(FocusNext)) + | _ => () + }; + }; + + setContainerRef(Some(ref))}> - ...{node => } + ...{node => + + } ; }; From eac23b6d0ad1a28ad420a5edfad367abe354e414 Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 17:59:16 +0100 Subject: [PATCH 06/13] use hash for findByPath --- src/Model/FsTreeNode.re | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index dd45cfeb93..7ae4e3052f 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -97,17 +97,20 @@ let findNodesByPath = (path, tree) => { let findByPath = (path, tree) => { let path = Workspace.toRelativePath(tree.path, path); - let pathSegments = path |> String.split_on_char(Filename.dir_sep.[0]); + let pathHashes = + path + |> String.split_on_char(Filename.dir_sep.[0]) + |> List.map(Hashtbl.hash); - let rec loop = (node, children, pathSegments) => - switch (pathSegments) { + let rec loop = (node, children, pathHashes) => + switch (pathHashes) { | [] => Some(node) - | [pathSegment, ...rest] => + | [hash, ...rest] => switch (children) { | [] => None | [node, ...children] => - if (node.displayName == pathSegment) { + if (node.hash == hash) { let children = switch (node.kind) { | Directory({children, _}) => children @@ -115,13 +118,13 @@ let findByPath = (path, tree) => { }; loop(node, children, rest); } else { - loop(node, children, pathSegments); + loop(node, children, pathHashes); } } }; switch (tree.kind) { - | Directory({children, _}) => loop(tree, children, pathSegments) + | Directory({children, _}) => loop(tree, children, pathHashes) | File => None }; }; From cf661bb3eb4fdce7aba1bc677ff91f390535d5c2 Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 19:25:44 +0100 Subject: [PATCH 07/13] remove FsTreeNode.id, add equals --- src/Model/FileExplorer.re | 18 ++++++------------ src/Model/FileExplorer.rei | 4 ++-- src/Model/FsTreeNode.re | 34 ++++++++++++++++++---------------- src/Model/FsTreeNode.rei | 8 ++++---- src/Store/FileExplorerStore.re | 24 ++++++++++++------------ src/UI/FileTreeView.re | 16 ++-------------- 6 files changed, 44 insertions(+), 60 deletions(-) diff --git a/src/Model/FileExplorer.re b/src/Model/FileExplorer.re index 7be65a2147..021314e997 100644 --- a/src/Model/FileExplorer.re +++ b/src/Model/FileExplorer.re @@ -1,4 +1,3 @@ -open Revery; open Oni_Core; open Oni_Extensions; @@ -13,16 +12,13 @@ type t = { [@deriving show({with_path: false})] type action = | TreeLoaded([@opaque] FsTreeNode.t) - | NodeLoaded(int, [@opaque] FsTreeNode.t) - | FocusNodeLoaded(int, [@opaque] FsTreeNode.t) + | NodeLoaded(string, [@opaque] FsTreeNode.t) + | FocusNodeLoaded(string, [@opaque] FsTreeNode.t) | NodeClicked([@opaque] FsTreeNode.t) | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) | FocusPrev | FocusNext; -module ExplorerId = - UniqueId.Make({}); - let getFileIcon = (languageInfo, iconTheme, filePath) => { let fileIcon = LanguageInfo.getLanguageFromFilePath(languageInfo, filePath) @@ -82,7 +78,6 @@ let getFilesAndFolders = (~ignored, cwd, getIcon) => { let rec getDirContent = (~loadChildren=false, cwd) => { let toFsTreeNode = file => { let path = Filename.concat(cwd, file); - let id = ExplorerId.getUniqueId(); if (isDirectory(path)) { let%lwt children = @@ -97,10 +92,11 @@ let getFilesAndFolders = (~ignored, cwd, getIcon) => { Lwt.return([]); }; - FsTreeNode.directory(path, ~id, ~icon=getIcon(path), ~children) - |> Lwt.return; + Lwt.return( + FsTreeNode.directory(path, ~icon=getIcon(path), ~children) + ) } else { - FsTreeNode.file(path, ~id, ~icon=getIcon(path)) |> Lwt.return; + FsTreeNode.file(path, ~icon=getIcon(path)) |> Lwt.return; }; }; @@ -119,7 +115,6 @@ let getFilesAndFolders = (~ignored, cwd, getIcon) => { }; let getDirectoryTree = (cwd, languageInfo, iconTheme, ignored) => { - let id = ExplorerId.getUniqueId(); let getIcon = getFileIcon(languageInfo, iconTheme); let children = getFilesAndFolders(~ignored, cwd, getIcon) @@ -128,7 +123,6 @@ let getDirectoryTree = (cwd, languageInfo, iconTheme, ignored) => { FsTreeNode.directory( cwd, - ~id, ~icon=getIcon(cwd), ~children, ~isOpen=true, diff --git a/src/Model/FileExplorer.rei b/src/Model/FileExplorer.rei index b5a2a92ff8..e5831c16e5 100644 --- a/src/Model/FileExplorer.rei +++ b/src/Model/FileExplorer.rei @@ -11,8 +11,8 @@ type t = { [@deriving show] type action = | TreeLoaded([@opaque] FsTreeNode.t) - | NodeLoaded(int, [@opaque] FsTreeNode.t) - | FocusNodeLoaded(int, [@opaque] FsTreeNode.t) + | NodeLoaded(string, [@opaque] FsTreeNode.t) + | FocusNodeLoaded(string, [@opaque] FsTreeNode.t) | NodeClicked([@opaque] FsTreeNode.t) | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) | FocusPrev diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index 7ae4e3052f..bb0529ff29 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -3,7 +3,6 @@ open Oni_Core; module ArrayEx = Utility.ArrayEx; type t = { - id: int, path: string, displayName: string, hash: int, // hash of basename, so only comparable locally @@ -30,35 +29,35 @@ let rec countExpandedSubtree = | _ => 1; -let file = (path, ~id, ~icon) => { +let file = (path, ~icon) => { let basename = Filename.basename(path); { - id, path, - displayName: basename, hash: Hashtbl.hash(basename), + displayName: basename, icon, kind: File, expandedSubtreeSize: 1, }; }; -let directory = (~isOpen=false, path, ~id, ~icon, ~children) => { +let directory = (~isOpen=false, path, ~icon, ~children) => { let kind = Directory({isOpen, children}); let basename = Filename.basename(path); { - id, path, - displayName: basename, hash: Hashtbl.hash(basename), + displayName: basename, icon, kind, expandedSubtreeSize: countExpandedSubtree(kind), }; }; +let equals = (a, b) => a.hash == b.hash && a.path == b.path; + let findNodesByPath = (path, tree) => { let path = Workspace.toRelativePath(tree.path, path); let pathHashes = @@ -68,12 +67,16 @@ let findNodesByPath = (path, tree) => { let rec loop = (focusedNodes, children, pathSegments) => switch (pathSegments) { - | [] => `Success(focusedNodes |> List.rev) + | [] => `Success(List.rev(focusedNodes)) | [hash, ...rest] => switch (children) { | [] => - let last = focusedNodes |> List.hd; - last.id == tree.id ? `Failed : `Partial(last); + let last = List.hd(focusedNodes); + if (equals(last, tree)) { + `Failed + } else { + `Partial(last); + } | [node, ...children] => if (node.hash == hash) { @@ -136,7 +139,7 @@ let prevExpandedNode = (path, tree) => // Has a parent, and therefore also siblings | [focus, {kind: Directory({children, _}), _} as parent, ..._] => let children = children |> Array.of_list; - let index = children |> ArrayEx.findIndex(child => child.id == focus.id); + let index = children |> ArrayEx.findIndex(equals(focus)); switch (index) { // is first child @@ -174,8 +177,7 @@ let nextExpandedNode = (path, tree) => ...[{kind: Directory({children, _}), _}, ..._] as ancestors, ] => let children = children |> Array.of_list; - let index = - children |> ArrayEx.findIndex(child => child.id == focus.id); + let index = children |> ArrayEx.findIndex(equals(focus)); switch (index) { // is last child @@ -205,10 +207,10 @@ let nextExpandedNode = (path, tree) => | `Failed => None // path does not exist in this tree }; -let update = (~tree, ~updater, nodeId) => { +let update = (~tree, ~updater, targetPath) => { let rec loop = fun - | {id, _} as node when id == nodeId => updater(node) + | {path, _} as node when path == targetPath => updater(node) | {kind: Directory({children, _} as dir), _} as node => { let kind = Directory({...dir, children: List.map(loop, children)}); @@ -223,7 +225,7 @@ let update = (~tree, ~updater, nodeId) => { let updateNodesInPath = (~tree, ~updater, nodes) => { let rec loop = (nodes, node) => switch (nodes) { - | [{id, kind, _}, ...rest] when id == node.id => + | [{hash, kind, _}, ...rest] when hash == node.hash => switch (kind) { | Directory({children, _} as dir) => let newChildren = List.map(loop(rest), children); diff --git a/src/Model/FsTreeNode.rei b/src/Model/FsTreeNode.rei index e3ab353d69..c9a01fbe15 100644 --- a/src/Model/FsTreeNode.rei +++ b/src/Model/FsTreeNode.rei @@ -1,6 +1,5 @@ type t = pri { - id: int, path: string, displayName: string, hash: int, // hash of basename, so only comparable locally @@ -17,12 +16,11 @@ and kind = }) | File; -let file: (string, ~id: int, ~icon: option(IconTheme.IconDefinition.t)) => t; +let file: (string, ~icon: option(IconTheme.IconDefinition.t)) => t; let directory: ( ~isOpen: bool=?, string, - ~id: int, ~icon: option(IconTheme.IconDefinition.t), ~children: list(t) ) => @@ -35,11 +33,13 @@ let findByPath: (string, t) => option(t); let prevExpandedNode: (string, t) => option(t); let nextExpandedNode: (string, t) => option(t); -let update: (~tree: t, ~updater: t => t, int) => t; +let update: (~tree: t, ~updater: t => t, string) => t; let updateNodesInPath: (~tree: t, ~updater: t => t, list(t)) => t; let toggleOpen: t => t; let setOpen: t => t; +let equals: (t, t) => bool; + module Model: { type nonrec t = t; diff --git a/src/Store/FileExplorerStore.re b/src/Store/FileExplorerStore.re index 153ca898c7..24bdaecd04 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -28,11 +28,11 @@ module Effects = { // Counts the number of axpanded nodes before the node specified by the given path let nodeOffsetByPath = (tree, path) => { - let rec loop = (node: FsTreeNode.t, path) => + let rec loop = (node, path) => switch (path) { | [] => `Found(0) - | [(focus: FsTreeNode.t), ...focusTail] => - if (focus.id != node.id) { + | [focus, ...focusTail] => + if (FsTreeNode.equals(focus, node)) { `NotFound(node.expandedSubtreeSize); } else { switch (node.kind) { @@ -91,7 +91,7 @@ let revealPath = (state: State.t, path) => { state.iconTheme, state.configuration, ~onComplete=node => - Actions.FileExplorer(FocusNodeLoaded(lastNode.id, node)) + Actions.FileExplorer(FocusNodeLoaded(lastNode.path, node)) ), ) @@ -119,10 +119,10 @@ let revealPath = (state: State.t, path) => { }; }; -let replaceNode = (nodeId, node, state: State.t) => +let replaceNode = (path, node, state: State.t) => switch (state.fileExplorer.tree) { | Some(tree) => - setTree(FsTreeNode.update(nodeId, ~tree, ~updater=_ => node), state) + setTree(FsTreeNode.update(path, ~tree, ~updater=_ => node), state) | None => state }; @@ -141,15 +141,15 @@ let start = () => { switch (action) { | TreeLoaded(tree) => (setTree(tree, state), Isolinear.Effect.none) - | NodeLoaded(id, node) => ( - replaceNode(id, node, state), + | NodeLoaded(path, node) => ( + replaceNode(path, node, state), Isolinear.Effect.none, ) - | FocusNodeLoaded(id, node) => + | FocusNodeLoaded(path, node) => switch (state.fileExplorer.active) { | Some(path) => - let state = replaceNode(id, node, state); + let state = replaceNode(path, node, state); revealPath(state, path); | None => (state, Isolinear.Effect.none) } @@ -163,7 +163,7 @@ let start = () => { (state |> setActive(Some(node.path)), openFileByPathEffect(path)) | {kind: Directory({isOpen, _}), _} => ( - replaceNode(node.id, FsTreeNode.toggleOpen(node), state), + replaceNode(node.path, FsTreeNode.toggleOpen(node), state), isOpen ? Isolinear.Effect.none : Effects.load( @@ -172,7 +172,7 @@ let start = () => { state.iconTheme, state.configuration, ~onComplete=newNode => - Actions.FileExplorer(NodeLoaded(node.id, newNode)) + Actions.FileExplorer(NodeLoaded(node.path, newNode)) ), ) }; diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index 523207a0b1..a10c0fa4b9 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -118,18 +118,6 @@ let%component make = [@warning "-27"] let State.{theme, uiFont as font, _} = state; - let fg = state.theme.sideBarForeground; - - let active = - active - |> Option.bind(path => FsTreeNode.findByPath(path, tree)) - |> Option.map((node: FsTreeNode.t) => node.id); - - let focus = - focus - |> Option.bind(path => FsTreeNode.findByPath(path, tree)) - |> Option.map((node: FsTreeNode.t) => node.id); - let FileExplorer.{scrollOffset, _} = state.fileExplorer; let onScrollOffsetChange = offset => GlobalContext.current().dispatch( @@ -162,8 +150,8 @@ let%component make = onClick=onNodeClick> ...{node => Date: Wed, 18 Dec 2019 19:28:14 +0100 Subject: [PATCH 08/13] fix warnings --- src/Store/FileExplorerStore.re | 4 ++-- src/UI/FileTreeView.re | 1 - src/UI/TreeView.re | 8 ++------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Store/FileExplorerStore.re b/src/Store/FileExplorerStore.re index 24bdaecd04..cdd3c284f5 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -148,9 +148,9 @@ let start = () => { | FocusNodeLoaded(path, node) => switch (state.fileExplorer.active) { - | Some(path) => + | Some(activePath) => let state = replaceNode(path, node, state); - revealPath(state, path); + revealPath(state, activePath); | None => (state, Isolinear.Effect.none) } diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index a10c0fa4b9..392ad1ef64 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -145,7 +145,6 @@ let%component make = scrollOffset onScrollOffsetChange tree - theme itemHeight=22 onClick=onNodeClick> ...{node => diff --git a/src/UI/TreeView.re b/src/UI/TreeView.re index cf482fa246..107694c752 100644 --- a/src/UI/TreeView.re +++ b/src/UI/TreeView.re @@ -53,7 +53,7 @@ module Styles = { bottom(0), ]; - let item = (~itemHeight, ~theme: Theme.t) => [ + let item = (~itemHeight) => [ height(itemHeight), cursor(Revery.MouseCursors.pointer), flexDirection(`Row), @@ -92,7 +92,6 @@ module Make = (Model: TreeModel) => { ~clipRange as (clipStart, clipEnd), ~onClick, ~node, - ~theme, (), ) => { let subtreeSize = Model.expandedSubtreeSize(node); @@ -103,7 +102,7 @@ module Make = (Model: TreeModel) => { let item = (~arrow, ()) => onClick(node)} - style={Styles.item(~itemHeight, ~theme)}> + style={Styles.item(~itemHeight)}> {renderContent(node)} ; @@ -119,7 +118,6 @@ module Make = (Model: TreeModel) => { clipRange=(clipStart - count, clipEnd - count) onClick node=child - theme />; loop( @@ -167,7 +165,6 @@ module Make = (Model: TreeModel) => { ~onScrollOffsetChange: [ | `Start(float) | `Middle(float)] => unit=_ => (), ~tree, - ~theme, (), ) => { let%hook (outerRef, setOuterRef) = Hooks.ref(None); @@ -256,7 +253,6 @@ module Make = (Model: TreeModel) => { clipRange onClick node=tree - theme /> From d5d85c372983c1b245f57e2222dff63d44b966fe Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 19:28:57 +0100 Subject: [PATCH 09/13] formatting --- src/Model/FileExplorer.re | 11 +++-------- src/Model/FsTreeNode.re | 4 ++-- src/UI/FileTreeView.re | 6 +----- src/UI/TreeView.re | 11 ++--------- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/Model/FileExplorer.re b/src/Model/FileExplorer.re index 021314e997..ffb30aa14d 100644 --- a/src/Model/FileExplorer.re +++ b/src/Model/FileExplorer.re @@ -93,8 +93,8 @@ let getFilesAndFolders = (~ignored, cwd, getIcon) => { }; Lwt.return( - FsTreeNode.directory(path, ~icon=getIcon(path), ~children) - ) + FsTreeNode.directory(path, ~icon=getIcon(path), ~children), + ); } else { FsTreeNode.file(path, ~icon=getIcon(path)) |> Lwt.return; }; @@ -121,12 +121,7 @@ let getDirectoryTree = (cwd, languageInfo, iconTheme, ignored) => { |> Lwt_main.run |> List.sort(sortByLoweredDisplayName); - FsTreeNode.directory( - cwd, - ~icon=getIcon(cwd), - ~children, - ~isOpen=true, - ); + FsTreeNode.directory(cwd, ~icon=getIcon(cwd), ~children, ~isOpen=true); }; let initial = { diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index bb0529ff29..5e525c84eb 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -73,10 +73,10 @@ let findNodesByPath = (path, tree) => { | [] => let last = List.hd(focusedNodes); if (equals(last, tree)) { - `Failed + `Failed; } else { `Partial(last); - } + }; | [node, ...children] => if (node.hash == hash) { diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index 392ad1ef64..f321a127e1 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -142,11 +142,7 @@ let%component make = setContainerRef(Some(ref))}> + scrollOffset onScrollOffsetChange tree itemHeight=22 onClick=onNodeClick> ...{node => { let item = (~arrow, ()) => onClick(node)} - style={Styles.item(~itemHeight)}> + onClick={() => onClick(node)} style={Styles.item(~itemHeight)}> {renderContent(node)} ; @@ -247,13 +246,7 @@ module Make = (Model: TreeModel) => { onMouseWheel=scroll> - + {showScrollbar ? : React.empty} From 5b4124c145317d78263d39724b15023ae3a07e4c Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 20:42:04 +0100 Subject: [PATCH 10/13] add Utility.Path --- src/Core/Utility.re | 12 ++++++++++++ src/Model/FsTreeNode.re | 9 +++++---- src/Model/Workspace.re | 5 ----- src/Store/QuickmenuStoreConnector.re | 5 +++-- src/UI/LocationListView.re | 3 ++- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Core/Utility.re b/src/Core/Utility.re index 28f892a4b1..8fb4157bd6 100644 --- a/src/Core/Utility.re +++ b/src/Core/Utility.re @@ -701,3 +701,15 @@ module ChunkyQueue: { let toList = ({front, rear, _}) => front @ (Queue.toList(rear) |> List.concat); }; + +module Path = { + // Not very robust path-handling utilities. + // TODO: Make good + + let toRelative = (~base, path) => { + let base = base == "/" ? base : base ++ Filename.dir_sep; + Str.replace_first(Str.regexp_string(base), "", path); + }; + + let explode = String.split_on_char(Filename.dir_sep.[0]); +}; diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index 5e525c84eb..7b0d6d8435 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -1,6 +1,7 @@ open Oni_Core; module ArrayEx = Utility.ArrayEx; +module Path = Utility.Path; type t = { path: string, @@ -59,10 +60,10 @@ let directory = (~isOpen=false, path, ~icon, ~children) => { let equals = (a, b) => a.hash == b.hash && a.path == b.path; let findNodesByPath = (path, tree) => { - let path = Workspace.toRelativePath(tree.path, path); let pathHashes = path - |> String.split_on_char(Filename.dir_sep.[0]) + |> Path.toRelative(~base=tree.path) + |> Path.explode |> List.map(Hashtbl.hash); let rec loop = (focusedNodes, children, pathSegments) => @@ -99,10 +100,10 @@ let findNodesByPath = (path, tree) => { }; let findByPath = (path, tree) => { - let path = Workspace.toRelativePath(tree.path, path); let pathHashes = path - |> String.split_on_char(Filename.dir_sep.[0]) + |> Path.toRelative(~base=tree.path) + |> Path.explode |> List.map(Hashtbl.hash); let rec loop = (node, children, pathHashes) => diff --git a/src/Model/Workspace.re b/src/Model/Workspace.re index aab3c53e2f..0235e3efd5 100644 --- a/src/Model/Workspace.re +++ b/src/Model/Workspace.re @@ -16,8 +16,3 @@ type workspace = { type t = option(workspace); let initial: t = None; - -let toRelativePath = (base, path) => { - let base = base == "/" ? base : base ++ Filename.dir_sep; - Str.replace_first(Str.regexp_string(base), "", path); -}; diff --git a/src/Store/QuickmenuStoreConnector.re b/src/Store/QuickmenuStoreConnector.re index 6619fc5d30..5a285a0ac5 100644 --- a/src/Store/QuickmenuStoreConnector.re +++ b/src/Store/QuickmenuStoreConnector.re @@ -12,6 +12,7 @@ module Actions = Model.Actions; module Animation = Model.Animation; module Quickmenu = Model.Quickmenu; module Utility = Core.Utility; +module Path = Utility.Path; module ExtensionContributions = Oni_Extensions.ExtensionContributions; module Log = (val Core.Log.withNamespace("Oni2.QuickmenuStore")); @@ -69,7 +70,7 @@ let start = (themeInfo: Model.ThemeInfo.t) => { Some( Actions.{ category: None, - name: Model.Workspace.toRelativePath(currentDirectory, path), + name: Path.toRelative(~base=currentDirectory, path), command: () => { Model.Actions.OpenFileByPath(path, None, None); }, @@ -375,7 +376,7 @@ let subscriptions = ripgrep => { let stringToCommand = (languageInfo, iconTheme, fullPath) => Actions.{ category: None, - name: Model.Workspace.toRelativePath(directory, fullPath), + name: Path.toRelative(~base=directory, fullPath), command: () => Model.Actions.OpenFileByPath(fullPath, None, None), icon: Model.FileExplorer.getFileIcon(languageInfo, iconTheme, fullPath), diff --git a/src/UI/LocationListView.re b/src/UI/LocationListView.re index 634883fdb9..fab48974e5 100644 --- a/src/UI/LocationListView.re +++ b/src/UI/LocationListView.re @@ -6,6 +6,7 @@ open Oni_Core; open Oni_Model; module Option = Utility.Option; +module Path = Utility.Path; // TODO: move to Revery let getFontAdvance = (fontFile, fontSize) => { @@ -84,7 +85,7 @@ let item = let locationText = Printf.sprintf( "%s:%n - ", - Workspace.toRelativePath(workingDirectory, item.file), + Path.toRelative(~base=workingDirectory, item.file), Index.toOneBased(item.location.line), ); From f8209df6dc17282e2b9282fbafdd4ed41d66f33a Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 20:47:48 +0100 Subject: [PATCH 11/13] abstract hash function --- src/Model/FsTreeNode.re | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index 7b0d6d8435..2ca664bca2 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -19,6 +19,10 @@ and kind = }) | File; +let _hash = Hashtbl.hash; +let _pathHashes = (~base, path) => + path |> Path.toRelative(~base) |> Path.explode |> List.map(_hash); + let rec countExpandedSubtree = fun | Directory({isOpen: true, children}) => @@ -35,7 +39,7 @@ let file = (path, ~icon) => { { path, - hash: Hashtbl.hash(basename), + hash: _hash(basename), displayName: basename, icon, kind: File, @@ -49,7 +53,7 @@ let directory = (~isOpen=false, path, ~icon, ~children) => { { path, - hash: Hashtbl.hash(basename), + hash: _hash(basename), displayName: basename, icon, kind, @@ -60,14 +64,8 @@ let directory = (~isOpen=false, path, ~icon, ~children) => { let equals = (a, b) => a.hash == b.hash && a.path == b.path; let findNodesByPath = (path, tree) => { - let pathHashes = - path - |> Path.toRelative(~base=tree.path) - |> Path.explode - |> List.map(Hashtbl.hash); - - let rec loop = (focusedNodes, children, pathSegments) => - switch (pathSegments) { + let rec loop = (focusedNodes, children, pathHashes) => + switch (pathHashes) { | [] => `Success(List.rev(focusedNodes)) | [hash, ...rest] => switch (children) { @@ -88,24 +86,19 @@ let findNodesByPath = (path, tree) => { }; loop([node, ...focusedNodes], children, rest); } else { - loop(focusedNodes, children, pathSegments); + loop(focusedNodes, children, pathHashes); } } }; switch (tree.kind) { - | Directory({children, _}) => loop([tree], children, pathHashes) + | Directory({children, _}) => + loop([tree], children, _pathHashes(~base=tree.path, path)) | File => `Failed }; }; let findByPath = (path, tree) => { - let pathHashes = - path - |> Path.toRelative(~base=tree.path) - |> Path.explode - |> List.map(Hashtbl.hash); - let rec loop = (node, children, pathHashes) => switch (pathHashes) { | [] => Some(node) @@ -128,7 +121,8 @@ let findByPath = (path, tree) => { }; switch (tree.kind) { - | Directory({children, _}) => loop(tree, children, pathHashes) + | Directory({children, _}) => + loop(tree, children, _pathHashes(~base=tree.path, path)) | File => None }; }; From 39c48fdd72a8e8f1b06606e1138c75e8dde60962 Mon Sep 17 00:00:00 2001 From: glennsl Date: Wed, 18 Dec 2019 20:57:58 +0100 Subject: [PATCH 12/13] FileExplorerStore.nodeOffsetByPath -> FsTreeNode.expandedIndex --- src/Model/FsTreeNode.re | 36 +++++++++++++++++++++++++++++++++ src/Model/FsTreeNode.rei | 2 ++ src/Store/FileExplorerStore.re | 37 +--------------------------------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index 2ca664bca2..b90f2f22f8 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -202,6 +202,42 @@ let nextExpandedNode = (path, tree) => | `Failed => None // path does not exist in this tree }; +// Counts the number of expanded nodes before the node specified by the given path +let expandedIndex = (tree, path) => { + let rec loop = (node, path) => + switch (path) { + | [] => `Found(0) + | [focus, ...focusTail] => + if (equals(focus, node)) { + `NotFound(node.expandedSubtreeSize); + } else { + switch (node.kind) { + | Directory({isOpen: false, _}) + | File => `Found(0) + + | Directory({isOpen: true, children}) => + let rec loopChildren = (count, children) => + switch (children) { + | [] => `NotFound(count) + | [child, ...childTail] => + switch (loop(child, focusTail)) { + | `Found(subtreeCount) => `Found(count + subtreeCount) + | `NotFound(subtreeCount) => + loopChildren(count + subtreeCount, childTail) + } + }; + + loopChildren(1, children); + }; + } + }; + + switch (loop(tree, path)) { + | `Found(count) => Some(count) + | `NotFound(_) => None + }; +}; + let update = (~tree, ~updater, targetPath) => { let rec loop = fun diff --git a/src/Model/FsTreeNode.rei b/src/Model/FsTreeNode.rei index c9a01fbe15..fc09a614b2 100644 --- a/src/Model/FsTreeNode.rei +++ b/src/Model/FsTreeNode.rei @@ -33,6 +33,8 @@ let findByPath: (string, t) => option(t); let prevExpandedNode: (string, t) => option(t); let nextExpandedNode: (string, t) => option(t); +let expandedIndex: (t, list(t)) => option(int); + let update: (~tree: t, ~updater: t => t, string) => t; let updateNodesInPath: (~tree: t, ~updater: t => t, list(t)) => t; let toggleOpen: t => t; diff --git a/src/Store/FileExplorerStore.re b/src/Store/FileExplorerStore.re index cdd3c284f5..45637589bc 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -26,41 +26,6 @@ module Effects = { }; }; -// Counts the number of axpanded nodes before the node specified by the given path -let nodeOffsetByPath = (tree, path) => { - let rec loop = (node, path) => - switch (path) { - | [] => `Found(0) - | [focus, ...focusTail] => - if (FsTreeNode.equals(focus, node)) { - `NotFound(node.expandedSubtreeSize); - } else { - switch (node.kind) { - | Directory({isOpen: false, _}) - | File => `Found(0) - - | Directory({isOpen: true, children}) => - let rec loopChildren = (count, children) => - switch (children) { - | [] => `NotFound(count) - | [child, ...childTail] => - switch (loop(child, focusTail)) { - | `Found(subtreeCount) => `Found(count + subtreeCount) - | `NotFound(subtreeCount) => - loopChildren(count + subtreeCount, childTail) - } - }; - loopChildren(1, children); - }; - } - }; - - switch (loop(tree, path)) { - | `Found(count) => Some(count) - | `NotFound(_) => None - }; -}; - let updateFileExplorer = (updater, state) => State.{...state, fileExplorer: updater(state.fileExplorer)}; let setTree = (tree, state) => @@ -104,7 +69,7 @@ let revealPath = (state: State.t, path) => { ~updater=FsTreeNode.setOpen, ); let offset = - switch (nodeOffsetByPath(tree, nodes)) { + switch (FsTreeNode.expandedIndex(tree, nodes)) { | Some(offset) => `Middle(float(offset)) | None => state.fileExplorer.scrollOffset }; From 8251fc7f9919c7292b51215af73876958b0044d5 Mon Sep 17 00:00:00 2001 From: glennsl Date: Thu, 19 Dec 2019 14:00:24 +0100 Subject: [PATCH 13/13] select on enter --- src/Model/FileExplorer.re | 1 + src/Model/FileExplorer.rei | 1 + src/Store/FileExplorerStore.re | 66 ++++++++++++++++++++-------------- src/UI/FileTreeView.re | 8 +++++ 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/Model/FileExplorer.re b/src/Model/FileExplorer.re index ffb30aa14d..e5d2d90720 100644 --- a/src/Model/FileExplorer.re +++ b/src/Model/FileExplorer.re @@ -16,6 +16,7 @@ type action = | FocusNodeLoaded(string, [@opaque] FsTreeNode.t) | NodeClicked([@opaque] FsTreeNode.t) | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) + | Select | FocusPrev | FocusNext; diff --git a/src/Model/FileExplorer.rei b/src/Model/FileExplorer.rei index e5831c16e5..6c42bddf09 100644 --- a/src/Model/FileExplorer.rei +++ b/src/Model/FileExplorer.rei @@ -15,6 +15,7 @@ type action = | FocusNodeLoaded(string, [@opaque] FsTreeNode.t) | NodeClicked([@opaque] FsTreeNode.t) | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) + | Select | FocusPrev | FocusNext; diff --git a/src/Store/FileExplorerStore.re b/src/Store/FileExplorerStore.re index 45637589bc..2142151e33 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -84,13 +84,6 @@ let revealPath = (state: State.t, path) => { }; }; -let replaceNode = (path, node, state: State.t) => - switch (state.fileExplorer.tree) { - | Some(tree) => - setTree(FsTreeNode.update(path, ~tree, ~updater=_ => node), state) - | None => state - }; - let start = () => { let (stream, _) = Isolinear.Stream.create(); @@ -102,6 +95,34 @@ let start = () => { }); }; + let replaceNode = (path, node, state: State.t) => + switch (state.fileExplorer.tree) { + | Some(tree) => + setTree(FsTreeNode.update(path, ~tree, ~updater=_ => node), state) + | None => state + }; + + let selectNode = (node: FsTreeNode.t, state) => + switch (node) { + | {kind: File, path, _} => + // Set active here to avoid scrolling in BufferEnter + (state |> setActive(Some(node.path)), openFileByPathEffect(path)) + + | {kind: Directory({isOpen, _}), _} => ( + replaceNode(node.path, FsTreeNode.toggleOpen(node), state), + isOpen + ? Isolinear.Effect.none + : Effects.load( + node.path, + state.languageInfo, + state.iconTheme, + state.configuration, + ~onComplete=newNode => + Actions.FileExplorer(NodeLoaded(node.path, newNode)) + ), + ) + }; + let updater = (state: State.t, action: FileExplorer.action) => { switch (action) { | TreeLoaded(tree) => (setTree(tree, state), Isolinear.Effect.none) @@ -121,32 +142,23 @@ let start = () => { | NodeClicked(node) => let state = state |> setFocus(Some(node.path)); - - switch (node) { - | {kind: File, path, _} => - // Set active here to avoid scrolling in BufferEnter - (state |> setActive(Some(node.path)), openFileByPathEffect(path)) - - | {kind: Directory({isOpen, _}), _} => ( - replaceNode(node.path, FsTreeNode.toggleOpen(node), state), - isOpen - ? Isolinear.Effect.none - : Effects.load( - node.path, - state.languageInfo, - state.iconTheme, - state.configuration, - ~onComplete=newNode => - Actions.FileExplorer(NodeLoaded(node.path, newNode)) - ), - ) - }; + selectNode(node, state); | ScrollOffsetChanged(offset) => ( setScrollOffset(offset, state), Isolinear.Effect.none, ) + | Select => + switch (state.fileExplorer.tree, state.fileExplorer.focus) { + | (Some(tree), Some(path)) => + switch (FsTreeNode.findByPath(path, tree)) { + | Some(node) => selectNode(node, state) + | None => (state, Isolinear.Effect.none) + } + | _ => (state, Isolinear.Effect.none) + } + | FocusPrev => switch (state.fileExplorer.tree, state.fileExplorer.focus) { | (Some(tree), Some(path)) => diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index f321a127e1..ea298232e4 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -131,10 +131,18 @@ let%component make = let onKeyDown = (event: NodeEvents.keyEventParams) => { switch (event.keycode) { + // Enter + | v when v == 13 => + GlobalContext.current().dispatch(Actions.FileExplorer(Select)) + + // arrow up | v when v == 1073741906 => GlobalContext.current().dispatch(Actions.FileExplorer(FocusPrev)) + + // arrow down | v when v == 1073741905 => GlobalContext.current().dispatch(Actions.FileExplorer(FocusNext)) + | _ => () }; };