diff --git a/lib/add-projects-view.js b/lib/add-projects-view.js new file mode 100644 index 00000000..431a9d7f --- /dev/null +++ b/lib/add-projects-view.js @@ -0,0 +1,36 @@ +module.exports = +class AddProjectView { + constructor () { + this.element = document.createElement('div') + this.element.id = 'add-projects-view' + + this.icon = document.createElement('div') + this.icon.classList.add('icon', 'icon-large', 'icon-telescope') + this.element.appendChild(this.icon) + + this.description = document.createElement('div') + this.description.classList.add('description') + this.description.innerText = 'Your project is currently empty' + this.element.appendChild(this.description) + + this.addProjectsButton = document.createElement('button') + this.addProjectsButton.classList.add('btn', 'btn-primary') + this.addProjectsButton.innerText = 'Add folders' + this.addProjectsButton.addEventListener('click', () => { + atom.pickFolder(paths => { + if (paths) { + atom.project.setPaths(paths) + } + }) + }) + this.element.appendChild(this.addProjectsButton) + + this.reopenProjectButton = document.createElement('button') + this.reopenProjectButton.classList.add('btn') + this.reopenProjectButton.innerText = 'Reopen a project' + this.reopenProjectButton.addEventListener('click', () => { + atom.commands.dispatch(this.element, 'application:reopen-project') + }) + this.element.appendChild(this.reopenProjectButton) + } +} diff --git a/lib/get-icon-services.js b/lib/get-icon-services.js index 2d0e6438..ffde058b 100644 --- a/lib/get-icon-services.js +++ b/lib/get-icon-services.js @@ -49,6 +49,9 @@ class IconServices { } updateDirectoryIcon (view) { + view.directoryName.className = '' + + const classes = ['name', 'icon'] if (this.elementIcons) { const disposable = this.elementIcons(view.directoryName, view.directory.path) this.elementIconDisposables.add(disposable) @@ -65,11 +68,14 @@ class IconServices { if (view.directory.submodule) iconClass = 'icon-file-submodule' } } - view.directoryName.classList.add(iconClass) + classes.push(iconClass) } + view.directoryName.classList.add(...classes) } updateFileIcon (view) { + view.fileName.className = '' + const classes = ['name', 'icon'] let iconClass if (this.elementIcons) { @@ -81,7 +87,8 @@ class IconServices { if (iconClass) { if (!Array.isArray(iconClass)) { iconClass = iconClass.toString().split(/\s+/g) - } classes.push(...iconClass) + } + classes.push(...iconClass) } view.fileName.classList.add(...classes) } diff --git a/lib/tree-view-package.js b/lib/tree-view-package.js index b0ee7b1f..f624e03d 100644 --- a/lib/tree-view-package.js +++ b/lib/tree-view-package.js @@ -1,5 +1,4 @@ const {Disposable, CompositeDisposable} = require('atom') -const path = require('path') const getIconServices = require('./get-icon-services') const TreeView = require('./tree-view') @@ -21,18 +20,12 @@ class TreeViewPackage { 'tree-view:show-current-file-in-file-manager': () => this.getTreeViewInstance().showCurrentFileInFileManager() })) - this.disposables.add(atom.project.onDidChangePaths(this.createOrDestroyTreeViewIfNeeded.bind(this))) - - if (this.shouldAttachTreeView()) { - const treeView = this.getTreeViewInstance() - const showOnAttach = !atom.workspace.getActivePaneItem() - this.treeViewOpenPromise = atom.workspace.open(treeView, { - activatePane: showOnAttach, - activateItem: showOnAttach - }) - } else { - this.treeViewOpenPromise = Promise.resolve() - } + const treeView = this.getTreeViewInstance() + const showOnAttach = !atom.workspace.getActivePaneItem() + this.treeViewOpenPromise = atom.workspace.open(treeView, { + activatePane: showOnAttach, + activateItem: showOnAttach + }) } async deactivate () { @@ -51,10 +44,8 @@ class TreeViewPackage { consumeFileIcons (service) { getIconServices().setFileIcons(service) - if (this.treeView) this.treeView.updateRoots() return new Disposable(() => { getIconServices().resetFileIcons() - if (this.treeView) this.treeView.updateRoots() }) } @@ -72,43 +63,4 @@ class TreeViewPackage { } return this.treeView } - - createOrDestroyTreeViewIfNeeded () { - if (this.shouldAttachTreeView()) { - const treeView = this.getTreeViewInstance() - const paneContainer = atom.workspace.paneContainerForURI(treeView.getURI()) - if (paneContainer) { - paneContainer.show() - } else { - atom.workspace.open(treeView, { - activatePane: false, - activateItem: false - }).then(() => { - const paneContainer = atom.workspace.paneContainerForURI(treeView.getURI()) - if (paneContainer) paneContainer.show() - }) - } - } else { - if (this.treeView) { - const pane = atom.workspace.paneForItem(this.treeView) - if (pane) pane.removeItem(this.treeView) - } - } - } - - shouldAttachTreeView () { - if (atom.project.getPaths().length === 0) return false - - // Avoid opening the tree view if Atom was opened as the Git editor... - // Only show it if the .git folder was explicitly opened. - if (path.basename(atom.project.getPaths()[0]) === '.git') { - return atom.project.getPaths()[0] === atom.getLoadSettings().pathToOpen - } - - return true - } - - shouldShowTreeViewAfterAttaching () { - if (atom.workspace.getActivePaneItem()) return false - } } diff --git a/lib/tree-view.coffee b/lib/tree-view.coffee index a3bf2efb..f848c60b 100644 --- a/lib/tree-view.coffee +++ b/lib/tree-view.coffee @@ -11,6 +11,8 @@ MoveDialog = require './move-dialog' CopyDialog = require './copy-dialog' IgnoredNames = null # Defer requiring until actually needed +AddProjectsView = require './add-projects-view' + Directory = require './directory' DirectoryView = require './directory-view' RootDragAndDrop = require './root-drag-and-drop' @@ -32,7 +34,6 @@ class TreeView @list = document.createElement('ol') @list.classList.add('tree-view-root', 'full-menu', 'list-tree', 'has-collapsable-children', 'focusable-panel') - @element.appendChild(@list) @disposables = new CompositeDisposable @emitter = new Emitter @@ -323,33 +324,45 @@ class TreeView root.directory.destroy() root.remove() - IgnoredNames ?= require('./ignored-names') - - @roots = for projectPath in atom.project.getPaths() - stats = fs.lstatSyncNoException(projectPath) - continue unless stats - stats = _.pick stats, _.keys(stats)... - for key in ["atime", "birthtime", "ctime", "mtime"] - stats[key] = stats[key].getTime() - - directory = new Directory({ - name: path.basename(projectPath) - fullPath: projectPath - symlink: false - isRoot: true - expansionState: expansionStates[projectPath] ? - oldExpansionStates[projectPath] ? - {isExpanded: true} - ignoredNames: new IgnoredNames() - @useSyncFS - stats - }) - root = new DirectoryView(directory).element - @list.appendChild(root) - root - - # The DOM has been recreated; reselect everything - @selectMultipleEntries(@entryForPath(selectedPath)) for selectedPath in selectedPaths + @roots = [] + + projectPaths = atom.project.getPaths() + if projectPaths.length > 0 + @element.appendChild(@list) unless @element.querySelector('tree-view-root') + + addProjectsViewElement = @element.querySelector('#add-projects-view') + @element.removeChild(addProjectsViewElement) if addProjectsViewElement + + IgnoredNames ?= require('./ignored-names') + + @roots = for projectPath in projectPaths + stats = fs.lstatSyncNoException(projectPath) + continue unless stats + stats = _.pick stats, _.keys(stats)... + for key in ["atime", "birthtime", "ctime", "mtime"] + stats[key] = stats[key].getTime() + + directory = new Directory({ + name: path.basename(projectPath) + fullPath: projectPath + symlink: false + isRoot: true + expansionState: expansionStates[projectPath] ? + oldExpansionStates[projectPath] ? + {isExpanded: true} + ignoredNames: new IgnoredNames() + @useSyncFS + stats + }) + root = new DirectoryView(directory).element + @list.appendChild(root) + root + + # The DOM has been recreated; reselect everything + @selectMultipleEntries(@entryForPath(selectedPath)) for selectedPath in selectedPaths + else + @element.removeChild(@list) if @element.querySelector('.tree-view-root') + @element.appendChild(new AddProjectsView().element) unless @element.querySelector('#add-projects-view') getActivePath: -> atom.workspace.getCenter().getActivePaneItem()?.getPath?() diff --git a/spec/tree-view-package-spec.coffee b/spec/tree-view-package-spec.coffee index 1ad8f4cd..1af6b5d5 100644 --- a/spec/tree-view-package-spec.coffee +++ b/spec/tree-view-package-spec.coffee @@ -98,115 +98,100 @@ describe "TreeView", -> it "makes the root folder non-draggable", -> expect(treeView.roots[0].hasAttribute('draggable')).toBe(false) - describe "when the project has no path", -> + describe "when the project has no paths", -> beforeEach -> atom.project.setPaths([]) - waitsForPromise -> - Promise.resolve(atom.packages.deactivatePackage("tree-view")) # Wrapped for both async and non-async versions of Atom - runs -> - expect(atom.workspace.getLeftDock().getActivePaneItem()).toBeUndefined() - waitsForPromise -> atom.packages.activatePackage("tree-view") + it "displays a view to add projects", -> + expect(treeView.element.querySelector('#add-projects-view')).toExist() + expect(treeView.element.querySelector('.tree-view-root')).not.toExist() - runs -> - treeView = atom.packages.getActivePackage("tree-view").mainModule.getTreeViewInstance() + describe "when clicking on 'Add projects'", -> + addProjectsButton = null - it "does not attach to the workspace or create a root node when initialized", -> - expect(treeView.element.parentElement).toBeFalsy() - expect(treeView.roots).toHaveLength(0) + beforeEach -> + addProjectsButton = treeView.element.querySelector('#add-projects-view .btn-primary') - it "does not attach to the workspace or create a root node when attach() is called", -> - expect(atom.workspace.getLeftDock().getActivePaneItem()).toBeUndefined() + it "opens up a folder picker", -> + spyOn(atom, 'pickFolder') - it "does not throw an exception when files are opened", -> - filePath = path.join(os.tmpdir(), 'non-project-file.txt') - fs.writeFileSync(filePath, 'test') + addProjectsButton.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - waitsForPromise -> - atom.workspace.open(filePath) - - it "does not reveal the active file", -> - filePath = path.join(os.tmpdir(), 'non-project-file.txt') - fs.writeFileSync(filePath, 'test') + expect(atom.pickFolder).toHaveBeenCalled() - waitsForPromise -> - atom.workspace.open(filePath) + it "sets the project paths with whatever folders are chosen", -> + done = false - waitsForPromise -> - treeView.revealActiveFile() + spyOn(atom.project, 'setPaths') + spyOn(atom, 'pickFolder').andCallFake (callback) -> + callback([path1, path2]) + expect(atom.project.setPaths).toHaveBeenCalledWith([path1, path2]) + done = true - runs -> - expect(treeView.element.parentElement).toBeFalsy() - expect(treeView.roots).toHaveLength(0) + addProjectsButton.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - describe "when the project is assigned a path because a new buffer is saved", -> - it "creates a root directory view and attaches to the workspace", -> - projectPath = temp.mkdirSync('atom-project') + waitsFor -> done - waitsForPromise -> - atom.workspace.open() + it "does not attempt to set any project paths if the folder picker was cancelled", -> + done = false - waitsFor (done) -> - atom.workspace.getCenter().getActivePaneItem().saveAs(path.join(projectPath, 'test.txt')) - atom.workspace.onDidOpen(done) + spyOn(atom.project, 'setPaths') + spyOn(atom, 'pickFolder').andCallFake (callback) -> + callback(null) + expect(atom.project.setPaths).not.toHaveBeenCalled() + done = true - runs -> - treeView = atom.workspace.getLeftDock().getActivePaneItem() - expect(treeView.roots).toHaveLength(1) - expect(fs.absolute(treeView.roots[0].getPath())).toBe fs.absolute(projectPath) + addProjectsButton.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - describe "when the root view is opened to a file path", -> - it "does not show the dock on activation", -> + waitsFor -> done - waitsForPromise -> - Promise.resolve(atom.packages.deactivatePackage("tree-view")) # Wrapped for both async and non-async versions of Atom + describe "when clicking on 'Reopen Projects'", -> + reopenProjectsButton = null - runs -> - atom.packages.packageStates = {} - atom.workspace.getLeftDock().hide() - expect(atom.workspace.getLeftDock().isVisible()).toBe(false) + beforeEach -> + reopenProjectsButton = treeView.element.querySelectorAll('#add-projects-view .btn')[1] - waitsForPromise -> - atom.workspace.open('tree-view.js') + it "opens a modal to choose an old project", -> + done = false - runs -> - expect(atom.workspace.getLeftDock().isVisible()).toBe(false) + atom.commands.onDidDispatch (event) -> + done = true if event.type is 'application:reopen-project' - waitForPackageActivation() + reopenProjectsButton.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - runs -> - expect(atom.workspace.getLeftDock().isVisible()).toBe(false) - atom.project.addPath(path.join(__dirname, 'fixtures')) + waitsFor -> done - waitsFor -> atom.workspace.getLeftDock().isVisible() + it "does not throw an exception when files are opened", -> + filePath = path.join(os.tmpdir(), 'non-project-file.txt') + fs.writeFileSync(filePath, 'test') - describe "when the root view is opened to a directory", -> - it "attaches to the workspace", -> - waitsForPromise -> atom.packages.activatePackage('tree-view') + waitsForPromise -> + atom.workspace.open(filePath) - runs -> - treeView = atom.packages.getActivePackage("tree-view").mainModule.getTreeViewInstance() - expect(treeView.element.parentElement).toBeTruthy() - expect(treeView.roots).toHaveLength(2) + it "does not reveal the active file", -> + filePath = path.join(os.tmpdir(), 'non-project-file.txt') + fs.writeFileSync(filePath, 'test') - describe "when the project is a .git folder", -> - it "does not create the tree view", -> - dotGit = path.join(temp.mkdirSync('repo'), '.git') - fs.makeTreeSync(dotGit) - atom.project.setPaths([dotGit]) + waitsForPromise -> + atom.workspace.open(filePath) waitsForPromise -> - Promise.resolve(atom.packages.deactivatePackage("tree-view")) # Wrapped for both async and non-async versions of Atom + treeView.revealActiveFile() runs -> - atom.packages.packageStates = {} + expect(treeView.roots).toHaveLength(0) - waitsForPromise -> - atom.packages.activatePackage('tree-view') + describe "when the project is assigned a path", -> + it "creates a root directory view", -> + projectPath = temp.mkdirSync('atom-project') - runs -> - {treeView} = atom.packages.getActivePackage("tree-view").mainModule - expect(treeView).toBeFalsy() + atom.project.setPaths([projectPath]) + + expect(treeView.roots).toHaveLength(1) + expect(fs.absolute(treeView.roots[0].getPath())).toBe fs.absolute(projectPath) + + expect(treeView.element.querySelector('.tree-view-root')).toExist() + expect(treeView.element.querySelector('#add-projects-view')).not.toExist() describe "on package deactivation", -> it "destroys the Tree View", -> diff --git a/styles/tree-view.less b/styles/tree-view.less index e37822b1..8133a342 100644 --- a/styles/tree-view.less +++ b/styles/tree-view.less @@ -13,6 +13,37 @@ display: flex; flex-direction: column; + #add-projects-view { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + text-align: center; + font-size: 1.25em; + padding: 30px; + cursor: default; + + > * { + margin: 10px 0; + } + + .description { + margin: 10px 0; + } + + .icon::before { + color: #c1c1c1; + } + } + + .icon-large::before { + margin-right: 0; + margin-bottom: 50px; + width: auto; + height: auto; + font-size: 8em; + } + .tree-view-root { padding-left: @component-icon-padding; padding-right: @component-padding;