Skip to content

Commit

Permalink
File Explorer (onivim#302)
Browse files Browse the repository at this point in the history
* Add [WIP] file explorer

* Switch to scroll view

* Add working toggleable tree nodes

* Add general treeview component and initial fileexplorer

* Remove unused open

* Add generalised tree item

* Initial rendering of FileExplorer with real fs list

* Add icons and increase explorer size

* Render folder icon if folder otherwise render file icon

* Add reminder todo comment

* Return and use icon definition not raw code

* Filter out ignore files and add children per dir

* Add descendants to fs tree

* Handle node click in parent element

* Fix duplicate entries in fs tree

* Create helper functions to neaten tree generation

* Initial Hack to get files opening on click

* Slightly improve ref used in update function

* Move state into isolinear thread

* Rename oniTree to UiTree

* Fix comment in explorer connector

* Move explorer logic out of connector into FileExplorer

* Remove lwt from store dune file

* Include Revery tree in Oni UiTree

* Fix comment typo

* Remove unnecessary second open

* Remove references to ReveryTree use UiTree instead

* Remove unused icon

* add binding for explorer

* Add toggle open and closed functionality to explorer

* Add explanatory comments and fix explorer close

* Revert changes to UI/dune

* remove unused variables

* Remove more unused vars

* Only open files not directories

* Fix recursive subdir opening

* Remove some more unused variables

* Add ignored files to configuration

* Fix unbound labels error

* Replace FontAwesome back where it was initially

* Add primary and secondary root icon props

* Separate out nodeRenderer function

* Toggle explorer on dock icon click

* Add spacing to comment

* Use sprintf for formatting error message

* Add initial implementation of depth limit to tree

* use more robost check for isDirectory

* Save max depth in Constants and on each node

* Add progressive fetching/updating of nodes

For nodes beyond the max depth on click of their
parents populate the subdirectories

* Fix bad rebase changes

* update comment for getFiles function

* Reduce explorer depth

* Remove unused children variable
  • Loading branch information
akinsho authored May 6, 2019
1 parent 62c4b1d commit 225b605
Show file tree
Hide file tree
Showing 22 changed files with 670 additions and 36 deletions.
3 changes: 2 additions & 1 deletion assets/configuration/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"workbench.iconTheme": "vs-seti",
"editor.insertSpaces": false,
"editor.indentSize": 4,
"editor.tabSize": 4
"editor.tabSize": 4,
"files.exclude": ["node_modules", "_esy"]
}
3 changes: 2 additions & 1 deletion assets/configuration/keybindings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{ "key": "<C-P>", "command": "menu.previous", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<D-N>", "command": "menu.next", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<D-P>", "command": "menu.previous", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<CR>", "command": "menu.select", "when": [["menuFocus"], ["textInputFocus"]] }
{ "key": "<CR>", "command": "menu.select", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<S-C-B>", "command": "explorer.toggle", "when": [["editorTextFocus"]]}
]
}
2 changes: 2 additions & 0 deletions src/editor/Core/Configuration.re
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type t = {
editorRenderIndentGuides: bool,
editorRenderWhitespace,
workbenchIconTheme: string,
filesExclude: list(string),
};

let default = {
Expand All @@ -37,6 +38,7 @@ let default = {
editorHighlightActiveIndentGuide: true,
editorRenderWhitespace: All,
workbenchIconTheme: "vs-seti",
filesExclude: ["node_modules", "_esy"],
};

let getBundledConfigPath = () => {
Expand Down
17 changes: 17 additions & 0 deletions src/editor/Core/ConfigurationParser.re
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ let parseInt = json =>
| _ => 0
};

let parseStringList = json => {
switch (json) {
| `List(items) =>
List.fold_left(
(accum, item) =>
switch (item) {
| `String(v) => [v, ...accum]
| _ => accum
},
[],
items,
)
| _ => []
};
};

