diff --git a/src/Core/Utility.re b/src/Core/Utility.re index 358b7db620..8fb4157bd6 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; @@ -683,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/FileExplorer.re b/src/Model/FileExplorer.re index a7660ac957..e5d2d90720 100644 --- a/src/Model/FileExplorer.re +++ b/src/Model/FileExplorer.re @@ -1,4 +1,3 @@ -open Revery; open Oni_Core; open Oni_Extensions; @@ -6,19 +5,20 @@ type t = { tree: option(FsTreeNode.t), isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], - focus: option(string) // path + active: option(string), // path + focus: option(string) // node id }; [@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)]); - -module ExplorerId = - UniqueId.Make({}); + | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) + | Select + | FocusPrev + | FocusNext; let getFileIcon = (languageInfo, iconTheme, filePath) => { let fileIcon = @@ -79,7 +79,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 = @@ -94,10 +93,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; }; }; @@ -116,25 +116,19 @@ 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) |> Lwt_main.run |> List.sort(sortByLoweredDisplayName); - FsTreeNode.directory( - cwd, - ~id, - ~icon=getIcon(cwd), - ~children, - ~isOpen=true, - ); + FsTreeNode.directory(cwd, ~icon=getIcon(cwd), ~children, ~isOpen=true); }; let initial = { tree: None, isOpen: true, scrollOffset: `Start(0.), + active: None, focus: None, }; diff --git a/src/Model/FileExplorer.rei b/src/Model/FileExplorer.rei index 7ccdf2f35c..6c42bddf09 100644 --- a/src/Model/FileExplorer.rei +++ b/src/Model/FileExplorer.rei @@ -4,16 +4,20 @@ type t = { tree: option(FsTreeNode.t), isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float)], - focus: option(string) // path + active: option(string), // path + focus: option(string) // node id }; [@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)]); + | ScrollOffsetChanged([ | `Start(float) | `Middle(float)]) + | Select + | FocusPrev + | FocusNext; let initial: t; diff --git a/src/Model/FsTreeNode.re b/src/Model/FsTreeNode.re index 3810f41b4e..b90f2f22f8 100644 --- a/src/Model/FsTreeNode.re +++ b/src/Model/FsTreeNode.re @@ -1,5 +1,9 @@ +open Oni_Core; + +module ArrayEx = Utility.ArrayEx; +module Path = Utility.Path; + type t = { - id: int, path: string, displayName: string, hash: int, // hash of basename, so only comparable locally @@ -15,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}) => @@ -26,49 +34,48 @@ let rec countExpandedSubtree = | _ => 1; -let file = (path, ~id, ~icon) => { +let file = (path, ~icon) => { let basename = Filename.basename(path); { - id, path, + hash: _hash(basename), displayName: basename, - hash: Hashtbl.hash(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, + hash: _hash(basename), displayName: basename, - hash: Hashtbl.hash(basename), icon, kind, expandedSubtreeSize: countExpandedSubtree(kind), }; }; -let findNodesByLocalPath = (path, tree) => { - let pathHashes = - path - |> String.split_on_char(Filename.dir_sep.[0]) - |> List.map(Hashtbl.hash); +let equals = (a, b) => a.hash == b.hash && a.path == b.path; - let rec loop = (focusedNodes, children, pathSegments) => - switch (pathSegments) { - | [] => `Success(focusedNodes |> List.rev) +let findNodesByPath = (path, tree) => { + let rec loop = (focusedNodes, children, pathHashes) => + switch (pathHashes) { + | [] => `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) { @@ -79,21 +86,162 @@ let findNodesByLocalPath = (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 update = (~tree, ~updater, nodeId) => { +let findByPath = (path, tree) => { + let rec loop = (node, children, pathHashes) => + switch (pathHashes) { + | [] => Some(node) + | [hash, ...rest] => + switch (children) { + | [] => None + + | [node, ...children] => + if (node.hash == hash) { + let children = + switch (node.kind) { + | Directory({children, _}) => children + | File => [] + }; + loop(node, children, rest); + } else { + loop(node, children, pathHashes); + } + } + }; + + switch (tree.kind) { + | Directory({children, _}) => + loop(tree, children, _pathHashes(~base=tree.path, path)) + | File => None + }; +}; + +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(equals(focus)); + + 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(equals(focus)); + + 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 + }; + +// 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 - | {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)}); @@ -108,7 +256,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 24fb589b52..fc09a614b2 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,25 +16,32 @@ 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) ) => t; -let findNodesByLocalPath: +let findNodesByPath: (string, t) => [ | `Success(list(t)) | `Partial(t) | `Failed]; +let findByPath: (string, t) => option(t); -let update: (~tree: t, ~updater: t => t, int) => 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; let setOpen: t => t; +let equals: (t, t) => bool; + module Model: { type nonrec t = t; 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/FileExplorerStore.re b/src/Store/FileExplorerStore.re index c277426fbe..2142151e33 100644 --- a/src/Store/FileExplorerStore.re +++ b/src/Store/FileExplorerStore.re @@ -26,58 +26,23 @@ 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) => - switch (path) { - | [] => `Found(0) - | [(focus: FsTreeNode.t), ...focusTail] => - if (focus.id != node.id) { - `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) => 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 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) @@ -91,7 +56,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)) ), ) @@ -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 }; @@ -113,9 +78,9 @@ let revealPath = (state: State.t, path) => { state |> setTree(tree) |> setScrollOffset(offset), Isolinear.Effect.none, ); - }; + } - | _ => (state, Isolinear.Effect.none) + | None => (state, Isolinear.Effect.none) }; }; @@ -130,51 +95,97 @@ 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 - }; + 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) - | NodeLoaded(id, node) => (replaceNode(id, node), Isolinear.Effect.none) + | NodeLoaded(path, node) => ( + replaceNode(path, node, state), + Isolinear.Effect.none, + ) - | FocusNodeLoaded(id, node) => - switch (state.fileExplorer.focus) { - | Some(path) => revealPath(replaceNode(id, node), path) + | FocusNodeLoaded(path, node) => + switch (state.fileExplorer.active) { + | Some(activePath) => + let state = replaceNode(path, node, state); + revealPath(state, activePath); | None => (state, Isolinear.Effect.none) } | NodeClicked(node) => - // Set focus here to avoid scrolling in BufferEnter - let state = setFocus(Some(node.path), state); - - switch (node) { - | {kind: File, path, _} => (state, openFileByPathEffect(path)) - - | {kind: Directory({isOpen, _}), _} => ( - replaceNode(node.id, FsTreeNode.toggleOpen(node)), - isOpen - ? Isolinear.Effect.none - : Effects.load( - node.path, - state.languageInfo, - state.iconTheme, - state.configuration, - ~onComplete=newNode => - Actions.FileExplorer(NodeLoaded(node.id, newNode)) - ), - ) - }; + let state = state |> setFocus(Some(node.path)); + 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)) => + 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) + } }; }; @@ -212,8 +223,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/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/FileExplorerView.re b/src/UI/FileExplorerView.re index 0abe2bffc9..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), focus, _} => - + | {tree: Some(tree), active, focus, _} => + }; }; diff --git a/src/UI/FileTreeView.re b/src/UI/FileTreeView.re index 07985d45aa..ea298232e4 100644 --- a/src/UI/FileTreeView.re +++ b/src/UI/FileTreeView.re @@ -1,33 +1,44 @@ +open Oni_Core; open Oni_Model; open Revery; open Revery.UI; -module Core = Oni_Core; - -let toNodePath = (workspace: Workspace.workspace, tree, path) => - Core.Log.perf("FileTreeview.toNodePath", () => { - let localPath = - Workspace.toRelativePath(workspace.workingDirectory, path); - - switch (FsTreeNode.findNodesByLocalPath(localPath, tree)) { - | `Success(nodes) => Some(nodes) - | `Partial(_) - | `Failed => None - }; - }); +module Option = Utility.Option; module Styles = { open Style; let container = [flexGrow(1)]; - let item = [flexDirection(`Row), alignItems(`Center)]; - - let text = (~fg, ~font: Core.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), @@ -49,7 +60,15 @@ let setiIcon = (~icon, ~fontSize as size, ~fg, ()) => { />; }; -let nodeView = (~font: Core.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) => @@ -67,56 +86,80 @@ let nodeView = (~font: Core.UiFont.t, ~fg, ~node: FsTreeNode.t, ()) => { switch (node.kind) { | Directory({isOpen, _}) => | _ => }; - + - + ; }; module TreeView = TreeView.Make(FsTreeNode.Model); -let make = - ( - ~tree: FsTreeNode.t, - ~focus: option(string), - ~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 focus = - switch (focus, state.workspace) { - | (Some(path), Some(workspace)) => toNodePath(workspace, tree, path) - | _ => None - }; - let FileExplorer.{scrollOffset, _} = state.fileExplorer; let onScrollOffsetChange = offset => GlobalContext.current().dispatch( FileExplorer(ScrollOffsetChanged(offset)), ); - + let onNodeClick = node => { + Option.iter(Revery.UI.Focus.focus, containerRef); + onNodeClick(node); + }; + + 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)) + + | _ => () + }; + }; + + setContainerRef(Some(ref))}> - ...{node => } + scrollOffset onScrollOffsetChange tree itemHeight=22 onClick=onNodeClick> + ...{node => + + } ; }; 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), ); diff --git a/src/UI/TreeView.re b/src/UI/TreeView.re index 208e75fe5e..cd5e9f6a49 100644 --- a/src/UI/TreeView.re +++ b/src/UI/TreeView.re @@ -53,14 +53,11 @@ module Styles = { bottom(0), ]; - let item = (~itemHeight, ~isFocused, ~theme: Theme.t) => [ + let item = (~itemHeight) => [ height(itemHeight), cursor(Revery.MouseCursors.pointer), flexDirection(`Row), overflow(`Hidden), - backgroundColor( - isFocused ? theme.menuSelectionBackground : Colors.transparentWhite, - ), ]; let placeholder = (~height) => [Style.height(height)]; @@ -91,31 +88,20 @@ module Make = (Model: TreeModel) => { let rec nodeView = ( ~renderContent, - ~focus, ~itemHeight, ~clipRange as (clipStart, clipEnd), ~onClick, ~node, - ~theme, (), ) => { let subtreeSize = Model.expandedSubtreeSize(node); - let (isFocused, childFocus) = - switch (focus) { - | 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, ~isFocused, ~theme)}> + onClick={() => onClick(node)} style={Styles.item(~itemHeight)}> {renderContent(node)} ; @@ -127,12 +113,10 @@ module Make = (Model: TreeModel) => { let element = ; loop( @@ -173,7 +157,6 @@ module Make = (Model: TreeModel) => { let%component make = ( ~children as renderContent, - ~focus: option(list(Model.t)), ~itemHeight, ~initialRowsToRender=10, ~onClick, @@ -181,7 +164,6 @@ module Make = (Model: TreeModel) => { ~onScrollOffsetChange: [ | `Start(float) | `Middle(float)] => unit=_ => (), ~tree, - ~theme, (), ) => { let%hook (outerRef, setOuterRef) = Hooks.ref(None); @@ -264,15 +246,7 @@ module Make = (Model: TreeModel) => { onMouseWheel=scroll> - + {showScrollbar ? : React.empty}