Skip to content
This repository has been archived by the owner on Apr 1, 2020. It is now read-only.

Commit

Permalink
Feature/add file/folder manipulation commands to explorer (#1839)
Browse files Browse the repository at this point in the history
* add yank and paste functionality [WIP]

* add yank paste commands and reducer to the store

* add initial working yank and paste command functionality

* allow yanks to be stored as array for multiselecting

* add timeout to remove yanked items after a minute idle
add leave handler to reset yank and paste register
allow toggle functionality if item already yanked

* add h and l to expand and collapse dirs

* add initial undo register and hopeless transitions

* [WIP] add initial working..not fully tested undo functionality

* move logic out of split into epics - for undo funcitionality
also add guards if none present and remove an undo if successful
from the register

* add is focused to sidebar type

* expose is focused method and check for it before applying bindings

* add initial tests for yank epic

* add epic test [wip]

* remove types for memory fs since it needs to be required?

* remove unsused vars

* remove paste folder in test

* add delete undo functionality
add new methods to filesystem to have it control
the shell commands

* fix lint error

* fix config typing typo

* add true delete command and associated function
rename methods of filesystem

* fix broken utility function

* add tests for Explorer file system

* fix failing test

* add coverage output directory

* update tests - add move collection add is ci utility

* commit utility file

* fix changed typing to moveNodes function

* add tests for small utilities

* rename persist file function

* switch to use of path.join in moving

* finalise animation and tidy up epics

* add reducer tests, use lodash utilities not homegrown

* fix tests [WIP] fix epic typing for delete

* fix notification message

* fix delete epic and map to refresh

* inject promisify as well as fs into oni file system

* fix type errors

* inject promisify and [wip] mock in jest test

* refactor move logic out of split into filesystem
add epic for pasting

* fix undo functionality

* refactor some method names for readablility

* consolidate actions for typing and reuse

* delegate error handling to epic catch clauses
for greater flexibility as this gives back access to the
observable so potentially a retry could be attempted or
map to another action etc.

* refactor notifications into epics
fix jest test

* add error notification with reason

* consolidate replicated code into a function

* set failures to warning level

* fix tests.ts and ad log to paste

* add get source node method to use to ensure undo causes expansion

* move jest test to unit tests dir :sad:

* add passing paste epic test

* rename explorer filestystem tests

* move fs-extra to deps add moar tests

* add undo epic tests

* add test for deletion in undo epic

* remove redundant function and replace with fs-extra fn

* add clear update functionality to prevent re-animating

* add test for clear update epics

* gate bindings not to apply if commandline or menu are open

* fix paste action error handling

* fix tests by ensuring mocks are async

* import specific helpers from rxjs not all of observable
fix tests simplify updating

* set jest to collect coverage from components dir only
  • Loading branch information
akinsho authored Apr 19, 2018
1 parent ee134d3 commit b96c4cf
Show file tree
Hide file tree
Showing 20 changed files with 1,531 additions and 517 deletions.
1 change: 1 addition & 0 deletions browser/src/Editor/NeovimEditor/NeovimEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ export class NeovimEditor extends Editor implements IEditor {

document.body.ondrop = ev => {
ev.preventDefault()
// TODO: the following line currently breaks explorer drag and drop functionality
ev.stopPropagation()

const { files } = ev.dataTransfer
Expand Down
24 changes: 24 additions & 0 deletions browser/src/Input/KeyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import * as Oni from "oni-api"
import * as Platform from "./../Platform"
import { Configuration } from "./../Services/Configuration"

interface ISidebar {
sidebar: {
activeEntryId: string
isFocused: boolean
}
}

export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configuration): void => {
const { editors, input, menu } = oni

Expand All @@ -20,6 +27,13 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati
const isInsertOrCommandMode = () =>
editors.activeEditor.mode === "insert" || editors.activeEditor.mode === "cmdline_normal"

const oniWithSidebar = oni as Oni.Plugin.Api & ISidebar
const isExplorerActive = () =>
oniWithSidebar.sidebar.activeEntryId === "oni.sidebar.explorer" &&
oniWithSidebar.sidebar.isFocused &&
!isInsertOrCommandMode() &&
!isMenuOpen()

const isMenuOpen = () => menu.isMenuOpen()

if (Platform.isMac()) {
Expand Down Expand Up @@ -119,6 +133,16 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati

input.bind("<s-c-b>", "sidebar.toggle", isNormalMode)

// Explorer
input.bind("d", "explorer.delete.persist", isExplorerActive)
input.bind("<c-d>", "explorer.delete", isExplorerActive)
input.bind("y", "explorer.yank", isExplorerActive)
input.bind("p", "explorer.paste", isExplorerActive)
input.bind("u", "explorer.undo", isExplorerActive)
input.bind("h", "explorer.collapse.directory", isExplorerActive)
input.bind("l", "explorer.expand.directory", isExplorerActive)

// Browser
input.bind("k", "browser.scrollUp")
input.bind("j", "browser.scrollDown")
input.bind("h", "browser.scrollLeft")
Expand Down
5 changes: 2 additions & 3 deletions browser/src/Plugins/PluginInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* Responsible for installing, updating, and uninstalling plugins.
*/

import * as fs from "fs"
import * as path from "path"

import { Event, IEvent } from "oni-types"
Expand All @@ -17,7 +16,7 @@ import { getUserConfigFolderPath } from "./../Services/Configuration"
// import { AnonymousPlugin } from "./AnonymousPlugin"
// import { Plugin } from "./Plugin"

import { FileSystem, IFileSystem } from "./../Services/Explorer/ExplorerFileSystem"
import { IFileSystem, OniFileSystem } from "./../Services/Explorer/ExplorerFileSystem"

import Process from "./Api/Process"

Expand Down Expand Up @@ -62,7 +61,7 @@ export class YarnPluginInstaller implements IPluginInstaller {
return this._onOperationError
}

constructor(private _fileSystem: IFileSystem = new FileSystem(fs)) {}
constructor(private _fileSystem: IFileSystem = OniFileSystem) {}

public async install(identifier: string): Promise<void> {
const eventInfo: IPluginInstallerOperationEvent = {
Expand Down
3 changes: 3 additions & 0 deletions browser/src/Services/Configuration/DefaultConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ const BaseConfiguration: IConfigurationValues = {

"editor.imageLayerExtensions": [".gif", ".jpg", ".jpeg", ".bmp", ".png"],

"explorer.persistDeletedFiles": true,
"explorer.maxUndoFileSizeInBytes": 500_000,

"environment.additionalPaths": [],

"keyDisplayer.showInInsertMode": false,
Expand Down
6 changes: 6 additions & 0 deletions browser/src/Services/Configuration/IConfigurationValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ export interface IConfigurationValues {
// 'zero-latency' mode typing, and increases responsiveness.
"editor.typingPrediction": boolean

// Files deleted in the explorer can be persisted for the duration
// of the session meaning that deletion can be undone is this is set
// to true
"explorer.persistDeletedFiles": boolean
"explorer.maxUndoFileSizeInBytes": number

"editor.fullScreenOnStart": boolean
"editor.maximizeScreenOnStart": boolean

Expand Down
107 changes: 107 additions & 0 deletions browser/src/Services/Explorer/ExplorerFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
*/

import * as fs from "fs"
import { emptyDirSync, ensureDirSync, move, remove } from "fs-extra"
import * as os from "os"
import * as path from "path"
import { promisify } from "util"

import { ExplorerNode } from "./ExplorerSelectors"
import { FolderOrFile } from "./ExplorerStore"

/**
Expand All @@ -16,6 +19,12 @@ import { FolderOrFile } from "./ExplorerStore"
export interface IFileSystem {
readdir(fullPath: string): Promise<FolderOrFile[]>
exists(fullPath: string): Promise<boolean>
persistNode(fullPath: string): Promise<void>
restoreNode(fullPath: string): Promise<void>
deleteNode(node: ExplorerNode): Promise<void>
canPersistNode(fullPath: string, size: number): Promise<boolean>
move(source: string, dest: string): Promise<void>
moveNodesBack(collection: Array<{ source: string; destination: string }>): Promise<void>
}

export class FileSystem implements IFileSystem {
Expand All @@ -25,12 +34,25 @@ export class FileSystem implements IFileSystem {
exists(path: string): Promise<boolean>
}

private _backupDirectory = path.join(os.tmpdir(), "oni_backup")

public get backupDir(): string {
return this._backupDirectory
}

constructor(nfs: typeof fs) {
this._fs = {
readdir: promisify(nfs.readdir.bind(nfs)),
stat: promisify(nfs.stat.bind(nfs)),
exists: promisify(nfs.exists.bind(nfs)),
}

this.init()
}

public init = () => {
ensureDirSync(this._backupDirectory)
emptyDirSync(this._backupDirectory)
}

public async readdir(directoryPath: string): Promise<FolderOrFile[]> {
Expand Down Expand Up @@ -58,4 +80,89 @@ export class FileSystem implements IFileSystem {
public exists(fullPath: string): Promise<boolean> {
return this._fs.exists(fullPath)
}

/**
* Delete a file or Folder
*
* @name deleteNode
* @function
* @param {ExplorerNode} node The file or folder node
*/
public deleteNode = async (node: ExplorerNode): Promise<void> => {
switch (node.type) {
case "folder":
await remove(node.folderPath)
break
case "file":
await remove(node.filePath)
break
default:
break
}
}

/**
* Move a file or folder from the backup dir to its original location
*
* @name restoreNode
* @function
* @param {string} fileOrFolder The file or folder path
*/
public restoreNode = async (prevPath: string): Promise<void> => {
const name = path.basename(prevPath)
const backupPath = path.join(this._backupDirectory, name)
await move(backupPath, prevPath)
}

public move = async (source: string, dest: string): Promise<void> => {
return this.areDifferent(source, dest) && move(source, dest)
}
/**
* Saves a file to the tmp directory to persist deleted files
*
* @name PersistNode
* @function
* @param {string} filename A file or folder path
*/
public persistNode = async (fileOrFolder: string): Promise<void> => {
const { size } = await this._fs.stat(fileOrFolder)
const hasEnoughSpace = os.freemem() > size
if (hasEnoughSpace) {
const filename = path.basename(fileOrFolder)
const newPath = path.join(this._backupDirectory, filename)
await move(fileOrFolder, newPath, { overwrite: true })
}
}

/**
* Moves an array of files and folders to their original locations
*
* @name moveNodesBack
* @function
* @param {Array} collection An array of object with a file/folder and its destination folder
* @returns {void}
*/
public moveNodesBack = async (
collection: Array<{ destination: string; source: string }>,
): Promise<void> => {
await Promise.all(
collection.map(
async ({ source, destination }) =>
this.areDifferent(source, destination) && move(destination, source),
),
)
}

/**
* canPersistNode
* Determine based on size whether the directory should be persisted
*/
public canPersistNode = async (fullPath: string, maxSize: number): Promise<boolean> => {
const { size } = await this._fs.stat(fullPath)
return size < maxSize
}

private areDifferent = (src: string, dest: string) => src !== dest
}

export const OniFileSystem = new FileSystem(fs)
9 changes: 9 additions & 0 deletions browser/src/Services/Explorer/ExplorerSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ export interface IFileNode {
indentationLevel: number
}

export const EmptyNode: ExplorerNode = {
type: null,
id: null,
modified: null,
filePath: null,
name: null,
indentationLevel: null,
}

export type ExplorerNode = IContainerNode | IFolderNode | IFileNode

export const isPathExpanded = (state: IExplorerState, pathToCheck: string): boolean => {
Expand Down
Loading

0 comments on commit b96c4cf

Please sign in to comment.