diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee new file mode 100644 index 000000000..780f2afd5 --- /dev/null +++ b/spec/default-directory-provider-spec.coffee @@ -0,0 +1,40 @@ +DefaultDirectoryProvider = require "../src/default-directory-provider" +path = require "path" +temp = require "temp" + +describe "DefaultDirectoryProvider", -> + describe ".directoryForURISync(uri)", -> + it "returns a Directory with a path that matches the uri", -> + provider = new DefaultDirectoryProvider() + tmp = temp.mkdirSync() + + directory = provider.directoryForURISync(tmp) + expect(directory.getPath()).toEqual tmp + + it "normalizes its input before creating a Directory for it", -> + provider = new DefaultDirectoryProvider() + tmp = temp.mkdirSync() + nonNormalizedPath = tmp + path.sep + ".." + path.sep + path.basename(tmp) + expect(tmp.contains("..")).toBe false + expect(nonNormalizedPath.contains("..")).toBe true + + directory = provider.directoryForURISync(nonNormalizedPath) + expect(directory.getPath()).toEqual tmp + + it "creates a Directory for its parent dir when passed a file", -> + provider = new DefaultDirectoryProvider() + tmp = temp.mkdirSync() + file = path.join(tmp, "example.txt") + fs.writeFileSync(file, "data") + + directory = provider.directoryForURISync(file) + expect(directory.getPath()).toEqual tmp + + describe ".directoryForURI(uri)", -> + it "returns a Promise that resolves to a Directory with a path that matches the uri", -> + provider = new DefaultDirectoryProvider() + tmp = temp.mkdirSync() + + waitsForPromise -> + provider.directoryForURI(tmp).then (directory) -> + expect(directory.getPath()).toEqual tmp diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 956895952..466431faf 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -14,6 +14,70 @@ describe "Project", -> atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) describe "constructor", -> + it "enables a custom DirectoryProvider to supersede the DefaultDirectoryProvider", -> + remotePath = "ssh://foreign-directory:8080/" + class DummyDirectory + constructor: (@path) -> + getPath: -> @path + getFile: -> existsSync: -> false + getSubdirectory: -> existsSync: -> false + isRoot: -> true + off: -> + contains: (filePath) -> filePath.startsWith(remotePath) + + directoryProvider = + directoryForURISync: (uri) -> + if uri.startsWith("ssh://") + new DummyDirectory(uri) + else + null + directoryForURI: (uri) -> throw new Error("This should not be called.") + atom.packages.serviceHub.provide( + "atom.directory-provider", "0.1.0", directoryProvider) + + tmp = temp.mkdirSync() + atom.project.setPaths([tmp, remotePath]) + directories = atom.project.getDirectories() + expect(directories.length).toBe 2 + + localDirectory = directories[0] + expect(localDirectory.getPath()).toBe tmp + expect(localDirectory instanceof Directory).toBe true + + dummyDirectory = directories[1] + expect(dummyDirectory.getPath()).toBe remotePath + expect(dummyDirectory instanceof DummyDirectory).toBe true + + expect(atom.project.getPaths()).toEqual([tmp, remotePath]) + + # Make sure that DummyDirectory.contains() is honored. + remotePathSubdirectory = remotePath + "a/subdirectory" + atom.project.addPath(remotePathSubdirectory) + expect(atom.project.getDirectories().length).toBe 2 + + # Make sure that a new DummyDirectory that is not contained by the first + # DummyDirectory can be added. + otherRemotePath = "ssh://other-foreign-directory:8080/" + atom.project.addPath(otherRemotePath) + newDirectories = atom.project.getDirectories() + expect(newDirectories.length).toBe 3 + otherDummyDirectory = newDirectories[2] + expect(otherDummyDirectory.getPath()).toBe otherRemotePath + expect(otherDummyDirectory instanceof DummyDirectory).toBe true + + it "uses the default directory provider if no custom provider can handle the URI", -> + directoryProvider = + directoryForURISync: (uri) -> null + directoryForURI: (uri) -> throw new Error("This should not be called.") + atom.packages.serviceHub.provide( + "atom.directory-provider", "0.1.0", directoryProvider) + + tmp = temp.mkdirSync() + atom.project.setPaths([tmp]) + directories = atom.project.getDirectories() + expect(directories.length).toBe 1 + expect(directories[0].getPath()).toBe tmp + it "tries to update repositories when a new RepositoryProvider is registered", -> tmp = temp.mkdirSync('atom-project') atom.project.setPaths([tmp]) diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee new file mode 100644 index 000000000..7ecdbd354 --- /dev/null +++ b/src/default-directory-provider.coffee @@ -0,0 +1,35 @@ +{Directory} = require 'pathwatcher' +fs = require 'fs-plus' +path = require 'path' + +module.exports = +class DefaultDirectoryProvider + + # Public: Create a Directory that corresponds to the specified URI. + # + # * `uri` {String} The path to the directory to add. This is guaranteed not to + # be contained by a {Directory} in `atom.project`. + # + # Returns: + # * {Directory} if the given URI is compatible with this provider. + # * `null` if the given URI is not compatibile with this provider. + directoryForURISync: (uri) -> + projectPath = path.normalize(uri) + + directoryPath = if fs.isDirectorySync(projectPath) + projectPath + else + path.dirname(projectPath) + + new Directory(directoryPath) + + # Public: Create a Directory that corresponds to the specified URI. + # + # * `uri` {String} The path to the directory to add. This is guaranteed not to + # be contained by a {Directory} in `atom.project`. + # + # Returns a Promise that resolves to: + # * {Directory} if the given URI is compatible with this provider. + # * `null` if the given URI is not compatibile with this provider. + directoryForURI: (uri) -> + Promise.resolve(@directoryForURISync(uri)) diff --git a/src/project.coffee b/src/project.coffee index 50bda3ed8..d3fbd88eb 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -8,9 +8,9 @@ Q = require 'q' {Model} = require 'theorist' {Subscriber} = require 'emissary' {Emitter} = require 'event-kit' +DefaultDirectoryProvider = require './default-directory-provider' Serializable = require 'serializable' TextBuffer = require 'text-buffer' -{Directory} = require 'pathwatcher' Grim = require 'grim' TextEditor = require './text-editor' @@ -41,6 +41,14 @@ class Project extends Model @rootDirectories = [] @repositories = [] + @directoryProviders = [new DefaultDirectoryProvider()] + atom.packages.serviceHub.consume( + 'atom.directory-provider', + '^0.1.0', + # New providers are added to the front of @directoryProviders because + # DefaultDirectoryProvider is a catch-all that will always provide a Directory. + (provider) => @directoryProviders.unshift(provider)) + # Mapping from the real path of a {Directory} to a {Promise} that resolves # to either a {Repository} or null. Ideally, the {Directory} would be used # as the key; however, there can be multiple {Directory} objects created for @@ -159,7 +167,7 @@ class Project extends Model # Public: Get an {Array} of {String}s containing the paths of the project's # directories. - getPaths: -> rootDirectory.path for rootDirectory in @rootDirectories + getPaths: -> rootDirectory.getPath() for rootDirectory in @rootDirectories getPath: -> Grim.deprecate("Use ::getPaths instead") @getPaths()[0] @@ -182,22 +190,22 @@ class Project extends Model Grim.deprecate("Use ::setPaths instead") @setPaths([path]) - # Public: Add a path the project's list of root paths + # Public: Add a path to the project's list of root paths # # * `projectPath` {String} The path to the directory to add. addPath: (projectPath, options) -> - projectPath = path.normalize(projectPath) + for directory in @getDirectories() + # Apparently a Directory does not believe it can contain itself, so we + # must also check whether the paths match. + return if directory.contains(projectPath) or directory.getPath() is projectPath - directoryPath = if fs.isDirectorySync(projectPath) - projectPath - else - path.dirname(projectPath) - - return if @getPaths().some (existingPath) -> - (directoryPath is existingPath) or - (directoryPath.indexOf(path.join(existingPath, path.sep)) is 0) - - directory = new Directory(directoryPath) + directory = null + for provider in @directoryProviders + break if directory = provider.directoryForURISync?(projectPath) + if directory is null + # This should never happen because DefaultDirectoryProvider should always + # return a Directory. + throw new Error(projectPath + ' could not be resolved to a directory') @rootDirectories.push(directory) repo = null