let parseLineNumberSetting = json =>
switch (json) {
| `String(v) =>
Expand Down Expand Up @@ -91,6 +107,7 @@ let configurationParsers: list(configurationTuple) = [
"editor.renderWhitespace",
(s, v) => {...s, editorRenderWhitespace: parseRenderWhitespace(v)},
),
("files.exclude", (s, v) => {...s, filesExclude: parseStringList(v)}),
(
"workbench.iconTheme",
(s, v) => {...s, workbenchIconTheme: parseString(v)},
Expand Down
6 changes: 6 additions & 0 deletions src/editor/Core/Constants.re
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ type t = {
minimapLineSpacing: int,
scrollBarThickness: int,
minimapMaxColumn: int,
/*
* Maximum levels of the file system to traverse
* when initially populating the file explorer
*/
maximumExplorerDepth: int,
};

let default: t = {
Expand All @@ -31,4 +36,5 @@ let default: t = {
minimapLineSpacing: 1,
scrollBarThickness: 15,
minimapMaxColumn: 120,
maximumExplorerDepth: 10,
};
8 changes: 6 additions & 2 deletions src/editor/Model/Actions.re
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type t =
| EditorMoveCursorToBottom(Cursor.move)
| SyntaxHighlightColorMap(ColorMap.t)
| SyntaxHighlightTokens(TextmateClient.TokenizationResult.t)
| OpenExplorer(string)
| SetExplorerTree(UiTree.t)
| UpdateExplorerNode(UiTree.t, UiTree.t)
| MenuSearch(string)
| MenuOpen(menuCreator)
| MenuUpdate(list(menuCommand))
Expand All @@ -50,8 +53,9 @@ type t =
| MenuPreviousItem
| MenuPosition(int)
| OpenFileByPath(string)
| AddRightDock(WindowManager.dock)
| AddLeftDock(WindowManager.dock)
| RegisterDockItem(WindowManager.dock)
| RemoveDockItem(WindowManager.docks)
| AddDockItem(WindowManager.docks)
| AddSplit(WindowManager.splitMetadata)
| RemoveSplit(int)
| OpenConfigFile(string)
Expand Down
186 changes: 186 additions & 0 deletions src/editor/Model/FileExplorer.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
open Revery;
open Oni_Core;

type t = {
directory: UiTree.t,
isOpen: bool,
};

module ExplorerId =
UniqueId.Make({});

let toFsNode = node =>
switch (node) {
| UiTree.FileSystemNode(n) => n
};

let getFileIcon = (languageInfo, iconTheme, filePath) => {
let fileIcon =
LanguageInfo.getLanguageFromFilePath(languageInfo, filePath)
|> IconTheme.getIconForFile(iconTheme, filePath);

switch (fileIcon) {
| Some(_) as x => x
| None => None
};
};

let createFsNode =
(~children, ~depth, ~path, ~displayName, ~fileIcon, ~isDirectory) => {
/**
TODO: Find an icon theme with folders and use those icons
Fallbacks are used for the directory icons. FontAwesome is not
accessible here so we specify no icons for directories.
*/
let (primary, secondary) = isDirectory ? (None, None) : (fileIcon, None);

UiTree.FileSystemNode({
path,
depth,
displayName,
children,
isDirectory,
icon: primary,
secondaryIcon: secondary,
});
};

let isDir = path => Sys.file_exists(path) ? Sys.is_directory(path) : false;

/**
getFilesAndFolders
This function uses Lwt to get all the files and folders in a directory
then for each we check if it is a file or folder.
if it is a directory we recursively call getFilesAndFolders on it
to resolves its subfolders and files. We do this concurrently using
Lwt_list.map_p. The recursion is gated by the depth value so it does
not recurse too far.
*/
let getFilesAndFolders = (~maxDepth, ~ignored, cwd, getIcon) => {
let rec getDirContent = (~depth, cwd, getIcon) => {
Lwt_unix.files_of_directory(cwd)
/* Filter out the relative name for current and parent directory*/
|> Lwt_stream.filter(name => name != ".." && name != ".")
/* Remove ignored files from search */
|> Lwt_stream.filter(name => !List.mem(name, ignored))
|> Lwt_stream.to_list
|> (
promise =>
Lwt.bind(promise, files =>
Lwt_list.map_p(
file => {
let path = Filename.concat(cwd, file);
let isDirectory = isDir(path);
let nextDepth = depth + 1;
let notMaximumDepth = nextDepth < maxDepth;

let%lwt children =
isDirectory && notMaximumDepth
? getDirContent(~depth=nextDepth, path, getIcon)
: Lwt.return([]);

createFsNode(
~path,
~children,
~isDirectory,
~depth=nextDepth,
~displayName=file,
~fileIcon=getIcon(path),
)
|> Lwt.return;
},
files,
)
)
);
};

try%lwt (getDirContent(~depth=0, cwd, getIcon)) {
| Failure(e) =>
Log.error(e);
Lwt.return([]);
| Unix.Unix_error(error, fn, arg) =>
Printf.sprintf(
"Error: %s encountered in %s called with %s",
Unix.error_message(error),
fn,
arg,
)
|> Log.error;
Lwt.return([]);
};
};

let rec listToTree = (~status, nodes, parent) => {
open UiTree;
let parentId = ExplorerId.getUniqueId();
let children =
List.map(
node => {
let fsNode = toFsNode(node);
let descendantNodes = List.map(toFsNode, fsNode.children);
let descendants =
List.map(
descendant =>
listToTree(
~status=Closed,
descendant.children,
FileSystemNode(descendant),
),
descendantNodes,
);

let id = ExplorerId.getUniqueId();
Node({id, data: node, status: Closed}, descendants);
},
nodes,
);

Node({id: parentId, data: parent, status}, children);
};

let getDirectoryTree = (cwd, languageInfo, iconTheme, ignored) => {
let getIcon = getFileIcon(languageInfo, iconTheme);
let maxDepth = Constants.default.maximumExplorerDepth;
let directory =
getFilesAndFolders(~maxDepth, ~ignored, cwd, getIcon) |> Lwt_main.run;

createFsNode(
~depth=0,
~path=cwd,
~displayName=Filename.basename(cwd),
~isDirectory=true,
~children=directory,
~fileIcon=getIcon(cwd),
)
|> listToTree(~status=Open, directory);
};

let getNodePath = node => {
UiTree.(
switch (node) {
| Node({data: FileSystemNode({path, _}), _}, _) => Some(path)
| Empty => None
}
);
};

let getNodeId = node => {
UiTree.(
switch (node) {
| Node({id, _}, _) => Some(id)
| Empty => None
}
);
};

let create = () => {directory: UiTree.Empty, isOpen: true};

let reduce = (state: t, action: Actions.t) => {
switch (action) {
| SetExplorerTree(tree) => {directory: tree, isOpen: true}
| RemoveDockItem(WindowManager.ExplorerDock) => {...state, isOpen: false}
| _ => state
};
};
2 changes: 2 additions & 0 deletions src/editor/Model/Oni_Model.re
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ module Tab = Tab;
module WhitespaceTokenFilter = WhitespaceTokenFilter;
module Wildmenu = Wildmenu;
module WindowManager = WindowManager;
module UiTree = UiTree;
module FileExplorer = FileExplorer;
43 changes: 32 additions & 11 deletions src/editor/Model/Reducer.re
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ let reduce: (State.t, Actions.t) => State.t =
wildmenu: Wildmenu.reduce(s.wildmenu, a),
commandline: Commandline.reduce(s.commandline, a),
statusBar: StatusBarReducer.reduce(s.statusBar, a),
fileExplorer: FileExplorer.reduce(s.fileExplorer, a),
};

switch (a) {
Expand All @@ -33,19 +34,39 @@ let reduce: (State.t, Actions.t) => State.t =
| SetInputControlMode(m) => {...s, inputControlMode: m}
| CommandlineShow(_) => {...s, inputControlMode: NeovimMenuFocus}
| CommandlineHide(_) => {...s, inputControlMode: EditorTextFocus}
| AddLeftDock(dock) => {
...s,
editorLayout: {
...s.editorLayout,
leftDock: [dock, ...s.editorLayout.leftDock],
},
| RegisterDockItem(dock) =>
switch (dock) {
| {position: Left, _} => {
...s,
editorLayout: {
...s.editorLayout,
leftDock: [dock, ...s.editorLayout.leftDock],
dockItems: [dock, ...s.editorLayout.dockItems],
},
}
| {position: Right, _} => {
...s,
editorLayout: {
...s.editorLayout,
rightDock: [dock, ...s.editorLayout.rightDock],
dockItems: [dock, ...s.editorLayout.dockItems],
},
}
}
| AddRightDock(dock) => {
| RemoveDockItem(id) => {
...s,
editorLayout: {
...s.editorLayout,
rightDock: [dock, ...s.editorLayout.rightDock],
},
editorLayout: WindowManager.removeDockItem(~id, s.editorLayout),
}
| AddDockItem(id) =>
switch (WindowManager.findDockItem(id, s.editorLayout)) {
| Some(dock) => {
...s,
editorLayout: {
...s.editorLayout,
leftDock: s.editorLayout.leftDock @ [dock],
},
}
| None => s
}
| AddSplit(split) =>
let id = WindowManager.WindowId.current();
Expand Down
2 changes: 2 additions & 0 deletions src/editor/Model/State.re
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type t = {
lifecycle: Lifecycle.t,
statusBar: StatusBarModel.t,
editorLayout: WindowManager.t,
fileExplorer: FileExplorer.t,
};

let create: unit => t =
Expand Down Expand Up @@ -55,4 +56,5 @@ let create: unit => t =
languageInfo: LanguageInfo.create(),
statusBar: StatusBarModel.create(),
editorLayout: WindowManager.create(),
fileExplorer: FileExplorer.create(),
};
Loading

0 comments on commit 225b605

Please sign in to comment.