From a095a2b244d02ddb1eba7ddff8162fd88aefa56b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 23 Feb 2018 10:55:46 -0500 Subject: [PATCH 01/10] directory.coffee -> directory.js --- lib/directory.coffee | 324 ------------------------------- lib/directory.js | 445 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 445 insertions(+), 324 deletions(-) delete mode 100644 lib/directory.coffee create mode 100644 lib/directory.js diff --git a/lib/directory.coffee b/lib/directory.coffee deleted file mode 100644 index 7c122355..00000000 --- a/lib/directory.coffee +++ /dev/null @@ -1,324 +0,0 @@ -path = require 'path' -_ = require 'underscore-plus' -{CompositeDisposable, Emitter} = require 'atom' -fs = require 'fs-plus' -PathWatcher = require 'pathwatcher' -File = require './file' -{repoForPath} = require './helpers' -realpathCache = {} - -module.exports = -class Directory - constructor: ({@name, fullPath, @symlink, @expansionState, @isRoot, @ignoredNames, @useSyncFS, @stats}) -> - @destroyed = false - @emitter = new Emitter() - @subscriptions = new CompositeDisposable() - - if atom.config.get('tree-view.squashDirectoryNames') and not @isRoot - fullPath = @squashDirectoryNames(fullPath) - - @path = fullPath - @realPath = @path - if fs.isCaseInsensitive() - @lowerCasePath = @path.toLowerCase() - @lowerCaseRealPath = @lowerCasePath - - @isRoot ?= false - @expansionState ?= {} - @expansionState.isExpanded ?= false - - # TODO: This can be removed after a sufficient amount - # of time has passed since @expansionState.entries - # has been converted to a Map - unless @expansionState.entries instanceof Map - convertEntriesToMap = (entries) -> - temp = new Map() - for name, entry of entries - entry.entries = convertEntriesToMap(entry.entries) if entry.entries? - temp.set(name, entry) - return temp - - @expansionState.entries = convertEntriesToMap(@expansionState.entries) - - @expansionState.entries ?= new Map() - @status = null - @entries = new Map() - - @submodule = repoForPath(@path)?.isSubmodule(@path) - - @subscribeToRepo() - @updateStatus() - @loadRealPath() - - destroy: -> - @destroyed = true - @unwatch() - @subscriptions.dispose() - @emitter.emit('did-destroy') - - onDidDestroy: (callback) -> - @emitter.on('did-destroy', callback) - - onDidStatusChange: (callback) -> - @emitter.on('did-status-change', callback) - - onDidAddEntries: (callback) -> - @emitter.on('did-add-entries', callback) - - onDidRemoveEntries: (callback) -> - @emitter.on('did-remove-entries', callback) - - onDidCollapse: (callback) -> - @emitter.on('did-collapse', callback) - - onDidExpand: (callback) -> - @emitter.on('did-expand', callback) - - loadRealPath: -> - if @useSyncFS - @realPath = fs.realpathSync(@path) - @lowerCaseRealPath = @realPath.toLowerCase() if fs.isCaseInsensitive() - else - fs.realpath @path, realpathCache, (error, realPath) => - return if @destroyed - if realPath and realPath isnt @path - @realPath = realPath - @lowerCaseRealPath = @realPath.toLowerCase() if fs.isCaseInsensitive() - @updateStatus() - - # Subscribe to project's repo for changes to the Git status of this directory. - subscribeToRepo: -> - repo = repoForPath(@path) - return unless repo? - - @subscriptions.add repo.onDidChangeStatus (event) => - @updateStatus(repo) if @contains(event.path) - @subscriptions.add repo.onDidChangeStatuses => - @updateStatus(repo) - - # Update the status property of this directory using the repo. - updateStatus: -> - repo = repoForPath(@path) - return unless repo? - - newStatus = null - if repo.isPathIgnored(@path) - newStatus = 'ignored' - else if @ignoredNames.matches(@path) - newStatus = 'ignored-name' - else - status = 0 - if @isRoot - # repo.getDirectoryStatus will always fail for the - # root because the path is relativized + concatenated with '/' - # making the matching string be '/'. Then path.indexOf('/') - # is run and will never match beginning of string with a leading '/' - for statusPath, statusId of repo.statuses - status |= parseInt(statusId, 10) - else - status = repo.getDirectoryStatus(@path) - - if repo.isStatusModified(status) - newStatus = 'modified' - else if repo.isStatusNew(status) - newStatus = 'added' - - if newStatus isnt @status - @status = newStatus - @emitter.emit('did-status-change', newStatus) - - # Is the given path ignored? - isPathIgnored: (filePath) -> - if atom.config.get('tree-view.hideVcsIgnoredFiles') - repo = repoForPath(@path) - return true if repo? and repo.isProjectAtRoot() and repo.isPathIgnored(filePath) - - if atom.config.get('tree-view.hideIgnoredNames') - return true if @ignoredNames.matches(filePath) - - false - - # Does given full path start with the given prefix? - isPathPrefixOf: (prefix, fullPath) -> - fullPath.indexOf(prefix) is 0 and fullPath[prefix.length] is path.sep - - isPathEqual: (pathToCompare) -> - @path is pathToCompare or @realPath is pathToCompare - - # Public: Does this directory contain the given path? - # - # See atom.Directory::contains for more details. - contains: (pathToCheck) -> - return false unless pathToCheck - - # Normalize forward slashes to back slashes on windows - pathToCheck = pathToCheck.replace(/\//g, '\\') if process.platform is 'win32' - - if fs.isCaseInsensitive() - directoryPath = @lowerCasePath - pathToCheck = pathToCheck.toLowerCase() - else - directoryPath = @path - - return true if @isPathPrefixOf(directoryPath, pathToCheck) - - # Check real path - if @realPath isnt @path - if fs.isCaseInsensitive() - directoryPath = @lowerCaseRealPath - else - directoryPath = @realPath - - return @isPathPrefixOf(directoryPath, pathToCheck) - - false - - # Public: Stop watching this directory for changes. - unwatch: -> - if @watchSubscription? - @watchSubscription.close() - @watchSubscription = null - - @entries.forEach (entry, key) => - entry.destroy() - @entries.delete(key) - - # Public: Watch this directory for changes. - watch: -> - try - @watchSubscription ?= PathWatcher.watch @path, (eventType) => - switch eventType - when 'change' then @reload() - when 'delete' then @destroy() - - getEntries: -> - try - names = fs.readdirSync(@path) - catch error - names = [] - names.sort(new Intl.Collator(undefined, {numeric: true, sensitivity: "base"}).compare) - - files = [] - directories = [] - - for name in names - fullPath = path.join(@path, name) - continue if @isPathIgnored(fullPath) - - stat = fs.lstatSyncNoException(fullPath) - symlink = stat.isSymbolicLink?() - stat = fs.statSyncNoException(fullPath) if symlink - statFlat = _.pick stat, _.keys(stat)... - for key in ["atime", "birthtime", "ctime", "mtime"] - statFlat[key] = statFlat[key]?.getTime() - - if stat.isDirectory?() - if @entries.has(name) - # push a placeholder since this entry already exists but this helps - # track the insertion index for the created views - directories.push(name) - else - expansionState = @expansionState.entries.get(name) - directories.push(new Directory({name, fullPath, symlink, expansionState, @ignoredNames, @useSyncFS, stats: statFlat})) - else if stat.isFile?() - if @entries.has(name) - # push a placeholder since this entry already exists but this helps - # track the insertion index for the created views - files.push(name) - else - files.push(new File({name, fullPath, symlink, realpathCache, @ignoredNames, @useSyncFS, stats: statFlat})) - - @sortEntries(directories.concat(files)) - - normalizeEntryName: (value) -> - normalizedValue = value.name - unless normalizedValue? - normalizedValue = value - if normalizedValue? - normalizedValue = normalizedValue.toLowerCase() - normalizedValue - - sortEntries: (combinedEntries) -> - if atom.config.get('tree-view.sortFoldersBeforeFiles') - combinedEntries - else - combinedEntries.sort (first, second) => - firstName = @normalizeEntryName(first) - secondName = @normalizeEntryName(second) - firstName.localeCompare(secondName) - - # Public: Perform a synchronous reload of the directory. - reload: -> - newEntries = [] - removedEntries = new Map(@entries) - index = 0 - - for entry in @getEntries() - if @entries.has(entry) - removedEntries.delete(entry) - index++ - continue - - entry.indexInParentDirectory = index - index++ - newEntries.push(entry) - - entriesRemoved = false - removedEntries.forEach (entry, name) => - entriesRemoved = true - entry.destroy() - - if @entries.has(name) - @entries.delete(name) - - if @expansionState.entries.has(name) - @expansionState.entries.delete(name) - - # Convert removedEntries to a Set containing only the entries for O(1) lookup - @emitter.emit('did-remove-entries', new Set(removedEntries.values())) if entriesRemoved - - if newEntries.length > 0 - @entries.set(entry.name, entry) for entry in newEntries - @emitter.emit('did-add-entries', newEntries) - - # Public: Collapse this directory and stop watching it. - collapse: -> - @expansionState.isExpanded = false - @expansionState = @serializeExpansionState() - @unwatch() - @emitter.emit('did-collapse') - - # Public: Expand this directory, load its children, and start watching it for - # changes. - expand: -> - @expansionState.isExpanded = true - @reload() - @watch() - @emitter.emit('did-expand') - - serializeExpansionState: -> - expansionState = {} - expansionState.isExpanded = @expansionState.isExpanded - expansionState.entries = new Map() - @entries.forEach (entry, name) -> - return unless entry.expansionState? - expansionState.entries.set(name, entry.serializeExpansionState()) - expansionState - - squashDirectoryNames: (fullPath) -> - squashedDirs = [@name] - loop - try - contents = fs.listSync fullPath - catch error - break - break if contents.length isnt 1 - break if not fs.isDirectorySync(contents[0]) - relativeDir = path.relative(fullPath, contents[0]) - squashedDirs.push relativeDir - fullPath = path.join(fullPath, relativeDir) - - if squashedDirs.length > 1 - @squashedNames = [squashedDirs[0..squashedDirs.length - 2].join(path.sep) + path.sep, _.last(squashedDirs)] - - return fullPath diff --git a/lib/directory.js b/lib/directory.js new file mode 100644 index 00000000..2e7bf140 --- /dev/null +++ b/lib/directory.js @@ -0,0 +1,445 @@ +const path = require('path') +const _ = require('underscore-plus') +const {CompositeDisposable, Emitter} = require('atom') +const fs = require('fs-plus') +const PathWatcher = require('pathwatcher') +const File = require('./file') +const {repoForPath} = require('./helpers') +const realpathCache = {} + +module.exports = +class Directory { + constructor({name, fullPath, symlink, expansionState, isRoot, ignoredNames, useSyncFS, stats}) { + this.name = name + this.symlink = symlink + this.expansionState = expansionState + this.isRoot = isRoot + this.ignoredNames = ignoredNames + this.useSyncFS = useSyncFS + this.stats = stats + this.destroyed = false + this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() + + if (atom.config.get('tree-view.squashDirectoryNames') && !this.isRoot) { + fullPath = this.squashDirectoryNames(fullPath) + } + + this.path = fullPath + this.realPath = this.path + if (fs.isCaseInsensitive()) { + this.lowerCasePath = this.path.toLowerCase() + this.lowerCaseRealPath = this.lowerCasePath + } + + if (this.isRoot == null) { + this.isRoot = false + } + + if (this.expansionState == null) { + this.expansionState = {} + } + + if (this.expansionState.isExpanded == null) { + this.expansionState.isExpanded = false + } + + // TODO: This can be removed after a sufficient amount + // of time has passed since @expansionState.entries + // has been converted to a Map + if (!(this.expansionState.entries instanceof Map)) { + const convertEntriesToMap = entries => { + const temp = new Map() + for (let name in entries) { + const entry = entries[name] + if (entry.entries != null) { + entry.entries = convertEntriesToMap(entry.entries) + } + temp.set(name, entry) + } + return temp + } + + this.expansionState.entries = convertEntriesToMap(this.expansionState.entries) + } + + if (this.expansionState.entries == null) { + this.expansionState.entries = new Map() + } + + this.status = null + this.entries = new Map() + + const repo = repoForPath(this.path) + this.submodule = repo && repo.isSubmodule(this.path) + + this.subscribeToRepo() + this.updateStatus() + this.loadRealPath() + } + + destroy() { + this.destroyed = true + this.unwatch() + this.subscriptions.dispose() + this.emitter.emit('did-destroy') + } + + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback) + } + + onDidStatusChange(callback) { + return this.emitter.on('did-status-change', callback) + } + + onDidAddEntries(callback) { + return this.emitter.on('did-add-entries', callback) + } + + onDidRemoveEntries(callback) { + return this.emitter.on('did-remove-entries', callback) + } + + onDidCollapse(callback) { + return this.emitter.on('did-collapse', callback) + } + + onDidExpand(callback) { + return this.emitter.on('did-expand', callback) + } + + loadRealPath() { + if (this.useSyncFS) { + this.realPath = fs.realpathSync(this.path) + if (fs.isCaseInsensitive()) { + this.lowerCaseRealPath = this.realPath.toLowerCase() + } + } else { + fs.realpath(this.path, realpathCache, (error, realPath) => { + if (this.destroyed) return + if (realPath && (realPath !== this.path)) { + this.realPath = realPath + if (fs.isCaseInsensitive()) { + this.lowerCaseRealPath = this.realPath.toLowerCase() + } + this.updateStatus() + } + }) + } + } + + // Subscribe to project's repo for changes to the Git status of this directory. + subscribeToRepo() { + const repo = repoForPath(this.path) + if (repo == null) return + + this.subscriptions.add(repo.onDidChangeStatus(event => { + if (this.contains(event.path)) { + this.updateStatus(repo) + } + })) + this.subscriptions.add(repo.onDidChangeStatuses(() => { + this.updateStatus(repo) + })) + } + + // Update the status property of this directory using the repo. + updateStatus() { + const repo = repoForPath(this.path) + if (repo == null) return + + let newStatus = null + if (repo.isPathIgnored(this.path)) { + newStatus = 'ignored' + } else if (this.ignoredNames.matches(this.path)) { + newStatus = 'ignored-name' + } else { + let status + if (this.isRoot) { + // repo.getDirectoryStatus will always fail for the + // root because the path is relativized + concatenated with '/' + // making the matching string be '/'. Then path.indexOf('/') + // is run and will never match beginning of string with a leading '/' + for (let statusPath in repo.statuses) { + status |= parseInt(repo.statuses[statusPath], 10) + } + } else { + status = repo.getDirectoryStatus(this.path) + } + + if (repo.isStatusModified(status)) { + newStatus = 'modified' + } else if (repo.isStatusNew(status)) { + newStatus = 'added' + } + } + + if (newStatus !== this.status) { + this.status = newStatus + this.emitter.emit('did-status-change', newStatus) + } + } + + // Is the given path ignored? + isPathIgnored(filePath) { + if (atom.config.get('tree-view.hideVcsIgnoredFiles')) { + const repo = repoForPath(this.path) + return repo != null && repo.isProjectAtRoot() && repo.isPathIgnored(filePath) + } + + if (atom.config.get('tree-view.hideIgnoredNames')) { + if (ignoredNames.match(filePath)) return true + } + + return false + } + + // Does given full path start with the given prefix? + isPathPrefixOf(prefix, fullPath) { + return fullPath.indexOf(prefix) === 0 && fullPath[prefix.length] === path.sep + } + + isPathEqual(pathToCompare) { + return this.path === pathToCompare || this.realPath === pathToCompare + } + + // Public: Does this directory contain the given path? + // + // See atom.Directory::contains for more details. + contains(pathToCheck) { + if (!pathToCheck) return false + + // Normalize forward slashes to back slashes on Windows + if (process.platform === 'win32') { + pathToCheck = pathToCheck.replace(/\//g, '\\') + } + + let directoryPath + if (fs.isCaseInsensitive()) { + directoryPath = this.lowerCasePath + pathToCheck = pathToCheck.toLowerCase() + } else { + directoryPath = this.path + } + + if (this.isPathPrefixOf(directoryPath, pathToCheck)) return true + + // Check real path + if (this.realPath !== this.path) { + if (fs.isCaseInsensitive()) { + directoryPath = this.lowerCaseRealPath + } else { + directoryPath = this.realPath + } + + return this.isPathPrefixOf(directoryPath, pathToCheck) + } + + return false + } + + // Public: Stop watching this directory for changes. + unwatch() { + if (this.watchSubscription != null) { + this.watchSubscription.close() + this.watchSubscription = null + } + + for(let [key, entry] of this.entries) { + entry.destroy() + this.entries.delete(key) + } + } + + // Public: Watch this directory for changes. + watch() { + if (this.watchSubscription != null) return + try { + this.watchSubscription = PathWatcher.watch(this.path, eventType => { + switch (eventType) { + case 'change': + this.reload() + break + case 'delete': + this.destroy() + break + } + }) + } catch (error) {} + } + + getEntries() { + let names + try { + names = fs.readdirSync(this.path) + } catch (error) { + names = [] + } + names.sort(new Intl.Collator(undefined, {numeric: true, sensitivity: "base"}).compare) + + const files = [] + const directories = [] + + for (let name of names) { + const fullPath = path.join(this.path, name) + if (this.isPathIgnored(fullPath)) continue + + let stat = fs.lstatSyncNoException(fullPath) + const symlink = typeof stat.isSymbolicLink === 'function' && stat.isSymbolicLink() + if (symlink) { + stat = fs.statSyncNoException(fullPath) + } + + const statFlat = _.pick(stat, _.keys(stat)) + for (let key of ["atime", "birthtime", "ctime", "mtime"]) { + statFlat[key] = statFlat[key] && statFlat[key].getTime() + } + + if (typeof stat.isDirectory === 'function' && stat.isDirectory()) { + if (this.entries.has(name)) { + // push a placeholder since this entry already exists but this helps + // track the insertion index for the created views + directories.push(name) + } else { + const expansionState = this.expansionState.entries.get(name) + directories.push(new Directory({ + name, fullPath, symlink, expansionState, + ignoredNames: this.ignoredNames, useSyncFS: this.useSyncFS, stats: statFlat + })) + } + } else if (typeof stat.isFile === 'function' && stat.isFile()) { + if (this.entries.has(name)) { + // push a placeholder since this entry already exists but this helps + // track the insertion index for the created views + files.push(name) + } else { + files.push(new File({name, fullPath, symlink, realpathCache, useSyncFS: this.useSyncFS, stats: statFlat})) + } + } + } + + return this.sortEntries(directories.concat(files)) + } + + normalizeEntryName(value) { + let normalizedValue = value.name + if (normalizedValue == null) { + normalizedValue = value + } + + if (normalizedValue != null) { + normalizedValue = normalizedValue.toLowerCase() + } + return normalizedValue + } + + sortEntries(combinedEntries) { + if (atom.config.get('tree-view.sortFoldersBeforeFiles')) { + return combinedEntries + } else { + return combinedEntries.sort((first, second) => { + const firstName = this.normalizeEntryName(first) + const secondName = this.normalizeEntryName(second) + return firstName.localeCompare(secondName) + }) + } + } + + // Public: Perform a synchronous reload of the directory. + reload() { + const newEntries = [] + const removedEntries = new Map(this.entries) + + let index = 0 + for (let entry of this.getEntries()) { + if (this.entries.has(entry)) { + removedEntries.delete(entry) + index++ + continue + } + + entry.indexInParentDirectory = index + index++ + newEntries.push(entry) + } + + let entriesRemoved = false + for (let [name, entry] of removedEntries) { + entriesRemoved = true + entry.destroy() + + if (this.entries.has(name)) { + this.entries.delete(name) + } + + if (this.expansionState.entries.has(name)) { + this.expansionState.entries.delete(name) + } + } + + // Convert removedEntries to a Set containing only the entries for O(1) lookup + if (entriesRemoved) { + this.emitter.emit('did-remove-entries', new Set(removedEntries.values())) + } + + if (newEntries.length > 0) { + for (let entry of newEntries) { + this.entries.set(entry.name, entry) + } + this.emitter.emit('did-add-entries', newEntries) + } + } + + // Public: Collapse this directory and stop watching it. + collapse() { + this.expansionState.isExpanded = false + this.expansionState = this.serializeExpansionState() + this.unwatch() + this.emitter.emit('did-collapse') + } + + // Public: Expand this directory, load its children, and start watching it for + // changes. + expand() { + this.expansionState.isExpanded = true + this.reload() + this.watch() + this.emitter.emit('did-expand') + } + + serializeExpansionState() { + const expansionState = {} + expansionState.isExpanded = this.expansionState.isExpanded + expansionState.entries = new Map() + for (let [name, entry] of this.entries) { + if (entry.expansionState == null) break + expansionState.entries.set(name, entry.serializeExpansionState()) + } + return expansionState + } + + squashDirectoryNames(fullPath) { + const squashedDirs = [this.name] + let contents + while (true) { + try { + contents = fs.listSync(fullPath) + } catch (error) { + break + } + + if (contents.length !== 1) break + if (!fs.isDirectorySync(contents[0])) break + const relativeDir = path.relative(fullPath, contents[0]) + squashedDirs.push(relativeDir) + fullPath = path.join(fullPath, relativeDir) + } + + if (squashedDirs.length > 1) { + this.squashedNames = [squashedDirs.slice(0, squashedDirs.length - 1).join(path.sep) + path.sep, _.last(squashedDirs)] + } + + return fullPath + } +} From bff2120742da9127e81c32cfe0f916942146d813 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 23 Feb 2018 11:02:07 -0500 Subject: [PATCH 02/10] directory-view.coffee -> directory-view.js --- lib/directory-view.coffee | 146 ----------------------------- lib/directory-view.js | 188 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 146 deletions(-) delete mode 100644 lib/directory-view.coffee create mode 100644 lib/directory-view.js diff --git a/lib/directory-view.coffee b/lib/directory-view.coffee deleted file mode 100644 index c8f83f36..00000000 --- a/lib/directory-view.coffee +++ /dev/null @@ -1,146 +0,0 @@ -{CompositeDisposable} = require 'atom' -getIconServices = require './get-icon-services' -Directory = require './directory' -FileView = require './file-view' - -module.exports = -class DirectoryView - constructor: (@directory) -> - @subscriptions = new CompositeDisposable() - @subscriptions.add @directory.onDidDestroy => @subscriptions.dispose() - @subscribeToDirectory() - - @element = document.createElement('li') - @element.setAttribute('is', 'tree-view-directory') - @element.classList.add('directory', 'entry', 'list-nested-item', 'collapsed') - - @header = document.createElement('div') - @header.classList.add('header', 'list-item') - - @directoryName = document.createElement('span') - @directoryName.classList.add('name', 'icon') - - @entries = document.createElement('ol') - @entries.classList.add('entries', 'list-tree') - - @updateIcon() - @subscriptions.add getIconServices().onDidChange => @updateIcon() - @directoryName.dataset.path = @directory.path - - if @directory.squashedNames? - @directoryName.dataset.name = @directory.squashedNames.join('') - @directoryName.title = @directory.squashedNames.join('') - squashedDirectoryNameNode = document.createElement('span') - squashedDirectoryNameNode.classList.add('squashed-dir') - squashedDirectoryNameNode.textContent = @directory.squashedNames[0] - @directoryName.appendChild(squashedDirectoryNameNode) - @directoryName.appendChild(document.createTextNode(@directory.squashedNames[1])) - else - @directoryName.dataset.name = @directory.name - @directoryName.title = @directory.name - @directoryName.textContent = @directory.name - - @element.appendChild(@header) - @header.appendChild(@directoryName) - @element.appendChild(@entries) - - if @directory.isRoot - @element.classList.add('project-root') - @header.classList.add('project-root-header') - else - @element.draggable = true - - @subscriptions.add @directory.onDidStatusChange => @updateStatus() - @updateStatus() - - @expand() if @directory.expansionState.isExpanded - - @element.collapse = @collapse.bind(this) - @element.expand = @expand.bind(this) - @element.toggleExpansion = @toggleExpansion.bind(this) - @element.reload = @reload.bind(this) - @element.isExpanded = @isExpanded - @element.updateStatus = @updateStatus.bind(this) - @element.isPathEqual = @isPathEqual.bind(this) - @element.getPath = @getPath.bind(this) - @element.directory = @directory - @element.header = @header - @element.entries = @entries - @element.directoryName = @directoryName - - updateIcon: -> - getIconServices().updateDirectoryIcon(this) - - updateStatus: -> - @element.classList.remove('status-ignored', 'status-ignored-name', 'status-modified', 'status-added') - @element.classList.add("status-#{@directory.status}") if @directory.status? - - subscribeToDirectory: -> - @subscriptions.add @directory.onDidAddEntries (addedEntries) => - return unless @isExpanded - - numberOfEntries = @entries.children.length - - for entry in addedEntries - view = @createViewForEntry(entry) - - insertionIndex = entry.indexInParentDirectory - if insertionIndex < numberOfEntries - @entries.insertBefore(view.element, @entries.children[insertionIndex]) - else - @entries.appendChild(view.element) - - numberOfEntries++ - - getPath: -> - @directory.path - - isPathEqual: (pathToCompare) -> - @directory.isPathEqual(pathToCompare) - - createViewForEntry: (entry) -> - if entry instanceof Directory - view = new DirectoryView(entry) - else - view = new FileView(entry) - - subscription = @directory.onDidRemoveEntries (removedEntries) -> - if removedEntries.has(entry) - view.element.remove() - subscription.dispose() - @subscriptions.add(subscription) - - view - - reload: -> - @directory.reload() if @isExpanded - - toggleExpansion: (isRecursive=false) -> - if @isExpanded then @collapse(isRecursive) else @expand(isRecursive) - - expand: (isRecursive=false) -> - unless @isExpanded - @isExpanded = true - @element.isExpanded = @isExpanded - @element.classList.add('expanded') - @element.classList.remove('collapsed') - @directory.expand() - - if isRecursive - for entry in @entries.children when entry.classList.contains('directory') - entry.expand(true) - - false - - collapse: (isRecursive=false) -> - @isExpanded = false - @element.isExpanded = false - - if isRecursive - for entry in @entries.children when entry.isExpanded - entry.collapse(true) - - @element.classList.remove('expanded') - @element.classList.add('collapsed') - @directory.collapse() - @entries.innerHTML = '' diff --git a/lib/directory-view.js b/lib/directory-view.js new file mode 100644 index 00000000..b3776387 --- /dev/null +++ b/lib/directory-view.js @@ -0,0 +1,188 @@ +const {CompositeDisposable} = require('atom') +const getIconServices = require('./get-icon-services') +const Directory = require('./directory') +const FileView = require('./file-view') + +module.exports = +class DirectoryView { + constructor(directory) { + this.directory = directory + this.subscriptions = new CompositeDisposable() + this.subscriptions.add(this.directory.onDidDestroy(() => this.subscriptions.dispose())) + this.subscribeToDirectory() + + this.element = document.createElement('li') + this.element.setAttribute('is', 'tree-view-directory') + this.element.classList.add('directory', 'entry', 'list-nested-item', 'collapsed') + + this.header = document.createElement('div') + this.header.classList.add('header', 'list-item') + + this.directoryName = document.createElement('span') + this.directoryName.classList.add('name', 'icon') + + this.entries = document.createElement('ol') + this.entries.classList.add('entries', 'list-tree') + + this.updateIcon() + this.subscriptions.add(getIconServices().onDidChange(() => this.updateIcon())) + this.directoryName.dataset.path = this.directory.path + + if (this.directory.squashedNames != null) { + this.directoryName.dataset.name = this.directory.squashedNames.join('') + this.directoryName.title = this.directory.squashedNames.join('') + + const squashedDirectoryNameNode = document.createElement('span') + squashedDirectoryNameNode.classList.add('squashed-dir') + squashedDirectoryNameNode.textContent = this.directory.squashedNames[0] + this.directoryName.appendChild(squashedDirectoryNameNode) + this.directoryName.appendChild(document.createTextNode(this.directory.squashedNames[1])) + } else { + this.directoryName.dataset.name = this.directory.name + this.directoryName.title = this.directory.name + this.directoryName.textContent = this.directory.name + } + + this.element.appendChild(this.header) + this.header.appendChild(this.directoryName) + this.element.appendChild(this.entries) + + if (this.directory.isRoot) { + this.element.classList.add('project-root') + this.header.classList.add('project-root-header') + } else { + this.element.draggable = true + } + + this.subscriptions.add(this.directory.onDidStatusChange(() => this.updateStatus())) + this.updateStatus() + + if (this.directory.expansionState.isExpanded) { + this.expand() + } + + this.element.collapse = this.collapse.bind(this) + this.element.expand = this.expand.bind(this) + this.element.toggleExpansion = this.toggleExpansion.bind(this) + this.element.reload = this.reload.bind(this) + this.element.isExpanded = this.isExpanded + this.element.updateStatus = this.updateStatus.bind(this) + this.element.isPathEqual = this.isPathEqual.bind(this) + this.element.getPath = this.getPath.bind(this) + this.element.directory = this.directory + this.element.header = this.header + this.element.entries = this.entries + this.element.directoryName = this.directoryName + } + + updateIcon() { + getIconServices().updateDirectoryIcon(this) + } + + updateStatus() { + this.element.classList.remove('status-ignored', 'status-ignored-name', 'status-modified', 'status-added') + if (this.directory.status != null) { + this.element.classList.add(`status-${this.directory.status}`) + } + } + + subscribeToDirectory() { + this.subscriptions.add(this.directory.onDidAddEntries(addedEntries => { + if (!this.isExpanded) return + + const numberOfEntries = this.entries.children.length + + for (let entry of addedEntries) { + const view = this.createViewForEntry(entry) + + const insertionIndex = entry.indexInParentDirectory + if (insertionIndex < numberOfEntries) { + this.entries.insertBefore(view.element, this.entries.children[insertionIndex]) + } else { + this.entries.appendChild(view.element) + } + } + })) + } + + getPath() { + return this.directory.path + } + + isPathEqual(pathToCompare) { + return this.directory.isPathEqual(pathToCompare) + } + + createViewForEntry(entry) { + const view = entry instanceof Directory ? new DirectoryView(entry) : new FileView(entry) + + const subscription = this.directory.onDidRemoveEntries(removedEntries => { + if (removedEntries.has(entry)) { + view.element.remove() + subscription.dispose() + } + }) + + this.subscriptions.add(subscription) + + return view + } + + reload() { + if (this.isExpanded) { + this.directory.reload() + } + } + + toggleExpansion(isRecursive) { + if (isRecursive == null) { + isRecursive = false + } + if (this.isExpanded) { + this.collapse(isRecursive) + } else { + this.expand(isRecursive) + } + } + + expand(isRecursive) { + if (isRecursive == null) { + isRecursive = false + } + + if (!this.isExpanded) { + this.isExpanded = true + this.element.isExpanded = this.isExpanded + this.element.classList.add('expanded') + this.element.classList.remove('collapsed') + this.directory.expand() + } + + if (isRecursive) { + for (let entry of this.entries.children) { + if (entry.classList.contains('directory')) { + entry.expand(true) + } + } + } + } + + collapse(isRecursive) { + if (isRecursive == null) isRecursive = false + this.isExpanded = false + this.element.isExpanded = false + + if (isRecursive) { + for (let entry of this.entries.children) { + if (entry.isExpanded) { + entry.collapse(true) + } + } + } + + this.element.classList.remove('expanded') + this.element.classList.add('collapsed') + this.directory.collapse() + this.entries.innerHTML = '' + } +} From a8ad1db817562b9f0f7dff3aa9692bccdd727bee Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 23 Feb 2018 11:05:41 -0500 Subject: [PATCH 03/10] file.coffee -> file.js --- lib/file.coffee | 71 ------------------------------------- lib/file.js | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 71 deletions(-) delete mode 100644 lib/file.coffee create mode 100644 lib/file.js diff --git a/lib/file.coffee b/lib/file.coffee deleted file mode 100644 index 3cd08b1a..00000000 --- a/lib/file.coffee +++ /dev/null @@ -1,71 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -{CompositeDisposable, Emitter} = require 'atom' -{repoForPath} = require './helpers' - -module.exports = -class File - constructor: ({@name, fullPath, @symlink, realpathCache, @ignoredNames, useSyncFS, @stats}) -> - @destroyed = false - @emitter = new Emitter() - @subscriptions = new CompositeDisposable() - - @path = fullPath - @realPath = @path - - @subscribeToRepo() - @updateStatus() - - if useSyncFS - @realPath = fs.realpathSync(@path) - else - fs.realpath @path, realpathCache, (error, realPath) => - return if @destroyed - if realPath and realPath isnt @path - @realPath = realPath - @updateStatus() - - destroy: -> - @destroyed = true - @subscriptions.dispose() - @emitter.emit('did-destroy') - - onDidDestroy: (callback) -> - @emitter.on('did-destroy', callback) - - onDidStatusChange: (callback) -> - @emitter.on('did-status-change', callback) - - # Subscribe to the project's repo for changes to the Git status of this file. - subscribeToRepo: -> - repo = repoForPath(@path) - return unless repo? - - @subscriptions.add repo.onDidChangeStatus (event) => - @updateStatus(repo) if @isPathEqual(event.path) - @subscriptions.add repo.onDidChangeStatuses => - @updateStatus(repo) - - # Update the status property of this directory using the repo. - updateStatus: -> - repo = repoForPath(@path) - return unless repo? - - newStatus = null - if repo.isPathIgnored(@path) - newStatus = 'ignored' - else if @ignoredNames.matches(@path) - newStatus = 'ignored-name' - else - status = repo.getCachedPathStatus(@path) - if repo.isStatusModified(status) - newStatus = 'modified' - else if repo.isStatusNew(status) - newStatus = 'added' - - if newStatus isnt @status - @status = newStatus - @emitter.emit('did-status-change', newStatus) - - isPathEqual: (pathToCompare) -> - @path is pathToCompare or @realPath is pathToCompare diff --git a/lib/file.js b/lib/file.js new file mode 100644 index 00000000..62d9aa5c --- /dev/null +++ b/lib/file.js @@ -0,0 +1,93 @@ +const path = require('path') +const fs = require('fs-plus') +const {CompositeDisposable, Emitter} = require('atom') +const {repoForPath} = require('./helpers') + +module.exports = +class File { + constructor({name, fullPath, symlink, realpathCache, ignoredNames, useSyncFS, stats}) { + this.name = name + this.symlink = symlink + this.ignoredNames = ignoredNames + this.stats = stats + this.destroyed = false + this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() + + this.path = fullPath + this.realPath = this.path + + this.subscribeToRepo() + this.updateStatus() + + if (useSyncFS) { + this.realPath = fs.realpathSync(this.path) + } else { + fs.realpath(this.path, realpathCache, (error, realPath) => { + if (this.destroyed) return + if (realPath && realPath !== this.path) { + this.realPath = realPath + this.updateStatus() + } + }) + } + } + + destroy() { + this.destroyed = true + this.subscriptions.dispose() + this.emitter.emit('did-destroy') + } + + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback) + } + + onDidStatusChange(callback) { + return this.emitter.on('did-status-change', callback) + } + + // Subscribe to the project's repo for changes to the Git status of this file. + subscribeToRepo() { + const repo = repoForPath(this.path) + if (repo == null) return + + this.subscriptions.add(repo.onDidChangeStatus(event => { + if (this.isPathEqual(event.path)) { + this.updateStatus(repo) + } + })) + this.subscriptions.add(repo.onDidChangeStatuses(() => { + this.updateStatus(repo) + })) + } + + // Update the status property of this directory using the repo. + updateStatus() { + const repo = repoForPath(this.path) + if (repo == null) return + + let newStatus = null + if (repo.isPathIgnored(this.path)) { + newStatus = 'ignored' + } else if (ignoredNames.matches(this.path)) { + newStatus = 'ignored-name' + } else { + const status = repo.getCachedPathStatus(this.path) + if (repo.isStatusModified(status)) { + newStatus = 'modified' + } else if (repo.isStatusNew(status)) { + newStatus = 'added' + } + } + + if (newStatus !== this.status) { + this.status = newStatus + this.emitter.emit('did-status-change', newStatus) + } + } + + isPathEqual(pathToCompare) { + return this.path === pathToCompare || this.realPath === pathToCompare + } +} From 86a9e707012ddc827191c5d3d6f7694635cecfe8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 23 Feb 2018 11:06:59 -0500 Subject: [PATCH 04/10] file-view.coffee -> file-view.js --- lib/file-view.coffee | 44 ------------------------------------ lib/file-view.js | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 44 deletions(-) delete mode 100644 lib/file-view.coffee create mode 100644 lib/file-view.js diff --git a/lib/file-view.coffee b/lib/file-view.coffee deleted file mode 100644 index d6e34748..00000000 --- a/lib/file-view.coffee +++ /dev/null @@ -1,44 +0,0 @@ -{CompositeDisposable} = require 'atom' -getIconServices = require './get-icon-services' - -module.exports = -class FileView - constructor: (@file) -> - @subscriptions = new CompositeDisposable() - @subscriptions.add @file.onDidDestroy => @subscriptions.dispose() - - @element = document.createElement('li') - @element.setAttribute('is', 'tree-view-file') - @element.draggable = true - @element.classList.add('file', 'entry', 'list-item') - - @fileName = document.createElement('span') - @fileName.classList.add('name', 'icon') - @element.appendChild(@fileName) - @fileName.textContent = @file.name - @fileName.title = @file.name - @fileName.dataset.name = @file.name - @fileName.dataset.path = @file.path - - @updateIcon() - @subscriptions.add @file.onDidStatusChange => @updateStatus() - @subscriptions.add getIconServices().onDidChange => @updateIcon() - @updateStatus() - - updateIcon: -> - getIconServices().updateFileIcon(this) - @element.getPath = @getPath.bind(this) - @element.isPathEqual = @isPathEqual.bind(this) - @element.file = @file - @element.fileName = @fileName - @element.updateStatus = @updateStatus.bind(this) - - updateStatus: -> - @element.classList.remove('status-ignored', 'status-ignored-name', 'status-modified', 'status-added') - @element.classList.add("status-#{@file.status}") if @file.status? - - getPath: -> - @fileName.dataset.path - - isPathEqual: (pathToCompare) -> - @file.isPathEqual(pathToCompare) diff --git a/lib/file-view.js b/lib/file-view.js new file mode 100644 index 00000000..30b04f72 --- /dev/null +++ b/lib/file-view.js @@ -0,0 +1,53 @@ +const {CompositeDisposable} = require('atom') +const getIconServices = require('./get-icon-services') + +module.exports = +class FileView { + constructor(file) { + this.file = file + this.subscriptions = new CompositeDisposable() + this.subscriptions.add(this.file.onDidDestroy(() => this.subscriptions.dispose())) + + this.element = document.createElement('li') + this.element.setAttribute('is', 'tree-view-file') + this.element.draggable = true + this.element.classList.add('file', 'entry', 'list-item') + + this.fileName = document.createElement('span') + this.fileName.classList.add('name', 'icon') + this.element.appendChild(this.fileName) + this.fileName.textContent = this.file.name + this.fileName.title = this.file.name + this.fileName.dataset.name = this.file.name + this.fileName.dataset.path = this.file.path + + this.updateIcon() + this.subscriptions.add(this.file.onDidStatusChange(() => this.updateStatus())) + this.subscriptions.add(getIconServices().onDidChange(() => this.updateIcon())) + this.updateStatus() + } + + updateIcon() { + getIconServices().updateFileIcon(this) + this.element.getPath = this.getPath.bind(this) + this.element.isPathEqual = this.isPathEqual.bind(this) + this.element.file = this.file + this.element.fileName = this.fileName + this.element.updateStatus = this.updateStatus.bind(this) + } + + updateStatus() { + this.element.classList.remove('status-ignored', 'status-ignored-name', 'status-modified', 'status-added') + if (this.file.status != null) { + this.element.classList.add(`status-${this.file.status}`) + } + } + + getPath() { + return this.fileName.dataset.path + } + + isPathEqual(pathToCompare) { + return this.file.isPathEqual(pathToCompare) + } +} From 7b030cde270001f5a25d310238476be6effc0a1c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 23 Feb 2018 12:04:41 -0500 Subject: [PATCH 05/10] Start to fix specs --- lib/directory-view.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/directory-view.js b/lib/directory-view.js index b3776387..073c0830 100644 --- a/lib/directory-view.js +++ b/lib/directory-view.js @@ -116,14 +116,22 @@ class DirectoryView { createViewForEntry(entry) { const view = entry instanceof Directory ? new DirectoryView(entry) : new FileView(entry) +<<<<<<< HEAD const subscription = this.directory.onDidRemoveEntries(removedEntries => { +======= + this.subscriptions.add(this.directory.onDidRemoveEntries(removedEntries => { +>>>>>>> Start to fix specs if (removedEntries.has(entry)) { view.element.remove() subscription.dispose() } +<<<<<<< HEAD }) this.subscriptions.add(subscription) +======= + })) +>>>>>>> Start to fix specs return view } From 089acd688aa2332386d3240f4c89f2da653a21ab Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 23 Feb 2018 15:25:29 -0500 Subject: [PATCH 06/10] Fix remaining specs --- lib/directory-view.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/directory-view.js b/lib/directory-view.js index 073c0830..9c49740a 100644 --- a/lib/directory-view.js +++ b/lib/directory-view.js @@ -116,15 +116,20 @@ class DirectoryView { createViewForEntry(entry) { const view = entry instanceof Directory ? new DirectoryView(entry) : new FileView(entry) +<<<<<<< HEAD <<<<<<< HEAD const subscription = this.directory.onDidRemoveEntries(removedEntries => { ======= this.subscriptions.add(this.directory.onDidRemoveEntries(removedEntries => { >>>>>>> Start to fix specs +======= + const subscription = this.directory.onDidRemoveEntries(removedEntries => { +>>>>>>> Fix remaining specs if (removedEntries.has(entry)) { view.element.remove() subscription.dispose() } +<<<<<<< HEAD <<<<<<< HEAD }) @@ -132,6 +137,11 @@ class DirectoryView { ======= })) >>>>>>> Start to fix specs +======= + }) + + this.subscriptions.add(subscription) +>>>>>>> Fix remaining specs return view } From 35f41023964a11c3f7585470daddfbde4991e620 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 28 Feb 2018 20:34:38 -0500 Subject: [PATCH 07/10] Oops. --- lib/directory-view.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/directory-view.js b/lib/directory-view.js index 9c49740a..b3776387 100644 --- a/lib/directory-view.js +++ b/lib/directory-view.js @@ -116,32 +116,14 @@ class DirectoryView { createViewForEntry(entry) { const view = entry instanceof Directory ? new DirectoryView(entry) : new FileView(entry) -<<<<<<< HEAD -<<<<<<< HEAD const subscription = this.directory.onDidRemoveEntries(removedEntries => { -======= - this.subscriptions.add(this.directory.onDidRemoveEntries(removedEntries => { ->>>>>>> Start to fix specs -======= - const subscription = this.directory.onDidRemoveEntries(removedEntries => { ->>>>>>> Fix remaining specs if (removedEntries.has(entry)) { view.element.remove() subscription.dispose() } -<<<<<<< HEAD -<<<<<<< HEAD - }) - - this.subscriptions.add(subscription) -======= - })) ->>>>>>> Start to fix specs -======= }) this.subscriptions.add(subscription) ->>>>>>> Fix remaining specs return view } From 0a0312178fb262b5e03975c78ad3c63d86858f06 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 28 Feb 2018 20:40:15 -0500 Subject: [PATCH 08/10] Hey it starts up now --- lib/directory.js | 2 +- lib/file.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/directory.js b/lib/directory.js index 2e7bf140..3c51514b 100644 --- a/lib/directory.js +++ b/lib/directory.js @@ -314,7 +314,7 @@ class Directory { // track the insertion index for the created views files.push(name) } else { - files.push(new File({name, fullPath, symlink, realpathCache, useSyncFS: this.useSyncFS, stats: statFlat})) + files.push(new File({name, fullPath, symlink, realpathCache, ignoredNames: this.ignoredNames, useSyncFS: this.useSyncFS, stats: statFlat})) } } } diff --git a/lib/file.js b/lib/file.js index 62d9aa5c..84142e53 100644 --- a/lib/file.js +++ b/lib/file.js @@ -70,7 +70,7 @@ class File { let newStatus = null if (repo.isPathIgnored(this.path)) { newStatus = 'ignored' - } else if (ignoredNames.matches(this.path)) { + } else if (this.ignoredNames.matches(this.path)) { newStatus = 'ignored-name' } else { const status = repo.getCachedPathStatus(this.path) From 730400a9db1d4ae39ae8ba8d88c01cb6f6579277 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 28 Feb 2018 22:03:26 -0500 Subject: [PATCH 09/10] Another fix --- lib/directory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/directory.js b/lib/directory.js index 3c51514b..993e22aa 100644 --- a/lib/directory.js +++ b/lib/directory.js @@ -189,7 +189,7 @@ class Directory { } if (atom.config.get('tree-view.hideIgnoredNames')) { - if (ignoredNames.match(filePath)) return true + if (this.ignoredNames.match(filePath)) return true } return false From 0a73d4c486c1af1cee169b8b434eb5a316f6447a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 6 Mar 2018 10:28:07 -0500 Subject: [PATCH 10/10] I cannot spell --- lib/directory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/directory.js b/lib/directory.js index 993e22aa..5e9f31cf 100644 --- a/lib/directory.js +++ b/lib/directory.js @@ -189,7 +189,7 @@ class Directory { } if (atom.config.get('tree-view.hideIgnoredNames')) { - if (this.ignoredNames.match(filePath)) return true + if (this.ignoredNames.matches(filePath)) return true } return false